@bridge_gpt/mcp-server 0.2.65 → 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 +269 -82
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +55 -34
- package/build/agent-launchers/executor-adapter.js +34 -4
- package/build/agents.generated.js +1 -1
- package/build/bounded-wait.js +67 -10
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +372 -10
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/doctor.js +3 -3
- package/build/conductor/epic-child-continuation.js +61 -34
- package/build/conductor/epic-implementer-cli.js +34 -6
- package/build/conductor/errors.js +4 -3
- package/build/conductor/guided-repair.js +327 -0
- 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 +46 -9
- package/build/conductor/readiness-sections.js +41 -6
- package/build/conductor/readiness.js +204 -30
- package/build/conductor/recovery-cli.js +703 -20
- package/build/conductor/recovery-operations.js +392 -3
- package/build/conductor/repair-contract.js +112 -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/credential-store.js +5 -4
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +8 -3
- package/build/doctor.js +70 -10
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +479 -131
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-implementer-bin.js +30 -26
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +44 -14
- package/build/executor/cli.js +3 -2
- package/build/executor/completion-contract.js +48 -5
- package/build/executor/completion-supervision.js +9 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/env.js +25 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +35 -0
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +38 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +485 -74
- package/build/executor/job-types.js +8 -13
- package/build/executor/observation.js +6 -1
- package/build/executor/preflight.js +136 -10
- package/build/executor/prompt-spec.js +113 -9
- package/build/executor/required-work-ledger.js +6 -1
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/runner.js +31 -2
- package/build/executor/service-unit.js +7 -1
- package/build/executor/stale-artifacts.js +5 -4
- package/build/executor/types.js +1 -0
- package/build/executor/watch-cli.js +35 -8
- package/build/executor/worker-command.js +43 -43
- package/build/executor/worker-finalization.js +161 -43
- package/build/executor/worker-guard-hook.js +152 -12
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/executor/worktree.js +82 -1
- package/build/index.js +482 -660
- package/build/init.js +107 -4
- package/build/install-bridge-conductor.js +8 -6
- package/build/install-bridge.js +287 -23
- package/build/install-doctor.js +172 -30
- package/build/install-reexec.js +69 -7
- package/build/macos-terminal-permission-guidance.js +16 -0
- package/build/model-thresholds-tool.js +185 -0
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipelines.generated.js +19 -25
- 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 +145 -6
- package/build/plane/supervisor.js +588 -177
- package/build/plane/types.js +54 -2
- package/build/plane/worker-exit-code.js +2 -0
- package/build/printed-remedy-commands.js +37 -0
- package/build/project-root.js +99 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +22 -0
- package/build/review-tickets.js +156 -13
- package/build/setup-epic.js +224 -127
- 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/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +41 -43
- package/build/sfcc/tools-configured.js +96 -0
- 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 +332 -79
- package/build/tool-error-envelope.js +61 -0
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +3 -5
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +136 -9
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
- package/docs/EPIC-IMPLEMENTER.md +662 -0
- package/docs/install/mcp-tool-integrations.md +25 -15
- package/docs/install/sfcc-integration.md +21 -8
- package/package.json +1 -1
- package/pipelines/implement-ticket.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,20 +994,32 @@ function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = f
|
|
|
878
994
|
* governed here.
|
|
879
995
|
*/
|
|
880
996
|
export function evaluateWorkerGuardPayload(payload, ctx) {
|
|
997
|
+
if (ctx.correctionContextInvalid)
|
|
998
|
+
return workflowDeny("correction-context-invalid");
|
|
881
999
|
const root = asRecord(payload);
|
|
882
|
-
if (root === undefined)
|
|
883
|
-
return { kind: "malformed" };
|
|
1000
|
+
if (root === undefined) {
|
|
1001
|
+
return ctx.correctionEnforced ? workflowDeny("correction-payload-invalid") : { kind: "malformed" };
|
|
1002
|
+
}
|
|
884
1003
|
const toolName = root.tool_name ?? root.toolName;
|
|
885
|
-
if (typeof toolName !== "string")
|
|
886
|
-
return { kind: "malformed" };
|
|
1004
|
+
if (typeof toolName !== "string") {
|
|
1005
|
+
return ctx.correctionEnforced ? workflowDeny("correction-payload-invalid") : { kind: "malformed" };
|
|
1006
|
+
}
|
|
1007
|
+
const input = asRecord(root.tool_input ?? root.toolInput);
|
|
1008
|
+
if (ctx.correctionEnforced) {
|
|
1009
|
+
if (input === undefined)
|
|
1010
|
+
return workflowDeny("correction-payload-invalid");
|
|
1011
|
+
const decision = evaluateCorrectionToolRole(toolName, input);
|
|
1012
|
+
if (decision !== undefined)
|
|
1013
|
+
return decision;
|
|
1014
|
+
}
|
|
887
1015
|
if (toolName !== "Bash")
|
|
888
1016
|
return { kind: "allow" };
|
|
889
|
-
const input = asRecord(root.tool_input ?? root.toolInput);
|
|
890
1017
|
if (input === undefined)
|
|
891
1018
|
return { kind: "malformed" };
|
|
892
1019
|
const command = input.command;
|
|
893
|
-
if (typeof command !== "string")
|
|
894
|
-
return { kind: "malformed" };
|
|
1020
|
+
if (typeof command !== "string") {
|
|
1021
|
+
return ctx.correctionEnforced ? workflowDeny("correction-payload-invalid") : { kind: "malformed" };
|
|
1022
|
+
}
|
|
895
1023
|
const rawCwd = root.cwd ?? input.cwd;
|
|
896
1024
|
const payloadCwd = typeof rawCwd === "string" ? rawCwd : undefined;
|
|
897
1025
|
for (const segment of tokenizeShellCommand(command)) {
|
|
@@ -908,18 +1036,23 @@ export function evaluateWorkerGuardPayload(payload, ctx) {
|
|
|
908
1036
|
* exception: the binary's job is to emit one bounded line and get out of the way.
|
|
909
1037
|
*/
|
|
910
1038
|
export function evaluateWorkerGuardInput(raw, ctx) {
|
|
1039
|
+
const unreadable = () => {
|
|
1040
|
+
if (ctx.correctionContextInvalid)
|
|
1041
|
+
return workflowDeny("correction-context-invalid");
|
|
1042
|
+
return ctx.correctionEnforced ? workflowDeny("correction-payload-invalid") : { kind: "malformed" };
|
|
1043
|
+
};
|
|
911
1044
|
if (typeof raw !== "string")
|
|
912
|
-
return
|
|
1045
|
+
return unreadable();
|
|
913
1046
|
if (raw.length > WORKER_GUARD_MAX_PAYLOAD_BYTES)
|
|
914
|
-
return
|
|
1047
|
+
return unreadable();
|
|
915
1048
|
if (raw.trim().length === 0)
|
|
916
|
-
return
|
|
1049
|
+
return unreadable();
|
|
917
1050
|
let parsed;
|
|
918
1051
|
try {
|
|
919
1052
|
parsed = JSON.parse(raw);
|
|
920
1053
|
}
|
|
921
1054
|
catch {
|
|
922
|
-
return
|
|
1055
|
+
return unreadable();
|
|
923
1056
|
}
|
|
924
1057
|
try {
|
|
925
1058
|
return evaluateWorkerGuardPayload(parsed, ctx);
|
|
@@ -927,7 +1060,7 @@ export function evaluateWorkerGuardInput(raw, ctx) {
|
|
|
927
1060
|
catch {
|
|
928
1061
|
// Defensive: an unexpected evaluator failure is a HOOK problem, not a worker
|
|
929
1062
|
// problem, so it takes the fail-open path rather than blocking the tool loop.
|
|
930
|
-
return
|
|
1063
|
+
return unreadable();
|
|
931
1064
|
}
|
|
932
1065
|
}
|
|
933
1066
|
/** The flag through which provisioning templates the trusted worktree root. */
|
|
@@ -968,5 +1101,12 @@ export function resolveWorkerGuardContext(env, cwd, argv = []) {
|
|
|
968
1101
|
const branch = env.BAPI_WORKER_BRANCH;
|
|
969
1102
|
if (typeof branch === "string" && branch.trim().length > 0)
|
|
970
1103
|
ctx.workerBranch = branch.trim();
|
|
1104
|
+
const flagIndex = argv.indexOf("--correction-intent-version");
|
|
1105
|
+
const flagValue = flagIndex >= 0 ? argv[flagIndex + 1] : undefined;
|
|
1106
|
+
const envValue = env.BAPI_CORRECTION_INTENT_VERSION;
|
|
1107
|
+
ctx.correctionEnforced = flagValue === "1" || envValue === "1";
|
|
1108
|
+
ctx.correctionContextInvalid = flagIndex >= 0 && flagValue !== "1" ||
|
|
1109
|
+
(envValue !== undefined && envValue !== "1") ||
|
|
1110
|
+
(flagIndex >= 0 && envValue !== undefined && envValue !== flagValue);
|
|
971
1111
|
return ctx;
|
|
972
1112
|
}
|
|
@@ -1,21 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-
|
|
2
|
+
* Per-attempt worker log tee primitives (BAPI-535, TDD §8, G5; BAPI-1330).
|
|
3
|
+
*
|
|
4
|
+
* Every worker's output is teed to a log file that belongs to exactly ONE claimed
|
|
5
|
+
* attempt: `<worktree>/.conductor/worker-job<id>-attempt<n>-<claim>.log`, where
|
|
6
|
+
* `<claim>` is an opaque identifier minted fresh for each claim. The file is
|
|
7
|
+
* created EXCLUSIVELY before the first write, so a retry, a reclaim, or a reused
|
|
8
|
+
* worktree can never truncate, rotate, or append into an earlier attempt's
|
|
9
|
+
* transcript — and a reference to one attempt can never resolve to another's
|
|
10
|
+
* contents.
|
|
11
|
+
*
|
|
12
|
+
* BAPI-1330 retired the fixed `.conductor/worker.log` path. Its BAPI-1021
|
|
13
|
+
* rotation (rename the old file to a timestamped archive, truncate a new one)
|
|
14
|
+
* left the job-log registry pointing at a MUTABLE path that the next attempt
|
|
15
|
+
* reused, and a timestamp is not collision protection. There is no active alias
|
|
16
|
+
* any more: readers go through the registry, which names the stable file.
|
|
3
17
|
*
|
|
4
|
-
* Every worker's output is teed to the fixed path `<worktree>/.conductor/worker.log`
|
|
5
|
-
* in human-readable form so `tail -f` reproduces the v1 tab experience on demand.
|
|
6
18
|
* The tee is PURELY a recorder: it never owns, signals, or kills the worker
|
|
7
|
-
* process
|
|
8
|
-
*
|
|
19
|
+
* process, and a logging failure never changes a worker's disposition — it only
|
|
20
|
+
* marks this attempt's transcript unavailable. All filesystem access is behind
|
|
21
|
+
* injected boundaries so this is unit-testable with no real I/O.
|
|
9
22
|
*/
|
|
23
|
+
import { randomBytes as nodeRandomBytes } from "node:crypto";
|
|
10
24
|
import { pathApiForExecutorPlatform } from "./worktree-inspection.js";
|
|
11
25
|
/** The fixed `.conductor` worker-log directory (relative to the worktree). */
|
|
12
26
|
export const CONDUCTOR_WORKER_LOG_DIR = ".conductor";
|
|
13
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* The RETIRED fixed worker-log filename (BAPI-1330). Nothing writes it any more;
|
|
29
|
+
* it is kept only so readers can recognize a legacy registry record that names
|
|
30
|
+
* it and report that record as unavailable instead of following a mutable alias.
|
|
31
|
+
*/
|
|
14
32
|
export const CONDUCTOR_WORKER_LOG_FILE = "worker.log";
|
|
15
|
-
/**
|
|
16
|
-
export
|
|
33
|
+
/** An opaque per-claim identifier: 32 lower-case hex characters. */
|
|
34
|
+
export const WORKER_LOG_CLAIM_ID_RE = /^[0-9a-f]{32}$/;
|
|
35
|
+
/**
|
|
36
|
+
* The stable attempt-log filename shape. Groups: job id, attempt, claim id.
|
|
37
|
+
* Readers validate a registry record against this before returning a pointer.
|
|
38
|
+
*/
|
|
39
|
+
export const WORKER_LOG_ATTEMPT_FILE_RE = /^worker-job([1-9][0-9]*)-attempt([0-9]+)-([0-9a-f]{32})\.log$/;
|
|
40
|
+
/** Mint a fresh opaque claim identifier (16 random bytes, hex). */
|
|
41
|
+
export function createWorkerLogClaimId(randomBytes = nodeRandomBytes) {
|
|
42
|
+
return randomBytes(16).toString("hex");
|
|
43
|
+
}
|
|
44
|
+
/** True for a well-formed attempt identity. */
|
|
45
|
+
export function isValidWorkerLogAttemptIdentity(identity) {
|
|
46
|
+
return (Number.isInteger(identity.jobId) &&
|
|
47
|
+
identity.jobId > 0 &&
|
|
48
|
+
Number.isInteger(identity.attempt) &&
|
|
49
|
+
identity.attempt >= 0 &&
|
|
50
|
+
typeof identity.claimId === "string" &&
|
|
51
|
+
WORKER_LOG_CLAIM_ID_RE.test(identity.claimId));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The stable filename for one attempt. Throws on a malformed identity: every
|
|
55
|
+
* component is validated so nothing can escape `.conductor` or collide with
|
|
56
|
+
* another attempt's name.
|
|
57
|
+
*/
|
|
58
|
+
export function workerLogAttemptFileName(identity) {
|
|
59
|
+
if (!isValidWorkerLogAttemptIdentity(identity)) {
|
|
60
|
+
throw new Error("invalid worker log attempt identity");
|
|
61
|
+
}
|
|
62
|
+
return `worker-job${identity.jobId}-attempt${identity.attempt}-${identity.claimId}.log`;
|
|
63
|
+
}
|
|
64
|
+
/** Resolve `<worktree>/.conductor/<attempt file>` with platform-correct separators. */
|
|
65
|
+
export function buildWorkerLogPath(worktreePath, identity, platform = process.platform) {
|
|
17
66
|
const pathApi = pathApiForExecutorPlatform(platform);
|
|
18
|
-
return pathApi.join(worktreePath, CONDUCTOR_WORKER_LOG_DIR,
|
|
67
|
+
return pathApi.join(worktreePath, CONDUCTOR_WORKER_LOG_DIR, workerLogAttemptFileName(identity));
|
|
19
68
|
}
|
|
20
69
|
/**
|
|
21
70
|
* Render one stdout/stderr chunk as human-readable log text. The worker's stream
|
|
@@ -26,81 +75,97 @@ export function buildWorkerLogPath(worktreePath, platform = process.platform) {
|
|
|
26
75
|
export function renderWorkerLogChunk(chunk, _stream = "stdout") {
|
|
27
76
|
return chunk;
|
|
28
77
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* The normal, expected outcome of probing a first-ever attempt's log path —
|
|
32
|
-
* never itself a reason to warn.
|
|
33
|
-
*/
|
|
34
|
-
function isMissingFileError(err) {
|
|
35
|
-
return typeof err === "object" && err !== null && err.code === "ENOENT";
|
|
78
|
+
function errorCode(err) {
|
|
79
|
+
return typeof err === "object" && err !== null ? err.code : undefined;
|
|
36
80
|
}
|
|
37
81
|
/**
|
|
38
|
-
* The bounded, secret-free
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* creation, whether the existence probe or the rename itself failed.
|
|
82
|
+
* The bounded, secret-free transcript-unavailable warning (BAPI-1330). Carries
|
|
83
|
+
* only the job id, attempt number and a closed reason — never a path, an
|
|
84
|
+
* exception message, or transcript content.
|
|
42
85
|
*/
|
|
43
|
-
export function
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
`(${currentLogPath} -> ${archivePath}); the new attempt continues.`
|
|
47
|
-
: `worker log archive: could not confirm whether a prior attempt's log exists ` +
|
|
48
|
-
`at ${currentLogPath}; the new attempt continues.`;
|
|
86
|
+
export function formatWorkerLogUnavailableWarning(identity, reason) {
|
|
87
|
+
return (`worker log unavailable for job ${identity.jobId} attempt ${identity.attempt} (${reason}); ` +
|
|
88
|
+
"the attempt continues without a recorded transcript.");
|
|
49
89
|
}
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
* previous attempt wrote. Best-effort throughout: a missing prior log is the
|
|
60
|
-
* normal first-attempt case (no warning); a probe or rename failure degrades
|
|
61
|
-
* to exactly one bounded warning and tee creation still proceeds.
|
|
62
|
-
*/
|
|
63
|
-
export async function createWorkerLogTee(worktreePath, deps) {
|
|
64
|
-
const platform = deps.platform ?? process.platform;
|
|
65
|
-
const pathApi = pathApiForExecutorPlatform(platform);
|
|
66
|
-
const dir = pathApi.join(worktreePath, CONDUCTOR_WORKER_LOG_DIR);
|
|
67
|
-
const logPath = pathApi.join(dir, CONDUCTOR_WORKER_LOG_FILE);
|
|
68
|
-
await deps.mkdir(dir, { recursive: true });
|
|
69
|
-
if (deps.stat && deps.rename) {
|
|
90
|
+
/** Create the attempt file exclusively. Resolves with a reason when it could not. */
|
|
91
|
+
async function allocateAttemptLog(logPath, deps) {
|
|
92
|
+
try {
|
|
93
|
+
if (deps.writeFileExclusive) {
|
|
94
|
+
await deps.writeFileExclusive(logPath, "");
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (!deps.stat)
|
|
98
|
+
return "allocation_failed";
|
|
70
99
|
try {
|
|
71
100
|
await deps.stat(logPath);
|
|
72
101
|
}
|
|
73
102
|
catch (err) {
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
// Missing (or unconfirmable) is the normal first-attempt case either
|
|
78
|
-
// way: fall through to the truncating write below without a rename.
|
|
103
|
+
if (errorCode(err) !== "ENOENT")
|
|
104
|
+
return "allocation_failed";
|
|
79
105
|
await deps.writeFile(logPath, "");
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
// The current log exists: archive it before truncating.
|
|
83
|
-
const clock = deps.now ?? Date.now;
|
|
84
|
-
const timestamp = new Date(clock()).toISOString().replace(/:/g, "-");
|
|
85
|
-
const archivePath = pathApi.join(dir, `worker.${timestamp}.log`);
|
|
86
|
-
try {
|
|
87
|
-
await deps.rename(logPath, archivePath);
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
deps.errorLog?.(formatWorkerLogArchivePreservationWarning(logPath, archivePath));
|
|
106
|
+
return null;
|
|
91
107
|
}
|
|
108
|
+
return "allocation_collision";
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
return errorCode(err) === "EEXIST" ? "allocation_collision" : "allocation_failed";
|
|
92
112
|
}
|
|
93
|
-
await deps.writeFile(logPath, "");
|
|
94
|
-
return createTeeHandle(logPath, deps);
|
|
95
113
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Allocate this attempt's log file and return a tee for it.
|
|
116
|
+
*
|
|
117
|
+
* The file is created EXCLUSIVELY before any append. An existing file is refused,
|
|
118
|
+
* never truncated or archived: a collision means some other claim already owns
|
|
119
|
+
* that name, and its transcript must survive. Allocation failure does NOT throw —
|
|
120
|
+
* the tee comes back `available: false` (appends become no-ops), one bounded
|
|
121
|
+
* diagnostic is emitted, and the worker runs exactly as it would have.
|
|
122
|
+
*/
|
|
123
|
+
export async function createWorkerLogTee(worktreePath, deps, identity) {
|
|
124
|
+
const platform = deps.platform ?? process.platform;
|
|
125
|
+
const pathApi = pathApiForExecutorPlatform(platform);
|
|
126
|
+
const dir = pathApi.join(worktreePath, CONDUCTOR_WORKER_LOG_DIR);
|
|
127
|
+
const logPath = buildWorkerLogPath(worktreePath, identity, platform);
|
|
128
|
+
let reason;
|
|
129
|
+
try {
|
|
130
|
+
await deps.mkdir(dir, { recursive: true });
|
|
131
|
+
reason = await allocateAttemptLog(logPath, deps);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
reason = "allocation_failed";
|
|
135
|
+
}
|
|
136
|
+
const handle = createTeeHandle(logPath, identity, deps);
|
|
137
|
+
if (reason !== null)
|
|
138
|
+
handle.markUnavailable(reason);
|
|
139
|
+
return handle;
|
|
140
|
+
}
|
|
141
|
+
/** Shared tee-handle construction. */
|
|
142
|
+
function createTeeHandle(logPath, identity, deps) {
|
|
98
143
|
let buffer = "";
|
|
99
144
|
let closed = false;
|
|
145
|
+
let unavailableReason;
|
|
146
|
+
const markUnavailable = (reason) => {
|
|
147
|
+
if (unavailableReason !== undefined)
|
|
148
|
+
return;
|
|
149
|
+
unavailableReason = reason;
|
|
150
|
+
try {
|
|
151
|
+
deps.errorLog?.(formatWorkerLogUnavailableWarning(identity, reason));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
/* the diagnostic is best-effort too */
|
|
155
|
+
}
|
|
156
|
+
};
|
|
100
157
|
return {
|
|
101
158
|
logPath,
|
|
159
|
+
identity,
|
|
160
|
+
get available() {
|
|
161
|
+
return unavailableReason === undefined;
|
|
162
|
+
},
|
|
163
|
+
get unavailableReason() {
|
|
164
|
+
return unavailableReason;
|
|
165
|
+
},
|
|
166
|
+
markUnavailable,
|
|
102
167
|
async append(chunk) {
|
|
103
|
-
if (closed)
|
|
168
|
+
if (closed || unavailableReason !== undefined)
|
|
104
169
|
return;
|
|
105
170
|
try {
|
|
106
171
|
if (deps.appendFile) {
|
|
@@ -112,7 +177,9 @@ function createTeeHandle(logPath, deps) {
|
|
|
112
177
|
}
|
|
113
178
|
}
|
|
114
179
|
catch {
|
|
115
|
-
|
|
180
|
+
// A write failure never fails a job; it makes this attempt's transcript
|
|
181
|
+
// incomplete, which readers must be told rather than left to guess.
|
|
182
|
+
markUnavailable("append_failed");
|
|
116
183
|
}
|
|
117
184
|
},
|
|
118
185
|
async close() {
|
|
@@ -146,7 +213,7 @@ export const MAX_SESSION_SCAN_CHARS = 2_000_000;
|
|
|
146
213
|
* The DISTINCT `session_id` values present in a worker log (BAPI-793).
|
|
147
214
|
*
|
|
148
215
|
* The incident signature this exists to catch is two Claude sessions writing
|
|
149
|
-
* into one
|
|
216
|
+
* into one attempt's worker log. Every stream-json record a
|
|
150
217
|
* worker emits carries its `session_id`, so one healthy job produces MANY
|
|
151
218
|
* records and exactly ONE distinct id — which is why this counts DISTINCT ids
|
|
152
219
|
* rather than occurrences. A count-based check would flag every normal job.
|
|
@@ -210,7 +277,7 @@ export function distinctWorkerLogSessionIds(logText) {
|
|
|
210
277
|
*/
|
|
211
278
|
export function formatWorkerLogSessionInvariantWarning(sessionCount) {
|
|
212
279
|
return (`worker log session invariant violated: ${sessionCount} distinct Claude sessions wrote to this ` +
|
|
213
|
-
"
|
|
280
|
+
"attempt's .conductor worker log, so more than one worker was active in this worktree. The " +
|
|
214
281
|
"worktree lock should have refused the second one — treat this as evidence it was bypassed " +
|
|
215
282
|
"(a pre-existing stale lock, a worktree entered outside the executor, or a manual claude run).");
|
|
216
283
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* never abort the whole sweep. All I/O is injected for offline unit testing.
|
|
11
11
|
*/
|
|
12
12
|
import { listGitWorktrees, pathApiForExecutorPlatform, } from "./worktree-inspection.js";
|
|
13
|
+
import { WORKER_LOG_ATTEMPT_FILE_RE } from "./worker-log.js";
|
|
13
14
|
/** Idle cutoff: a worktree must be idle strictly longer than 24h to be eligible. */
|
|
14
15
|
export const EXECUTOR_WORKTREE_GC_IDLE_MS = 24 * 60 * 60 * 1000;
|
|
15
16
|
function conductorMarkerPaths(worktreePath, platform) {
|
|
@@ -17,30 +18,53 @@ function conductorMarkerPaths(worktreePath, platform) {
|
|
|
17
18
|
const dir = pathApi.join(worktreePath, ".conductor");
|
|
18
19
|
return {
|
|
19
20
|
dir,
|
|
20
|
-
workerLog: pathApi.join(dir, "worker.log"),
|
|
21
21
|
resultJson: pathApi.join(dir, "result.json"),
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* The per-attempt worker logs in `.conductor` (BAPI-1330). Only names matching
|
|
26
|
+
* the stable attempt-log shape count; the retired fixed `worker.log` is never
|
|
27
|
+
* probed. A listing failure yields none — the other markers still apply.
|
|
28
|
+
*/
|
|
29
|
+
async function attemptLogPaths(dir, deps) {
|
|
30
|
+
if (!deps.listDir)
|
|
31
|
+
return [];
|
|
32
|
+
let names;
|
|
33
|
+
try {
|
|
34
|
+
names = await deps.listDir(dir);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const pathApi = pathApiForExecutorPlatform(deps.platform ?? process.platform);
|
|
40
|
+
return names.filter((name) => WORKER_LOG_ATTEMPT_FILE_RE.test(name)).map((name) => pathApi.join(dir, name));
|
|
41
|
+
}
|
|
24
42
|
/**
|
|
25
43
|
* Conservatively identify an executor/conductor-managed worktree by the presence
|
|
26
|
-
* of a `.conductor` directory
|
|
44
|
+
* of a `.conductor` directory or `.conductor/result.json`.
|
|
27
45
|
*/
|
|
28
46
|
export async function isExecutorManagedWorktree(worktreePath, deps) {
|
|
29
47
|
const markers = conductorMarkerPaths(worktreePath, deps.platform ?? process.platform);
|
|
30
|
-
for (const marker of [markers.
|
|
48
|
+
for (const marker of [markers.resultJson, markers.dir]) {
|
|
31
49
|
if ((await deps.statMtimeMs(marker)) !== null)
|
|
32
50
|
return true;
|
|
33
51
|
}
|
|
34
52
|
return false;
|
|
35
53
|
}
|
|
36
54
|
/**
|
|
37
|
-
* Compute last activity as the NEWEST mtime among
|
|
38
|
-
* `.conductor/result.json`,
|
|
55
|
+
* Compute last activity as the NEWEST mtime among the per-attempt worker logs
|
|
56
|
+
* in `.conductor` (the newest attempt log wins), `.conductor/result.json`, the
|
|
57
|
+
* `.conductor` directory, and the worktree directory itself. Returns `null`
|
|
39
58
|
* when none can be stat'd (activity cannot be proven → the caller retains).
|
|
40
59
|
*/
|
|
41
60
|
export async function getWorktreeLastActivityMs(worktreePath, deps) {
|
|
42
61
|
const markers = conductorMarkerPaths(worktreePath, deps.platform ?? process.platform);
|
|
43
|
-
const candidates = [
|
|
62
|
+
const candidates = [
|
|
63
|
+
...(await attemptLogPaths(markers.dir, deps)),
|
|
64
|
+
markers.resultJson,
|
|
65
|
+
markers.dir,
|
|
66
|
+
worktreePath,
|
|
67
|
+
];
|
|
44
68
|
let newest = null;
|
|
45
69
|
for (const candidate of candidates) {
|
|
46
70
|
const mtime = await deps.statMtimeMs(candidate);
|
|
@@ -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
|
+
}
|