@bridge_gpt/mcp-server 0.2.53 → 0.2.55
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 +116 -56
- package/build/agent-guidance.generated.js +3 -0
- package/build/agent-launchers/claude-executor-adapter.js +3 -0
- package/build/agent-launchers/claude.js +3 -3
- package/build/agent-launchers/prompt.js +8 -11
- package/build/agent-notes.js +178 -0
- package/build/agent-registry.js +5 -2
- package/build/agent-utils.js +58 -0
- package/build/agents.generated.js +1 -1
- package/build/base-ref.js +33 -9
- package/build/bounded-wait.js +174 -0
- package/build/codex-skill-adapter.js +55 -0
- package/build/commands.generated.js +6 -5
- package/build/conductor/bridge-api-client.js +235 -14
- package/build/conductor/bring-up-facts.js +187 -0
- package/build/conductor/claude-hook.js +7 -5
- package/build/conductor/cli.js +28 -0
- package/build/conductor/doctor.js +80 -9
- package/build/conductor/epic-implementer-cli.js +1298 -0
- package/build/conductor/epic-runtime.js +134 -98
- package/build/conductor/errors.js +2 -2
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-hooks.js +28 -14
- package/build/conductor/install-doctor.js +11 -5
- package/build/conductor/readiness-cli.js +10 -10
- package/build/conductor/readiness-sections.js +58 -9
- package/build/conductor/readiness.js +120 -4
- package/build/conductor/recovery-cli.js +289 -10
- package/build/conductor/recovery-operations.js +125 -2
- package/build/conductor/repair-contract.js +58 -0
- package/build/conductor/run-branch.js +137 -0
- package/build/conductor/store.js +2 -2
- package/build/conductor/supervisor-runtime.js +1 -1
- package/build/conductor/test-run-branch-vectors.js +165 -0
- package/build/conductor-bin.js +2 -139
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
- package/build/conductor-removed-stub-bin.js +30 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +145 -18
- package/build/drive-epic.js +752 -90
- package/build/epic-implementer-bin.js +145 -0
- package/build/epic-implementer-bundle-cli.js +264 -0
- package/build/epic-implementer-claude-hook-bin.js +3 -0
- package/build/epic-integration-pr.js +5 -3
- package/build/executor/claim-scope.js +104 -0
- package/build/executor/cli.js +14 -25
- package/build/executor/env-file-guard.js +82 -3
- package/build/executor/env.js +6 -0
- package/build/executor/interrupted-worktree.js +60 -0
- package/build/executor/job-errors.js +45 -0
- package/build/executor/job-runner.js +334 -9
- package/build/executor/job-types.js +25 -9
- package/build/executor/merge-tree-classifier.js +171 -0
- package/build/executor/reconcile-overlap-governance.js +129 -0
- package/build/executor/reconcile-overlap-job.js +989 -0
- package/build/executor/reconcile-overlap-types.js +14 -0
- package/build/executor/spawn-job-driver.js +1 -0
- package/build/executor/types.js +2 -0
- package/build/executor/worker-finalization.js +25 -2
- package/build/executor/worker-guard-hook.js +15 -7
- package/build/implement-epic/bridge-client.js +773 -0
- package/build/implement-epic/checkpoint-store.js +542 -0
- package/build/implement-epic/cli.js +3158 -0
- package/build/implement-epic/cut-protocol.js +392 -0
- package/build/implement-epic/lock.js +302 -0
- package/build/implement-epic/pr-state.js +286 -0
- package/build/implement-epic/spawn.js +113 -0
- package/build/index.js +657 -481
- package/build/init.js +72 -8
- package/build/install-bridge-conductor.js +5 -5
- package/build/install-bridge.js +403 -70
- package/build/local-artifact-storage.js +130 -0
- package/build/mcp-host-config.js +22 -60
- package/build/mcp-host-entry-adapter.js +18 -0
- package/build/mcp-host-targets.js +1 -21
- package/build/merge-pull-request.js +1 -1
- package/build/pipelines.generated.js +23 -16
- package/build/plan-epic-conductor-eligibility.js +1 -1
- package/build/plane/cli.js +321 -41
- package/build/plane/manifest.js +209 -1
- package/build/plane/member-roster.js +70 -0
- package/build/plane/preflight.js +128 -12
- package/build/plane/shutdown.js +18 -5
- package/build/plane/status.js +35 -1
- package/build/plane/supervisor.js +546 -164
- package/build/plane/types.js +25 -2
- package/build/polling-policy.js +72 -0
- package/build/readiness-check.js +3 -3
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +219 -0
- package/build/run-unit-tests-launcher.js +6 -1
- package/build/setup-epic.js +567 -38
- package/build/start-tickets-conductor.js +8 -7
- package/build/ticket-key-utils.js +4 -3
- package/build/ticket-review-artifact-gate.js +461 -0
- package/build/upgrade-cli.js +5 -26
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +1 -1
- package/docs/CONDUCTOR.md +8 -6
- package/docs/install/mcp-tool-integrations.md +23 -1
- package/package.json +5 -3
- package/pipelines/review-ticket.json +17 -4
|
@@ -83,9 +83,10 @@ export const DEFAULT_CONDUCTOR_GATE_NAME = "implement-ticket";
|
|
|
83
83
|
* Exported since BAPI-1020 so the executor resolves the worker-guard bin through
|
|
84
84
|
* the SAME resolver rather than growing a second one. Correct in both build
|
|
85
85
|
* shapes because this module compiles to the build ROOT: the esbuild bundle puts
|
|
86
|
-
* `import.meta.url` at `build/index.js` / `build/
|
|
87
|
-
* it at `build/start-tickets-conductor.js`. A resolver living in a
|
|
88
|
-
* module would resolve to `build/<subdir>/<filename>` under `tsc`
|
|
86
|
+
* `import.meta.url` at `build/index.js` / `build/epic-implementer-bin.js`, and
|
|
87
|
+
* `tsc` puts it at `build/start-tickets-conductor.js`. A resolver living in a
|
|
88
|
+
* subdirectory module would resolve to `build/<subdir>/<filename>` under `tsc`
|
|
89
|
+
* and be wrong.
|
|
89
90
|
*/
|
|
90
91
|
export function defaultResolveBinPath(filename) {
|
|
91
92
|
return fileURLToPath(new URL(`./${filename}`, import.meta.url));
|
|
@@ -124,12 +125,12 @@ export async function createStartTicketsConductorContext(options, agent, deps) {
|
|
|
124
125
|
gateName,
|
|
125
126
|
supervisorMode,
|
|
126
127
|
agentName: agent.name,
|
|
127
|
-
cliFile: resolveBinPath("
|
|
128
|
+
cliFile: resolveBinPath("epic-implementer-bin.js"),
|
|
128
129
|
// Capture the conductor process's Node executable so dispatched workers run
|
|
129
|
-
//
|
|
130
|
-
// worker's own Node (BAPI-527).
|
|
130
|
+
// epic-implementer-bin.js under THIS runtime (which owns the ledger), never
|
|
131
|
+
// the worker's own Node (BAPI-527).
|
|
131
132
|
conductorNodePath: deps.execPath ?? process.execPath,
|
|
132
|
-
hookBinPath: resolveBinPath("
|
|
133
|
+
hookBinPath: resolveBinPath("epic-implementer-claude-hook-bin.js"),
|
|
133
134
|
};
|
|
134
135
|
if (options.epic) {
|
|
135
136
|
context.epic = options.epic;
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* | `start-tickets.ts` | `^[A-Z]+-[0-9]+$` | no |
|
|
11
11
|
*
|
|
12
12
|
* A repository configured with `jira_ticket_key = "MY_PROJ"` therefore minted
|
|
13
|
-
* keys the server considered valid and the clients rejected —
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* keys the server considered valid and the clients rejected — the pre-rename
|
|
14
|
+
* `/full-automation` command (renamed to `/idea-to-pr` in BAPI-1058) silently
|
|
15
|
+
* dropped them. Jira project keys may contain underscores too, so this was never
|
|
16
|
+
* a local-mode-only defect.
|
|
16
17
|
*
|
|
17
18
|
* This module mirrors the public Python contract
|
|
18
19
|
* (`api.library.config.field_validators.TICKET_KEY_PREFIX_PATTERN_SOURCE` plus a
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generation-aware persistence gate for ticket review (BAPI-1121, BAPI-1129).
|
|
3
|
+
*
|
|
4
|
+
* WHO CALLS THIS. Both calling conventions, and nothing else:
|
|
5
|
+
* `wait_for_ticket_review` (the non-blocking submit-then-wait pattern the
|
|
6
|
+
* review-ticket recipe uses) and `request_ticket_review` with
|
|
7
|
+
* `wait_for_result: true` (the older blocking convention, routed here by
|
|
8
|
+
* BAPI-1129). This module is therefore the SOLE IMPLEMENTATION of review
|
|
9
|
+
* classification and persistence — not the wait, which is merely one of its
|
|
10
|
+
* two callers. A second copy of the advancement rules in either handler is
|
|
11
|
+
* the defect this arrangement exists to prevent.
|
|
12
|
+
*
|
|
13
|
+
* THE PROBLEM THIS CLOSES. BAPI-1104 made `wait_for_ticket_review` refuse a
|
|
14
|
+
* `200` whose generation had not advanced past the pre-submission baseline —
|
|
15
|
+
* but the review-ticket RECIPE still retrieved and saved BOTH legs
|
|
16
|
+
* unconditionally after a ready wait (`get_clarifying_questions` +
|
|
17
|
+
* `get_ticket_critique`), regardless of what the wait actually proved. A
|
|
18
|
+
* fresh clarify beside a stale, un-advanced critique attachment was still
|
|
19
|
+
* written to the critique's canonical path — the exact stale-attachment
|
|
20
|
+
* failure the generation baseline was built to catch, reintroduced one layer
|
|
21
|
+
* up by an unconditional write.
|
|
22
|
+
*
|
|
23
|
+
* THE FIX. Persistence authority moves INTO the wait. This module classifies
|
|
24
|
+
* the observed combined envelope against the submission baseline — using
|
|
25
|
+
* `parseReviewGeneration` / `generationAdvanced`, never file presence — and
|
|
26
|
+
* writes ONLY the legs proven to be this submission's own output. A leg that
|
|
27
|
+
* is delivered but not proven (stale, or carrying no readable identity) is
|
|
28
|
+
* WITHHELD: never written, and any pre-existing canonical file for it is
|
|
29
|
+
* removed so a later file-presence read cannot resurrect it. The returned
|
|
30
|
+
* manifest (`accepted_legs`, `saved_legs`, `withheld_legs`, `absent_legs`,
|
|
31
|
+
* `freshness_basis`) becomes the authoritative source set for every
|
|
32
|
+
* downstream consumer (see `evaluate-and-recommend.md` /
|
|
33
|
+
* `capture-review-decisions.md`).
|
|
34
|
+
*
|
|
35
|
+
* ACCEPTED VERSUS SAVED (BAPI-1129). `accepted_legs` is the classification
|
|
36
|
+
* verdict — which legs are this review's own output — and `saved_legs` is the
|
|
37
|
+
* durable-write record. They coincide on a persistence-enabled call and
|
|
38
|
+
* deliberately diverge under `persistenceEnabled: false`, the
|
|
39
|
+
* classification-only mode `request_ticket_review`'s `save_locally: false`
|
|
40
|
+
* convention needs: every leg is classified so withheld content is never
|
|
41
|
+
* handed back as current-review evidence, and no file is written, removed,
|
|
42
|
+
* reconciled, or recorded. A consumer choosing which document bodies to
|
|
43
|
+
* present reads `accepted_legs`; one reporting what landed on disk reads
|
|
44
|
+
* `saved_legs`.
|
|
45
|
+
*
|
|
46
|
+
* NO-BASELINE FALLBACK. A resumed session (or an out-of-band submission) has
|
|
47
|
+
* no baseline to prove freshness against. Terminality alone decides, exactly
|
|
48
|
+
* as the two `get_*` tools behaved before this ticket — but the manifest
|
|
49
|
+
* reports `freshness_basis: "terminality_only"` and an empty `advanced_legs`
|
|
50
|
+
* so nothing here is silently upgraded to a proven claim.
|
|
51
|
+
*/
|
|
52
|
+
import path from "path";
|
|
53
|
+
import { createHash } from "node:crypto";
|
|
54
|
+
import { parseReviewGeneration, generationAdvanced } from "./review-generation.js";
|
|
55
|
+
import { writeCanonicalArtifact, removeCanonicalArtifact } from "./local-artifact-storage.js";
|
|
56
|
+
// Documented leg ordering (BAPI-1121): critique before clarify, matching the
|
|
57
|
+
// pre-existing wait_for_ticket_review response's delivered_legs order so this
|
|
58
|
+
// ticket does not silently reorder an established response field.
|
|
59
|
+
const LEG_NAMES = ["critique", "clarify"];
|
|
60
|
+
function legContent(envelope, leg) {
|
|
61
|
+
const raw = envelope[leg]?.content;
|
|
62
|
+
return typeof raw === "string" ? raw : "";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether the resolved submission policy expected `leg`, from
|
|
66
|
+
* `expectedLegCount`. Every valid resolved policy configures at least one
|
|
67
|
+
* clarify round (`_validate_bucket_policy` in `review_ticket_policy.py`
|
|
68
|
+
* rejects a zero-clarify policy), so `expectedLegCount` counting 1 always
|
|
69
|
+
* means "clarify only" and 2 always means "both" — clarify is therefore
|
|
70
|
+
* always expected whenever a count was supplied at all, and critique is
|
|
71
|
+
* expected only when the count is exactly 2.
|
|
72
|
+
*/
|
|
73
|
+
function expectedByPolicy(leg, expectedLegCount) {
|
|
74
|
+
if (expectedLegCount === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
return leg === "clarify" ? expectedLegCount >= 1 : expectedLegCount >= 2;
|
|
77
|
+
}
|
|
78
|
+
/** Same order-of-magnitude lifetime as `ReviewGenerationRegistry`'s baseline TTL. */
|
|
79
|
+
const COMPLETED_RESULT_TTL_MS = 2 * 60 * 60 * 1000;
|
|
80
|
+
const MAX_COMPLETED_RESULT_KEYS = 64;
|
|
81
|
+
const COMPLETED_RESULTS = new Map();
|
|
82
|
+
const KEY_SEPARATOR = "\u0000";
|
|
83
|
+
function completedResultKey(repoName, ticketNumber) {
|
|
84
|
+
return `${repoName.trim().toLowerCase()}${KEY_SEPARATOR}${ticketNumber.trim().toUpperCase()}`;
|
|
85
|
+
}
|
|
86
|
+
function evictExpiredCompletedResults() {
|
|
87
|
+
const cutoff = Date.now() - COMPLETED_RESULT_TTL_MS;
|
|
88
|
+
for (const [key, bySignature] of COMPLETED_RESULTS) {
|
|
89
|
+
for (const [signature, entry] of bySignature) {
|
|
90
|
+
if (entry.recordedAtMs <= cutoff)
|
|
91
|
+
bySignature.delete(signature);
|
|
92
|
+
}
|
|
93
|
+
if (bySignature.size === 0)
|
|
94
|
+
COMPLETED_RESULTS.delete(key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A stable signature for one delivered leg: its readable generation when one
|
|
99
|
+
* exists, or a content digest when it does not. The digest keeps two distinct
|
|
100
|
+
* footerless documents from colliding onto the same signature (a bare
|
|
101
|
+
* generation-less key would otherwise treat every unstamped document as
|
|
102
|
+
* identical).
|
|
103
|
+
*/
|
|
104
|
+
function legSignaturePart(content) {
|
|
105
|
+
const trimmed = content.trim();
|
|
106
|
+
if (trimmed.length === 0)
|
|
107
|
+
return "absent";
|
|
108
|
+
const generation = parseReviewGeneration(trimmed);
|
|
109
|
+
if (generation !== null)
|
|
110
|
+
return `g${generation}`;
|
|
111
|
+
return `d${createHash("sha256").update(trimmed).digest("hex").slice(0, 16)}`;
|
|
112
|
+
}
|
|
113
|
+
function envelopeSignature(envelope) {
|
|
114
|
+
return LEG_NAMES.map((leg) => `${leg}:${legSignaturePart(legContent(envelope, leg))}`).join("|");
|
|
115
|
+
}
|
|
116
|
+
function recordCompletedResult(repoName, ticketNumber, signature, manifest) {
|
|
117
|
+
evictExpiredCompletedResults();
|
|
118
|
+
const key = completedResultKey(repoName, ticketNumber);
|
|
119
|
+
let bySignature = COMPLETED_RESULTS.get(key);
|
|
120
|
+
if (!bySignature) {
|
|
121
|
+
bySignature = new Map();
|
|
122
|
+
COMPLETED_RESULTS.set(key, bySignature);
|
|
123
|
+
}
|
|
124
|
+
// Delete-then-set so Map iteration order tracks recency, matching the
|
|
125
|
+
// baseline registry's own eviction technique.
|
|
126
|
+
bySignature.delete(signature);
|
|
127
|
+
bySignature.set(signature, { manifest, recordedAtMs: Date.now() });
|
|
128
|
+
while (COMPLETED_RESULTS.size > MAX_COMPLETED_RESULT_KEYS) {
|
|
129
|
+
const oldest = COMPLETED_RESULTS.keys().next();
|
|
130
|
+
if (oldest.done)
|
|
131
|
+
break;
|
|
132
|
+
COMPLETED_RESULTS.delete(oldest.value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function getCompletedResult(repoName, ticketNumber, signature) {
|
|
136
|
+
evictExpiredCompletedResults();
|
|
137
|
+
return COMPLETED_RESULTS.get(completedResultKey(repoName, ticketNumber))?.get(signature)?.manifest ?? null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Invalidate every completed-result entry for this repo/ticket. Called when a
|
|
141
|
+
* new submission attempt begins (`recordReviewGenerationBaseline`) so a prior
|
|
142
|
+
* completion can never satisfy a later, not-yet-advanced submission.
|
|
143
|
+
*/
|
|
144
|
+
export function invalidateCompletedReviewResult(repoName, ticketNumber) {
|
|
145
|
+
COMPLETED_RESULTS.delete(completedResultKey(repoName, ticketNumber));
|
|
146
|
+
}
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Classification + persistence
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
/**
|
|
151
|
+
* The single wording authority for every classification outcome (BAPI-1129).
|
|
152
|
+
*
|
|
153
|
+
* Both calling conventions render this verbatim — `wait_for_ticket_review` in
|
|
154
|
+
* its JSON `message` field, blocking `request_ticket_review` appended after
|
|
155
|
+
* the accepted document bodies. Neither re-derives freshness, withholding,
|
|
156
|
+
* absence, suppression or zero-advance prose of its own; a second wording
|
|
157
|
+
* would be a second contract, and the two would drift.
|
|
158
|
+
*
|
|
159
|
+
* `acceptedLegs` (not `savedLegs`) drives the proceed/nothing-vouched-for
|
|
160
|
+
* branch, because a classification-only call accepts legs while saving none.
|
|
161
|
+
*/
|
|
162
|
+
function buildMessage(freshnessBasis, acceptedLegs, savedLegs, withheldLegs, absentLegs, persistenceEnabled) {
|
|
163
|
+
// Rendered after the outcome sentence on every branch that could otherwise
|
|
164
|
+
// read as "these files are on disk now".
|
|
165
|
+
const persistenceNote = persistenceEnabled
|
|
166
|
+
? ""
|
|
167
|
+
: ` PERSISTENCE DISABLED (save_locally: false) — this was a classification-only call: ` +
|
|
168
|
+
`saved leg(s): none, and no file was written, removed, or reconciled.`;
|
|
169
|
+
if (freshnessBasis === "terminality_only") {
|
|
170
|
+
return (`⚠ DEGRADED FRESHNESS — no submission baseline was available, so acceptance is ` +
|
|
171
|
+
`terminality-only: delivery is proven, generation advancement is NOT. Accepted leg(s): ` +
|
|
172
|
+
`${acceptedLegs.length > 0 ? acceptedLegs.join(", ") : "none"}. Saved leg(s): ` +
|
|
173
|
+
`${savedLegs.length > 0 ? savedLegs.join(", ") : "none"}. You may proceed on these ` +
|
|
174
|
+
`documents, but they may be indistinguishable from a prior attachment; the downstream ` +
|
|
175
|
+
`identity-footer agreement check remains the protective backstop. If that check ` +
|
|
176
|
+
`disagrees, STOP and escalate to an operator rather than proceeding. To prove freshness ` +
|
|
177
|
+
`instead, submit the review with request_ticket_review and wait within the same MCP ` +
|
|
178
|
+
`session.` + persistenceNote + describeAbsent(absentLegs));
|
|
179
|
+
}
|
|
180
|
+
if (acceptedLegs.length === 0) {
|
|
181
|
+
return (`No current-review leg could be vouched for against the submission baseline yet — every ` +
|
|
182
|
+
`delivered leg is either at the baseline generation or carries no readable identity. ` +
|
|
183
|
+
`This is a pending-compatible outcome, not a failure; no file was written or removed. ` +
|
|
184
|
+
`The submission baseline is retained: call wait_for_ticket_review to keep waiting for ` +
|
|
185
|
+
`this submission's own output.` +
|
|
186
|
+
describeWithheld(withheldLegs, false) +
|
|
187
|
+
describeAbsent(absentLegs));
|
|
188
|
+
}
|
|
189
|
+
let message = `The review can proceed. Accepted leg(s): ${acceptedLegs.join(", ")}.`;
|
|
190
|
+
message += persistenceEnabled
|
|
191
|
+
? ` Saved leg(s): ${savedLegs.length > 0 ? savedLegs.join(", ") : "none"}.`
|
|
192
|
+
: persistenceNote;
|
|
193
|
+
message += describeWithheld(withheldLegs, persistenceEnabled);
|
|
194
|
+
message += describeAbsent(absentLegs);
|
|
195
|
+
return message;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Name every withheld leg with exactly one closed-vocabulary reason. The
|
|
199
|
+
* quarantine sentence is stated only when persistence actually ran — a
|
|
200
|
+
* classification-only call removed nothing and must not claim otherwise.
|
|
201
|
+
*/
|
|
202
|
+
function describeWithheld(withheldLegs, persistenceEnabled) {
|
|
203
|
+
if (withheldLegs.length === 0)
|
|
204
|
+
return "";
|
|
205
|
+
const summary = withheldLegs.map((leg) => `${leg.leg} (${leg.reason})`).join(", ");
|
|
206
|
+
return (` WARNING — withheld as NOT current-review evidence: ${summary}. These were not saved` +
|
|
207
|
+
(persistenceEnabled
|
|
208
|
+
? `, and any pre-existing local file for them has been removed so it cannot be mistaken ` +
|
|
209
|
+
`for this submission's output.`
|
|
210
|
+
: `, and their content is not this review's output; no local file was removed because ` +
|
|
211
|
+
`persistence was disabled.`));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Name every absent leg with `not_delivered`, separating a leg the resolved
|
|
215
|
+
* policy EXPECTED (prominent — something the submission configured never
|
|
216
|
+
* arrived) from ordinary policy-permitted absence (a one-leg policy simply
|
|
217
|
+
* did not configure it, which is not a problem at all).
|
|
218
|
+
*/
|
|
219
|
+
function describeAbsent(absentLegs) {
|
|
220
|
+
if (absentLegs.length === 0)
|
|
221
|
+
return "";
|
|
222
|
+
const expected = absentLegs.filter((leg) => leg.expectedByPolicy === true);
|
|
223
|
+
const permitted = absentLegs.filter((leg) => leg.expectedByPolicy !== true);
|
|
224
|
+
let text = "";
|
|
225
|
+
if (expected.length > 0) {
|
|
226
|
+
text +=
|
|
227
|
+
` WARNING — the resolved policy EXPECTED ${expected.map((l) => l.leg).join(", ")} but it ` +
|
|
228
|
+
`was not delivered (not_delivered) — see absent_legs.`;
|
|
229
|
+
}
|
|
230
|
+
if (permitted.length > 0) {
|
|
231
|
+
text +=
|
|
232
|
+
` NOTE: ${permitted.map((l) => l.leg).join(", ")} was not delivered (not_delivered); the ` +
|
|
233
|
+
`resolved policy did not require it, so this is ordinary policy-permitted absence.`;
|
|
234
|
+
}
|
|
235
|
+
return text;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Classify one delivered leg's advancement against `baseline`.
|
|
239
|
+
*
|
|
240
|
+
* `baselineAvailable=false` means terminality alone decides (no proof of
|
|
241
|
+
* freshness is possible): every delivered leg is accepted, `advanced` stays
|
|
242
|
+
* false, and `generation` is still reported for operator visibility.
|
|
243
|
+
*/
|
|
244
|
+
function classifyDelivery(content, legBaseline, baselineAvailable) {
|
|
245
|
+
if (content.trim().length === 0) {
|
|
246
|
+
return { delivered: false, generation: null, advanced: false, reason: "not_delivered" };
|
|
247
|
+
}
|
|
248
|
+
const generation = parseReviewGeneration(content);
|
|
249
|
+
if (!baselineAvailable) {
|
|
250
|
+
return { delivered: true, generation, advanced: false };
|
|
251
|
+
}
|
|
252
|
+
if (generation === null) {
|
|
253
|
+
return { delivered: true, generation: null, advanced: false, reason: "identity_unreadable" };
|
|
254
|
+
}
|
|
255
|
+
if (generationAdvanced(legBaseline, generation)) {
|
|
256
|
+
return { delivered: true, generation, advanced: true };
|
|
257
|
+
}
|
|
258
|
+
return { delivered: true, generation, advanced: false, reason: "generation_not_advanced" };
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Classify the observed combined envelope against the submission baseline,
|
|
262
|
+
* persist exactly the accepted legs, remove any stale withheld canonical
|
|
263
|
+
* file, and record a completed-result entry on full success. Returns a
|
|
264
|
+
* structured failure — never throws — on any write or removal failure.
|
|
265
|
+
*
|
|
266
|
+
* Idempotent: a repeated call with the SAME observed envelope for the same
|
|
267
|
+
* repo/ticket replays the previously completed result (reconciling canonical
|
|
268
|
+
* files from the current envelope) rather than degrading to a fresh
|
|
269
|
+
* terminality-only classification after the live baseline has been cleared.
|
|
270
|
+
*
|
|
271
|
+
* BAPI-1129: `ctx.persistenceEnabled === false` makes the whole call
|
|
272
|
+
* classification-only — the same rules produce the same `accepted_legs`, and
|
|
273
|
+
* nothing is written, removed, reconciled, or recorded.
|
|
274
|
+
*/
|
|
275
|
+
export async function classifyAndPersistReviewEnvelope(ctx) {
|
|
276
|
+
const signature = envelopeSignature(ctx.envelope);
|
|
277
|
+
// Default true: an omitted flag keeps the pre-BAPI-1129 durable-write
|
|
278
|
+
// behavior, so persistence can only ever be given up deliberately.
|
|
279
|
+
const persistenceEnabled = ctx.persistenceEnabled !== false;
|
|
280
|
+
const replayed = getCompletedResult(ctx.repoName, ctx.ticketNumber, signature);
|
|
281
|
+
if (replayed && !persistenceEnabled) {
|
|
282
|
+
// Reuse the recorded CLASSIFICATION — it was produced by these same rules
|
|
283
|
+
// against the same envelope, and is strictly better evidence than a fresh
|
|
284
|
+
// terminality-only read once the live baseline has been cleared — but
|
|
285
|
+
// perform none of the replay's reconciliation, and do not mutate the
|
|
286
|
+
// cached entry. A dry run observes; it never repairs.
|
|
287
|
+
return {
|
|
288
|
+
...replayed,
|
|
289
|
+
saved_legs: [],
|
|
290
|
+
legs: replayed.legs.map((leg) => ({ ...leg, saved: false })),
|
|
291
|
+
withheld_legs: replayed.withheld_legs.map((leg) => ({ ...leg, saved: false })),
|
|
292
|
+
absent_legs: replayed.absent_legs.map((leg) => ({ ...leg, saved: false })),
|
|
293
|
+
replay: true,
|
|
294
|
+
message: `REPLAY (classification only) — reproducing the previously completed classification for ` +
|
|
295
|
+
`this submission; no write, removal, or reconciliation was performed. ` +
|
|
296
|
+
buildMessage(replayed.freshness_basis, replayed.accepted_legs, [], replayed.withheld_legs, replayed.absent_legs, false),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (replayed) {
|
|
300
|
+
for (const leg of replayed.saved_legs) {
|
|
301
|
+
const content = legContent(ctx.envelope, leg);
|
|
302
|
+
const dir = path.dirname(ctx.canonicalPaths[leg]);
|
|
303
|
+
const filename = path.basename(ctx.canonicalPaths[leg]);
|
|
304
|
+
const outcome = await writeCanonicalArtifact(dir, filename, content);
|
|
305
|
+
if (!outcome.ok) {
|
|
306
|
+
// BAPI-1121 (Step 7): a reconciliation failure does NOT drop the
|
|
307
|
+
// completed entry — a subsequent retry must still be able to repair
|
|
308
|
+
// the canonical state from the same recorded manifest.
|
|
309
|
+
return {
|
|
310
|
+
ok: false,
|
|
311
|
+
error: "ARTIFACT_PERSISTENCE_FAILED",
|
|
312
|
+
status: 500,
|
|
313
|
+
message: `Failed to reconcile ${leg} artifact during replay: ${outcome.message}`,
|
|
314
|
+
saved_legs: [],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// BAPI-1121 (code review finding): also re-enforce the withheld side of
|
|
319
|
+
// the manifest. Nothing in this process recreates a withheld leg's
|
|
320
|
+
// canonical file between two waits, but the manifest-is-authoritative
|
|
321
|
+
// contract must hold even if something outside this process did — a
|
|
322
|
+
// replay that only reconciled saved_legs could let a withheld file
|
|
323
|
+
// resurface and be read by a downstream step that trusts file presence.
|
|
324
|
+
for (const legStatus of replayed.withheld_legs) {
|
|
325
|
+
const outcome = await removeCanonicalArtifact(ctx.canonicalPaths[legStatus.leg]);
|
|
326
|
+
if (!outcome.ok) {
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
error: "ARTIFACT_PERSISTENCE_FAILED",
|
|
330
|
+
status: 500,
|
|
331
|
+
message: `Failed to re-quarantine withheld ${legStatus.leg} artifact during replay: ${outcome.message}`,
|
|
332
|
+
saved_legs: replayed.saved_legs,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
...replayed,
|
|
338
|
+
replay: true,
|
|
339
|
+
message: `REPLAY — reproducing the previously completed result for this submission (no reclassification performed). ${replayed.message}`,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const baselineAvailable = ctx.baseline !== null;
|
|
343
|
+
const legs = LEG_NAMES.map((leg) => {
|
|
344
|
+
const content = legContent(ctx.envelope, leg);
|
|
345
|
+
const legBaseline = leg === "clarify" ? ctx.baseline?.clarify ?? null : ctx.baseline?.critique ?? null;
|
|
346
|
+
const classification = classifyDelivery(content, legBaseline, baselineAvailable);
|
|
347
|
+
return {
|
|
348
|
+
leg,
|
|
349
|
+
delivered: classification.delivered,
|
|
350
|
+
generation: classification.generation,
|
|
351
|
+
advanced: classification.advanced,
|
|
352
|
+
saved: false,
|
|
353
|
+
canonicalPath: ctx.canonicalPaths[leg],
|
|
354
|
+
expectedByPolicy: expectedByPolicy(leg, ctx.expectedLegCount),
|
|
355
|
+
reason: classification.reason,
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
const freshnessBasis = baselineAvailable ? "generation" : "terminality_only";
|
|
359
|
+
// The shared ACCEPTANCE rule, and the only place it is expressed: every
|
|
360
|
+
// delivered leg under terminality-only fallback (no proof is possible, so
|
|
361
|
+
// terminal delivery alone is accepted — today's pre-BAPI-1121 behavior);
|
|
362
|
+
// only ADVANCED legs under a generation baseline. BAPI-1129 separated this
|
|
363
|
+
// from "which legs get written": under `persistenceEnabled: false` the same
|
|
364
|
+
// legs are accepted and none is written.
|
|
365
|
+
const acceptedLegs = legs.filter((leg) => (baselineAvailable ? leg.advanced : leg.delivered));
|
|
366
|
+
const legsToSave = persistenceEnabled ? acceptedLegs : [];
|
|
367
|
+
// Withheld = delivered but not accepted, ONLY under a generation baseline —
|
|
368
|
+
// terminality-only has no concept of withholding. Under suppression these
|
|
369
|
+
// are still classified and reported; they are simply not quarantined.
|
|
370
|
+
const withheldCandidates = baselineAvailable
|
|
371
|
+
? legs.filter((leg) => leg.delivered && !leg.advanced)
|
|
372
|
+
: [];
|
|
373
|
+
// Nothing proven yet: do not write, do not remove, do not record
|
|
374
|
+
// completion. `wait_for_ticket_review` never reaches this branch — its own
|
|
375
|
+
// bounded-wait readiness gate already required at least one advanced leg
|
|
376
|
+
// before calling here. Blocking `request_ticket_review` DOES (BAPI-1129):
|
|
377
|
+
// `pollForResult` treats only 404/202 as pending, so a review that outran
|
|
378
|
+
// the backend's `_GENERATING_TTL_SECONDS` terminates the poll on a `200`
|
|
379
|
+
// serving both legs' prior attachments. This is that call's honest
|
|
380
|
+
// negative — and because it writes nothing, the submission baseline stays
|
|
381
|
+
// live for a follow-up `wait_for_ticket_review`.
|
|
382
|
+
if (baselineAvailable && acceptedLegs.length === 0) {
|
|
383
|
+
const withheldOnly = legs.map((leg) => ({ ...leg, reason: leg.reason ?? "not_delivered" }));
|
|
384
|
+
return {
|
|
385
|
+
ok: true,
|
|
386
|
+
delivered_legs: legs.filter((l) => l.delivered).map((l) => l.leg),
|
|
387
|
+
advanced_legs: [],
|
|
388
|
+
accepted_legs: [],
|
|
389
|
+
saved_legs: [],
|
|
390
|
+
withheld_legs: withheldOnly.filter((l) => l.delivered),
|
|
391
|
+
absent_legs: withheldOnly.filter((l) => !l.delivered),
|
|
392
|
+
freshness_basis: freshnessBasis,
|
|
393
|
+
generation_baseline_available: baselineAvailable,
|
|
394
|
+
legs: withheldOnly,
|
|
395
|
+
message: buildMessage(freshnessBasis, [], [], withheldOnly.filter((l) => l.delivered), withheldOnly.filter((l) => !l.delivered), persistenceEnabled),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const savedLegNames = [];
|
|
399
|
+
for (const leg of legsToSave) {
|
|
400
|
+
const content = legContent(ctx.envelope, leg.leg);
|
|
401
|
+
const dir = path.dirname(ctx.canonicalPaths[leg.leg]);
|
|
402
|
+
const filename = path.basename(ctx.canonicalPaths[leg.leg]);
|
|
403
|
+
const outcome = await writeCanonicalArtifact(dir, filename, content);
|
|
404
|
+
if (!outcome.ok) {
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
error: "ARTIFACT_PERSISTENCE_FAILED",
|
|
408
|
+
status: 500,
|
|
409
|
+
message: `Failed to save ${leg.leg} artifact: ${outcome.message}. Sibling artifacts already saved were preserved.`,
|
|
410
|
+
saved_legs: savedLegNames,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
leg.saved = true;
|
|
414
|
+
savedLegNames.push(leg.leg);
|
|
415
|
+
}
|
|
416
|
+
// Stale-file cleanup: remove the exact canonical target for every withheld
|
|
417
|
+
// leg so a downstream file-presence read can never reintroduce it. Only
|
|
418
|
+
// performed once at least one leg was genuinely accepted this call — see
|
|
419
|
+
// the early return above for the zero-advance case — and only when
|
|
420
|
+
// persistence is enabled: a classification-only call touches no file at
|
|
421
|
+
// all, in either direction (BAPI-1129).
|
|
422
|
+
for (const leg of persistenceEnabled ? withheldCandidates : []) {
|
|
423
|
+
const outcome = await removeCanonicalArtifact(leg.canonicalPath);
|
|
424
|
+
if (!outcome.ok) {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
error: "ARTIFACT_PERSISTENCE_FAILED",
|
|
428
|
+
status: 500,
|
|
429
|
+
message: `Failed to remove withheld ${leg.leg} artifact: ${outcome.message}.`,
|
|
430
|
+
saved_legs: savedLegNames,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const deliveredLegs = legs.filter((l) => l.delivered).map((l) => l.leg);
|
|
435
|
+
const advancedLegs = legs.filter((l) => l.advanced).map((l) => l.leg);
|
|
436
|
+
const withheldStatuses = legs.filter((l) => withheldCandidates.some((w) => w.leg === l.leg));
|
|
437
|
+
const absentStatuses = legs.filter((l) => !l.delivered);
|
|
438
|
+
const manifest = {
|
|
439
|
+
ok: true,
|
|
440
|
+
delivered_legs: deliveredLegs,
|
|
441
|
+
advanced_legs: advancedLegs,
|
|
442
|
+
accepted_legs: acceptedLegs.map((l) => l.leg),
|
|
443
|
+
saved_legs: savedLegNames,
|
|
444
|
+
withheld_legs: withheldStatuses,
|
|
445
|
+
absent_legs: absentStatuses,
|
|
446
|
+
freshness_basis: freshnessBasis,
|
|
447
|
+
generation_baseline_available: baselineAvailable,
|
|
448
|
+
legs,
|
|
449
|
+
message: buildMessage(freshnessBasis, acceptedLegs.map((l) => l.leg), savedLegNames, withheldStatuses, absentStatuses, persistenceEnabled),
|
|
450
|
+
};
|
|
451
|
+
// Recorded only after a persistence-enabled call actually landed something.
|
|
452
|
+
// `savedLegNames` is empty for every suppressed call, so a dry run seeds
|
|
453
|
+
// nothing and a later persistence-enabled wait still does the real
|
|
454
|
+
// reconciliation. The `persistenceEnabled` conjunct is redundant today and
|
|
455
|
+
// deliberately explicit: it keeps the invariant true if the saved-leg
|
|
456
|
+
// accounting ever changes.
|
|
457
|
+
if (persistenceEnabled && savedLegNames.length > 0) {
|
|
458
|
+
recordCompletedResult(ctx.repoName, ctx.ticketNumber, signature, manifest);
|
|
459
|
+
}
|
|
460
|
+
return manifest;
|
|
461
|
+
}
|
package/build/upgrade-cli.js
CHANGED
|
@@ -41,8 +41,6 @@ import { isNewerVersion } from "./update-check.js";
|
|
|
41
41
|
import { fetchLatestVersion } from "./cli-release.js";
|
|
42
42
|
import { CANONICAL_UPGRADE_COMMAND } from "./upgrade-advice.js";
|
|
43
43
|
import { describeLauncherReason, inspectLauncherConfigs, isApplicableStandardLauncher, isPrerequisiteFailure, LAUNCHER_CONFIG_TARGETS, } from "./launcher-config-inspection.js";
|
|
44
|
-
import { buildGenericAgentShellCommand, detectTerminal, getDefaultSpawnTerminalTabForPlatform, createDefaultStartTicketsDeps, } from "./start-tickets.js";
|
|
45
|
-
import { AGENT_REGISTRY } from "./agent-registry.js";
|
|
46
44
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
47
45
|
/** The sentinel this CLI passes to the child it re-execs from `@latest`. */
|
|
48
46
|
const INTERNAL_REEXEC_FLAG = "--internal-reexec";
|
|
@@ -90,21 +88,6 @@ function defaultRemoveCompetingLocalInstall(cwd) {
|
|
|
90
88
|
child.on("error", () => resolve(false));
|
|
91
89
|
});
|
|
92
90
|
}
|
|
93
|
-
async function defaultSpawnReconnect(cwd, command) {
|
|
94
|
-
try {
|
|
95
|
-
const terminal = detectTerminal(undefined, process.env);
|
|
96
|
-
const deps = createDefaultStartTicketsDeps();
|
|
97
|
-
const spawnTerminalTab = getDefaultSpawnTerminalTabForPlatform(process.platform);
|
|
98
|
-
const result = await spawnTerminalTab(deps, terminal, command, {
|
|
99
|
-
key: "reconnect",
|
|
100
|
-
worktreePath: cwd,
|
|
101
|
-
});
|
|
102
|
-
return result.ok;
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
91
|
/** Build the production dependency set. */
|
|
109
92
|
export function createDefaultUpgradeCliDeps(cwd) {
|
|
110
93
|
return {
|
|
@@ -115,7 +98,6 @@ export function createDefaultUpgradeCliDeps(cwd) {
|
|
|
115
98
|
removeCompetingLocalInstall: defaultRemoveCompetingLocalInstall,
|
|
116
99
|
runInit,
|
|
117
100
|
reexecFromLatest: defaultReexec,
|
|
118
|
-
spawnReconnect: defaultSpawnReconnect,
|
|
119
101
|
stdout: (m) => console.log(m),
|
|
120
102
|
stderr: (m) => console.error(m),
|
|
121
103
|
env: process.env,
|
|
@@ -308,7 +290,7 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
308
290
|
stdout(`- Would print: Already up-to-date.`);
|
|
309
291
|
}
|
|
310
292
|
stdout(`- Would refresh scaffolded artifacts (commands, agents, pipelines).`);
|
|
311
|
-
stdout(`- Would
|
|
293
|
+
stdout(`- Would print a reminder to reload/restart your MCP.`);
|
|
312
294
|
return 0;
|
|
313
295
|
}
|
|
314
296
|
// ---- Phase 4: competing local install — a reconciliation, not a transition ----
|
|
@@ -440,13 +422,10 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
440
422
|
}
|
|
441
423
|
else {
|
|
442
424
|
stdout(`\nUpgrade complete: launcher pins are at ${target.version}.`);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const spawned = await deps.spawnReconnect(deps.cwd, spawnCommand);
|
|
448
|
-
if (!spawned) {
|
|
449
|
-
stdout(`\nTo continue, manually run:\n${spawnCommand}\n`);
|
|
425
|
+
// The running session still holds the OLD MCP connection — MCP config is only
|
|
426
|
+
// read at session startup, so a launcher repin does not hot-reload it. Tell the
|
|
427
|
+
// user the concrete action instead of spawning a fresh session for them.
|
|
428
|
+
stdout("Reload your MCP now (run /mcp) or restart your session to pick up the upgraded server.");
|
|
450
429
|
}
|
|
451
430
|
return 0;
|
|
452
431
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
|
|
2
|
-
export const VERSION = "0.2.
|
|
3
|
-
export const BUILD_COMMIT = "
|
|
4
|
-
export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
2
|
+
export const VERSION = "0.2.55";
|
|
3
|
+
export const BUILD_COMMIT = "b96c1451f24e";
|
|
4
|
+
export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.55", "serve"];
|