@bridge_gpt/mcp-server 0.2.42 → 0.2.43
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/README.md +321 -182
- package/build/agents.generated.js +2 -2
- package/build/claude-review-workflow.js +510 -45
- package/build/commands.generated.js +4 -3
- package/build/conduct-epic/cli.js +195 -0
- package/build/conductor/bridge-api-client.js +121 -0
- package/build/conductor/cli.js +63 -0
- package/build/conductor/recovery-cli.js +313 -0
- package/build/conductor/recovery-operations.js +219 -0
- package/build/conductor-bin.js +9 -5
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +13 -3
- package/build/drive-epic.js +375 -0
- package/build/executor/http-client.js +71 -3
- package/build/executor/job-errors.js +9 -0
- package/build/executor/job-runner.js +50 -3
- package/build/executor/observation.js +105 -13
- package/build/executor/runner.js +219 -0
- package/build/executor/worker-finalization.js +233 -56
- package/build/executor/worktree.js +8 -1
- package/build/index.js +2156 -98
- package/build/install-bridge.js +23 -9
- package/build/pipelines.generated.js +304 -14
- package/build/plane/cli.js +73 -7
- package/build/plane/defaults.js +14 -4
- package/build/plane/manifest.js +90 -0
- package/build/plane/preflight.js +19 -0
- package/build/plane/shutdown.js +71 -3
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +75 -5
- package/build/setup-epic.js +82 -8
- package/build/version.generated.js +2 -1
- package/build/worktree-core.js +31 -17
- package/package.json +1 -1
- package/pipelines/greenfield-setup.json +286 -0
|
@@ -20,6 +20,20 @@ import { readdirSync } from "fs";
|
|
|
20
20
|
import { spawnSync } from "child_process";
|
|
21
21
|
import path from "path";
|
|
22
22
|
const DEFAULT_BUILD_DIR = "build";
|
|
23
|
+
// BAPI-877 — the SECOND directory the unit lane discovers. `mcp_server/scripts/`
|
|
24
|
+
// holds hand-authored ESM (`"type": "module"`) that the build itself runs:
|
|
25
|
+
// bundle-version.js, sync-agent-mirrors.js, and friends. They are NOT tsc inputs
|
|
26
|
+
// (tsconfig.json is rootDir "src" / include "src/**/*.ts"), so they compile to
|
|
27
|
+
// nothing and a `*.test.js` beside them has no build/ counterpart to discover.
|
|
28
|
+
// Before this constant existed, such a file ran in NO lane at all: not `npm
|
|
29
|
+
// test` (which walked build/ only), not the integration lane, not the pre-push
|
|
30
|
+
// gate. sync-agent-mirrors.test.js sat unrun for months that way.
|
|
31
|
+
//
|
|
32
|
+
// Discovered from source rather than from build/ for that reason, and unioned
|
|
33
|
+
// into the "normal" group — a `scripts/…` path can never match the
|
|
34
|
+
// build/integration/ prefix, so it needs no filter change to land there.
|
|
35
|
+
const SCRIPT_TEST_DIR = "scripts";
|
|
36
|
+
export const SCRIPTS_DIR_PREFIX = "scripts/";
|
|
23
37
|
// The Windows CreateProcess command-line limit is ~8191 characters. This
|
|
24
38
|
// budget stays well under it (accounting for execPath + node options +
|
|
25
39
|
// separators) so a single batch can never approach the real ceiling even on a
|
|
@@ -28,9 +42,9 @@ const DEFAULT_BUILD_DIR = "build";
|
|
|
28
42
|
export const MAX_COMMAND_LENGTH = 6000;
|
|
29
43
|
// Explicit manifest: only tests that call node:test's `mock.module()` API
|
|
30
44
|
// need --experimental-test-module-mocks. Everything else discovered under
|
|
31
|
-
// build/ is eligible for the "normal" group automatically — new
|
|
32
|
-
// need to be registered anywhere else. Add an entry here only when
|
|
33
|
-
// needs module mocking.
|
|
45
|
+
// build/ OR scripts/ is eligible for the "normal" group automatically — new
|
|
46
|
+
// tests do NOT need to be registered anywhere else. Add an entry here only when
|
|
47
|
+
// a new test needs module mocking.
|
|
34
48
|
export const MODULE_MOCK_MANIFEST = [
|
|
35
49
|
"build/attachment-download.test.js",
|
|
36
50
|
"build/attachment-upload.test.js",
|
|
@@ -40,6 +54,8 @@ export const MODULE_MOCK_MANIFEST = [
|
|
|
40
54
|
"build/conductor/git-inspection.test.js",
|
|
41
55
|
"build/conductor/paths.test.js",
|
|
42
56
|
"build/conductor/pr-ci-producer-emit-seam.test.js",
|
|
57
|
+
"build/conductor/recovery-cli.test.js",
|
|
58
|
+
"build/conductor/recovery-operations.test.js",
|
|
43
59
|
"build/conductor/security-regressions.test.js",
|
|
44
60
|
"build/conductor/store-lifecycle.test.js",
|
|
45
61
|
"build/conductor/store-queries.test.js",
|
|
@@ -57,6 +73,7 @@ export const MODULE_MOCK_MANIFEST = [
|
|
|
57
73
|
"build/index-heavy-read-truncation.test.js",
|
|
58
74
|
"build/index-output-path.test.js",
|
|
59
75
|
"build/index.review-rounds.test.js",
|
|
76
|
+
"build/plane/cli.test.js",
|
|
60
77
|
"build/recovery-formatting.test.js",
|
|
61
78
|
"build/sfcc/client.test.js",
|
|
62
79
|
"build/sfcc/permissions.test.js",
|
|
@@ -128,6 +145,10 @@ export const INTEGRATION_DISCOVERY_EXCLUSIONS = [
|
|
|
128
145
|
// discovered into the unit lane automatically and needs no registration —
|
|
129
146
|
// tests/pytest/mcp/test_mcp_server_test_registration_multicomponent.py fails if
|
|
130
147
|
// a *.integration.test.ts ends up in neither lane.
|
|
148
|
+
//
|
|
149
|
+
// BAPI-877 — a `scripts/…` path must NEVER be listed here. This is not a style
|
|
150
|
+
// rule: `discoverIntegrationTestFiles` walks build/ only, so the entry would fail
|
|
151
|
+
// its own presence check below and throw on EVERY `npm run test:integration` run.
|
|
131
152
|
export const INTEGRATION_LANE_ONLY = [
|
|
132
153
|
"build/conductor/conductor-runtime.integration.test.js",
|
|
133
154
|
];
|
|
@@ -160,6 +181,55 @@ export function discoverTestFiles(options) {
|
|
|
160
181
|
results.sort();
|
|
161
182
|
return results;
|
|
162
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* BAPI-877 — hand-authored `scripts/*.test.js`, sorted and package-relative.
|
|
186
|
+
*
|
|
187
|
+
* A thin delegation rather than a second walker: `buildDir` already generalizes
|
|
188
|
+
* (the walk is directory-agnostic and `path.relative(root, …)` yields
|
|
189
|
+
* `scripts/x.test.js` for free), and duplicating the ENOENT swallow, the sort,
|
|
190
|
+
* and the separator normalization would be three chances to diverge.
|
|
191
|
+
*
|
|
192
|
+
* Deliberately does NOT apply the empty-discovery guard — `discoverUnitTestFiles`
|
|
193
|
+
* owns that, so this stays a pure query usable in isolation.
|
|
194
|
+
*/
|
|
195
|
+
export function discoverScriptTestFiles(options) {
|
|
196
|
+
return discoverTestFiles({ ...options, buildDir: SCRIPT_TEST_DIR });
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* BAPI-877 — everything the unit lane runs: compiled `build/**` plus authored
|
|
200
|
+
* `scripts/**`.
|
|
201
|
+
*
|
|
202
|
+
* WHY THE EMPTY-SCRIPTS GUARD IS SEPARATE. `selectTestGroup`'s "selected group
|
|
203
|
+
* is empty" refusal cannot fire for this: ~390 compiled files are always
|
|
204
|
+
* present, so a scripts-discovery regression would be masked and the lane would
|
|
205
|
+
* report green over zero scripts tests — the exact "coverage that exists only if
|
|
206
|
+
* someone runs it by hand" failure BAPI-877 exists to remove. Refusing here
|
|
207
|
+
* matches `runIntegrationLane` and `discoverIntegrationTestFiles`, which take the
|
|
208
|
+
* same posture for the same reason.
|
|
209
|
+
*
|
|
210
|
+
* The cost is deliberate: deleting every `scripts/*.test.js` breaks `npm test`
|
|
211
|
+
* until someone edits this file. Deleting all coverage for the build's version
|
|
212
|
+
* generator and agent-mirror synchronizer SHOULD require touching the line that
|
|
213
|
+
* says so, rather than silently shrinking the suite.
|
|
214
|
+
*
|
|
215
|
+
* WHY IT LIVES HERE and not in `runSelectedGroup`: the guard belongs on the
|
|
216
|
+
* production path only. Every test that injects its own `discoverFn` supplies a
|
|
217
|
+
* build-only fixture tree, and firing there would force `scripts/*` entries into
|
|
218
|
+
* BASELINE_NORMAL_FILES — falsifying the independent historical oracle this
|
|
219
|
+
* suite is built on.
|
|
220
|
+
*/
|
|
221
|
+
export function discoverUnitTestFiles(options) {
|
|
222
|
+
const compiled = discoverTestFiles(options);
|
|
223
|
+
const authored = discoverScriptTestFiles(options);
|
|
224
|
+
if (authored.length === 0) {
|
|
225
|
+
throw new Error(`run-unit-tests: discovered zero ${SCRIPTS_DIR_PREFIX}*.test.js under ` +
|
|
226
|
+
`"${options.root}" — refusing to run. A scripts-lane discovery regression is ` +
|
|
227
|
+
`invisible beside the compiled tests that still ran, which is the failure ` +
|
|
228
|
+
`this lane exists to prevent (BAPI-877). If ${SCRIPTS_DIR_PREFIX} genuinely ` +
|
|
229
|
+
`has no test file any more, remove this guard deliberately.`);
|
|
230
|
+
}
|
|
231
|
+
return [...compiled, ...authored].sort();
|
|
232
|
+
}
|
|
163
233
|
/** Throws if the module-mock, force-exit-quarantine, or integration-lane manifest has drifted from the discovered build output. */
|
|
164
234
|
export function validateManifest(discovered) {
|
|
165
235
|
const discoveredSet = new Set(discovered);
|
|
@@ -264,8 +334,8 @@ export function runSelectedGroup(mode, options) {
|
|
|
264
334
|
if (mode !== "normal" && mode !== "module-mocks") {
|
|
265
335
|
throw new Error(`run-unit-tests: unknown group "${mode}" (expected "normal" or "module-mocks")`);
|
|
266
336
|
}
|
|
267
|
-
const { root, execPath = process.execPath, spawnFn = spawnSync, discoverFn = (opts) =>
|
|
268
|
-
const discovered = discoverFn({ root });
|
|
337
|
+
const { root, execPath = process.execPath, spawnFn = spawnSync, discoverFn = (opts) => discoverUnitTestFiles(opts), fsImpl, log = console.log, } = options;
|
|
338
|
+
const discovered = discoverFn({ root, fsImpl });
|
|
269
339
|
const selected = selectTestGroup(mode, discovered);
|
|
270
340
|
// Split out the force-exit quarantine: those files run last, each as its own
|
|
271
341
|
// single-file spawn WITH --test-force-exit (exact tally, prompt exit despite
|
package/build/setup-epic.js
CHANGED
|
@@ -55,8 +55,14 @@
|
|
|
55
55
|
* A run with no feature branch is byte-identical to before: no parse read, no
|
|
56
56
|
* `git`, no scope, no polling, no extra JSON keys.
|
|
57
57
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
58
|
+
* Reads the plan sidecar and the optional policy file from the local
|
|
59
|
+
* filesystem. As of BAPI-872 it can also WRITE the local filesystem in one
|
|
60
|
+
* narrow case: after a successful create/reuse, if a live `plane up` manifest
|
|
61
|
+
* exists for this exact repository root, setup-epic binds the resolved
|
|
62
|
+
* `epic_run_id` onto it (`.bridge/plane/plane.json`) so a later `plane down`
|
|
63
|
+
* can stop that run automatically instead of guessing from repository-wide
|
|
64
|
+
* active-run state. Every other workflow (remote, no local plane) is
|
|
65
|
+
* unaffected and performs no such write.
|
|
60
66
|
*/
|
|
61
67
|
import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
|
|
62
68
|
import os from "node:os";
|
|
@@ -75,6 +81,11 @@ import { getConfigFieldBaseBranch, getIndexScopeLifecycle, getParseStatus, } fro
|
|
|
75
81
|
import { createExecFileRunCommand, ensureCommitResolvableLocally, normalizeCommitSha, performExactIndexScopeCut, pollIndexScopeLifecycle, readRemoteBranchHead, runGit, SCOPE_LIFECYCLE_LABELS, } from "./conduct-epic/cut-protocol.js";
|
|
76
82
|
import { hashPlan } from "./conductor/plan.js";
|
|
77
83
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
84
|
+
// BAPI-872: local plane-manifest binding, so `plane down` can later stop the
|
|
85
|
+
// run this setup resolved WITHOUT guessing from repository-wide active-run
|
|
86
|
+
// state. See `planeBinding` on `SetupEpicDeps` below.
|
|
87
|
+
import { createPlaneFsDeps, createPlaneProcessDeps } from "./plane/defaults.js";
|
|
88
|
+
import { bindPlaneManifestEpicRun, manifestHasLiveProcess, readPlaneManifest, } from "./plane/manifest.js";
|
|
78
89
|
/** Accepted `policy_json.review_policy.source` values (the `ReviewPolicy` surface). */
|
|
79
90
|
export const SETUP_EPIC_REVIEW_POLICY_SOURCES = [
|
|
80
91
|
"verdict_protocol",
|
|
@@ -114,6 +125,11 @@ export function createDefaultSetupEpicDeps() {
|
|
|
114
125
|
isTTY: Boolean(process.stdin.isTTY),
|
|
115
126
|
promptLine: defaultPromptLine,
|
|
116
127
|
runCommand: createExecFileRunCommand(),
|
|
128
|
+
planeBinding: {
|
|
129
|
+
readManifest: (repoRoot) => readPlaneManifest(repoRoot, createPlaneFsDeps()),
|
|
130
|
+
isPlaneProcessAlive: (pid) => createPlaneProcessDeps().isAlive(pid),
|
|
131
|
+
bind: (repoRoot, planeId, epicRunId) => bindPlaneManifestEpicRun(repoRoot, planeId, epicRunId, createPlaneFsDeps()),
|
|
132
|
+
},
|
|
117
133
|
};
|
|
118
134
|
}
|
|
119
135
|
/** User-facing usage text. */
|
|
@@ -159,6 +175,12 @@ export function getSetupEpicUsage() {
|
|
|
159
175
|
" post-create PATCH is needed. Contradicts nothing",
|
|
160
176
|
" silently: if the file and --feature-branch or",
|
|
161
177
|
" --review-policy disagree, setup-epic errors naming both.",
|
|
178
|
+
" policy_json.job_timeouts values must be below the job",
|
|
179
|
+
" type's gate ceiling (implement / spec_review / resume",
|
|
180
|
+
" 10799; remediate / ci_fix 3599; rebase / smoke 2699;",
|
|
181
|
+
" merge 1799); the server rejects a higher value with a",
|
|
182
|
+
" 422 naming the offending field. Full table:",
|
|
183
|
+
" docs/claude/epic-conductor-v2-operator-runbook.md §4.",
|
|
162
184
|
" --replace-policy Authorize replacing a LIVE run's stored policy with the",
|
|
163
185
|
" policy file, as a complete replacement. Without it, a",
|
|
164
186
|
" divergent policy on a reused run is refused with a",
|
|
@@ -1016,12 +1038,19 @@ async function resolveScopeIdForRun(access, fetchImpl, epicRunId) {
|
|
|
1016
1038
|
const match = listing.value.scopes.find((scope) => scope.epic_run_id === epicRunId);
|
|
1017
1039
|
return match?.scope_id ?? null;
|
|
1018
1040
|
}
|
|
1019
|
-
/**
|
|
1020
|
-
|
|
1041
|
+
/**
|
|
1042
|
+
* Operator-facing guidance for a bounded scope failure category.
|
|
1043
|
+
*
|
|
1044
|
+
* BAPI-872: abandonment guidance names the conductor CLI verb — never a raw
|
|
1045
|
+
* `PATCH` recipe an operator would have to hand-roll. `epicRunId` is the
|
|
1046
|
+
* concrete, already-resolved run ID at this call site (the failure is scoped to
|
|
1047
|
+
* ONE run), so it is always named explicitly rather than left as a placeholder.
|
|
1048
|
+
*/
|
|
1049
|
+
function scopeFailureGuidance(reason, featureBranch, epicRunId) {
|
|
1021
1050
|
if (reason === "canonical_index_advanced") {
|
|
1022
1051
|
return (`The canonical index advanced before the scope could be seeded. The cut must be ` +
|
|
1023
1052
|
`re-driven at the newer commit: delete origin/${featureBranch}, abandon this run ` +
|
|
1024
|
-
`(
|
|
1053
|
+
`(conductor abandon-run --epic-run-id ${epicRunId}), and re-run setup-epic.`);
|
|
1025
1054
|
}
|
|
1026
1055
|
return `Recovery: ${SETUP_EPIC_SCOPE_RECOVERY_COMMAND}.`;
|
|
1027
1056
|
}
|
|
@@ -1246,7 +1275,7 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1246
1275
|
else if (err instanceof ConductorBridgeApiError && err.status === 409) {
|
|
1247
1276
|
deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs — it is wedged, and every ` +
|
|
1248
1277
|
`plan call will keep failing. Abandon the duplicate before retrying:\n` +
|
|
1249
|
-
`
|
|
1278
|
+
` conductor abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
|
|
1250
1279
|
`Detail: ${errorDetail(err)}`);
|
|
1251
1280
|
return 1;
|
|
1252
1281
|
}
|
|
@@ -1332,7 +1361,7 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1332
1361
|
`validated because the epic itself is wedged, and every plan call will ` +
|
|
1333
1362
|
`keep failing. This is NOT a problem with your plan. Abandon the ` +
|
|
1334
1363
|
`duplicate before retrying:\n` +
|
|
1335
|
-
`
|
|
1364
|
+
` conductor abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
|
|
1336
1365
|
`No run was created and no automation-start charge occurred.\n` +
|
|
1337
1366
|
`Detail: ${errorDetail(err)}`);
|
|
1338
1367
|
return 1;
|
|
@@ -1617,6 +1646,51 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1617
1646
|
return 1;
|
|
1618
1647
|
}
|
|
1619
1648
|
}
|
|
1649
|
+
// --- Plane-manifest binding (BAPI-872) ------------------------------------
|
|
1650
|
+
// A live `plane up` for THIS exact repository root can now be told which run
|
|
1651
|
+
// it is driving, so a later `plane down` can stop it automatically instead
|
|
1652
|
+
// of guessing from repository-wide active-run state (which risks stopping
|
|
1653
|
+
// the WRONG run). Absence of a local plane — remote runs, CI, any non-plane
|
|
1654
|
+
// workflow — is normal and produces neither an attempt nor a warning.
|
|
1655
|
+
if (deps.planeBinding) {
|
|
1656
|
+
try {
|
|
1657
|
+
const manifestRead = await deps.planeBinding.readManifest(deps.cwd);
|
|
1658
|
+
if (manifestRead.kind === "valid") {
|
|
1659
|
+
const manifest = manifestRead.manifest;
|
|
1660
|
+
const isLive = manifestHasLiveProcess(manifest, {
|
|
1661
|
+
isAlive: deps.planeBinding.isPlaneProcessAlive,
|
|
1662
|
+
});
|
|
1663
|
+
if (isLive) {
|
|
1664
|
+
const bound = await deps.planeBinding.bind(deps.cwd, manifest.planeId, result.epic_run_id);
|
|
1665
|
+
if (bound.ok) {
|
|
1666
|
+
say(bound.alreadyBound
|
|
1667
|
+
? `Plane: already bound to run ${result.epic_run_id}`
|
|
1668
|
+
: `Plane: bound to run ${result.epic_run_id} — \`plane down\` will stop it automatically`);
|
|
1669
|
+
}
|
|
1670
|
+
else {
|
|
1671
|
+
const msg = `Could not bind the local plane to run ${result.epic_run_id} (${bound.message}). ` +
|
|
1672
|
+
"A later `plane down` cannot be guaranteed to stop it automatically — if you need " +
|
|
1673
|
+
`to stop this run, run \`conductor stop-run --epic-run-id ${result.epic_run_id}\`.`;
|
|
1674
|
+
warnings.push(msg);
|
|
1675
|
+
say(`Plane: [warn] ${msg}`);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
// A recorded-but-not-live manifest is normal: `plane up` will revalidate
|
|
1679
|
+
// and replace a stale record on its own, and there is nothing running to
|
|
1680
|
+
// hand this binding to yet.
|
|
1681
|
+
}
|
|
1682
|
+
// A missing or unvalidated manifest is normal — no attempt, no warning.
|
|
1683
|
+
}
|
|
1684
|
+
catch (err) {
|
|
1685
|
+
// The binding capability itself must never fail setup-epic as a whole;
|
|
1686
|
+
// an unexpected error here is reported as a warning, never thrown.
|
|
1687
|
+
const msg = `Could not check for a local plane to bind run ${result.epic_run_id} to (${errorDetail(err)}). ` +
|
|
1688
|
+
"A later `plane down` cannot be guaranteed to stop it automatically — if you need to stop " +
|
|
1689
|
+
`this run, run \`conductor stop-run --epic-run-id ${result.epic_run_id}\`.`;
|
|
1690
|
+
warnings.push(msg);
|
|
1691
|
+
say(`Plane: [warn] ${msg}`);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1620
1694
|
// --- The exact cut (BAPI-850) --------------------------------------------
|
|
1621
1695
|
// AFTER the run exists and BEFORE the plan is stored/approved: the scope's run
|
|
1622
1696
|
// association is immutable server-side, so the cut is driven with this run's
|
|
@@ -1806,7 +1880,7 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1806
1880
|
deps.errorLog(`Index scope ${scopeId} FAILED (reason: ${verdict.reason}) — this is a recorded ` +
|
|
1807
1881
|
`failure, not an in-progress state. Ticket dispatch remains blocked: the reconciler ` +
|
|
1808
1882
|
`will not dispatch against a failed scope.`);
|
|
1809
|
-
deps.errorLog(scopeFailureGuidance(verdict.reason, effectiveFeatureBranch ?? ""));
|
|
1883
|
+
deps.errorLog(scopeFailureGuidance(verdict.reason, effectiveFeatureBranch ?? "", result.epic_run_id));
|
|
1810
1884
|
exitCode = 1;
|
|
1811
1885
|
}
|
|
1812
1886
|
else {
|
package/build/worktree-core.js
CHANGED
|
@@ -97,21 +97,32 @@ function pickWorktreePathField(parsed) {
|
|
|
97
97
|
return undefined;
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
* The remediation sentence for a branch left over by a prior FAILED run.
|
|
101
|
+
* Unchanged since the guard was introduced.
|
|
102
|
+
*/
|
|
103
|
+
function staleLeftoverRemedy(branch, baseRef) {
|
|
104
|
+
return (`it carries commits not on the resolved base (likely a leftover from a prior run). ` +
|
|
105
|
+
`Refusing to reuse a stale worktree — delete it (git worktree remove + git branch -D ${branch}) ` +
|
|
106
|
+
`or rebase it onto ${baseRef}, then re-dispatch.`);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* BAPI-862: the remediation for a branch that carries a SUCCEEDED implement whose
|
|
110
|
+
* pull request was never attached.
|
|
106
111
|
*
|
|
107
|
-
* `
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* against the local `main` branch name.
|
|
112
|
+
* Names the `pr_not_attached` gate observation so the executor refusal and the
|
|
113
|
+
* reconciler's own state are one searchable vocabulary, points at attachment, and
|
|
114
|
+
* states plainly that the two remedies above destroy completed work. It must not
|
|
115
|
+
* contain the words "delete" or "rebase" at all: an operator scanning a wall of
|
|
116
|
+
* job output for the next command is exactly who lost work last time.
|
|
113
117
|
*/
|
|
114
|
-
|
|
118
|
+
function unattachedSuccessRemedy(branch) {
|
|
119
|
+
return (`it carries the commits of a SUCCEEDED implement whose pull request was never attached ` +
|
|
120
|
+
`(implement gate observation: pr_not_attached). Refusing to reuse the worktree — but this ` +
|
|
121
|
+
`branch is finished work, not a leftover. Open or attach a pull request from '${branch}' and ` +
|
|
122
|
+
`let the reconciler bind it. Do NOT remove or re-write the branch: either would destroy a ` +
|
|
123
|
+
`completed implementation.`);
|
|
124
|
+
}
|
|
125
|
+
export async function isExistingBranchSafeToReuse(deps, branch, baseStartPoint, classification = "unknown") {
|
|
115
126
|
let baseRef = baseStartPoint;
|
|
116
127
|
const originRef = `origin/${baseStartPoint}`;
|
|
117
128
|
const originExists = await deps.runCommand("git", ["rev-parse", "--verify", "--quiet", originRef], { cwd: deps.cwd });
|
|
@@ -122,11 +133,14 @@ export async function isExistingBranchSafeToReuse(deps, branch, baseStartPoint)
|
|
|
122
133
|
const ancestor = await deps.runCommand("git", ["merge-base", "--is-ancestor", branch, baseRef], { cwd: deps.cwd });
|
|
123
134
|
if (commandSucceeded(ancestor))
|
|
124
135
|
return { safe: true };
|
|
136
|
+
// The REFUSAL is identical either way (BAPI-862 non-goal: refusal behavior is
|
|
137
|
+
// unchanged). Only the diagnosis and the remedy differ.
|
|
138
|
+
const remedy = classification === "succeeded_pr_not_attached"
|
|
139
|
+
? unattachedSuccessRemedy(branch)
|
|
140
|
+
: staleLeftoverRemedy(branch, baseRef);
|
|
125
141
|
return {
|
|
126
142
|
safe: false,
|
|
127
|
-
reason: `existing branch '${branch}' is not an ancestor of ${baseRef};
|
|
128
|
-
`resolved base (likely a leftover from a prior run). Refusing to reuse a stale worktree — delete it ` +
|
|
129
|
-
`(git worktree remove + git branch -D ${branch}) or rebase it onto ${baseRef}, then re-dispatch.`,
|
|
143
|
+
reason: `existing branch '${branch}' is not an ancestor of ${baseRef}; ${remedy}`,
|
|
130
144
|
};
|
|
131
145
|
}
|
|
132
146
|
/**
|
|
@@ -263,7 +277,7 @@ export async function createWorktreeForTicket(deps, key, branchOverrides, worktr
|
|
|
263
277
|
// check compares against the SAME effective base start point (branch name or
|
|
264
278
|
// fetched SHA) that a NEW branch would be cut from below.
|
|
265
279
|
if (exists && guardStaleWorktree) {
|
|
266
|
-
const safety = await isExistingBranchSafeToReuse(deps, branch, baseStartPoint);
|
|
280
|
+
const safety = await isExistingBranchSafeToReuse(deps, branch, baseStartPoint, behavior.staleBranchClassification ?? "unknown");
|
|
267
281
|
if (!safety.safe) {
|
|
268
282
|
return {
|
|
269
283
|
key,
|
package/package.json
CHANGED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "greenfield-setup",
|
|
3
|
+
"description": "Onboard a brand-new project that has no code to learn from yet. Offers a bounded interview (or generic defaults), ratifies the open non-functional requirements on a decision page, settles version control, convenes a general council, synthesizes one coherent set of project standards behind a single approval, writes the unset standards fields, and closes with the one authoritative install-manifest apply plus the read-after-write manifest /install-bridge resumes from.",
|
|
4
|
+
"variables": [
|
|
5
|
+
"docs_dir",
|
|
6
|
+
"repo_name"
|
|
7
|
+
],
|
|
8
|
+
"steps": [
|
|
9
|
+
{
|
|
10
|
+
"type": "mcp_call",
|
|
11
|
+
"tool": "ping",
|
|
12
|
+
"params": {},
|
|
13
|
+
"description": "Verify Bridge API connectivity"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"type": "agent_task",
|
|
17
|
+
"instruction": "Create the required output directory by running: mkdir -p {docs_dir}/greenfield/{repo_name}/standards",
|
|
18
|
+
"description": "Create the greenfield working directory"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "mcp_call",
|
|
22
|
+
"id": "read_install_manifest",
|
|
23
|
+
"tool": "get_install_manifest",
|
|
24
|
+
"params": {},
|
|
25
|
+
"description": "Read the install manifest once and keep its snapshot token for the final apply"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "mcp_call",
|
|
29
|
+
"id": "fetch_architecture_instructions",
|
|
30
|
+
"tool": "config_field",
|
|
31
|
+
"params": {
|
|
32
|
+
"field_name": "architecture_instructions",
|
|
33
|
+
"operation": "get"
|
|
34
|
+
},
|
|
35
|
+
"description": "Check whether architecture_instructions already has content",
|
|
36
|
+
"on_error": "warn_and_continue"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"type": "mcp_call",
|
|
40
|
+
"id": "fetch_review_instructions",
|
|
41
|
+
"tool": "config_field",
|
|
42
|
+
"params": {
|
|
43
|
+
"field_name": "review_instructions",
|
|
44
|
+
"operation": "get"
|
|
45
|
+
},
|
|
46
|
+
"description": "Check whether review_instructions already has content",
|
|
47
|
+
"on_error": "warn_and_continue"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "mcp_call",
|
|
51
|
+
"id": "fetch_documentation_instructions",
|
|
52
|
+
"tool": "config_field",
|
|
53
|
+
"params": {
|
|
54
|
+
"field_name": "documentation_instructions",
|
|
55
|
+
"operation": "get"
|
|
56
|
+
},
|
|
57
|
+
"description": "Check whether documentation_instructions already has content",
|
|
58
|
+
"on_error": "warn_and_continue"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "mcp_call",
|
|
62
|
+
"id": "fetch_unit_testing_instructions",
|
|
63
|
+
"tool": "config_field",
|
|
64
|
+
"params": {
|
|
65
|
+
"field_name": "unit_testing_instructions",
|
|
66
|
+
"operation": "get"
|
|
67
|
+
},
|
|
68
|
+
"description": "Check whether unit_testing_instructions already has content",
|
|
69
|
+
"on_error": "warn_and_continue"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"type": "mcp_call",
|
|
73
|
+
"id": "fetch_e2e_testing_instructions",
|
|
74
|
+
"tool": "config_field",
|
|
75
|
+
"params": {
|
|
76
|
+
"field_name": "e2e_testing_instructions",
|
|
77
|
+
"operation": "get"
|
|
78
|
+
},
|
|
79
|
+
"description": "Check whether e2e_testing_instructions already has content",
|
|
80
|
+
"on_error": "warn_and_continue"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"type": "mcp_call",
|
|
84
|
+
"id": "fetch_frontend_correctness_standards",
|
|
85
|
+
"tool": "config_field",
|
|
86
|
+
"params": {
|
|
87
|
+
"field_name": "frontend_correctness_standards",
|
|
88
|
+
"operation": "get"
|
|
89
|
+
},
|
|
90
|
+
"description": "Check whether frontend_correctness_standards already has content",
|
|
91
|
+
"on_error": "warn_and_continue"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"type": "mcp_call",
|
|
95
|
+
"id": "fetch_backend_correctness_standards",
|
|
96
|
+
"tool": "config_field",
|
|
97
|
+
"params": {
|
|
98
|
+
"field_name": "backend_correctness_standards",
|
|
99
|
+
"operation": "get"
|
|
100
|
+
},
|
|
101
|
+
"description": "Check whether backend_correctness_standards already has content",
|
|
102
|
+
"on_error": "warn_and_continue"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"type": "mcp_call",
|
|
106
|
+
"id": "fetch_template_correctness_standards",
|
|
107
|
+
"tool": "config_field",
|
|
108
|
+
"params": {
|
|
109
|
+
"field_name": "template_correctness_standards",
|
|
110
|
+
"operation": "get"
|
|
111
|
+
},
|
|
112
|
+
"description": "Check whether template_correctness_standards already has content",
|
|
113
|
+
"on_error": "warn_and_continue"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"type": "mcp_call",
|
|
117
|
+
"id": "fetch_style_correctness_standards",
|
|
118
|
+
"tool": "config_field",
|
|
119
|
+
"params": {
|
|
120
|
+
"field_name": "style_correctness_standards",
|
|
121
|
+
"operation": "get"
|
|
122
|
+
},
|
|
123
|
+
"description": "Check whether style_correctness_standards already has content",
|
|
124
|
+
"on_error": "warn_and_continue"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"type": "mcp_call",
|
|
128
|
+
"id": "fetch_design_principles",
|
|
129
|
+
"tool": "config_field",
|
|
130
|
+
"params": {
|
|
131
|
+
"field_name": "design_principles",
|
|
132
|
+
"operation": "get"
|
|
133
|
+
},
|
|
134
|
+
"description": "Check whether design_principles already has content",
|
|
135
|
+
"on_error": "warn_and_continue"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"type": "agent_task",
|
|
139
|
+
"id": "greenfield_interview",
|
|
140
|
+
"instruction_file": "greenfield-interview.md",
|
|
141
|
+
"description": "Offer the interview, settle the project framing and version control, and commit the decisions"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"type": "agent_task",
|
|
145
|
+
"id": "synthesize_standards",
|
|
146
|
+
"instruction_file": "greenfield-synthesize-standards.md",
|
|
147
|
+
"description": "Draft the project standards from the committed framing and take one approval"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"type": "mcp_call",
|
|
151
|
+
"id": "upload_architecture_instructions",
|
|
152
|
+
"tool": "config_field",
|
|
153
|
+
"params": {
|
|
154
|
+
"field_name": "architecture_instructions",
|
|
155
|
+
"file_path": "{docs_dir}/greenfield/{repo_name}/standards/architecture_instructions.md",
|
|
156
|
+
"operation": "update",
|
|
157
|
+
"only_if_null": true
|
|
158
|
+
},
|
|
159
|
+
"description": "Save the architecture standard",
|
|
160
|
+
"on_error": "warn_and_continue"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"type": "mcp_call",
|
|
164
|
+
"id": "upload_review_instructions",
|
|
165
|
+
"tool": "config_field",
|
|
166
|
+
"params": {
|
|
167
|
+
"field_name": "review_instructions",
|
|
168
|
+
"file_path": "{docs_dir}/greenfield/{repo_name}/standards/review_instructions.md",
|
|
169
|
+
"operation": "update",
|
|
170
|
+
"only_if_null": true
|
|
171
|
+
},
|
|
172
|
+
"description": "Save the code-review standard",
|
|
173
|
+
"on_error": "warn_and_continue"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"type": "mcp_call",
|
|
177
|
+
"id": "upload_documentation_instructions",
|
|
178
|
+
"tool": "config_field",
|
|
179
|
+
"params": {
|
|
180
|
+
"field_name": "documentation_instructions",
|
|
181
|
+
"file_path": "{docs_dir}/greenfield/{repo_name}/standards/documentation_instructions.md",
|
|
182
|
+
"operation": "update",
|
|
183
|
+
"only_if_null": true
|
|
184
|
+
},
|
|
185
|
+
"description": "Save the documentation standard",
|
|
186
|
+
"on_error": "warn_and_continue"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"type": "mcp_call",
|
|
190
|
+
"id": "upload_unit_testing_instructions",
|
|
191
|
+
"tool": "config_field",
|
|
192
|
+
"params": {
|
|
193
|
+
"field_name": "unit_testing_instructions",
|
|
194
|
+
"file_path": "{docs_dir}/greenfield/{repo_name}/standards/unit_testing_instructions.md",
|
|
195
|
+
"operation": "update",
|
|
196
|
+
"only_if_null": true
|
|
197
|
+
},
|
|
198
|
+
"description": "Save the unit-testing standard",
|
|
199
|
+
"on_error": "warn_and_continue"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"type": "mcp_call",
|
|
203
|
+
"id": "upload_e2e_testing_instructions",
|
|
204
|
+
"tool": "config_field",
|
|
205
|
+
"params": {
|
|
206
|
+
"field_name": "e2e_testing_instructions",
|
|
207
|
+
"file_path": "{docs_dir}/greenfield/{repo_name}/standards/e2e_testing_instructions.md",
|
|
208
|
+
"operation": "update",
|
|
209
|
+
"only_if_null": true
|
|
210
|
+
},
|
|
211
|
+
"description": "Save the end-to-end testing standard",
|
|
212
|
+
"on_error": "warn_and_continue"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"type": "mcp_call",
|
|
216
|
+
"id": "upload_frontend_correctness_standards",
|
|
217
|
+
"tool": "config_field",
|
|
218
|
+
"params": {
|
|
219
|
+
"field_name": "frontend_correctness_standards",
|
|
220
|
+
"file_path": "{docs_dir}/greenfield/{repo_name}/standards/frontend_correctness_standards.md",
|
|
221
|
+
"operation": "update",
|
|
222
|
+
"only_if_null": true
|
|
223
|
+
},
|
|
224
|
+
"description": "Save the frontend correctness standard",
|
|
225
|
+
"on_error": "warn_and_continue"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"type": "mcp_call",
|
|
229
|
+
"id": "upload_backend_correctness_standards",
|
|
230
|
+
"tool": "config_field",
|
|
231
|
+
"params": {
|
|
232
|
+
"field_name": "backend_correctness_standards",
|
|
233
|
+
"file_path": "{docs_dir}/greenfield/{repo_name}/standards/backend_correctness_standards.md",
|
|
234
|
+
"operation": "update",
|
|
235
|
+
"only_if_null": true
|
|
236
|
+
},
|
|
237
|
+
"description": "Save the backend correctness standard",
|
|
238
|
+
"on_error": "warn_and_continue"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"type": "mcp_call",
|
|
242
|
+
"id": "upload_template_correctness_standards",
|
|
243
|
+
"tool": "config_field",
|
|
244
|
+
"params": {
|
|
245
|
+
"field_name": "template_correctness_standards",
|
|
246
|
+
"file_path": "{docs_dir}/greenfield/{repo_name}/standards/template_correctness_standards.md",
|
|
247
|
+
"operation": "update",
|
|
248
|
+
"only_if_null": true
|
|
249
|
+
},
|
|
250
|
+
"description": "Save the template correctness standard",
|
|
251
|
+
"on_error": "warn_and_continue"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"type": "mcp_call",
|
|
255
|
+
"id": "upload_style_correctness_standards",
|
|
256
|
+
"tool": "config_field",
|
|
257
|
+
"params": {
|
|
258
|
+
"field_name": "style_correctness_standards",
|
|
259
|
+
"file_path": "{docs_dir}/greenfield/{repo_name}/standards/style_correctness_standards.md",
|
|
260
|
+
"operation": "update",
|
|
261
|
+
"only_if_null": true
|
|
262
|
+
},
|
|
263
|
+
"description": "Save the style correctness standard",
|
|
264
|
+
"on_error": "warn_and_continue"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"type": "mcp_call",
|
|
268
|
+
"id": "upload_design_principles",
|
|
269
|
+
"tool": "config_field",
|
|
270
|
+
"params": {
|
|
271
|
+
"field_name": "design_principles",
|
|
272
|
+
"file_path": "{docs_dir}/greenfield/{repo_name}/standards/design_principles.md",
|
|
273
|
+
"operation": "update",
|
|
274
|
+
"only_if_null": true
|
|
275
|
+
},
|
|
276
|
+
"description": "Save the design principles",
|
|
277
|
+
"on_error": "warn_and_continue"
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"type": "agent_task",
|
|
281
|
+
"id": "apply_and_report",
|
|
282
|
+
"instruction_file": "greenfield-apply-and-report.md",
|
|
283
|
+
"description": "Save the project setup and report where the project stands"
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
}
|