@prover-coder-ai/docker-git 1.0.31 → 1.0.32

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.
@@ -361,6 +361,8 @@ class DockerAccessError extends Data.TaggedError("DockerAccessError") {
361
361
  }
362
362
  class CloneFailedError extends Data.TaggedError("CloneFailedError") {
363
363
  }
364
+ class AgentFailedError extends Data.TaggedError("AgentFailedError") {
365
+ }
364
366
  class PortProbeError extends Data.TaggedError("PortProbeError") {
365
367
  }
366
368
  class CommandFailedError extends Data.TaggedError("CommandFailedError") {
@@ -777,23 +779,26 @@ const renderDockerAccessActionPlan = (issue) => {
777
779
  ];
778
780
  return issue === "PermissionDenied" ? permissionDeniedPlan.join("\n") : daemonUnavailablePlan.join("\n");
779
781
  };
782
+ const renderDockerCommandError = ({ exitCode }) => [
783
+ `docker compose failed with exit code ${exitCode}`,
784
+ "Hint: ensure Docker daemon is running and current user can access /var/run/docker.sock (for example via the docker group).",
785
+ "Hint: if output above contains 'port is already allocated', retry with a free SSH port via --ssh-port <port> (for example --ssh-port 2235), or stop the conflicting project/container.",
786
+ "Hint: if output above contains 'all predefined address pools have been fully subnetted', run `docker network prune -f`, configure Docker `default-address-pools`, or use shared network mode (`--network-mode shared`).",
787
+ "Hint: if output above contains 'lookup auth.docker.io' or 'read udp ... [::1]:53 ... connection refused', fix Docker DNS resolver (set working DNS in host/daemon config) and retry."
788
+ ].join("\n");
789
+ const renderDockerAccessError = ({ details, issue }) => [
790
+ renderDockerAccessHeadline(issue),
791
+ "Hint: ensure Docker daemon is running and current user can access the docker socket.",
792
+ "Hint: if you use rootless Docker, set DOCKER_HOST to your user socket (for example unix:///run/user/$UID/docker.sock).",
793
+ renderDockerAccessActionPlan(issue),
794
+ `Details: ${details}`
795
+ ].join("\n");
780
796
  const renderPrimaryError = (error) => Match.value(error).pipe(
781
797
  Match.when({ _tag: "FileExistsError" }, ({ path }) => `File already exists: ${path} (use --force to overwrite)`),
782
- Match.when({ _tag: "DockerCommandError" }, ({ exitCode }) => [
783
- `docker compose failed with exit code ${exitCode}`,
784
- "Hint: ensure Docker daemon is running and current user can access /var/run/docker.sock (for example via the docker group).",
785
- "Hint: if output above contains 'port is already allocated', retry with a free SSH port via --ssh-port <port> (for example --ssh-port 2235), or stop the conflicting project/container.",
786
- "Hint: if output above contains 'all predefined address pools have been fully subnetted', run `docker network prune -f`, configure Docker `default-address-pools`, or use shared network mode (`--network-mode shared`).",
787
- "Hint: if output above contains 'lookup auth.docker.io' or 'read udp ... [::1]:53 ... connection refused', fix Docker DNS resolver (set working DNS in host/daemon config) and retry."
788
- ].join("\n")),
789
- Match.when({ _tag: "DockerAccessError" }, ({ details, issue }) => [
790
- renderDockerAccessHeadline(issue),
791
- "Hint: ensure Docker daemon is running and current user can access the docker socket.",
792
- "Hint: if you use rootless Docker, set DOCKER_HOST to your user socket (for example unix:///run/user/$UID/docker.sock).",
793
- renderDockerAccessActionPlan(issue),
794
- `Details: ${details}`
795
- ].join("\n")),
798
+ Match.when({ _tag: "DockerCommandError" }, renderDockerCommandError),
799
+ Match.when({ _tag: "DockerAccessError" }, renderDockerAccessError),
796
800
  Match.when({ _tag: "CloneFailedError" }, ({ repoRef, repoUrl, targetDir }) => `Clone failed for ${repoUrl} (${repoRef}) into ${targetDir}`),
801
+ Match.when({ _tag: "AgentFailedError" }, ({ agentMode, targetDir }) => `Agent (${agentMode}) failed in ${targetDir}`),
797
802
  Match.when({ _tag: "PortProbeError" }, ({ message, port }) => `SSH port check failed for ${port}: ${message}`),
798
803
  Match.when(
799
804
  { _tag: "CommandFailedError" },
@@ -2249,6 +2254,8 @@ GITHUB_TOKEN="\${GITHUB_TOKEN:-\${GH_TOKEN:-}}"
2249
2254
  GIT_USER_NAME="\${GIT_USER_NAME:-}"
2250
2255
  GIT_USER_EMAIL="\${GIT_USER_EMAIL:-}"
2251
2256
  CODEX_AUTO_UPDATE="\${CODEX_AUTO_UPDATE:-1}"
2257
+ AGENT_MODE="\${AGENT_MODE:-}"
2258
+ AGENT_AUTO="\${AGENT_AUTO:-}"
2252
2259
  MCP_PLAYWRIGHT_ENABLE="\${MCP_PLAYWRIGHT_ENABLE:-${config.enableMcpPlaywright ? "1" : "0"}}"
2253
2260
  MCP_PLAYWRIGHT_CDP_ENDPOINT="\${MCP_PLAYWRIGHT_CDP_ENDPOINT:-}"
2254
2261
  MCP_PLAYWRIGHT_ISOLATED="\${MCP_PLAYWRIGHT_ISOLATED:-1}"
@@ -3568,6 +3575,164 @@ EOF
3568
3575
  chown 1000:1000 "$OPENCODE_CONFIG_JSON" || true
3569
3576
  fi`;
3570
3577
  const renderEntrypointOpenCodeConfig = (config) => entrypointOpenCodeTemplate.replaceAll("__SSH_USER__", config.sshUser).replaceAll("__CODEX_HOME__", config.codexHome);
3578
+ const indentBlock = (block, size = 2) => {
3579
+ const prefix = " ".repeat(size);
3580
+ return block.split("\n").map((line) => `${prefix}${line}`).join("\n");
3581
+ };
3582
+ const renderAgentPrompt = () => String.raw`AGENT_PROMPT=""
3583
+ ISSUE_NUM=""
3584
+ if [[ "$REPO_REF" =~ ^issue-([0-9]+)$ ]]; then
3585
+ ISSUE_NUM="${"${"}BASH_REMATCH[1]}"
3586
+ fi
3587
+
3588
+ if [[ "$AGENT_AUTO" == "1" ]]; then
3589
+ if [[ -n "$ISSUE_NUM" ]]; then
3590
+ AGENT_PROMPT="Read GitHub issue #$ISSUE_NUM for this repository (use gh issue view $ISSUE_NUM). Implement the requested changes, commit them, create a PR that closes #$ISSUE_NUM, and push it."
3591
+ else
3592
+ AGENT_PROMPT="Analyze this repository, implement any pending tasks, commit changes, create a PR, and push it."
3593
+ fi
3594
+ fi`;
3595
+ const renderAgentSetup = () => [
3596
+ String.raw`AGENT_DONE_PATH="/run/docker-git/agent.done"
3597
+ AGENT_FAIL_PATH="/run/docker-git/agent.failed"
3598
+ AGENT_PROMPT_FILE="/run/docker-git/agent-prompt.txt"
3599
+ rm -f "$AGENT_DONE_PATH" "$AGENT_FAIL_PATH" "$AGENT_PROMPT_FILE"`,
3600
+ String.raw`# Collect tokens for agent environment (su - dev does not always inherit profile.d)
3601
+ AGENT_ENV_FILE="/run/docker-git/agent-env.sh"
3602
+ {
3603
+ [[ -f /etc/profile.d/gh-token.sh ]] && cat /etc/profile.d/gh-token.sh
3604
+ [[ -f /etc/profile.d/claude-config.sh ]] && cat /etc/profile.d/claude-config.sh
3605
+ } > "$AGENT_ENV_FILE" 2>/dev/null || true
3606
+ chmod 644 "$AGENT_ENV_FILE"`,
3607
+ renderAgentPrompt(),
3608
+ String.raw`AGENT_OK=0
3609
+ if [[ -n "$AGENT_PROMPT" ]]; then
3610
+ printf "%s" "$AGENT_PROMPT" > "$AGENT_PROMPT_FILE"
3611
+ chmod 644 "$AGENT_PROMPT_FILE"
3612
+ fi`
3613
+ ].join("\n\n");
3614
+ const renderAgentPromptCommand = (mode) => mode === "claude" ? String.raw`claude --dangerously-skip-permissions -p \"\$(cat $AGENT_PROMPT_FILE)\"` : String.raw`codex --approval-mode full-auto \"\$(cat $AGENT_PROMPT_FILE)\"`;
3615
+ const renderAgentModeBlock = (config, mode) => {
3616
+ const startMessage = `[agent] starting ${mode}...`;
3617
+ const interactiveMessage = `[agent] ${mode} started in interactive mode (use SSH to connect)`;
3618
+ return String.raw`"${mode}")
3619
+ echo "${startMessage}"
3620
+ if [[ -n "$AGENT_PROMPT" ]]; then
3621
+ if su - ${config.sshUser} \
3622
+ -c ". /run/docker-git/agent-env.sh 2>/dev/null; cd '$TARGET_DIR' && ${renderAgentPromptCommand(mode)}"; then
3623
+ AGENT_OK=1
3624
+ fi
3625
+ else
3626
+ echo "${interactiveMessage}"
3627
+ AGENT_OK=1
3628
+ fi
3629
+ ;;`;
3630
+ };
3631
+ const renderAgentModeCase = (config) => [
3632
+ String.raw`case "$AGENT_MODE" in`,
3633
+ indentBlock(renderAgentModeBlock(config, "claude")),
3634
+ indentBlock(renderAgentModeBlock(config, "codex")),
3635
+ indentBlock(
3636
+ String.raw`*)
3637
+ echo "[agent] unknown agent mode: $AGENT_MODE"
3638
+ ;;`
3639
+ ),
3640
+ "esac"
3641
+ ].join("\n");
3642
+ const renderAgentIssueComment = (config) => String.raw`echo "[agent] posting review comment to issue #$ISSUE_NUM..."
3643
+
3644
+ PR_BODY=""
3645
+ PR_BODY=$(su - ${config.sshUser} -c ". /run/docker-git/agent-env.sh 2>/dev/null; cd '$TARGET_DIR' && gh pr list --head '$REPO_REF' --json body --jq '.[0].body'" 2>/dev/null) || true
3646
+
3647
+ if [[ -z "$PR_BODY" ]]; then
3648
+ PR_BODY=$(su - ${config.sshUser} -c ". /run/docker-git/agent-env.sh 2>/dev/null; cd '$TARGET_DIR' && git log --format='%B' -1" 2>/dev/null) || true
3649
+ fi
3650
+
3651
+ if [[ -n "$PR_BODY" ]]; then
3652
+ COMMENT_FILE="/run/docker-git/agent-comment.txt"
3653
+ printf "%s" "$PR_BODY" > "$COMMENT_FILE"
3654
+ chmod 644 "$COMMENT_FILE"
3655
+ su - ${config.sshUser} -c ". /run/docker-git/agent-env.sh 2>/dev/null; cd '$TARGET_DIR' && gh issue comment '$ISSUE_NUM' --body-file '$COMMENT_FILE'" || echo "[agent] failed to comment on issue #$ISSUE_NUM"
3656
+ else
3657
+ echo "[agent] no PR body or commit message found, skipping comment"
3658
+ fi`;
3659
+ const renderProjectMoveScript = () => String.raw`#!/bin/bash
3660
+ . /run/docker-git/agent-env.sh 2>/dev/null || true
3661
+ cd "$1" || exit 1
3662
+ ISSUE_NUM="$2"
3663
+
3664
+ ISSUE_NODE_ID=$(gh issue view "$ISSUE_NUM" --json id --jq '.id' 2>/dev/null) || true
3665
+ if [[ -z "$ISSUE_NODE_ID" ]]; then
3666
+ echo "[agent] could not get issue node ID, skipping move"
3667
+ exit 0
3668
+ fi
3669
+
3670
+ GQL_QUERY='query($nodeId: ID!) { node(id: $nodeId) { ... on Issue { projectItems(first: 1) { nodes { id project { id field(name: "Status") { ... on ProjectV2SingleSelectField { id options { id name } } } } } } } } }'
3671
+ ALL_IDS=$(gh api graphql -F nodeId="$ISSUE_NODE_ID" -f query="$GQL_QUERY" \
3672
+ --jq '(.data.node.projectItems.nodes // [])[0] // empty | [.id, .project.id, .project.field.id, ([.project.field.options[] | select(.name | test("review"; "i"))][0].id)] | @tsv' 2>/dev/null) || true
3673
+
3674
+ if [[ -z "$ALL_IDS" ]]; then
3675
+ echo "[agent] issue #$ISSUE_NUM is not in a project board, skipping move"
3676
+ exit 0
3677
+ fi
3678
+
3679
+ ITEM_ID=$(printf "%s" "$ALL_IDS" | cut -f1)
3680
+ PROJECT_ID=$(printf "%s" "$ALL_IDS" | cut -f2)
3681
+ STATUS_FIELD_ID=$(printf "%s" "$ALL_IDS" | cut -f3)
3682
+ REVIEW_OPTION_ID=$(printf "%s" "$ALL_IDS" | cut -f4)
3683
+ if [[ -z "$STATUS_FIELD_ID" || -z "$REVIEW_OPTION_ID" || "$STATUS_FIELD_ID" == "null" || "$REVIEW_OPTION_ID" == "null" ]]; then
3684
+ echo "[agent] review status not found in project board, skipping move"
3685
+ exit 0
3686
+ fi
3687
+
3688
+ MUTATION='mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { updateProjectV2ItemFieldValue(input: { projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: { singleSelectOptionId: $optionId } }) { projectV2Item { id } } }'
3689
+ MOVE_RESULT=$(gh api graphql \
3690
+ -F projectId="$PROJECT_ID" \
3691
+ -F itemId="$ITEM_ID" \
3692
+ -F fieldId="$STATUS_FIELD_ID" \
3693
+ -F optionId="$REVIEW_OPTION_ID" \
3694
+ -f query="$MUTATION" 2>&1) || true
3695
+
3696
+ if [[ "$MOVE_RESULT" == *"projectV2Item"* ]]; then
3697
+ echo "[agent] issue #$ISSUE_NUM moved to review"
3698
+ else
3699
+ echo "[agent] failed to move issue #$ISSUE_NUM in project board"
3700
+ fi`;
3701
+ const renderAgentIssueMove = (config) => [
3702
+ String.raw`echo "[agent] moving issue #$ISSUE_NUM to review..."
3703
+ MOVE_SCRIPT="/run/docker-git/project-move.sh"`,
3704
+ String.raw`cat > "$MOVE_SCRIPT" << 'EOFMOVE'
3705
+ ${renderProjectMoveScript()}
3706
+ EOFMOVE`,
3707
+ String.raw`chmod +x "$MOVE_SCRIPT"
3708
+ su - ${config.sshUser} -c "$MOVE_SCRIPT '$TARGET_DIR' '$ISSUE_NUM'" || true`
3709
+ ].join("\n");
3710
+ const renderAgentIssueReview = (config) => [
3711
+ String.raw`if [[ "$AGENT_OK" -eq 1 && "$AGENT_AUTO" == "1" && -n "$ISSUE_NUM" ]]; then`,
3712
+ indentBlock(renderAgentIssueComment(config)),
3713
+ "",
3714
+ renderAgentIssueMove(config),
3715
+ "fi"
3716
+ ].join("\n");
3717
+ const renderAgentFinalize = () => String.raw`if [[ "$AGENT_OK" -eq 1 ]]; then
3718
+ echo "[agent] done"
3719
+ touch "$AGENT_DONE_PATH"
3720
+ else
3721
+ echo "[agent] failed"
3722
+ touch "$AGENT_FAIL_PATH"
3723
+ fi`;
3724
+ const renderAgentLaunch = (config) => [
3725
+ String.raw`# 3) Auto-launch agent if AGENT_MODE is set
3726
+ if [[ "$CLONE_OK" -eq 1 && -n "$AGENT_MODE" ]]; then`,
3727
+ indentBlock(renderAgentSetup()),
3728
+ "",
3729
+ indentBlock(renderAgentModeCase(config)),
3730
+ "",
3731
+ renderAgentIssueReview(config),
3732
+ "",
3733
+ indentBlock(renderAgentFinalize()),
3734
+ "fi"
3735
+ ].join("\n");
3571
3736
  const renderEntrypointAutoUpdate = () => `# 1) Keep Codex CLI up to date if requested (bun only)
3572
3737
  if [[ "$CODEX_AUTO_UPDATE" == "1" ]]; then
3573
3738
  if command -v bun >/dev/null 2>&1; then
@@ -3744,6 +3909,8 @@ const renderEntrypointBackgroundTasks = (config) => `# 4) Start background tasks
3744
3909
  ${renderEntrypointAutoUpdate()}
3745
3910
 
3746
3911
  ${renderEntrypointClone(config)}
3912
+
3913
+ ${renderAgentLaunch(config)}
3747
3914
  ) &`;
3748
3915
  const renderEntrypoint = (config) => [
3749
3916
  renderEntrypointHeader(config),
@@ -3779,6 +3946,10 @@ const renderCodexAuthLabelEnv = (codexAuthLabel) => codexAuthLabel.length > 0 ?
3779
3946
  ` : "";
3780
3947
  const renderClaudeAuthLabelEnv = (claudeAuthLabel) => claudeAuthLabel.length > 0 ? ` CLAUDE_AUTH_LABEL: "${claudeAuthLabel}"
3781
3948
  ` : "";
3949
+ const renderAgentModeEnv = (agentMode) => agentMode !== void 0 && agentMode.length > 0 ? ` AGENT_MODE: "${agentMode}"
3950
+ ` : "";
3951
+ const renderAgentAutoEnv = (agentAuto) => agentAuto === true ? ` AGENT_AUTO: "1"
3952
+ ` : "";
3782
3953
  const buildPlaywrightFragments = (config, networkName) => {
3783
3954
  if (!config.enableMcpPlaywright) {
3784
3955
  return {
@@ -3831,6 +4002,8 @@ const buildComposeFragments = (config) => {
3831
4002
  const maybeGitTokenLabelEnv = renderGitTokenLabelEnv(gitTokenLabel);
3832
4003
  const maybeCodexAuthLabelEnv = renderCodexAuthLabelEnv(codexAuthLabel);
3833
4004
  const maybeClaudeAuthLabelEnv = renderClaudeAuthLabelEnv(claudeAuthLabel);
4005
+ const maybeAgentModeEnv = renderAgentModeEnv(config.agentMode);
4006
+ const maybeAgentAutoEnv = renderAgentAutoEnv(config.agentAuto);
3834
4007
  const playwright = buildPlaywrightFragments(config, networkName);
3835
4008
  return {
3836
4009
  networkMode,
@@ -3838,6 +4011,8 @@ const buildComposeFragments = (config) => {
3838
4011
  maybeGitTokenLabelEnv,
3839
4012
  maybeCodexAuthLabelEnv,
3840
4013
  maybeClaudeAuthLabelEnv,
4014
+ maybeAgentModeEnv,
4015
+ maybeAgentAutoEnv,
3841
4016
  maybeDependsOn: playwright.maybeDependsOn,
3842
4017
  maybePlaywrightEnv: playwright.maybePlaywrightEnv,
3843
4018
  maybeBrowserService: playwright.maybeBrowserService,
@@ -3856,7 +4031,7 @@ const renderComposeServices = (config, fragments) => `services:
3856
4031
  FORK_REPO_URL: "${fragments.forkRepoUrl}"
3857
4032
  ${fragments.maybeGitTokenLabelEnv} # Optional token label selector (maps to GITHUB_TOKEN__<LABEL>/GIT_AUTH_TOKEN__<LABEL>)
3858
4033
  ${fragments.maybeCodexAuthLabelEnv} # Optional Codex account label selector (maps to CODEX_AUTH_LABEL)
3859
- ${fragments.maybeClaudeAuthLabelEnv} # Optional Claude account label selector (maps to CLAUDE_AUTH_LABEL)
4034
+ ${fragments.maybeClaudeAuthLabelEnv}${fragments.maybeAgentModeEnv}${fragments.maybeAgentAutoEnv} # Optional Claude account label selector (maps to CLAUDE_AUTH_LABEL)
3860
4035
  TARGET_DIR: "${config.targetDir}"
3861
4036
  CODEX_HOME: "${config.codexHome}"
3862
4037
  ${fragments.maybePlaywrightEnv}${fragments.maybeDependsOn} env_file:
@@ -5407,8 +5582,11 @@ const listProjectStatus = Effect.asVoid(
5407
5582
  )
5408
5583
  );
5409
5584
  const clonePollInterval = Duration.seconds(1);
5585
+ const agentPollInterval = Duration.seconds(2);
5410
5586
  const cloneDonePath = "/run/docker-git/clone.done";
5411
5587
  const cloneFailPath = "/run/docker-git/clone.failed";
5588
+ const agentDonePath = "/run/docker-git/agent.done";
5589
+ const agentFailPath = "/run/docker-git/agent.failed";
5412
5590
  const logSshAccess = (baseDir, config) => Effect.gen(function* (_) {
5413
5591
  const fs = yield* _(FileSystem.FileSystem);
5414
5592
  const path = yield* _(Path.Path);
@@ -5467,6 +5645,47 @@ const waitForCloneCompletion = (cwd, config) => Effect.gen(function* (_) {
5467
5645
  );
5468
5646
  }
5469
5647
  });
5648
+ const checkAgentState = (cwd, containerName) => Effect.gen(function* (_) {
5649
+ const failed = yield* _(runDockerExecExitCode(cwd, containerName, ["test", "-f", agentFailPath]));
5650
+ if (failed === 0) {
5651
+ return "failed";
5652
+ }
5653
+ const done = yield* _(runDockerExecExitCode(cwd, containerName, ["test", "-f", agentDonePath]));
5654
+ return done === 0 ? "done" : "pending";
5655
+ });
5656
+ const waitForAgentCompletion = (cwd, config) => Effect.gen(function* (_) {
5657
+ const logsFiber = yield* _(
5658
+ runDockerComposeLogsFollow(cwd).pipe(
5659
+ Effect.tapError(
5660
+ (error) => Effect.logWarning(
5661
+ `docker compose logs --follow failed: ${error instanceof Error ? error.message : String(error)}`
5662
+ )
5663
+ ),
5664
+ Effect.fork
5665
+ )
5666
+ );
5667
+ const result = yield* _(
5668
+ checkAgentState(cwd, config.containerName).pipe(
5669
+ Effect.repeat(
5670
+ Schedule.addDelay(
5671
+ Schedule.recurUntil((state) => state !== "pending"),
5672
+ () => agentPollInterval
5673
+ )
5674
+ )
5675
+ )
5676
+ );
5677
+ yield* _(Fiber$1.interrupt(logsFiber));
5678
+ if (result === "failed") {
5679
+ return yield* _(
5680
+ Effect.fail(
5681
+ new AgentFailedError({
5682
+ agentMode: config.agentMode ?? "unknown",
5683
+ targetDir: config.targetDir
5684
+ })
5685
+ )
5686
+ );
5687
+ }
5688
+ });
5470
5689
  const runDockerComposeUpByMode = (resolvedOutDir, projectConfig, force, forceEnv) => Effect.gen(function* (_) {
5471
5690
  yield* _(ensureComposeNetworkReady(resolvedOutDir, projectConfig));
5472
5691
  if (force) {
@@ -5511,9 +5730,16 @@ const runDockerUpIfNeeded = (resolvedOutDir, projectConfig, options) => Effect.g
5511
5730
  yield* _(Effect.log("Streaming container logs until clone completes..."));
5512
5731
  yield* _(waitForCloneCompletion(resolvedOutDir, projectConfig));
5513
5732
  }
5733
+ if (options.waitForAgent) {
5734
+ yield* _(Effect.log("Waiting for agent to complete..."));
5735
+ yield* _(waitForAgentCompletion(resolvedOutDir, projectConfig));
5736
+ }
5514
5737
  yield* _(Effect.log("Docker environment is up"));
5515
5738
  yield* _(logSshAccess(resolvedOutDir, projectConfig));
5516
5739
  });
5740
+ const runDockerDownCleanup = (resolvedOutDir) => runDockerComposeDownVolumes(resolvedOutDir).pipe(
5741
+ Effect.tap(() => Effect.log("Container and volumes removed."))
5742
+ );
5517
5743
  const resolvePathFromBase = (path, baseDir, targetPath) => path.isAbsolute(targetPath) ? targetPath : path.resolve(baseDir, targetPath);
5518
5744
  const toPosixPath = (value) => value.replaceAll("\\", "/");
5519
5745
  const resolveDockerGitRootRelativePath = (path, projectsRoot, inputPath) => {
@@ -5712,7 +5938,7 @@ const formatStateSyncLabel = (repoUrl) => {
5712
5938
  return repoPath.length > 0 ? repoPath : repoUrl;
5713
5939
  };
5714
5940
  const isInteractiveTty = () => process.stdin.isTTY && process.stdout.isTTY;
5715
- const buildSshArgs = (config, sshKeyPath) => {
5941
+ const buildSshArgs = (config, sshKeyPath, remoteCommand) => {
5716
5942
  const args = [];
5717
5943
  if (sshKeyPath !== null) {
5718
5944
  args.push("-i", sshKeyPath);
@@ -5730,21 +5956,25 @@ const buildSshArgs = (config, sshKeyPath) => {
5730
5956
  String(config.sshPort),
5731
5957
  `${config.sshUser}@localhost`
5732
5958
  );
5959
+ if (remoteCommand !== void 0) {
5960
+ args.push(remoteCommand);
5961
+ }
5733
5962
  return args;
5734
5963
  };
5735
- const openSshBestEffort = (template) => Effect.gen(function* (_) {
5964
+ const openSshBestEffort = (template, remoteCommand) => Effect.gen(function* (_) {
5736
5965
  const fs = yield* _(FileSystem.FileSystem);
5737
5966
  const path = yield* _(Path.Path);
5738
5967
  const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd()));
5739
5968
  const sshCommand = buildSshCommand(template, sshKey);
5740
- yield* _(Effect.log(`Opening SSH: ${sshCommand}`));
5969
+ const remoteCommandLabel = remoteCommand === void 0 ? "" : ` (${remoteCommand})`;
5970
+ yield* _(Effect.log(`Opening SSH: ${sshCommand}${remoteCommandLabel}`));
5741
5971
  yield* _(ensureTerminalCursorVisible());
5742
5972
  yield* _(
5743
5973
  runCommandWithExitCodes(
5744
5974
  {
5745
5975
  cwd: process.cwd(),
5746
5976
  command: "ssh",
5747
- args: buildSshArgs(template, sshKey)
5977
+ args: buildSshArgs(template, sshKey, remoteCommand)
5748
5978
  },
5749
5979
  [0, 130],
5750
5980
  (exitCode) => new CommandFailedError({ command: "ssh", exitCode })
@@ -5757,6 +5987,23 @@ const openSshBestEffort = (template) => Effect.gen(function* (_) {
5757
5987
  onSuccess: () => Effect.void
5758
5988
  })
5759
5989
  );
5990
+ const resolveInteractiveRemoteCommand = (projectConfig, interactiveAgent) => interactiveAgent && projectConfig.agentMode !== void 0 ? `cd '${projectConfig.targetDir}' && ${projectConfig.agentMode}` : void 0;
5991
+ const maybeOpenSsh = (command, hasAgent, waitForAgent, projectConfig) => Effect.gen(function* (_) {
5992
+ const interactiveAgent = hasAgent && !waitForAgent;
5993
+ if (!command.openSsh || hasAgent && !interactiveAgent) {
5994
+ return;
5995
+ }
5996
+ if (!command.runUp) {
5997
+ yield* _(Effect.logWarning("Skipping SSH auto-open: docker compose up disabled (--no-up)."));
5998
+ return;
5999
+ }
6000
+ if (!isInteractiveTty()) {
6001
+ yield* _(Effect.logWarning("Skipping SSH auto-open: not running in an interactive TTY."));
6002
+ return;
6003
+ }
6004
+ const remoteCommand = resolveInteractiveRemoteCommand(projectConfig, interactiveAgent);
6005
+ yield* _(openSshBestEffort(projectConfig, remoteCommand));
6006
+ }).pipe(Effect.asVoid);
5760
6007
  const runCreateProject = (path, command) => Effect.gen(function* (_) {
5761
6008
  if (command.runUp) {
5762
6009
  yield* _(ensureDockerDaemonAccess(process.cwd()));
@@ -5773,10 +6020,13 @@ const runCreateProject = (path, command) => Effect.gen(function* (_) {
5773
6020
  })
5774
6021
  );
5775
6022
  yield* _(logCreatedProject(resolvedOutDir, createdFiles));
6023
+ const hasAgent = resolvedConfig.agentMode !== void 0;
6024
+ const waitForAgent = hasAgent && (resolvedConfig.agentAuto ?? false);
5776
6025
  yield* _(
5777
6026
  runDockerUpIfNeeded(resolvedOutDir, projectConfig, {
5778
6027
  runUp: command.runUp,
5779
6028
  waitForClone: command.waitForClone,
6029
+ waitForAgent,
5780
6030
  force: command.force,
5781
6031
  forceEnv: command.forceEnv
5782
6032
  })
@@ -5784,16 +6034,12 @@ const runCreateProject = (path, command) => Effect.gen(function* (_) {
5784
6034
  if (command.runUp) {
5785
6035
  yield* _(logDockerAccessInfo(resolvedOutDir, projectConfig));
5786
6036
  }
5787
- yield* _(autoSyncState(`chore(state): update ${formatStateSyncLabel(projectConfig.repoUrl)}`));
5788
- if (command.openSsh) {
5789
- if (!command.runUp) {
5790
- yield* _(Effect.logWarning("Skipping SSH auto-open: docker compose up disabled (--no-up)."));
5791
- } else if (isInteractiveTty()) {
5792
- yield* _(openSshBestEffort(projectConfig));
5793
- } else {
5794
- yield* _(Effect.logWarning("Skipping SSH auto-open: not running in an interactive TTY."));
5795
- }
6037
+ if (waitForAgent) {
6038
+ yield* _(Effect.log("Agent finished. Cleaning up container..."));
6039
+ yield* _(runDockerDownCleanup(resolvedOutDir));
5796
6040
  }
6041
+ yield* _(autoSyncState(`chore(state): update ${formatStateSyncLabel(projectConfig.repoUrl)}`));
6042
+ yield* _(maybeOpenSsh(command, hasAgent, waitForAgent, projectConfig));
5797
6043
  }).pipe(Effect.asVoid);
5798
6044
  const createProject = (command) => Path.Path.pipe(Effect.flatMap((path) => runCreateProject(path, command)));
5799
6045
  const trimEdgeUnderscores = (value) => {
@@ -7462,7 +7708,10 @@ const booleanFlagUpdaters = {
7462
7708
  "--wipe": (raw) => ({ ...raw, wipe: true }),
7463
7709
  "--no-wipe": (raw) => ({ ...raw, wipe: false }),
7464
7710
  "--web": (raw) => ({ ...raw, authWeb: true }),
7465
- "--include-default": (raw) => ({ ...raw, includeDefault: true })
7711
+ "--include-default": (raw) => ({ ...raw, includeDefault: true }),
7712
+ "--claude": (raw) => ({ ...raw, agentClaude: true }),
7713
+ "--codex": (raw) => ({ ...raw, agentCodex: true }),
7714
+ "--auto": (raw) => ({ ...raw, agentAuto: true })
7466
7715
  };
7467
7716
  const valueFlagUpdaters = {
7468
7717
  repoUrl: (raw, value) => ({ ...raw, repoUrl: value }),
@@ -7808,7 +8057,14 @@ const resolveCreateBehavior = (raw) => ({
7808
8057
  forceEnv: raw.forceEnv ?? false,
7809
8058
  enableMcpPlaywright: raw.enableMcpPlaywright ?? false
7810
8059
  });
8060
+ const resolveAgentMode = (raw) => {
8061
+ if (raw.agentClaude) return "claude";
8062
+ if (raw.agentCodex) return "codex";
8063
+ return void 0;
8064
+ };
7811
8065
  const buildTemplateConfig = ({
8066
+ agentAuto,
8067
+ agentMode,
7812
8068
  claudeAuthLabel,
7813
8069
  codexAuthLabel,
7814
8070
  dockerNetworkMode,
@@ -7840,7 +8096,9 @@ const buildTemplateConfig = ({
7840
8096
  dockerNetworkMode,
7841
8097
  dockerSharedNetworkName,
7842
8098
  enableMcpPlaywright,
7843
- pnpmVersion: defaultTemplateConfig.pnpmVersion
8099
+ pnpmVersion: defaultTemplateConfig.pnpmVersion,
8100
+ agentMode,
8101
+ agentAuto
7844
8102
  });
7845
8103
  const buildCreateCommand = (raw) => Either.gen(function* (_) {
7846
8104
  const repo = yield* _(resolveRepoBasics(raw));
@@ -7854,6 +8112,8 @@ const buildCreateCommand = (raw) => Either.gen(function* (_) {
7854
8112
  const dockerSharedNetworkName = yield* _(
7855
8113
  nonEmpty("--shared-network", raw.dockerSharedNetworkName, defaultTemplateConfig.dockerSharedNetworkName)
7856
8114
  );
8115
+ const agentMode = resolveAgentMode(raw);
8116
+ const agentAuto = raw.agentAuto ?? false;
7857
8117
  return {
7858
8118
  _tag: "Create",
7859
8119
  outDir: paths.outDir,
@@ -7871,7 +8131,9 @@ const buildCreateCommand = (raw) => Either.gen(function* (_) {
7871
8131
  gitTokenLabel,
7872
8132
  codexAuthLabel,
7873
8133
  claudeAuthLabel,
7874
- enableMcpPlaywright: behavior.enableMcpPlaywright
8134
+ enableMcpPlaywright: behavior.enableMcpPlaywright,
8135
+ agentMode,
8136
+ agentAuto
7875
8137
  })
7876
8138
  };
7877
8139
  });
@@ -8175,6 +8437,9 @@ Options:
8175
8437
  --up | --no-up Run docker compose up after init (default: --up)
8176
8438
  --ssh | --no-ssh Auto-open SSH after create/clone (default: clone=--ssh, create=--no-ssh)
8177
8439
  --mcp-playwright | --no-mcp-playwright Enable Playwright MCP + Chromium sidecar (default: --no-mcp-playwright)
8440
+ --claude Start Claude Code agent inside container after clone
8441
+ --codex Start Codex agent inside container after clone
8442
+ --auto Auto-execute: agent completes the task, creates PR and pushes (requires --claude or --codex)
8178
8443
  --force Overwrite existing files and wipe compose volumes (docker compose down -v)
8179
8444
  --force-env Reset project env defaults only (keep workspace volume/data)
8180
8445
  -h, --help Show this help
@@ -11100,6 +11365,7 @@ const program = pipe(
11100
11365
  Effect.catchTag("DockerAccessError", logWarningAndExit),
11101
11366
  Effect.catchTag("DockerCommandError", logWarningAndExit),
11102
11367
  Effect.catchTag("AuthError", logWarningAndExit),
11368
+ Effect.catchTag("AgentFailedError", logWarningAndExit),
11103
11369
  Effect.catchTag("CommandFailedError", logWarningAndExit),
11104
11370
  Effect.catchTag("ScrapArchiveNotFoundError", logErrorAndExit),
11105
11371
  Effect.catchTag("ScrapTargetDirUnsupportedError", logErrorAndExit),