@bridge_gpt/mcp-server 0.2.66 → 0.2.67
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 +252 -77
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +43 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +1 -1
- package/build/bounded-wait.js +67 -10
- package/build/commands.generated.js +11 -11
- package/build/conductor/bridge-api-client.js +143 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/doctor.js +3 -3
- package/build/conductor/epic-implementer-cli.js +22 -6
- package/build/conductor/errors.js +4 -3
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +18 -2
- package/build/conductor/readiness-sections.js +41 -6
- package/build/conductor/readiness.js +181 -29
- package/build/conductor/recovery-cli.js +200 -8
- package/build/conductor/recovery-operations.js +69 -3
- package/build/conductor/repair-contract.js +54 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/docs.generated.js +8 -3
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +85 -54
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-implementer-bin.js +22 -20
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/executor/cli.js +3 -2
- package/build/executor/completion-contract.js +48 -5
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/env.js +19 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/job-errors.js +25 -0
- package/build/executor/job-runner.js +225 -41
- package/build/executor/observation.js +6 -1
- package/build/executor/preflight.js +51 -14
- package/build/executor/required-work-ledger.js +6 -1
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/runner.js +8 -2
- package/build/executor/service-unit.js +7 -1
- package/build/executor/worker-finalization.js +82 -29
- package/build/executor/worker-guard-hook.js +137 -33
- package/build/executor/worktree.js +82 -1
- package/build/index.js +403 -429
- package/build/install-bridge-conductor.js +8 -6
- package/build/install-bridge.js +3 -3
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipelines.generated.js +18 -24
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +120 -1
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +30 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +37 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +22 -0
- package/build/setup-epic.js +44 -19
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/tool-wrapper.js +39 -43
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets.js +179 -31
- package/build/tool-error-envelope.js +61 -0
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +3 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +130 -4
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
- package/docs/EPIC-IMPLEMENTER.md +662 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +1 -1
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
|
@@ -69,6 +69,24 @@ export function renderWorkerGuardReason(category, family) {
|
|
|
69
69
|
"stdin-fed arguments hide what git will run. Use literal arguments instead; " +
|
|
70
70
|
"for a commit message, write it to a file and run git commit -F <file>.");
|
|
71
71
|
}
|
|
72
|
+
if (category === "workflow-entry" ||
|
|
73
|
+
category === "workflow-launch-skill" ||
|
|
74
|
+
category === "workflow-launch-shell" ||
|
|
75
|
+
category === "workflow-planner-tool") {
|
|
76
|
+
return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
|
|
77
|
+
"A correction worker must not start a full workflow. Reading or editing files " +
|
|
78
|
+
"that mention a workflow is allowed; make the focused correction directly.");
|
|
79
|
+
}
|
|
80
|
+
if (category === "correction-context-invalid") {
|
|
81
|
+
return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
|
|
82
|
+
"The correction context for this worker is not valid, so no tool call can be " +
|
|
83
|
+
"checked. Stop and report the launch problem.");
|
|
84
|
+
}
|
|
85
|
+
if (category === "correction-payload-invalid") {
|
|
86
|
+
return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
|
|
87
|
+
"The tool call could not be read as a tool name with a tool input, so it cannot " +
|
|
88
|
+
"be checked. Retry with a well-formed call.");
|
|
89
|
+
}
|
|
72
90
|
return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
|
|
73
91
|
"This command family is restricted to the worker's own branch, worktree, and " +
|
|
74
92
|
"read-only network access. Use a different approach and continue.");
|
|
@@ -780,6 +798,52 @@ function stripStdinPrefixOptions(argv) {
|
|
|
780
798
|
}
|
|
781
799
|
return argv.slice(i);
|
|
782
800
|
}
|
|
801
|
+
/** The workflow names a correction worker must never launch. */
|
|
802
|
+
const WORKFLOW_NAMES = "implement-ticket|plan-ticket|idea-to-pr|review-and-implement";
|
|
803
|
+
/**
|
|
804
|
+
* A slash-form workflow launch: the slash starts the text or follows whitespace, and
|
|
805
|
+
* the name ends at whitespace or the end. A file path such as
|
|
806
|
+
* `docs/claude/idea-to-pr.md` has neither, so it never matches.
|
|
807
|
+
*/
|
|
808
|
+
const WORKFLOW_SLASH_LAUNCH = new RegExp(`(?:^|\\s)/(?:bridge-)?(?:${WORKFLOW_NAMES})(?=\\s|$)`, "i");
|
|
809
|
+
/** A bare skill or command name, with optional leading slash and `plugin:` prefix. */
|
|
810
|
+
const WORKFLOW_NAME_EXACT = new RegExp(`^/?(?:[A-Za-z0-9_-]+:)?(?:bridge-)?(?:${WORKFLOW_NAMES})$`, "i");
|
|
811
|
+
/** The token after `npx` that names the Claude CLI package. */
|
|
812
|
+
const CLAUDE_PACKAGE_TOKEN = /^(?:@anthropic-ai\/)?claude(?:-code)?(?:@\S+)?$/;
|
|
813
|
+
/** The argv after the Claude CLI name, or `undefined` when this is not a Claude launch. */
|
|
814
|
+
function claudeLaunchArgs(argv) {
|
|
815
|
+
const base = path.basename(argv[0] ?? "");
|
|
816
|
+
if (base === "claude")
|
|
817
|
+
return argv.slice(1);
|
|
818
|
+
if (base === "npx") {
|
|
819
|
+
const index = argv.findIndex((token, i) => i > 0 && CLAUDE_PACKAGE_TOKEN.test(token));
|
|
820
|
+
return index === -1 ? undefined : argv.slice(index + 1);
|
|
821
|
+
}
|
|
822
|
+
return undefined;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Is this a correction-mode Claude print run that launches a workflow (or that cannot
|
|
826
|
+
* be read well enough to rule one out)?
|
|
827
|
+
*
|
|
828
|
+
* Runs on tokenized, de-quoted argv, so a quoted prompt is one plain token. The whole
|
|
829
|
+
* argv is scanned for the print flag and for a workflow slash launch, because the
|
|
830
|
+
* prompt may sit anywhere among the options. A print run in a segment the tokenizer
|
|
831
|
+
* marked unsafe denies without trying to resolve the substitution.
|
|
832
|
+
*/
|
|
833
|
+
function isClaudeWorkflowPrintLaunch(segment) {
|
|
834
|
+
const args = claudeLaunchArgs(segment.argv);
|
|
835
|
+
if (args === undefined)
|
|
836
|
+
return false;
|
|
837
|
+
const printing = args.some((token) => token === "-p" || token === "--print" || token.startsWith("--print="));
|
|
838
|
+
if (!printing)
|
|
839
|
+
return false;
|
|
840
|
+
if (segment.unsafe)
|
|
841
|
+
return true;
|
|
842
|
+
return args.some((token) => {
|
|
843
|
+
const value = token.startsWith("--print=") ? token.slice("--print=".length) : token;
|
|
844
|
+
return WORKFLOW_SLASH_LAUNCH.test(value);
|
|
845
|
+
});
|
|
846
|
+
}
|
|
783
847
|
/** Evaluate one already-tokenized simple command. */
|
|
784
848
|
function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = false) {
|
|
785
849
|
const argv = unwrapTransparentPrefixes(segment.argv);
|
|
@@ -828,6 +892,9 @@ function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = f
|
|
|
828
892
|
return evaluateSimpleCommand({ argv: rest, unsafe: segment.unsafe }, ctx, payloadCwd, depth + 1, true);
|
|
829
893
|
}
|
|
830
894
|
segment = { argv, unsafe: segment.unsafe };
|
|
895
|
+
if (ctx.correctionEnforced && isClaudeWorkflowPrintLaunch(segment)) {
|
|
896
|
+
return { kind: "deny", category: "workflow-launch-shell", family: "workflow" };
|
|
897
|
+
}
|
|
831
898
|
const family = classify(executable);
|
|
832
899
|
if (family === null)
|
|
833
900
|
return { kind: "allow" };
|
|
@@ -865,6 +932,55 @@ function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = f
|
|
|
865
932
|
}
|
|
866
933
|
return { kind: "allow" };
|
|
867
934
|
}
|
|
935
|
+
function workflowDeny(category) {
|
|
936
|
+
return { kind: "deny", category, family: "workflow" };
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Tools that read or change files. Their path and pattern fields name files, and a
|
|
940
|
+
* file named after a workflow is not a workflow launch, so they are never matched.
|
|
941
|
+
*/
|
|
942
|
+
const FILE_TOOLS = new Set(["Read", "Edit", "Write", "MultiEdit", "NotebookEdit", "Grep", "Glob"]);
|
|
943
|
+
/** The fields of each invocation tool that can carry a workflow launch. */
|
|
944
|
+
const INVOCATION_FIELDS = {
|
|
945
|
+
Skill: ["skill", "args"],
|
|
946
|
+
Task: ["prompt", "description"],
|
|
947
|
+
Agent: ["prompt", "description"],
|
|
948
|
+
SlashCommand: ["command"],
|
|
949
|
+
};
|
|
950
|
+
/** An `mcp__` tool whose name is a planner or recipe entry point. */
|
|
951
|
+
const PLANNER_TOOL_NAME = /(?:request_plan_generation|get_pipeline_recipe|plan_ticket|implement_ticket)/i;
|
|
952
|
+
/**
|
|
953
|
+
* Correction-mode dispatch by tool role. Returns a decision for a file tool, an
|
|
954
|
+
* invocation tool, a planner tool, or an unknown role, and `undefined` for `Bash`,
|
|
955
|
+
* which the caller evaluates as a shell command.
|
|
956
|
+
*/
|
|
957
|
+
function evaluateCorrectionToolRole(toolName, input) {
|
|
958
|
+
if (FILE_TOOLS.has(toolName))
|
|
959
|
+
return { kind: "allow" };
|
|
960
|
+
if (toolName === "Bash")
|
|
961
|
+
return undefined;
|
|
962
|
+
const fields = INVOCATION_FIELDS[toolName];
|
|
963
|
+
if (fields !== undefined) {
|
|
964
|
+
for (const field of fields) {
|
|
965
|
+
const value = input[field];
|
|
966
|
+
if (value === undefined)
|
|
967
|
+
continue;
|
|
968
|
+
if (typeof value !== "string")
|
|
969
|
+
return workflowDeny("workflow-launch-skill");
|
|
970
|
+
const nameField = toolName === "Skill" ? field === "skill" : toolName === "SlashCommand";
|
|
971
|
+
if (WORKFLOW_SLASH_LAUNCH.test(value) || (nameField && WORKFLOW_NAME_EXACT.test(value.trim().split(/\s+/)[0] ?? ""))) {
|
|
972
|
+
return workflowDeny("workflow-launch-skill");
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return { kind: "allow" };
|
|
976
|
+
}
|
|
977
|
+
if (/^mcp__/i.test(toolName) && PLANNER_TOOL_NAME.test(toolName)) {
|
|
978
|
+
return workflowDeny("workflow-planner-tool");
|
|
979
|
+
}
|
|
980
|
+
// Any other role: only an explicit workflow launch in a string argument denies.
|
|
981
|
+
const args = Object.values(input).filter((v) => typeof v === "string").join(" ");
|
|
982
|
+
return WORKFLOW_SLASH_LAUNCH.test(args) ? workflowDeny("workflow-entry") : undefined;
|
|
983
|
+
}
|
|
868
984
|
/**
|
|
869
985
|
* Evaluate a parsed PreToolUse payload.
|
|
870
986
|
*
|
|
@@ -878,46 +994,32 @@ function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = f
|
|
|
878
994
|
* governed here.
|
|
879
995
|
*/
|
|
880
996
|
export function evaluateWorkerGuardPayload(payload, ctx) {
|
|
881
|
-
const root = asRecord(payload);
|
|
882
997
|
if (ctx.correctionContextInvalid)
|
|
883
|
-
return
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
998
|
+
return workflowDeny("correction-context-invalid");
|
|
999
|
+
const root = asRecord(payload);
|
|
1000
|
+
if (root === undefined) {
|
|
1001
|
+
return ctx.correctionEnforced ? workflowDeny("correction-payload-invalid") : { kind: "malformed" };
|
|
1002
|
+
}
|
|
888
1003
|
const toolName = root.tool_name ?? root.toolName;
|
|
889
|
-
if (typeof toolName !== "string")
|
|
890
|
-
return ctx.correctionEnforced
|
|
891
|
-
|
|
892
|
-
: { kind: "malformed" };
|
|
1004
|
+
if (typeof toolName !== "string") {
|
|
1005
|
+
return ctx.correctionEnforced ? workflowDeny("correction-payload-invalid") : { kind: "malformed" };
|
|
1006
|
+
}
|
|
893
1007
|
const input = asRecord(root.tool_input ?? root.toolInput);
|
|
894
1008
|
if (ctx.correctionEnforced) {
|
|
895
1009
|
if (input === undefined)
|
|
896
|
-
return
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const plannerTool = /(?:request_plan_generation|get_pipeline_recipe|plan_ticket|implement_ticket)/i;
|
|
901
|
-
if (recipePath.test(args) || fullEntry.test(args) ||
|
|
902
|
-
(plannerTool.test(toolName) && toolName !== "Bash")) {
|
|
903
|
-
return { kind: "deny", category: "workflow-entry", family: "workflow" };
|
|
904
|
-
}
|
|
905
|
-
if (/^(?:Task|Agent|Skill|SlashCommand|mcp__)/i.test(toolName) &&
|
|
906
|
-
(fullEntry.test(args) || plannerTool.test(args))) {
|
|
907
|
-
return { kind: "deny", category: "workflow-entry", family: "workflow" };
|
|
908
|
-
}
|
|
1010
|
+
return workflowDeny("correction-payload-invalid");
|
|
1011
|
+
const decision = evaluateCorrectionToolRole(toolName, input);
|
|
1012
|
+
if (decision !== undefined)
|
|
1013
|
+
return decision;
|
|
909
1014
|
}
|
|
910
1015
|
if (toolName !== "Bash")
|
|
911
1016
|
return { kind: "allow" };
|
|
912
1017
|
if (input === undefined)
|
|
913
|
-
return
|
|
914
|
-
? { kind: "deny", category: "workflow-entry", family: "workflow" }
|
|
915
|
-
: { kind: "malformed" };
|
|
1018
|
+
return { kind: "malformed" };
|
|
916
1019
|
const command = input.command;
|
|
917
|
-
if (typeof command !== "string")
|
|
918
|
-
return ctx.correctionEnforced
|
|
919
|
-
|
|
920
|
-
: { kind: "malformed" };
|
|
1020
|
+
if (typeof command !== "string") {
|
|
1021
|
+
return ctx.correctionEnforced ? workflowDeny("correction-payload-invalid") : { kind: "malformed" };
|
|
1022
|
+
}
|
|
921
1023
|
const rawCwd = root.cwd ?? input.cwd;
|
|
922
1024
|
const payloadCwd = typeof rawCwd === "string" ? rawCwd : undefined;
|
|
923
1025
|
for (const segment of tokenizeShellCommand(command)) {
|
|
@@ -934,9 +1036,11 @@ export function evaluateWorkerGuardPayload(payload, ctx) {
|
|
|
934
1036
|
* exception: the binary's job is to emit one bounded line and get out of the way.
|
|
935
1037
|
*/
|
|
936
1038
|
export function evaluateWorkerGuardInput(raw, ctx) {
|
|
937
|
-
const unreadable = () =>
|
|
938
|
-
|
|
939
|
-
|
|
1039
|
+
const unreadable = () => {
|
|
1040
|
+
if (ctx.correctionContextInvalid)
|
|
1041
|
+
return workflowDeny("correction-context-invalid");
|
|
1042
|
+
return ctx.correctionEnforced ? workflowDeny("correction-payload-invalid") : { kind: "malformed" };
|
|
1043
|
+
};
|
|
940
1044
|
if (typeof raw !== "string")
|
|
941
1045
|
return unreadable();
|
|
942
1046
|
if (raw.length > WORKER_GUARD_MAX_PAYLOAD_BYTES)
|
|
@@ -10,7 +10,8 @@ import { fetchAndResolveBaseSha, probeClaudeReviewWorkflowDrift, resolveReposito
|
|
|
10
10
|
// BAPI-941: the shared drift diagnostic formatter (a leaf module — no cycle).
|
|
11
11
|
import { formatClaudeReviewWorkflowDriftDiagnostic } from "../claude-review-workflow-drift.js";
|
|
12
12
|
import { commandSucceeded } from "../start-tickets-prereqs.js";
|
|
13
|
-
import { createWorktreeForTicket } from "../worktree-core.js";
|
|
13
|
+
import { createWorktreeForTicket, provePriorHeadLineage } from "../worktree-core.js";
|
|
14
|
+
import { findWorktreeByBranch, listGitWorktrees } from "./worktree-inspection.js";
|
|
14
15
|
import { provisionCommandsForWorktree } from "../command-provisioning.js";
|
|
15
16
|
/**
|
|
16
17
|
* Resolve the branch for a real spawn job: `expected_branch` wins; else
|
|
@@ -252,6 +253,13 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
252
253
|
// is neither), and it never contributes to the fail-CLOSED base-resolution
|
|
253
254
|
// decision above.
|
|
254
255
|
await emitExecutorWorkflowDriftAdvisory(deps, options.baseBranch);
|
|
256
|
+
// BAPI-1367: a retry of an `implement` job with a server-minted prior head
|
|
257
|
+
// resumes its own branch instead of being refused as a stale leftover. The
|
|
258
|
+
// gate lives HERE, not in the shared primitive, which learns no job types.
|
|
259
|
+
const priorHeadSha = eligiblePriorHead(job, policy);
|
|
260
|
+
if (priorHeadSha !== null) {
|
|
261
|
+
return ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, options, deps);
|
|
262
|
+
}
|
|
255
263
|
const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseSha, guardStaleWorktree, {
|
|
256
264
|
alignExistingBranchTo: baseSha,
|
|
257
265
|
verifyHeadMatches: baseSha,
|
|
@@ -268,3 +276,76 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
268
276
|
}
|
|
269
277
|
return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
|
|
270
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* BAPI-1367: the prior head a worktree policy may act on, or `null`. Requires an
|
|
281
|
+
* `implement` job on a real retry and a syntactically valid full SHA; anything
|
|
282
|
+
* else keeps the ordinary fresh-dispatch behavior.
|
|
283
|
+
*/
|
|
284
|
+
function eligiblePriorHead(job, policy) {
|
|
285
|
+
if (policy.reuseExistingBranch === true)
|
|
286
|
+
return null;
|
|
287
|
+
if (job.job_type !== "implement")
|
|
288
|
+
return null;
|
|
289
|
+
if (typeof job.attempt_count !== "number" || job.attempt_count <= 1)
|
|
290
|
+
return null;
|
|
291
|
+
const head = policy.priorAttemptHeadSha;
|
|
292
|
+
return typeof head === "string" && FULL_SHA_PATTERN.test(head) ? head : null;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* BAPI-1367: prepare the worktree for a retry that resumes prior-attempt work.
|
|
296
|
+
*
|
|
297
|
+
* Non-destructive by construction: no `alignExistingBranchTo`, no
|
|
298
|
+
* `verifyHeadMatches: baseSha`, no reset-based freshen, no `git clean -fd`. The
|
|
299
|
+
* shared primitive proves lineage against the resolved base and refuses before
|
|
300
|
+
* any mutation otherwise.
|
|
301
|
+
*
|
|
302
|
+
* When the worktree is missing it is recreated from the pushed branch, so
|
|
303
|
+
* `origin/<branch>` is fetched first and its lineage is proven here as well as
|
|
304
|
+
* (when a local branch exists) the local tip's lineage in the primitive —
|
|
305
|
+
* recreation may only use a remote source that passes. A conflicting ref refuses
|
|
306
|
+
* without cleanup. The result says plainly whether prior uncommitted files could
|
|
307
|
+
* survive.
|
|
308
|
+
*/
|
|
309
|
+
async function ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, options, deps) {
|
|
310
|
+
const entries = await listGitWorktrees(deps.runCommand, deps.cwd);
|
|
311
|
+
const retained = findWorktreeByBranch(entries, branch) !== undefined;
|
|
312
|
+
const coreDeps = toWorktreeCoreDeps(deps);
|
|
313
|
+
const policy = { priorHeadSha, baseRef: baseSha };
|
|
314
|
+
if (!retained) {
|
|
315
|
+
// Recreation reads origin/<branch>; make sure that ref is current and proven.
|
|
316
|
+
const fetch = await deps.runCommand("git", ["fetch", "origin", branch], { cwd: deps.cwd });
|
|
317
|
+
if (!commandSucceeded(fetch)) {
|
|
318
|
+
const reason = (fetch.stderr || fetch.stdout || "").trim();
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
error: `cannot resume prior-attempt work on '${branch}': git fetch origin ${branch} failed` +
|
|
322
|
+
`${reason ? `: ${reason}` : ""}`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const remote = await provePriorHeadLineage(coreDeps, branch, policy, "origin_branch");
|
|
326
|
+
if (!remote.ok) {
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
error: `stale worktree guard: pushed branch 'origin/${branch}' cannot be resumed: ${remote.reason}`,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const row = await createWorktreeForTicket(coreDeps, key, { [key]: branch }, options.worktrunkBinary, baseSha, true, { allowedPriorHead: policy });
|
|
334
|
+
if (row.status !== "created" || typeof row.path !== "string") {
|
|
335
|
+
return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
|
|
336
|
+
}
|
|
337
|
+
const headSha = await resolveCommitSha(deps, row.path, "HEAD");
|
|
338
|
+
if (headSha === null) {
|
|
339
|
+
return { ok: false, error: `failed to resolve HEAD of the resumed worktree for '${branch}'` };
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
ok: true,
|
|
343
|
+
worktreePath: row.path,
|
|
344
|
+
branch,
|
|
345
|
+
expectedHeadSha: headSha,
|
|
346
|
+
priorWork: {
|
|
347
|
+
kind: retained ? "retained_worktree" : "recreated_from_pushed_branch",
|
|
348
|
+
priorHeadSha,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|