@bridge_gpt/mcp-server 0.2.48 → 0.2.50
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 +24 -7
- package/build/base-ref.js +28 -3
- package/build/claude-review-workflow-drift-probe.js +130 -0
- package/build/claude-review-workflow-drift.js +173 -0
- package/build/claude-review-workflow.js +81 -16
- package/build/commands.generated.js +5 -5
- package/build/conductor/done-gate.js +25 -3
- package/build/conductor/install-doctor.js +65 -5
- package/build/conductor/latest-check-selector.js +170 -0
- package/build/conductor/local-merge.js +8 -6
- package/build/conductor-bin.js +1 -1
- package/build/{brainstorm-files.js → council-files.js} +15 -15
- package/build/decision-page-schema.js +1 -1
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +162 -4
- package/build/executor/worktree.js +46 -1
- package/build/index.js +92 -51
- package/build/init.js +9 -2
- package/build/install-bridge.js +60 -2
- package/build/install-reexec.js +47 -9
- package/build/pipelines.generated.js +1 -1
- package/build/plane/cli.js +12 -2
- package/build/plane/manifest.js +25 -1
- package/build/plane/member-roster.js +61 -7
- package/build/plane/preflight.js +24 -9
- package/build/plane/supervisor.js +77 -5
- package/build/plane/types.js +23 -3
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +2 -1
- package/build/stale-worktree-doctor.js +120 -0
- package/build/start-tickets-prereqs.js +70 -0
- package/build/start-tickets.js +91 -3
- package/build/version.generated.js +3 -2
- package/package.json +4 -2
- package/build/chain-orchestrator.js +0 -1457
- package/build/chain-utils.js +0 -68
- package/build/command-catalog.js +0 -376
- package/build/schedule-run.js +0 -1300
- package/build/schedule-store.js +0 -172
- package/build/scheduled-prompt.js +0 -115
- package/build/scheduler-backends/at-fallback.js +0 -139
- package/build/scheduler-backends/escaping.js +0 -143
- package/build/scheduler-backends/index.js +0 -72
- package/build/scheduler-backends/launchd.js +0 -225
- package/build/scheduler-backends/systemd-user.js +0 -250
- package/build/scheduler-backends/task-scheduler.js +0 -214
- package/build/scheduler-backends/types.js +0 -23
package/build/init.js
CHANGED
|
@@ -15,7 +15,7 @@ import os from "os";
|
|
|
15
15
|
import { COMMANDS } from "./commands.generated.js";
|
|
16
16
|
import { AGENTS } from "./agents.generated.js";
|
|
17
17
|
import { DOCS } from "./docs.generated.js";
|
|
18
|
-
import { VERSION } from "./version.generated.js";
|
|
18
|
+
import { VERSION, LAUNCHER_ARGS } from "./version.generated.js";
|
|
19
19
|
import { reconstructAgentMarkdown, translateAgentToCopilot } from "./agent-utils.js";
|
|
20
20
|
import { validateRepoName, resolveRepoNameForProjectRoot } from "./bridge-config.js";
|
|
21
21
|
import { ensureGitignored as ensureGitignoredShared } from "./git-ignore-utils.js";
|
|
@@ -58,9 +58,16 @@ export function buildBridgeApiEntry(cwd, metadata) {
|
|
|
58
58
|
// BAPI-728: `repoName`/`baseUrl` are non-secret and always supplied by the
|
|
59
59
|
// caller when it has already resolved them. `BAPI_API_KEY` remains absent in
|
|
60
60
|
// EVERY branch — the server self-resolves it at runtime.
|
|
61
|
+
//
|
|
62
|
+
// BAPI-930: `LAUNCHER_ARGS` (generated by scripts/launcher-args.js ->
|
|
63
|
+
// scripts/bundle-version.js -> version.generated.ts) is the canonical launcher
|
|
64
|
+
// token sequence; `mcp_server/README.md`'s hand-configuration examples are
|
|
65
|
+
// rendered from the same generated constant by
|
|
66
|
+
// scripts/sync-readme-host-examples.js, so the two cannot drift. Spread into a
|
|
67
|
+
// fresh array so each registration gets its own independently mutable `args`.
|
|
61
68
|
return {
|
|
62
69
|
command: "npx",
|
|
63
|
-
args: [
|
|
70
|
+
args: [...LAUNCHER_ARGS],
|
|
64
71
|
env: {
|
|
65
72
|
BAPI_BASE_URL: metadata?.baseUrl ?? DEFAULT_BRIDGE_BASE_URL,
|
|
66
73
|
BAPI_REPO_NAME: metadata?.repoName ?? PLACEHOLDER_REPO_NAME,
|
package/build/install-bridge.js
CHANGED
|
@@ -165,6 +165,9 @@ import { runInstallBridgeConductorCli, } from "./install-bridge-conductor.js";
|
|
|
165
165
|
import { runConductorInstallDoctor, CONDUCTOR_PROFILE_TOKEN, } from "./conductor/install-doctor.js";
|
|
166
166
|
import { resolveConductorBridgeApiAccess, } from "./conductor/bridge-api-client.js";
|
|
167
167
|
import { claudeReviewWorkflowPath, writeClaudeReviewWorkflow, } from "./claude-review-workflow.js";
|
|
168
|
+
// BAPI-941: read-only workflow-lineage probe, wired into the conductor install
|
|
169
|
+
// doctor's optional drift seam below.
|
|
170
|
+
import { probeClaudeReviewWorkflowDrift, resolveCurrentBranch, resolveRepositoryDefaultBranch, } from "./claude-review-workflow-drift-probe.js";
|
|
168
171
|
import { runSetupEpicCli } from "./setup-epic.js";
|
|
169
172
|
import { ensureGitignored as ensureGitignoredShared, } from "./git-ignore-utils.js";
|
|
170
173
|
import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
@@ -927,6 +930,10 @@ export function sanitizePrewarmEnv(env) {
|
|
|
927
930
|
delete sanitized.BAPI_API_KEY;
|
|
928
931
|
delete sanitized.BAPI_INVITE;
|
|
929
932
|
delete sanitized.BAPI_SIGNUP_EMAIL;
|
|
933
|
+
// BAPI-931: the provenance marker is a label, not a secret, but it describes a
|
|
934
|
+
// credential this probe deliberately no longer carries. Stripping it keeps the
|
|
935
|
+
// prewarm environment internally consistent rather than leaving an orphan.
|
|
936
|
+
delete sanitized[INSTALL_REEXEC_KEY_SOURCE_ENV];
|
|
930
937
|
return sanitized;
|
|
931
938
|
}
|
|
932
939
|
function spawnPrewarmDefault(command, args, env) {
|
|
@@ -959,6 +966,10 @@ function spawnPrewarmDefault(command, args, env) {
|
|
|
959
966
|
}
|
|
960
967
|
/** Build default deps from the live process. */
|
|
961
968
|
export function createDefaultInstallBridgeDeps() {
|
|
969
|
+
// BAPI-941: hoisted so the conductor install doctor's read-only
|
|
970
|
+
// workflow-lineage seam below can reuse the SAME command runner the rest of
|
|
971
|
+
// installation uses, rather than constructing a second one.
|
|
972
|
+
const startTicketsDeps = createDefaultStartTicketsDeps();
|
|
962
973
|
const isTTY = Boolean(process.stdin.isTTY);
|
|
963
974
|
// One production fetch, reused for both the `fetch` seam and the default
|
|
964
975
|
// resolver, so a direct caller of this factory gets a resolver bound to the
|
|
@@ -1005,7 +1016,7 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
1005
1016
|
discardBootstrapPending: discardBootstrapPendingCredential,
|
|
1006
1017
|
buildShellCommand: buildGenericAgentShellCommand,
|
|
1007
1018
|
spawnTerminalTab: getDefaultSpawnTerminalTabForPlatform(process.platform),
|
|
1008
|
-
startTicketsDeps
|
|
1019
|
+
startTicketsDeps,
|
|
1009
1020
|
log: (m) => console.log(m),
|
|
1010
1021
|
errorLog: (m) => console.error(m),
|
|
1011
1022
|
// Debug-only sink (BAPI-666): gated on BAPI_INSTALL_DEBUG so it is silent on a
|
|
@@ -1022,6 +1033,24 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
1022
1033
|
fetch: params.fetch,
|
|
1023
1034
|
reviewPolicySource: params.reviewPolicySource,
|
|
1024
1035
|
readWorkflowFile: () => params.readFile(claudeReviewWorkflowPath(params.cwd)),
|
|
1036
|
+
// BAPI-941: read-only workflow-lineage classification. Runs only after
|
|
1037
|
+
// the file is confirmed present, uses the same command runner the rest
|
|
1038
|
+
// of installation already has, and touches the network never — two
|
|
1039
|
+
// `git show` reads against the local object database. Any failure
|
|
1040
|
+
// resolves to `unverified` inside the probe, which leaves the doctor's
|
|
1041
|
+
// presence state exactly as it was.
|
|
1042
|
+
classifyWorkflowDrift: async () => {
|
|
1043
|
+
const probeDeps = {
|
|
1044
|
+
runCommand: startTicketsDeps.runCommand,
|
|
1045
|
+
cwd: params.cwd,
|
|
1046
|
+
};
|
|
1047
|
+
const baseRef = await resolveCurrentBranch(probeDeps);
|
|
1048
|
+
const defaultRef = await resolveRepositoryDefaultBranch(probeDeps);
|
|
1049
|
+
return probeClaudeReviewWorkflowDrift(probeDeps, {
|
|
1050
|
+
baseRef: baseRef ?? "",
|
|
1051
|
+
defaultRef,
|
|
1052
|
+
});
|
|
1053
|
+
},
|
|
1025
1054
|
// BAPI-775: the SAME project root the rest of installation resolves
|
|
1026
1055
|
// against, and the SAME read-only host-config inspector the
|
|
1027
1056
|
// tool-visibility phase reports from — so the doctor's profile-token
|
|
@@ -1242,6 +1271,28 @@ function buildDoctorServiceStateInspector(deps) {
|
|
|
1242
1271
|
return inspection.state;
|
|
1243
1272
|
};
|
|
1244
1273
|
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Internal marker carrying credential PROVENANCE — never a credential VALUE —
|
|
1276
|
+
* across the `install` self-re-exec (BAPI-931).
|
|
1277
|
+
*
|
|
1278
|
+
* `prepareInstallReexecArguments` lifts `--api-key <value>` out of child argv into
|
|
1279
|
+
* `BAPI_API_KEY` so the secret stays out of `ps`. Without this marker the child
|
|
1280
|
+
* then resolves from its environment and attributes a 401 to "the BAPI_API_KEY
|
|
1281
|
+
* environment variable" — naming a variable the operator never set, at the exact
|
|
1282
|
+
* moment their install is failing. That regressed BAPI-668 R11 for every user
|
|
1283
|
+
* whose installed copy is behind `@latest`, which is the common case for the
|
|
1284
|
+
* documented unpinned `npx … install`.
|
|
1285
|
+
*
|
|
1286
|
+
* NOT a public interface. It is undocumented, set only by this package's own
|
|
1287
|
+
* re-exec alongside `BAPI_API_KEY`, read only by {@link resolveApiKey}, and
|
|
1288
|
+
* stripped by {@link sanitizePrewarmEnv}. Its vocabulary is CLOSED: only the
|
|
1289
|
+
* exact string `"flag"` relabels; anything else — absent, blank, misspelled, or
|
|
1290
|
+
* hand-set — falls through to the ordinary `env` attribution, so a malformed or
|
|
1291
|
+
* forged value can never produce a source that is not one of the three real ones.
|
|
1292
|
+
*/
|
|
1293
|
+
export const INSTALL_REEXEC_KEY_SOURCE_ENV = "BAPI_INTERNAL_KEY_SOURCE";
|
|
1294
|
+
/** The only value {@link INSTALL_REEXEC_KEY_SOURCE_ENV} may carry. */
|
|
1295
|
+
export const INSTALL_REEXEC_KEY_SOURCE_FLAG = "flag";
|
|
1245
1296
|
/**
|
|
1246
1297
|
* Resolve the ordinary credential entry: `--api-key` → `BAPI_API_KEY` env →
|
|
1247
1298
|
* interactive no-echo prompt. Fails (secret-free) when none is available and
|
|
@@ -1264,7 +1315,14 @@ export async function resolveApiKey(options, deps) {
|
|
|
1264
1315
|
}
|
|
1265
1316
|
const fromEnv = deps.env.BAPI_API_KEY;
|
|
1266
1317
|
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
1267
|
-
|
|
1318
|
+
// BAPI-931: in a re-exec'd child the flag was lifted into this env var to keep
|
|
1319
|
+
// it out of `ps`, so "env" would name a variable the operator never set. The
|
|
1320
|
+
// marker restores the truth. Strict equality against the closed vocabulary —
|
|
1321
|
+
// any other value keeps today's `env` attribution byte-for-byte.
|
|
1322
|
+
const source = deps.env[INSTALL_REEXEC_KEY_SOURCE_ENV] === INSTALL_REEXEC_KEY_SOURCE_FLAG
|
|
1323
|
+
? "flag"
|
|
1324
|
+
: "env";
|
|
1325
|
+
return { ok: true, value: fromEnv.trim(), source };
|
|
1268
1326
|
}
|
|
1269
1327
|
if (deps.isTTY && deps.promptSecret) {
|
|
1270
1328
|
// BAPI-708 (A-3): the prompt no longer asks the user to pre-classify their own
|
package/build/install-reexec.js
CHANGED
|
@@ -34,12 +34,20 @@
|
|
|
34
34
|
* the child down the bare-onboarding branch instead. The failure diagnostic is a
|
|
35
35
|
* fixed string — it interpolates no argv, no environment value, and no exception
|
|
36
36
|
* text.
|
|
37
|
+
*
|
|
38
|
+
* PROVENANCE SURVIVES THE HAND-OFF (BAPI-931). Lifting `--api-key` into the
|
|
39
|
+
* environment destroys the evidence of which input the operator actually used, so
|
|
40
|
+
* the child reported a 401 as coming from "the BAPI_API_KEY environment variable"
|
|
41
|
+
* — naming a variable nobody set. A separate `provenanceEnv` overlay carries the
|
|
42
|
+
* LABEL `flag` alongside the secret so the child can attribute the failure
|
|
43
|
+
* truthfully. It is a label, never a value, and it is kept out of `secretEnv`
|
|
44
|
+
* precisely so that overlay's "never logged" contract stays true of every member.
|
|
37
45
|
*/
|
|
38
46
|
import { spawn } from "child_process";
|
|
39
47
|
import { VERSION } from "./version.generated.js";
|
|
40
48
|
import { isNewerVersion } from "./update-check.js";
|
|
41
49
|
import { fetchLatestVersion } from "./cli-release.js";
|
|
42
|
-
import { runInstallBridgeCli } from "./install-bridge.js";
|
|
50
|
+
import { runInstallBridgeCli, INSTALL_REEXEC_KEY_SOURCE_ENV, INSTALL_REEXEC_KEY_SOURCE_FLAG, } from "./install-bridge.js";
|
|
43
51
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
44
52
|
/** The sentinel that marks an already-re-exec'd child. Matches `upgrade-cli.ts`. */
|
|
45
53
|
export const INSTALL_REEXEC_SENTINEL = "--internal-reexec";
|
|
@@ -91,6 +99,7 @@ export function stripInternalReexecSentinels(argv) {
|
|
|
91
99
|
export function prepareInstallReexecArguments(argv) {
|
|
92
100
|
const forwardedArgs = [];
|
|
93
101
|
const secretEnv = {};
|
|
102
|
+
const provenanceEnv = {};
|
|
94
103
|
for (let i = 0; i < argv.length; i++) {
|
|
95
104
|
const arg = argv[i];
|
|
96
105
|
if (arg === "--api-key" || arg.startsWith("--api-key=")) {
|
|
@@ -109,6 +118,11 @@ export function prepareInstallReexecArguments(argv) {
|
|
|
109
118
|
continue;
|
|
110
119
|
}
|
|
111
120
|
secretEnv.BAPI_API_KEY = value;
|
|
121
|
+
// BAPI-931: record that the FLAG was the real source. Only set here,
|
|
122
|
+
// beside the lift that destroys the argv evidence — never for --invite
|
|
123
|
+
// (no 401 attribution on that path) and never for a malformed --api-key
|
|
124
|
+
// (handed to the installer's parser above, so nothing was lifted).
|
|
125
|
+
provenanceEnv[INSTALL_REEXEC_KEY_SOURCE_ENV] = INSTALL_REEXEC_KEY_SOURCE_FLAG;
|
|
112
126
|
if (consumedNext)
|
|
113
127
|
i += 1;
|
|
114
128
|
continue;
|
|
@@ -137,7 +151,7 @@ export function prepareInstallReexecArguments(argv) {
|
|
|
137
151
|
}
|
|
138
152
|
forwardedArgs.push(arg);
|
|
139
153
|
}
|
|
140
|
-
return { forwardedArgs, secretEnv };
|
|
154
|
+
return { forwardedArgs, secretEnv, provenanceEnv };
|
|
141
155
|
}
|
|
142
156
|
/**
|
|
143
157
|
* The public `install` / legacy `install-bridge` entry point.
|
|
@@ -164,11 +178,33 @@ export async function runInstallBridgeWithLatestCli(argv, deps = {}) {
|
|
|
164
178
|
const errorLog = deps.errorLog ?? ((message) => console.error(message));
|
|
165
179
|
const localVersion = deps.localVersion ?? VERSION;
|
|
166
180
|
const { args: cleanedArgs, sentinelPresent } = stripInternalReexecSentinels(argv);
|
|
167
|
-
// Terminating condition, the nested-conductor bypass,
|
|
168
|
-
// (BAPI-818, R2) all skip the registry entirely. A dry
|
|
169
|
-
// contract is "no network" — a version-freshness lookup is
|
|
170
|
-
// call, so it cannot run first even though it is fail-open
|
|
171
|
-
|
|
181
|
+
// Terminating condition, the nested-conductor bypass, `--dry-run`
|
|
182
|
+
// (BAPI-818, R2), and help (BAPI-952) all skip the registry entirely. A dry
|
|
183
|
+
// run's documented contract is "no network" — a version-freshness lookup is
|
|
184
|
+
// still a network call, so it cannot run first even though it is fail-open
|
|
185
|
+
// and unauthenticated.
|
|
186
|
+
//
|
|
187
|
+
// HELP IS NOT AN INSTALL (BAPI-952). `-h`/`--help` short-circuits inside
|
|
188
|
+
// `runInstallBridgeCli` (`install-bridge.ts`, `parsed.status === "help"`)
|
|
189
|
+
// having initialized nothing, so a registry lookup on that path buys nothing
|
|
190
|
+
// and costs two live resources: an undici socket from `fetchLatestVersion`'s
|
|
191
|
+
// `fetch` and the timer behind its `AbortSignal.timeout`. `dispatchCliSubcommand`
|
|
192
|
+
// returns straight into `process.exit(cliExitCode)` in `index.ts`, so both were
|
|
193
|
+
// still open at teardown. On `windows-latest` that killed the process with
|
|
194
|
+
// `3221226505` (`0xC0000409`, STATUS_STACK_BUFFER_OVERRUN) AFTER the complete,
|
|
195
|
+
// correct help text had already been printed — the reported symptom exactly.
|
|
196
|
+
// It also made `install --help` need the network to answer, which no help
|
|
197
|
+
// screen should.
|
|
198
|
+
//
|
|
199
|
+
// The predicate MUST stay identical to `parseInstallBridgeArgs`'s own help
|
|
200
|
+
// detection (`argv.includes("-h") || argv.includes("--help")`). If this test
|
|
201
|
+
// were narrower the two would disagree and a genuine install could skip the
|
|
202
|
+
// freshness check; if it were wider, help would still pay for the lookup.
|
|
203
|
+
const isHelpInvocation = cleanedArgs.includes("-h") || cleanedArgs.includes("--help");
|
|
204
|
+
if (sentinelPresent ||
|
|
205
|
+
cleanedArgs[0] === "conductor" ||
|
|
206
|
+
cleanedArgs.includes("--dry-run") ||
|
|
207
|
+
isHelpInvocation) {
|
|
172
208
|
return runLocal(cleanedArgs);
|
|
173
209
|
}
|
|
174
210
|
// Default the comparison target to the local version: an unusable registry
|
|
@@ -186,7 +222,7 @@ export async function runInstallBridgeWithLatestCli(argv, deps = {}) {
|
|
|
186
222
|
if (!isNewerVersion(localVersion, latestVersion)) {
|
|
187
223
|
return runLocal(cleanedArgs);
|
|
188
224
|
}
|
|
189
|
-
const { forwardedArgs, secretEnv } = prepareInstallReexecArguments(cleanedArgs);
|
|
225
|
+
const { forwardedArgs, secretEnv, provenanceEnv } = prepareInstallReexecArguments(cleanedArgs);
|
|
190
226
|
const npxCmd = platform === "win32" ? "npx.cmd" : "npx";
|
|
191
227
|
const childArgs = [
|
|
192
228
|
"-y",
|
|
@@ -214,7 +250,9 @@ export async function runInstallBridgeWithLatestCli(argv, deps = {}) {
|
|
|
214
250
|
stdio: "inherit",
|
|
215
251
|
cwd,
|
|
216
252
|
// Explicit CLI values win over an inherited value of the same key.
|
|
217
|
-
|
|
253
|
+
// The provenance label rides alongside so the child can attribute a 401
|
|
254
|
+
// to the flag the operator actually used (BAPI-931).
|
|
255
|
+
env: { ...env, ...secretEnv, ...provenanceEnv },
|
|
218
256
|
});
|
|
219
257
|
}
|
|
220
258
|
catch {
|
|
@@ -893,7 +893,7 @@ export const INSTRUCTIONS = {
|
|
|
893
893
|
"learn-style-correctness.md": "## Objective\n\nExplore the codebase to identify correctness standards for style files, then draft the corresponding correctness standards document.\n\n## Target Type\n\n- **Type**: `style_correctness`\n- **Field name**: `style_correctness_standards`\n- **Scope**: Style files: CSS, SCSS, SASS, LESS, Styled Components, Tailwind configs.\n\n## Instructions\n\n### Phase 1 — Explore Correctness Patterns\n\nFocus on implementation correctness: how to write code that is correct, idiomatic, and robust within this project's conventions.\n\n1. **File Type Detection**: Search by filename pattern for files matching `**/*.css`, `**/*.scss`, `**/*.sass`, `**/*.less` (excluding `node_modules/`). If very few or no files exist, note this and draft minimal instructions.\n\n2. **Convention Analysis**: Read 3-5 representative style files to identify:\n - Structure patterns (imports, exports, class structure, function ordering)\n - Naming conventions (variables, functions, classes, files)\n - Framework conventions and idioms\n - Best practices followed\n - Issues and inconsistencies\n\n### Phase 2 — Draft\n\nDraft correctness standards as clear, actionable instructions for an AI code generation agent. Cover:\n- Code structure and organization requirements\n- Naming conventions to follow\n- Framework-specific patterns and idioms\n- Security requirements relevant to this code type\n- Performance considerations\n- Common mistakes to avoid\n- Guards against common AI weaknesses: duplicative code, verbose implementations, security vulnerabilities\n\nWrite the draft to `{docs_dir}/standards/style_correctness_standards.md`.\n\n## Length Budget\n\nThe platform ceiling for a configuration field is **40,000 characters**, and the server enforces it\nat upload time: a draft over the limit is rejected outright and ``style_correctness_standards`` is simply not learned. Bound\nthe draft here, at write time, rather than discovering the limit through a failed upload.\n\n1. When the draft is complete, measure its length in **characters** — not tokens, not words, not\n bytes.\n2. If it exceeds 40,000 characters, condense it: remove redundancy, collapse repetitive passages, and\n shorten or drop verbose code examples.\n3. Keep every required section listed above. Never meet the budget by deleting a required section,\n and never truncate the draft mid-sentence.\n4. Re-measure after condensing and repeat until the draft is at most 40,000 characters.\n\n## Return\n\nReturn a brief summary of what was learned about style-file correctness conventions (structure, naming, methodology), citing the key files inspected, and confirm the draft was written to `{docs_dir}/standards/style_correctness_standards.md`.\n\nAlso report the draft's final length in characters, the applicable maximum (40,000 characters),\nwhether the draft was condensed to meet that maximum (`condensed`: true/false), and — when it was\ncondensed — the reason it needed condensing.\n",
|
|
894
894
|
"learn-template-correctness.md": "## Objective\n\nExplore the codebase to identify correctness standards for template files, then draft the corresponding correctness standards document.\n\n## Target Type\n\n- **Type**: `template_correctness`\n- **Field name**: `template_correctness_standards`\n- **Scope**: Template files: HTML, Jinja2, Handlebars, EJS, ERB, Blade, Pug, Twig.\n\n## Instructions\n\n### Phase 1 — Explore Correctness Patterns\n\nFocus on implementation correctness: how to write code that is correct, idiomatic, and robust within this project's conventions.\n\n1. **File Type Detection**: Search by filename pattern for files matching `**/*.html`, `**/*.jinja2`, `**/*.j2` in `templates/` and similar directories (excluding `node_modules/`). If very few or no files exist, note this and draft minimal instructions.\n\n2. **Convention Analysis**: Read 3-5 representative template files to identify:\n - Structure patterns (imports, exports, class structure, function ordering)\n - Naming conventions (variables, functions, classes, files)\n - Framework conventions and idioms\n - Best practices followed\n - Issues and inconsistencies\n\n### Phase 2 — Draft\n\nDraft correctness standards as clear, actionable instructions for an AI code generation agent. Cover:\n- Code structure and organization requirements\n- Naming conventions to follow\n- Framework-specific patterns and idioms\n- Security requirements relevant to this code type\n- Performance considerations\n- Common mistakes to avoid\n- Guards against common AI weaknesses: duplicative code, verbose implementations, security vulnerabilities\n\nWrite the draft to `{docs_dir}/standards/template_correctness_standards.md`.\n\n## Length Budget\n\nThe platform ceiling for a configuration field is **40,000 characters**, and the server enforces it\nat upload time: a draft over the limit is rejected outright and ``template_correctness_standards`` is simply not learned. Bound\nthe draft here, at write time, rather than discovering the limit through a failed upload.\n\n1. When the draft is complete, measure its length in **characters** — not tokens, not words, not\n bytes.\n2. If it exceeds 40,000 characters, condense it: remove redundancy, collapse repetitive passages, and\n shorten or drop verbose code examples.\n3. Keep every required section listed above. Never meet the budget by deleting a required section,\n and never truncate the draft mid-sentence.\n4. Re-measure after condensing and repeat until the draft is at most 40,000 characters.\n\n## Return\n\nReturn a brief summary of what was learned about template-file correctness conventions (structure, naming, framework idioms), citing the key files inspected, and confirm the draft was written to `{docs_dir}/standards/template_correctness_standards.md`.\n\nAlso report the draft's final length in characters, the applicable maximum (40,000 characters),\nwhether the draft was condensed to meet that maximum (`condensed`: true/false), and — when it was\ncondensed — the reason it needed condensing.\n",
|
|
895
895
|
"learn-unit-testing.md": "## Objective\n\nExplore the codebase to identify the test runner, assertion library, mocking framework, and testing patterns, then draft `unit_testing_instructions` for the project config.\n\n## Instructions\n\n### Phase 1 — Explore Testing Infrastructure\n\n1. **Test Runner and Framework Detection**: Search for test runner configs (`pytest.ini`, `pyproject.toml` `[tool.pytest]` section, `jest.config.*`) and read `package.json` test scripts. Read the `tests/` directory structure.\n\n2. **Testing Patterns**: Read 3-5 representative test files in `tests/pytest/` to identify:\n - Assertion library and style (`assert`, `expect`, custom matchers)\n - Mocking framework (`unittest.mock`, `jest.mock`, `sinon`, etc.)\n - Fixture patterns (setup/teardown)\n - Test organization (by module, feature, layer)\n - Exemplary tests vs. weak tests\n\n3. **How to Run Tests**: Read `pyproject.toml`, `package.json`, and `Makefile` (if present) to determine exact commands for: full suite, single file, by name pattern, with verbose output.\n\n4. **Mocking vs. Fidelity**: Read test helper files in `tests/pytest/helpers/` to document how external APIs are mocked, whether integration tests exist alongside unit tests, and patterns for avoiding third-party calls in tests.\n\n### Phase 2 — Draft\n\nIf there is nothing repository-specific to cite, say that no repository evidence was found and write a short conservative standard; do not invent files, commands, or conventions.\n\nDraft `unit_testing_instructions` as clear, actionable instructions for an AI agent writing unit tests. Cover:\n- How to run tests (exact commands)\n- Which test framework and assertion library to use\n- How to mock external dependencies without calling third parties\n- How to structure test files and test functions\n- What constitutes a thorough test (not just happy path)\n- How to avoid shallow tests that pass but don't verify meaningful behavior\n- Guards against common AI weaknesses: tests that mock the thing being tested, trivially passing assertions, overly complex setup\n\nWrite the draft to `{docs_dir}/standards/unit_testing_instructions.md`.\n\n## Length Budget\n\nThe platform ceiling for a configuration field is **40,000 characters**, and the server enforces it\nat upload time: a draft over the limit is rejected outright and ``unit_testing_instructions`` is simply not learned. Bound\nthe draft here, at write time, rather than discovering the limit through a failed upload.\n\n1. When the draft is complete, measure its length in **characters** — not tokens, not words, not\n bytes.\n2. If it exceeds 40,000 characters, condense it: remove redundancy, collapse repetitive passages, and\n shorten or drop verbose code examples.\n3. Keep every required section listed above. Never meet the budget by deleting a required section,\n and never truncate the draft mid-sentence.\n4. Re-measure after condensing and repeat until the draft is at most 40,000 characters.\n\n## Return\n\nReturn a brief summary of what was learned about the project's unit testing setup (test runner, assertion library, mocking framework, run commands), citing the key files inspected, and confirm the draft was written to `{docs_dir}/standards/unit_testing_instructions.md`.\n\nAlso report the draft's final length in characters, the applicable maximum (40,000 characters),\nwhether the draft was condensed to meet that maximum (`condensed`: true/false), and — when it was\ncondensed — the reason it needed condensing.\n",
|
|
896
|
-
"monitor-ci-checks.md": "Monitor CI checks for the most recent commit. The behavior is dispatched on the repo-specific `ci_followup_config` JSON value: `poll_only`, `fix_and_iterate`, or `custom`. Read this entire file once before doing anything, then follow only the matching branch.\n\n\n**Required-check source**: Both the `poll_only` (Step 5) and `fix_and_iterate` (Step 6) branches gate progression on the *required* check subset, not the aggregate `all_passed` flag. Each check returned by `resolve_ci_checks`/`poll_ci_checks` carries a `required` field (from GitHub Branch Protection, or an LLM classification fallback) — treat `required: false` as non-required (e.g. `pip-audit`) and a missing field or `required: true` as required. This is the tool-provided proxy for the Conductor done-gate's authoritative required-checks set (`mcp_server/src/conductor/done-gate.ts`); do not re-derive required/non-required status in prose.\n\n## Entry state and ownership boundary\n\n**Re-resolve the git context before anything else.** Run `git branch --show-current`\nand `git rev-parse HEAD` when this step begins. In the implement pipeline the pull\nrequest is opened *before* the bounded post-finalization verification phase, and\nthat phase may have pushed a correction on top of the commit the PR was originally\nopened at. The head you monitor must be the branch's current pushed head, not a SHA\ncarried over from PR creation.\n\n**Ownership is split, and the split matters.** The bounded verification phase\n(`verify-plan.md`) owns findings produced by its own local, touched-area commands.\nThis step owns everything subsequently reported by the authoritative `ci` and\n`code_review` gates — failing required checks and requested review changes. Do not\nre-adjudicate the other phase's findings, and do not assume a finding it reported\nhas been fixed unless a pushed commit shows it.\n\n**Do not substitute a broad local run for the authoritative checks.** Running the\nfull local suite here does not establish that CI passed; it duplicates the work the\n`conductor-ci` gate already performs on the pull request, and it is exactly the\nbudget sink this protocol was reordered to avoid. Use the structured CI failure\ndetail from `poll_ci_checks` to target a fix, and scope any local reproduction to\nthe failing area.\n\n**Keep every correction on the existing pull request branch**, and commit *and push*\neach correction before polling resumes. Polling always restarts against the new\npushed head — an unpushed correction is invisible to CI, to review, and to the\nreconciler.\n\nThroughout, report the check and review states you **observed**. Observing that a\ncheck is green is not the same as issuing a verdict: the done-gate evaluation is\nserver-side and the reconciler decides. This step never emits a control signal of\nits own.\n\n## Step 3 — Parse `ci_followup_config`\n\nLook at the response from the immediately preceding `config_field` call (the pipeline step that ran right before this one). The response envelope's `value` field is itself a JSON string and must be parsed again with `JSON.parse` (i.e., the `value` is double-encoded — the outer envelope is JSON, and the inner `value` is a JSON-encoded string of the actual config object).\n\nIf ANY of the following hold, log a warning and use the defaults `{\"strategy\":\"poll_only\",\"max_iterations\":1,\"max_minutes\":10}`:\n\n- The `config_field` response is missing or unavailable (e.g., the step warned-and-continued).\n- The response `value` is `null`.\n- Parsing `value` with `JSON.parse` fails (the persisted text is not valid JSON).\n- The parsed result is not a JSON object.\n- One or more of the required keys (`strategy`, `max_iterations`, `max_minutes`, `instructions`) is missing.\n- `strategy` is not one of `poll_only`, `fix_and_iterate`, or `custom`.\n\n## Step 4 — Dispatch on `strategy`\n\nRead this whole file once and then follow only the matching branch:\n\n- `poll_only` → follow Step 5.\n- `fix_and_iterate` → follow Step 6.\n- `custom` → follow Step 7.\n\nIf `strategy` is unrecognized, log a warning and fall through to Step 5 (`poll_only`).\n\n## Step 5 — `poll_only`\n\nPreserve the baseline polling behavior. The configured `max_minutes` is IGNORED in this branch — `poll_only` always uses the existing 10-minute baseline.\n\n1. Run `git rev-parse HEAD` to get the current commit SHA.\n2. Call the `resolve_ci_checks` tool with `commit_ref` set to that SHA. This discovers and classifies the CI checks for the repository, including each check's `required` field.\n3. Poll CI status by calling `poll_ci_checks` with `commit_ref` set to the same SHA. Check the response for `all_complete`, and note each check's own `required`/green status — do not use the aggregate `all_passed` flag to decide pass/fail (see step 6 below).\n4. **Conflicting-head escalation**: if the poll shows zero check-runs for the current head after roughly two minutes of polling, call `gh pr view --json mergeable,mergeStateStatus` for the current branch's pull request. A `mergeable` value of `CONFLICTING` or a `mergeStateStatus` of `DIRTY` means GitHub will not start `pull_request` workflows for that head, so continuing to poll the same SHA is futile — merge the current base branch into your branch, resolve any conflicts, push the result, obtain the new head SHA (`git rev-parse HEAD`), and restart polling from step 1 against that new SHA. When the mergeability response is not `CONFLICTING`/`DIRTY`, zero checks alone is not a CI failure — continue with the ordinary wait behavior below.\n5. If checks are not yet complete, wait 30 seconds and poll again. Repeat until all checks are complete or 10 minutes have elapsed.\n6. Compute `required_green` = every check with `required: true` (or a missing `required` field) is complete and green. If `required_green` is `true`, report success — non-required failures (e.g. `pip-audit` with `required: false`) are reported for visibility only and never flip the Passed/Failed classification.\n7. **Review verdict gating**: if `claude-review` is one of the required checks, its GitHub check reaching a non-pending/\"success\" state means only that the review action *ran* — this is transport completion, not approval. Fetch the PR's comments and confirm the sticky comment contains `claude-review-verdict: approved` on its own line with a `Reviewed-SHA:` line matching the current commit SHA before treating the review as approved; `claude-review-verdict: changes_requested`, a missing verdict, or a stale `Reviewed-SHA:` all mean the review is not yet approved and success is not yet reached. `docs/claude/claude-review-verdict-contract.md` is the authoritative definition of this grammar, of which comment wins when several carry a verdict, and of every fail-closed outcome; consult it rather than re-deriving the rules, and do not restate a different version of them here. A verdict can also be *temporarily* absent because a running review rewrote the sticky comment in place (the vanishing window, contract §7): while a `claude-review` run is still pending, treat absence as transient and keep polling; once it has completed, absence is final and fails closed.\n8. If any required checks fail, report which required checks failed (and any non-required failures for visibility) and include any available annotations or log details from the poll response. Do NOT attempt to fix failures — just report them clearly.\n9. If CI status is unavailable (resolver/poll returns `available: false`), report unavailable status and exit; do not attempt fixes.\n10. If the 10-minute timeout is reached, report timeout and exit.\n\n### Polling Directive\n\nDuring the polling loop, execute `sleep 30` silently. Do NOT output any inline commentary, reasoning, or partial status updates between polls. Only output a status message when:\n- All checks are complete (pass or fail), OR\n- The 10-minute timeout is reached.\n\nThis minimizes context window consumption during long-running CI waits.\n\n## Step 6 — `fix_and_iterate`\n\nThis is a self-contained loop where `iteration` is the number of correction rounds already pushed and `start_time` is captured before the first iteration. `max_minutes` is the TOTAL wall-clock cap across all iterations, not an additional per-iteration budget. The 10-minute per-iteration `poll_ci_checks` cap is INSIDE that total budget.\n\nInitialize:\n\n- `iteration = 0`\n- `start_time = now()`\n\nBefore starting each iteration AND before applying corrections, check the total wall-clock budget. If `now() - start_time >= max_minutes`, warn and exit.\n\nPer iteration:\n\n1. Run `git rev-parse HEAD` to get the current commit SHA. The previous push may have changed it; always read fresh.\n2. Run `git branch --show-current` to get the current branch. Always read fresh.\n3. Call `resolve_ci_checks` with `commit_ref` set to the current SHA (once per new SHA — the server caches per project but the agent should still call it for each new SHA). Each returned check carries a `required` field — this is the tool-provided proxy for the done-gate's authoritative required-checks set.\n4. Poll `poll_ci_checks` with `commit_ref` set to the current SHA. Stop when `all_complete` is true, OR the per-iteration 10-minute timeout is reached, OR the remaining total wall-clock budget is exhausted.\n5. **Conflicting-head escalation**: if the poll shows zero check-runs for the current head after roughly two minutes of polling, call `gh pr view --json mergeable,mergeStateStatus` for the current branch's pull request. A `mergeable` value of `CONFLICTING` or a `mergeStateStatus` of `DIRTY` means GitHub will not start `pull_request` workflows for that head, so continuing to poll the same SHA is futile — merge the current base branch into your branch, resolve any conflicts, push the result, obtain the new head SHA (`git rev-parse HEAD`), and restart polling from step 1 of this per-iteration block against that new SHA. When the mergeability response is not `CONFLICTING`/`DIRTY`, zero checks alone is not a CI failure — continue with the ordinary per-iteration behavior below.\n6. If CI status is unavailable (`available: false`), warn and exit the loop — automated remediation cannot make reliable progress without CI signals.\n7. Compute `required_green` = every check with `required: true` (or a missing `required` field) is complete and green; non-required failures (e.g. `pip-audit` with `required: false`) are reported for visibility but never gate exit condition 1 below. If `claude-review` is a required check, its GitHub check reaching a non-pending/\"success\" state is transport completion only, not approval — fetch the PR's comments and confirm the sticky comment contains `claude-review-verdict: approved` on its own line with a `Reviewed-SHA:` line matching the current commit SHA before treating the review as approved; `claude-review-verdict: changes_requested`, a missing verdict, or a stale `Reviewed-SHA:` mean the review is not yet approved. `docs/claude/claude-review-verdict-contract.md` is the authoritative definition of this grammar, of which comment wins when several carry a verdict, and of every fail-closed outcome; consult it rather than re-deriving the rules, and do not restate a different version of them here. A verdict can also be *temporarily* absent because a running review rewrote the sticky comment in place (the vanishing window, contract §7): while a `claude-review` run is still pending, treat absence as transient and keep polling; once it has completed, absence is final and fails closed.\n8. Apply repo-specific `instructions` ONLY when the `instructions` field is non-empty. If the repo `instructions` reference templated placeholder tokens for the GitHub owner, repo, or PR number — e.g., the literal tokens written as a left brace, the word `owner`/`repo`/`pr`, then a right brace — resolve them from the local git/VCS context. Use `gh pr list --head <branch> --json number` to get the PR number; parse the remote URL (`git config --get remote.origin.url`) for owner/repo. If `instructions` is empty, skip repo-specific signal gathering and use only structured CI failure information.\n\n9. Evaluate exit conditions in this order:\n 1. `required_green` is true AND (if `claude-review` is required) the verdict token confirms approval for the current head AND any repo-specific exit criteria from `instructions` are met → success. If there are no repo-specific exit criteria, `required_green` (plus verdict-token approval when `claude-review` is required) alone satisfies the success condition. Then return.\n 2. `iteration >= max_iterations` → warn and exit (iteration cap reached).\n 3. Total elapsed wall-clock time `>= max_minutes` → warn and exit (total wall-clock cap reached).\n 4. After attempting corrections, `git status --porcelain` is empty → warn and exit (nothing to commit; avoids infinite loop on stuck failures).\n\n10. Apply corrections ONLY for failing **required** checks — skip failures on non-required checks (e.g. `pip-audit` with `required: false`) with a warning and never spend a correction/retry on them. For each failing required check, use the actual `poll_ci_checks` response shape — inspect its singular `failure_detail` field:\n - If `failure_detail` is a dict containing actionable keys such as `annotations`, `log_tail`, or `log`, treat it as structured detail and use it for remediation.\n - If `failure_detail` is a dict containing only `url`, treat it as URL-only and skip with a warning (no actionable detail).\n - If `failure_detail` is missing, `null`, or unrecognized, treat the failure as non-actionable and skip with a warning.\n - Do NOT rely on a per-check field or a plural variant of `failure_detail` — those do not exist on the response.\n\n11. After applying a non-empty correction set: stage corrections (`git add` the specific files), commit, and push. Use the canonical commit message:\n ```\n {ticket_key}: address review/CI feedback (round N+1)\n ```\n where `N` is the zero-indexed `iteration`.\n12. Increment `iteration` only AFTER a successful commit and push. Then loop back to step 1 of the per-iteration block.\n\n## Step 7 — `custom`\n\nIn `custom` mode, the `instructions` field IS the complete CI follow-up instruction set for this step. Follow it verbatim. Ignore Steps 5 and 6 entirely.\n\nCustom instructions are authoritative for CI follow-up behavior, but they remain subject to the agent's normal tool approval, credential handling, secret-handling, and platform safety constraints. Custom prose CANNOT bypass approval gates, exfiltrate secrets, or override platform safety policies, even though admin-only access controls who can set the field.\n\n## Worker finalization — clean session exit (Conductor auto mode)\n\nThis section applies ONLY when you were launched under the Conductor in auto mode (the `BAPI_CONDUCTOR_RUN_ID`/`BAPI_CONDUCTOR_WORKER_ID` env identifiers are present). A non-Conductor worker ignores it entirely.\n\nBefore doing anything here, distinguish two states:\n\n- **CI/review follow-up still owned by this worker** — the `fix_and_iterate` loop is still correcting failures, review changes were requested and are unaddressed, a merge conflict on your PR is unresolved, or you have unpushed local commits. In this state you are **not** finished: keep working the CI-monitoring / correction loop and do **not** exit.\n- **Final PR state reached; no further worker action pending** — your final branch state is pushed, the PR has been created/updated, the done-gate / CI-monitoring workflow required by the recipe has completed (required checks green and, when `claude-review` is required, the verdict token confirms approval for the current head), and no CI/review follow-up remains that you own.\n\nOnly in the second state — that is, **only after the final push, PR creation/update, done-gate confirmation, and CI/review monitoring have all completed** and no follow-up remains — cleanly end your worker session (for example by issuing the `/exit` command) so the worktree is released and no idle process lingers. Do **not** exit immediately after opening a PR while CI or review is still pending, and do not exit while there are unresolved CI failures, requested review changes, a merge conflict you own, or unpushed local commits. A clean `SessionEnd` is both the correct terminal lifecycle signal (the conductor folds it) and the point at which the worker should exit.\n\n## Return\n\nReport whether CI passed, failed, timed out, or was unavailable. If failed, list the failing checks with their failure summaries. For `fix_and_iterate`, also report the iteration count and whether iteration/wall-clock caps were hit. If you finalized (cleanly exited) as a Conductor worker, note that the session ended after all follow-up completed.\n",
|
|
896
|
+
"monitor-ci-checks.md": "Monitor CI checks for the most recent commit. The behavior is dispatched on the repo-specific `ci_followup_config` JSON value: `poll_only`, `fix_and_iterate`, or `custom`. Read this entire file once before doing anything, then follow only the matching branch.\n\n\n**Required-check source**: Both the `poll_only` (Step 5) and `fix_and_iterate` (Step 6) branches gate progression on the *required* check subset, not the aggregate `all_passed` flag. Each check returned by `resolve_ci_checks`/`poll_ci_checks` carries a `required` field (from GitHub Branch Protection, or an LLM classification fallback) — treat `required: false` as non-required (e.g. `pip-audit`) and a missing field or `required: true` as required. This is the tool-provided proxy for the Conductor done-gate's authoritative required-checks set (`mcp_server/src/conductor/done-gate.ts`); do not re-derive required/non-required status in prose.\n\n## Entry state and ownership boundary\n\n**Re-resolve the git context before anything else.** Run `git branch --show-current`\nand `git rev-parse HEAD` when this step begins. In the implement pipeline the pull\nrequest is opened *before* the bounded post-finalization verification phase, and\nthat phase may have pushed a correction on top of the commit the PR was originally\nopened at. The head you monitor must be the branch's current pushed head, not a SHA\ncarried over from PR creation.\n\n**Ownership is split, and the split matters.** The bounded verification phase\n(`verify-plan.md`) owns findings produced by its own local, touched-area commands.\nThis step owns everything subsequently reported by the authoritative `ci` and\n`code_review` gates — failing required checks and requested review changes. Do not\nre-adjudicate the other phase's findings, and do not assume a finding it reported\nhas been fixed unless a pushed commit shows it.\n\n**Do not substitute a broad local run for the authoritative checks.** Running the\nfull local suite here does not establish that CI passed; it duplicates the work the\n`conductor-ci` gate already performs on the pull request, and it is exactly the\nbudget sink this protocol was reordered to avoid. Use the structured CI failure\ndetail from `poll_ci_checks` to target a fix, and scope any local reproduction to\nthe failing area.\n\n**Keep every correction on the existing pull request branch**, and commit *and push*\neach correction before polling resumes. Polling always restarts against the new\npushed head — an unpushed correction is invisible to CI, to review, and to the\nreconciler.\n\nThroughout, report the check and review states you **observed**. Observing that a\ncheck is green is not the same as issuing a verdict: the done-gate evaluation is\nserver-side and the reconciler decides. This step never emits a control signal of\nits own.\n\n## Step 3 — Parse `ci_followup_config`\n\nLook at the response from the immediately preceding `config_field` call (the pipeline step that ran right before this one). The response envelope's `value` field is itself a JSON string and must be parsed again with `JSON.parse` (i.e., the `value` is double-encoded — the outer envelope is JSON, and the inner `value` is a JSON-encoded string of the actual config object).\n\nIf ANY of the following hold, log a warning and use the defaults `{\"strategy\":\"poll_only\",\"max_iterations\":1,\"max_minutes\":10}`:\n\n- The `config_field` response is missing or unavailable (e.g., the step warned-and-continued).\n- The response `value` is `null`.\n- Parsing `value` with `JSON.parse` fails (the persisted text is not valid JSON).\n- The parsed result is not a JSON object.\n- One or more of the required keys (`strategy`, `max_iterations`, `max_minutes`, `instructions`) is missing.\n- `strategy` is not one of `poll_only`, `fix_and_iterate`, or `custom`.\n\n## Step 4 — Dispatch on `strategy`\n\nRead this whole file once and then follow only the matching branch:\n\n- `poll_only` → follow Step 5.\n- `fix_and_iterate` → follow Step 6.\n- `custom` → follow Step 7.\n\nIf `strategy` is unrecognized, log a warning and fall through to Step 5 (`poll_only`).\n\n## Step 5 — `poll_only`\n\nPreserve the baseline polling behavior. The configured `max_minutes` is IGNORED in this branch — `poll_only` always uses the existing 10-minute baseline.\n\n1. Run `git rev-parse HEAD` to get the current commit SHA.\n2. Call the `resolve_ci_checks` tool with `commit_ref` set to that SHA. This discovers and classifies the CI checks for the repository, including each check's `required` field.\n3. Poll CI status by calling `poll_ci_checks` with `commit_ref` set to the same SHA. Check the response for `all_complete`, and note each check's own `required`/green status — do not use the aggregate `all_passed` flag to decide pass/fail (see step 6 below).\n4. **Conflicting-head escalation**: if the poll shows zero check-runs for the current head after roughly two minutes of polling, call `gh pr view --json mergeable,mergeStateStatus` for the current branch's pull request. A `mergeable` value of `CONFLICTING` or a `mergeStateStatus` of `DIRTY` means GitHub will not start `pull_request` workflows for that head, so continuing to poll the same SHA is futile — merge the current base branch into your branch, resolve any conflicts, push the result, obtain the new head SHA (`git rev-parse HEAD`), and restart polling from step 1 against that new SHA. When the mergeability response is not `CONFLICTING`/`DIRTY`, zero checks alone is not a CI failure — continue with the ordinary wait behavior below.\n5. If checks are not yet complete, wait 30 seconds and poll again. Repeat until all checks are complete or 10 minutes have elapsed.\n6. Compute `required_green` = every check with `required: true` (or a missing `required` field) is complete and green. If `required_green` is `true`, report success — non-required failures (e.g. `pip-audit` with `required: false`) are reported for visibility only and never flip the Passed/Failed classification.\n7. **Review verdict gating**: if `claude-review` is one of the required checks, its GitHub check reaching a non-pending/\"success\" state means only that the review action *ran* — this is transport completion, not approval. Fetch the PR's comments and confirm the sticky comment contains `claude-review-verdict: approved` on its own line with a `Reviewed-SHA:` line matching the current commit SHA before treating the review as approved; `claude-review-verdict: changes_requested`, a missing verdict, or a stale `Reviewed-SHA:` all mean the review is not yet approved and success is not yet reached. `docs/claude/claude-review-verdict-contract.md` is the authoritative definition of this grammar, of which comment wins when several carry a verdict, and of every fail-closed outcome; consult it rather than re-deriving the rules, and do not restate a different version of them here. A verdict can also be *temporarily* absent because a running review rewrote the sticky comment in place (the vanishing window, contract §7): while a `claude-review` run is still pending, treat absence as transient and keep polling; once it has completed, absence is final and fails closed.\n7a. **A `claude-review` failure blocked by the preflight is TERMINAL — never retry it.** When `claude-review` fails within about twelve seconds and no verdict comment was ever posted, read the run's `Preflight the review action` step. The review action refuses to run whenever this workflow differs from the repository's default-branch copy, and the preflight names which of the two causes it found:\n - `status=workflow_modified` — the pull request's own commits changed `.github/workflows/claude-review.yml`. Such a pull request must be reviewed and merged by hand; it can never be conductor-merged.\n - `status=base_workflow_stale` — the pull request changed no workflow file and **inherited** a stale copy from its base branch. The stale ref is the **base branch**, not the pull request.\n\n Both aggregate to `reason=review_action_blocked_by_workflow_modification`. In both cases the preflight is a deterministic blob-SHA comparison over unchanged inputs, so **retrying, re-running, or continuing to poll reproduces byte-identical evidence and can never succeed** — stop the retry loop and report it rather than spending the remaining budget. For the stale-base case the only remedy is to reconcile the base branch with the default branch; see `docs/claude/runbooks/claude-review-base-branch-drift.md` for detection, diagnosis, and both remediation paths.\n\n8. If any required checks fail, report which required checks failed (and any non-required failures for visibility) and include any available annotations or log details from the poll response. Do NOT attempt to fix failures — just report them clearly.\n9. If CI status is unavailable (resolver/poll returns `available: false`), report unavailable status and exit; do not attempt fixes.\n10. If the 10-minute timeout is reached, report timeout and exit.\n\n### Polling Directive\n\nDuring the polling loop, execute `sleep 30` silently. Do NOT output any inline commentary, reasoning, or partial status updates between polls. Only output a status message when:\n- All checks are complete (pass or fail), OR\n- The 10-minute timeout is reached.\n\nThis minimizes context window consumption during long-running CI waits.\n\n## Step 6 — `fix_and_iterate`\n\nThis is a self-contained loop where `iteration` is the number of correction rounds already pushed and `start_time` is captured before the first iteration. `max_minutes` is the TOTAL wall-clock cap across all iterations, not an additional per-iteration budget. The 10-minute per-iteration `poll_ci_checks` cap is INSIDE that total budget.\n\nInitialize:\n\n- `iteration = 0`\n- `start_time = now()`\n\nBefore starting each iteration AND before applying corrections, check the total wall-clock budget. If `now() - start_time >= max_minutes`, warn and exit.\n\nPer iteration:\n\n1. Run `git rev-parse HEAD` to get the current commit SHA. The previous push may have changed it; always read fresh.\n2. Run `git branch --show-current` to get the current branch. Always read fresh.\n3. Call `resolve_ci_checks` with `commit_ref` set to the current SHA (once per new SHA — the server caches per project but the agent should still call it for each new SHA). Each returned check carries a `required` field — this is the tool-provided proxy for the done-gate's authoritative required-checks set.\n4. Poll `poll_ci_checks` with `commit_ref` set to the current SHA. Stop when `all_complete` is true, OR the per-iteration 10-minute timeout is reached, OR the remaining total wall-clock budget is exhausted.\n5. **Conflicting-head escalation**: if the poll shows zero check-runs for the current head after roughly two minutes of polling, call `gh pr view --json mergeable,mergeStateStatus` for the current branch's pull request. A `mergeable` value of `CONFLICTING` or a `mergeStateStatus` of `DIRTY` means GitHub will not start `pull_request` workflows for that head, so continuing to poll the same SHA is futile — merge the current base branch into your branch, resolve any conflicts, push the result, obtain the new head SHA (`git rev-parse HEAD`), and restart polling from step 1 of this per-iteration block against that new SHA. When the mergeability response is not `CONFLICTING`/`DIRTY`, zero checks alone is not a CI failure — continue with the ordinary per-iteration behavior below.\n6. If CI status is unavailable (`available: false`), warn and exit the loop — automated remediation cannot make reliable progress without CI signals.\n7. Compute `required_green` = every check with `required: true` (or a missing `required` field) is complete and green; non-required failures (e.g. `pip-audit` with `required: false`) are reported for visibility but never gate exit condition 1 below. If `claude-review` is a required check, its GitHub check reaching a non-pending/\"success\" state is transport completion only, not approval — fetch the PR's comments and confirm the sticky comment contains `claude-review-verdict: approved` on its own line with a `Reviewed-SHA:` line matching the current commit SHA before treating the review as approved; `claude-review-verdict: changes_requested`, a missing verdict, or a stale `Reviewed-SHA:` mean the review is not yet approved. `docs/claude/claude-review-verdict-contract.md` is the authoritative definition of this grammar, of which comment wins when several carry a verdict, and of every fail-closed outcome; consult it rather than re-deriving the rules, and do not restate a different version of them here. A verdict can also be *temporarily* absent because a running review rewrote the sticky comment in place (the vanishing window, contract §7): while a `claude-review` run is still pending, treat absence as transient and keep polling; once it has completed, absence is final and fails closed.\n7a. **A `claude-review` failure blocked by the preflight is TERMINAL — never spend a correction or a retry on it.** When `claude-review` fails within about twelve seconds and no verdict comment was ever posted, read the run's `Preflight the review action` step. The review action refuses to run whenever this workflow differs from the repository's default-branch copy, and the preflight names which of the two causes it found:\n - `status=workflow_modified` — the pull request's own commits changed `.github/workflows/claude-review.yml`. It must be reviewed and merged by hand; it can never be conductor-merged.\n - `status=base_workflow_stale` — the pull request changed no workflow file and **inherited** a stale copy from its base branch. The stale ref is the **base branch**, not the pull request, so no correction you can make on this branch will clear it.\n\n Both aggregate to `reason=review_action_blocked_by_workflow_modification`, and the preflight is a deterministic blob-SHA comparison over unchanged inputs — every iteration reproduces byte-identical evidence. Treat it as non-actionable, exit the loop with that reason reported, and do not consume the remaining iteration or wall-clock budget. Remedy for the stale-base case: reconcile the base branch with the default branch. See `docs/claude/runbooks/claude-review-base-branch-drift.md`.\n\n8. Apply repo-specific `instructions` ONLY when the `instructions` field is non-empty. If the repo `instructions` reference templated placeholder tokens for the GitHub owner, repo, or PR number — e.g., the literal tokens written as a left brace, the word `owner`/`repo`/`pr`, then a right brace — resolve them from the local git/VCS context. Use `gh pr list --head <branch> --json number` to get the PR number; parse the remote URL (`git config --get remote.origin.url`) for owner/repo. If `instructions` is empty, skip repo-specific signal gathering and use only structured CI failure information.\n\n9. Evaluate exit conditions in this order:\n 1. `required_green` is true AND (if `claude-review` is required) the verdict token confirms approval for the current head AND any repo-specific exit criteria from `instructions` are met → success. If there are no repo-specific exit criteria, `required_green` (plus verdict-token approval when `claude-review` is required) alone satisfies the success condition. Then return.\n 2. `iteration >= max_iterations` → warn and exit (iteration cap reached).\n 3. Total elapsed wall-clock time `>= max_minutes` → warn and exit (total wall-clock cap reached).\n 4. After attempting corrections, `git status --porcelain` is empty → warn and exit (nothing to commit; avoids infinite loop on stuck failures).\n\n10. Apply corrections ONLY for failing **required** checks — skip failures on non-required checks (e.g. `pip-audit` with `required: false`) with a warning and never spend a correction/retry on them. For each failing required check, use the actual `poll_ci_checks` response shape — inspect its singular `failure_detail` field:\n - If `failure_detail` is a dict containing actionable keys such as `annotations`, `log_tail`, or `log`, treat it as structured detail and use it for remediation.\n - If `failure_detail` is a dict containing only `url`, treat it as URL-only and skip with a warning (no actionable detail).\n - If `failure_detail` is missing, `null`, or unrecognized, treat the failure as non-actionable and skip with a warning.\n - Do NOT rely on a per-check field or a plural variant of `failure_detail` — those do not exist on the response.\n\n11. After applying a non-empty correction set: stage corrections (`git add` the specific files), commit, and push. Use the canonical commit message:\n ```\n {ticket_key}: address review/CI feedback (round N+1)\n ```\n where `N` is the zero-indexed `iteration`.\n12. Increment `iteration` only AFTER a successful commit and push. Then loop back to step 1 of the per-iteration block.\n\n## Step 7 — `custom`\n\nIn `custom` mode, the `instructions` field IS the complete CI follow-up instruction set for this step. Follow it verbatim. Ignore Steps 5 and 6 entirely.\n\nCustom instructions are authoritative for CI follow-up behavior, but they remain subject to the agent's normal tool approval, credential handling, secret-handling, and platform safety constraints. Custom prose CANNOT bypass approval gates, exfiltrate secrets, or override platform safety policies, even though admin-only access controls who can set the field.\n\n## Worker finalization — clean session exit (Conductor auto mode)\n\nThis section applies ONLY when you were launched under the Conductor in auto mode (the `BAPI_CONDUCTOR_RUN_ID`/`BAPI_CONDUCTOR_WORKER_ID` env identifiers are present). A non-Conductor worker ignores it entirely.\n\nBefore doing anything here, distinguish two states:\n\n- **CI/review follow-up still owned by this worker** — the `fix_and_iterate` loop is still correcting failures, review changes were requested and are unaddressed, a merge conflict on your PR is unresolved, or you have unpushed local commits. In this state you are **not** finished: keep working the CI-monitoring / correction loop and do **not** exit.\n- **Final PR state reached; no further worker action pending** — your final branch state is pushed, the PR has been created/updated, the done-gate / CI-monitoring workflow required by the recipe has completed (required checks green and, when `claude-review` is required, the verdict token confirms approval for the current head), and no CI/review follow-up remains that you own.\n\nOnly in the second state — that is, **only after the final push, PR creation/update, done-gate confirmation, and CI/review monitoring have all completed** and no follow-up remains — cleanly end your worker session (for example by issuing the `/exit` command) so the worktree is released and no idle process lingers. Do **not** exit immediately after opening a PR while CI or review is still pending, and do not exit while there are unresolved CI failures, requested review changes, a merge conflict you own, or unpushed local commits. A clean `SessionEnd` is both the correct terminal lifecycle signal (the conductor folds it) and the point at which the worker should exit.\n\n## Return\n\nReport whether CI passed, failed, timed out, or was unavailable. If failed, list the failing checks with their failure summaries. For `fix_and_iterate`, also report the iteration count and whether iteration/wall-clock caps were hit. If you finalized (cleanly exited) as a Conductor worker, note that the session ended after all follow-up completed.\n",
|
|
897
897
|
"preflight-and-readiness.md": "Initialize the idea-to-ticket run directory and classify the idea's readiness and scope.\n\n## Inputs\n\n- Idea: `{idea}`\n- Slug: `{slug}`\n- Run ID: `{run_id}`\n- Docs directory: `{docs_dir}`\n- Project standards: response from the immediately preceding `get_project_standards` step. If that step returned an error envelope or a 404, treat the project standards as unavailable and proceed; do not halt.\n\n## Instructions\n\n1. Create the run directory:\n ```\n mkdir -p {docs_dir}/idea-to-ticket/{slug}-{run_id}\n ```\n Every artifact produced by this pipeline run lives under this run directory. No Jira mutation may occur in any later step until `run-manifest.json` has been written to this directory.\n\n2. Classify the idea on two independent axes:\n\n **Readiness** (one of):\n - `ready_to_draft` — the idea is concrete enough that a clear ticket draft can be produced.\n - `needs_clarification` — the idea is reasonable but missing key answers; clarifying questions must be raised in `open-questions.md` later.\n - `research_first` — drafting is blocked on external/codebase research; deep or narrow research must come first.\n - `too_vague_to_ticket` — the idea is not actionable yet; do not produce a ticket.\n\n **Scope** (one of):\n - `task` — a single Jira Task (default when ambiguous).\n - `spike` — a single Jira Spike for primarily discovery/research work.\n - `epic_candidate` — the idea decomposes into a Jira Epic plus multiple child tickets.\n\n3. Halt locally if readiness is `too_vague_to_ticket`. Write the manifest anyway (see step 4) so the local artifacts record the halt; then stop without continuing the rest of the pipeline. Do not attempt any Jira mutation.\n\n4. Write `run-manifest.json` to `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`. Required fields:\n - `idea` — the original `{idea}` text.\n - `slug` — `{slug}`.\n - `run_id` — `{run_id}`.\n - `run_dir` — `{docs_dir}/idea-to-ticket/{slug}-{run_id}/`.\n - `readiness` — one of the four readiness values above.\n - `scope` — one of the three scope values above.\n - `project_standards_available` — `true` if `get_project_standards` returned a usable result, `false` otherwise.\n - `idempotency_label` — `bapi-idea-to-ticket-{run_id}` (per-run label; lets downstream steps resume THIS run by label).\n - `stable_label` — `bapi-idea-hash-{idea_hash}` (stable across runs of the same idea; lets the duplicate-detection step catch a PRIOR run of the same idea by label, not just fuzzy text).\n - `created_at` — ISO 8601 timestamp.\n\n5. The manifest is the resumability artifact for the whole run. Do not include secrets or raw credentials. Keep the file under a few KB.\n\n## Return\n\nConfirm the run directory and `run-manifest.json` were created, and report the classified `readiness` and `scope`. If readiness is `too_vague_to_ticket`, also report that the pipeline must stop without Jira mutation.\n",
|
|
898
898
|
"render-ticket-manifest.md": "Render every entry of an approved ticket manifest into a drafted body, one `jira-ticket-writer` invocation per entry, without re-deciding the split.\n\nThis stage sits between **decomposition** (which decided the split once and froze\nit) and **creation** (`upload-epic-hierarchy.md` for an epic, ordinary\n`create_ticket` calls for siblings). It owns exactly one job: turning frozen\nmanifest entries into drafted bodies. It decides nothing.\n\n## Inputs\n\n- **The approved manifest** — the ordered plan the decomposition pass froze.\n On the `idea-to-ticket` path this is `decomposition-plan.json` in the run\n directory, produced by `decompose-epic-candidate.md`. On the `plan-epic` path\n it is the sub-task list in `epic-plan.md`, produced by `decompose-epic.md`. On\n a client-driven path (`/explore-ticket` Stage 9) it is the approved outline\n shown at the gate.\n- **The approval state** for that manifest. Whether approval is an explicit\n affirmative or a pipeline auto-approval variable is the calling surface's own\n rule; this stage only needs to know it resolved.\n- **The research and framing artifacts** each entry's writer invocation needs —\n the same set the standalone drafting path passes.\n\n## Preflight — fail closed, before any body is rendered\n\nCheck all of the following first. Each one is a **hard failure** that stops the\nflow before a single writer invocation and long before any Jira mutation. None\nof them degrades to a partial run:\n\n1. **Approval unresolved.** The manifest's approval gate has not resolved, or\n resolved as declined. Nothing renders and nothing is created.\n2. **Manifest identity changed.** The manifest presented here is not the one that\n was approved — a different entry count, different ordering, renumbered\n entries, or an altered scope boundary. A substituted split never silently\n replaces the approved one.\n3. **An entry past the size ceiling.** An entry whose stated scope runs beyond\n roughly 40 files or ~3000 LOC is over the bound wherever it sits — a\n standalone ticket and an epic child alike. Fail and name the entry; do not\n draft it, do not split it here, and do not shrink its stated scope to fit. An\n `XL` entry below that ceiling is not a violation and needs no supporting\n evidence: `XL` is the preferred shape for work that does not fit in `L`.\n4. **An entry with no writer draft after rendering.** Every entry must have\n produced a body. A missing draft is a failure, not an entry to skip.\n\nFail-open applies **only** to enrichment and context gathering — an optional\nresearch artifact that is missing may degrade the context a body is written\nfrom. Approval, manifest identity, sizing validity, and writer-output\ncompleteness all fail closed.\n\n## Instructions\n\n1. Read the approved manifest in full. Record its entry count, its order, and\n each entry's identity, so the identity check above has something to compare\n against.\n\n2. **Fan out one `jira-ticket-writer` invocation per entry**, in manifest order —\n the epic parent, then each child; or each ordinary sibling. Each invocation\n is bound to exactly one entry and receives, verbatim:\n - the entry's identity, position in the order, scope boundary, **size band**,\n parent relationship, `depends_on`, and `recommended_after`;\n - the acceptance criteria and design material for that entry's slice;\n - the shared research, materials, and framing artifacts;\n - an explicit output path for the drafted body.\n\n State in every prompt that the decomposition is **frozen**: the invocation\n renders its entry and may not add, remove, merge, reorder, renumber, or\n rescope anything, and may not write about sibling entries as though it were\n deciding them.\n\n3. **Do not batch.** One invocation per entry, never one invocation asked to emit\n every body. Two failure modes are both real and this shape avoids both: N\n fully independent decisions overlap, omit dependencies, and contradict the\n parent — which is why the decomposition is frozen upstream rather than\n re-derived per child; and one invocation emitting the whole set produces\n unreliable output as the manifest grows. Separating the decision from the\n rendering is what lets the rendering fan out safely, so a large manifest is\n handled by *more* invocations, never by collapsing back to one.\n\n4. Every entry type gets the same treatment — the same research protocol,\n materials inventory, secret-redaction rules, required sections, and Jira\n description bounds. An epic parent is not a thinner document than a\n standalone ticket, and an epic child is not a thinner document than either.\n\n5. When every entry has a draft, re-run the preflight checks against the\n rendered set and hand off to creation:\n - **Epic** — `upload-epic-hierarchy.md`, unchanged. It creates the parent with\n `issue_type = \"Epic\"`, captures the resolved epic key, then calls\n `create_ticket(parent_key=<epic_key>)` per child in manifest order with\n `track_ticket` after each, and carries idempotency labels throughout so a\n partial failure resumes rather than duplicating. Nothing new is required to\n *create* an epic — only to propose one.\n - **Siblings** — ordinary `create_ticket` calls in manifest order, unparented,\n with no epic parent synthesized.\n\n6. This stage emits **no conductor invocation**. The handoff belongs to the\n calling surface and names exactly one entry point, `drive-epic`.\n\n## Return\n\nReport the manifest's shape (`epic` or `siblings`), the entry count, each entry's\ndrafted body path in manifest order, and the preflight verdict. On a preflight\nfailure, name the violated prerequisite and the offending entry, and confirm that\nno ticket was created.\n",
|
|
899
899
|
"research-decision.md": "Decide which research tools to run for this idea, biased toward cheap local research first.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json` (must already exist from the preflight step).\n\n## Instructions\n\n1. Read `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`. This is the source of truth for `idea`, `readiness`, `scope`, and `run_id`. If the file does not exist, halt locally — the preflight step did not complete.\n\n2. Decide which research tools should run for this idea, in roughly this priority order:\n - **Local codebase research first.** Inspect the working tree (search, grep, file reads) for prior art, related modules, and existing tests. Prefer this for anything that touches code you already own.\n - **Narrow web search second.** Use targeted web search for short factual lookups: a specific library API, a known external standard, a public spec.\n - **Deep research only when justified.** Deep research is expensive and slow; it must be earned by one of the rubric items below.\n\n3. Deep-research allowance rubric. Deep research is only allowed when at least one of these is true:\n - **blast radius**: the change spans many systems or has high reversibility cost (e.g., schema migrations, auth, billing, public APIs).\n - **unfamiliar external domain**: the idea depends on a third-party domain or specification the repository has no prior coverage of.\n - **compliance/security uncertainty**: there is real compliance or security uncertainty (SOC2, PII, secret handling, access control).\n - **cheaper research failed**: a cheaper round (local + narrow web search) already happened in this run and left blocking unknowns.\n - **explicit user request**: the user explicitly asked for deep research.\n\n4. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-plan.json`. Required fields:\n - `selected_tools` — array of tool identifiers to run, drawn from at least `[\"codebase_search\", \"web_search\", \"deep_research\"]`. Empty array is allowed when no research is needed.\n - `rationale` — short string explaining the choice in terms of the rubric above.\n - `deep_research_query` — string. Required when `deep_research` is in `selected_tools`, otherwise empty string.\n - `web_search_topics` — array of strings; may be empty.\n - `codebase_search_topics` — array of strings; may be empty.\n - `expected_unknowns` — array of strings describing what the research is expected to resolve.\n\n5. Do not invoke any research tool from this step — that happens in `execute-research.md`. This step only writes the plan.\n\n## Return\n\nConfirm `research-plan.json` was written, list `selected_tools`, and quote the rationale.\n",
|
package/build/plane/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ import { PLANE_RUNTIME_ENTRYPOINT_REFUSAL } from "./build-freshness.js";
|
|
|
17
17
|
import { relativeLogPathFor } from "./manifest.js";
|
|
18
18
|
import { claimPlaneManifest } from "./manifest.js";
|
|
19
19
|
import { runPlanePreflight } from "./preflight.js";
|
|
20
|
-
import { buildPlaneMemberRoster } from "./member-roster.js";
|
|
20
|
+
import { buildPlaneMemberRoster, resolvePlaneServerEndpoint } from "./member-roster.js";
|
|
21
21
|
import { getPlaneStatus, formatPlaneStatus } from "./status.js";
|
|
22
22
|
import { shutdownPlane, formatPlaneShutdown } from "./shutdown.js";
|
|
23
23
|
import { launchPlaneSupervisor, runPlaneRuntime, } from "./supervisor.js";
|
|
@@ -454,7 +454,7 @@ function printStartupBanner(sinks, context, memberCount, executors, launch) {
|
|
|
454
454
|
sinks.stdout("");
|
|
455
455
|
sinks.stdout(`Plane up — ${memberCount} members (${executors} executor lane(s))`);
|
|
456
456
|
sinks.stdout(` repository ${context.repoName} (${context.repoRoot})`);
|
|
457
|
-
sinks.stdout(` server ${context.baseUrl} (no --reload)`);
|
|
457
|
+
sinks.stdout(` server ${context.endpoint.baseUrl} (no --reload)`);
|
|
458
458
|
sinks.stdout(` supervisor pid ${launch.supervisorPid}, process group ${launch.supervisorPgid}`);
|
|
459
459
|
sinks.stdout(` logs ${PLANE_RUNTIME_DIR}/`);
|
|
460
460
|
sinks.stdout(" crash policy members are NOT restarted; a member exit is reported loudly.");
|
|
@@ -525,9 +525,19 @@ async function runRuntimeAction(executors, overrides) {
|
|
|
525
525
|
});
|
|
526
526
|
return result.exitCode;
|
|
527
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Bind preflight's dependencies to the real platform for one environment.
|
|
530
|
+
*
|
|
531
|
+
* This is the ONLY production path that reads the plane's server-port override
|
|
532
|
+
* (BAPI-950). Both `plane up` and the private `plane __runtime` action go
|
|
533
|
+
* through here, so the launcher and the detached runtime it spawns resolve the
|
|
534
|
+
* endpoint by exactly the same rule — a second lookup elsewhere is what would
|
|
535
|
+
* let the two disagree about which port the plane is on.
|
|
536
|
+
*/
|
|
528
537
|
function buildPreflightDeps(env) {
|
|
529
538
|
return {
|
|
530
539
|
env,
|
|
540
|
+
endpoint: resolvePlaneServerEndpoint(env),
|
|
531
541
|
platform: process.platform,
|
|
532
542
|
homedir: resolveHomedir,
|
|
533
543
|
fs: createPlaneFsDeps(),
|
package/build/plane/manifest.js
CHANGED
|
@@ -259,14 +259,38 @@ export function manifestHasLiveProcess(manifest, proc) {
|
|
|
259
259
|
export function formatPlaneManifest(manifest) {
|
|
260
260
|
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
261
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Monotonic per-process discriminator for temporary manifest filenames.
|
|
264
|
+
*
|
|
265
|
+
* Module-local and used for nothing else. It never appears in the published
|
|
266
|
+
* manifest, in a diagnostic, or in any path that outlives the rename below.
|
|
267
|
+
*/
|
|
268
|
+
let manifestWriteCounter = 0;
|
|
262
269
|
/**
|
|
263
270
|
* Persist a state transition through a same-directory temporary file and an
|
|
264
271
|
* atomic rename, so a crash mid-write can never leave a half-written manifest
|
|
265
272
|
* that a subsequent `plane down` would read as a kill list.
|
|
273
|
+
*
|
|
274
|
+
* The temporary name is unique per WRITE, not per plane (BAPI-950). It used to
|
|
275
|
+
* be keyed only by `planeId`, which every writer of the same plane shares — so
|
|
276
|
+
* two overlapping writes wrote the one temp file simultaneously and the rename
|
|
277
|
+
* published their interleaved bytes as `plane.json`. `readPlaneManifest` then
|
|
278
|
+
* reported "manifest is not valid JSON" and `plane down` refused to remove a
|
|
279
|
+
* manifest it could not validate, leaving the plane un-windable.
|
|
280
|
+
*
|
|
281
|
+
* The runtime serializes its own writes (see `runPlaneRuntime`), which is the
|
|
282
|
+
* primary fix; this is defense in depth for INDEPENDENT writers — the launcher,
|
|
283
|
+
* the claim path, an epic-run binding — that no single in-process queue can
|
|
284
|
+
* order. `process.pid` separates processes, the counter separates writes within
|
|
285
|
+
* one process, and `planeId` is retained so a stray temp file is still
|
|
286
|
+
* attributable to the plane that produced it.
|
|
266
287
|
*/
|
|
267
288
|
export async function writePlaneManifest(manifest, fs) {
|
|
268
289
|
const { manifestPath } = getPlanePaths(manifest.repoRoot);
|
|
269
|
-
|
|
290
|
+
manifestWriteCounter += 1;
|
|
291
|
+
// Same directory as `plane.json`, so the rename stays a same-filesystem
|
|
292
|
+
// atomic replacement rather than a copy across a mount boundary.
|
|
293
|
+
const tempPath = `${manifestPath}.${manifest.planeId}.${process.pid}.${manifestWriteCounter}.tmp`;
|
|
270
294
|
await fs.writeFile(tempPath, formatPlaneManifest(manifest));
|
|
271
295
|
await fs.rename(tempPath, manifestPath);
|
|
272
296
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* member name, log path, or formatted description is built from a secret.
|
|
18
18
|
*/
|
|
19
19
|
import path from "path";
|
|
20
|
-
import { PLANE_SERVER_HOST, PLANE_SERVER_PORT, } from "./types.js";
|
|
20
|
+
import { PLANE_SERVER_HOST, PLANE_SERVER_PORT, PLANE_SERVER_PORT_ENV_VAR, } from "./types.js";
|
|
21
21
|
import { relativeLogPathFor } from "./manifest.js";
|
|
22
22
|
/** How long a member with a readiness probe gets to start listening. */
|
|
23
23
|
export const PLANE_READINESS_TIMEOUT_MS = 60_000;
|
|
@@ -35,6 +35,55 @@ export function resolveUvicornExecutable(env) {
|
|
|
35
35
|
? configured.trim()
|
|
36
36
|
: "uvicorn";
|
|
37
37
|
}
|
|
38
|
+
/** Highest port number a TCP endpoint can occupy. */
|
|
39
|
+
const MAX_TCP_PORT = 65_535;
|
|
40
|
+
/**
|
|
41
|
+
* The only diagnostic an invalid override ever produces.
|
|
42
|
+
*
|
|
43
|
+
* Fixed prose, built from the variable's own name and the accepted range and
|
|
44
|
+
* nothing else. The supplied value is deliberately NOT echoed: an operator who
|
|
45
|
+
* pasted a secret into the wrong shell export would otherwise have it printed
|
|
46
|
+
* to the terminal and copied into the preflight trace on disk.
|
|
47
|
+
*/
|
|
48
|
+
export const PLANE_SERVER_PORT_INVALID_MESSAGE = `${PLANE_SERVER_PORT_ENV_VAR} must be a whole TCP port number in the range 1..${MAX_TCP_PORT}. ` +
|
|
49
|
+
"Unset it to use the default port " +
|
|
50
|
+
`${PLANE_SERVER_PORT}, or export a valid port and retry.`;
|
|
51
|
+
/** Build an endpoint from a port, deriving the base URL from the same fields. */
|
|
52
|
+
function endpointForPort(port) {
|
|
53
|
+
return { host: PLANE_SERVER_HOST, port, baseUrl: `http://${PLANE_SERVER_HOST}:${port}` };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the local server endpoint from the environment, exactly once.
|
|
57
|
+
*
|
|
58
|
+
* Absent or blank leaves the historical contract untouched: the default is
|
|
59
|
+
* `127.0.0.1:8000` and `http://127.0.0.1:8000`, identical to the constants that
|
|
60
|
+
* were previously read directly.
|
|
61
|
+
*
|
|
62
|
+
* Anything present is validated STRICTLY and, when invalid, *refused* rather
|
|
63
|
+
* than quietly falling back to 8000. A silent fallback is the failure mode this
|
|
64
|
+
* override exists to remove: an operator who set the variable precisely because
|
|
65
|
+
* 8000 was occupied would watch the plane bind 8000 anyway and see a port
|
|
66
|
+
* collision they had already worked around.
|
|
67
|
+
*
|
|
68
|
+
* The host is never read from the environment — see {@link PLANE_SERVER_HOST}.
|
|
69
|
+
*/
|
|
70
|
+
export function resolvePlaneServerEndpoint(env) {
|
|
71
|
+
const configured = env[PLANE_SERVER_PORT_ENV_VAR];
|
|
72
|
+
if (typeof configured !== "string" || configured.trim().length === 0) {
|
|
73
|
+
return { ok: true, endpoint: endpointForPort(PLANE_SERVER_PORT) };
|
|
74
|
+
}
|
|
75
|
+
const raw = configured.trim();
|
|
76
|
+
// Digits only, deliberately: `Number()` accepts "0x1f4", "1e3", " 8000 ", and
|
|
77
|
+
// "+8000", and `parseInt` accepts "8000x" by stopping at the first non-digit.
|
|
78
|
+
// Either would let a typo resolve to a port the operator never wrote.
|
|
79
|
+
if (!/^[0-9]+$/.test(raw))
|
|
80
|
+
return { ok: false, message: PLANE_SERVER_PORT_INVALID_MESSAGE };
|
|
81
|
+
const port = Number(raw);
|
|
82
|
+
if (!Number.isInteger(port) || port < 1 || port > MAX_TCP_PORT) {
|
|
83
|
+
return { ok: false, message: PLANE_SERVER_PORT_INVALID_MESSAGE };
|
|
84
|
+
}
|
|
85
|
+
return { ok: true, endpoint: endpointForPort(port) };
|
|
86
|
+
}
|
|
38
87
|
/**
|
|
39
88
|
* Build a child environment: a copy of the parent plus the resolved values the
|
|
40
89
|
* existing processes already expect.
|
|
@@ -51,7 +100,7 @@ export function buildPlaneChildEnv(parentEnv, context) {
|
|
|
51
100
|
env[key] = value;
|
|
52
101
|
}
|
|
53
102
|
env.BAPI_REPO_NAME = context.repoName;
|
|
54
|
-
env.BAPI_BASE_URL = context.baseUrl;
|
|
103
|
+
env.BAPI_BASE_URL = context.endpoint.baseUrl;
|
|
55
104
|
env.BAPI_API_KEY = context.bridgeApiKey;
|
|
56
105
|
delete env.CONDUCTOR_DEAD_MAN_ONLY;
|
|
57
106
|
return env;
|
|
@@ -64,6 +113,11 @@ export function buildPlaneMemberRoster(params) {
|
|
|
64
113
|
const { context, executors, parentEnv, nodeExecutable } = params;
|
|
65
114
|
const env = buildPlaneChildEnv(parentEnv, context);
|
|
66
115
|
const cwd = context.repoRoot;
|
|
116
|
+
// One resolved endpoint for the bind argv, the readiness probe, every
|
|
117
|
+
// executor's `--base-url`, and the child `BAPI_BASE_URL`. Reading the default
|
|
118
|
+
// constants here instead is what would let uvicorn bind one port while the
|
|
119
|
+
// probe and the executors addressed another.
|
|
120
|
+
const endpoint = context.endpoint;
|
|
67
121
|
const server = {
|
|
68
122
|
name: "server",
|
|
69
123
|
command: resolveUvicornExecutable(parentEnv),
|
|
@@ -73,16 +127,16 @@ export function buildPlaneMemberRoster(params) {
|
|
|
73
127
|
args: [
|
|
74
128
|
"main:app",
|
|
75
129
|
"--host",
|
|
76
|
-
|
|
130
|
+
endpoint.host,
|
|
77
131
|
"--port",
|
|
78
|
-
String(
|
|
132
|
+
String(endpoint.port),
|
|
79
133
|
],
|
|
80
134
|
cwd,
|
|
81
135
|
env,
|
|
82
136
|
logPath: relativeLogPathFor("server"),
|
|
83
137
|
readiness: {
|
|
84
|
-
host:
|
|
85
|
-
port:
|
|
138
|
+
host: endpoint.host,
|
|
139
|
+
port: endpoint.port,
|
|
86
140
|
timeoutMs: PLANE_READINESS_TIMEOUT_MS,
|
|
87
141
|
},
|
|
88
142
|
};
|
|
@@ -111,7 +165,7 @@ export function buildPlaneMemberRoster(params) {
|
|
|
111
165
|
"--repo",
|
|
112
166
|
context.repoName,
|
|
113
167
|
"--base-url",
|
|
114
|
-
|
|
168
|
+
endpoint.baseUrl,
|
|
115
169
|
"--executor-id",
|
|
116
170
|
buildExecutorId(context.repoName, lane),
|
|
117
171
|
],
|