Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
parameters:
ignoreErrors:
-
message: '#^Direct usage of DateTimeImmutable is not allowed\. Use DateAndTimeService\:\:getDateTimeImmutable\(\) instead\.$#'
identifier: noDirectDateTimeUsage
count: 1
path: src/WorkspaceMgmt/Domain/Service/WorkspaceGitService.php
1 change: 1 addition & 0 deletions phpstan.dist.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
includes:
- vendor/enterprise-tooling-for-symfony/shared-bundle/config/phpstan.app.dist.neon
- phpstan-baseline.neon

parameters:
# Use container cache directory for better performance in Docker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ public function __invoke(RunEditSessionMessage $message): void
$project->remoteContentAssetsManifestUrls
);

// Set git context info for the agent
try {
$gitInfo = $this->workspaceMgmtFacade->getGitInfo($conversation->getWorkspaceId());
$this->executionContext->setGitInfo($gitInfo);
} catch (Throwable $e) {
// Log but don't fail the session - git info is nice-to-have
$this->logger->debug('Failed to get git info for agent', [
'workspaceId' => $conversation->getWorkspaceId(),
'error' => $e->getMessage(),
]);
}

// Build agent configuration from project settings (#79).
$agentConfig = new AgentConfigDto(
$project->agentBackgroundInstructions,
Expand Down
5 changes: 5 additions & 0 deletions src/LlmContentEditor/Domain/Agent/ContentEditorAgent.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ public function instructions(): string
$base .= "\n\nWORKING FOLDER (use for all path-based tools): " . $this->agentConfig->workingFolderPath;
}

$gitContextInfo = $this->sitebuilderFacade->getGitContextInfo();
if ($gitContextInfo !== '') {
$base .= "\n\n" . $gitContextInfo;
}

$history = $this->resolveChatHistory();
if ($history instanceof TurnActivityProviderInterface) {
$summary = $history->getTurnActivitySummary();
Expand Down
32 changes: 32 additions & 0 deletions src/WorkspaceMgmt/Domain/Service/WorkspaceGitService.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@

use App\ProjectMgmt\Facade\ProjectMgmtFacadeInterface;
use App\WorkspaceMgmt\Domain\Entity\Workspace;
use App\WorkspaceMgmt\Facade\Dto\WorkspaceCommitDto;
use App\WorkspaceMgmt\Facade\Dto\WorkspaceGitInfoDto;
use App\WorkspaceMgmt\Infrastructure\Adapter\Dto\RawCommitDto;
use App\WorkspaceMgmt\Infrastructure\Adapter\GitAdapterInterface;
use App\WorkspaceMgmt\Infrastructure\Adapter\GitHubAdapterInterface;
use App\WorkspaceMgmt\Infrastructure\Service\GitHubUrlServiceInterface;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use RuntimeException;
Expand Down Expand Up @@ -270,4 +274,32 @@ private function buildPrBody(

return implode("\n", $lines);
}

/**
* Get git context information for a workspace.
*/
public function getGitInfo(Workspace $workspace, int $commitLimit = 10): WorkspaceGitInfoDto
{
$workspacePath = $this->getWorkspacePath($workspace);

$currentBranch = $this->gitAdapter->getCurrentBranch($workspacePath);
$rawCommits = $this->gitAdapter->getRecentCommits($workspacePath, $commitLimit);
$branches = $this->gitAdapter->getBranches($workspacePath);

$commits = array_map(
static function (RawCommitDto $raw): WorkspaceCommitDto {
$committedAt = new DateTimeImmutable($raw->timestamp);

return new WorkspaceCommitDto(
$raw->hash,
$raw->subject,
$raw->body,
$committedAt
);
},
$rawCommits
);

return new WorkspaceGitInfoDto($currentBranch, $commits, $branches);
}
}
21 changes: 21 additions & 0 deletions src/WorkspaceMgmt/Facade/Dto/WorkspaceCommitDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\WorkspaceMgmt\Facade\Dto;

use DateTimeImmutable;

/**
* DTO representing a git commit.
*/
final readonly class WorkspaceCommitDto
{
public function __construct(
public string $hash,
public string $message,
public string $body,
public DateTimeImmutable $committedAt,
) {
}
}
22 changes: 22 additions & 0 deletions src/WorkspaceMgmt/Facade/Dto/WorkspaceGitInfoDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace App\WorkspaceMgmt\Facade\Dto;

/**
* DTO representing git context information for a workspace.
*/
final readonly class WorkspaceGitInfoDto
{
/**
* @param list<WorkspaceCommitDto> $recentCommits
* @param list<string> $localBranches
*/
public function __construct(
public string $currentBranch,
public array $recentCommits,
public array $localBranches,
) {
}
}
8 changes: 8 additions & 0 deletions src/WorkspaceMgmt/Facade/WorkspaceMgmtFacade.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\WorkspaceMgmt\Domain\Service\WorkspaceGitService;
use App\WorkspaceMgmt\Domain\Service\WorkspaceService;
use App\WorkspaceMgmt\Domain\Service\WorkspaceStatusGuard;
use App\WorkspaceMgmt\Facade\Dto\WorkspaceGitInfoDto;
use App\WorkspaceMgmt\Facade\Dto\WorkspaceInfoDto;
use App\WorkspaceMgmt\Facade\Enum\WorkspaceStatus;
use App\WorkspaceMgmt\Infrastructure\Adapter\FilesystemAdapterInterface;
Expand Down Expand Up @@ -257,6 +258,13 @@ public function runBuild(string $workspaceId): string
return $process->getOutput();
}

public function getGitInfo(string $workspaceId, int $commitLimit = 10): WorkspaceGitInfoDto
{
$workspace = $this->getWorkspaceOrFail($workspaceId);

return $this->gitService->getGitInfo($workspace, $commitLimit);
}

/**
* Resolve a relative path to an absolute path and validate it's within the workspace.
*/
Expand Down
12 changes: 12 additions & 0 deletions src/WorkspaceMgmt/Facade/WorkspaceMgmtFacadeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\WorkspaceMgmt\Facade;

use App\WorkspaceMgmt\Facade\Dto\WorkspaceGitInfoDto;
use App\WorkspaceMgmt\Facade\Dto\WorkspaceInfoDto;

/**
Expand Down Expand Up @@ -137,4 +138,15 @@ public function writeWorkspaceFile(string $workspaceId, string $relativePath, st
* @throws \Symfony\Component\Process\Exception\ProcessFailedException if the build fails
*/
public function runBuild(string $workspaceId): string;

/**
* Get git context information for a workspace.
*
* Returns the current branch name, recent commits (with hash, message, body, timestamp),
* and all local branches.
*
* @param string $workspaceId the workspace ID
* @param int $commitLimit the maximum number of recent commits to return
*/
public function getGitInfo(string $workspaceId, int $commitLimit = 10): WorkspaceGitInfoDto;
}
20 changes: 20 additions & 0 deletions src/WorkspaceMgmt/Infrastructure/Adapter/Dto/RawCommitDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace App\WorkspaceMgmt\Infrastructure\Adapter\Dto;

/**
* Raw commit data from git adapter.
* Internal DTO used between adapter and service layer.
*/
final readonly class RawCommitDto
{
public function __construct(
public string $hash,
public string $subject,
public string $body,
public string $timestamp,
) {
}
}
30 changes: 30 additions & 0 deletions src/WorkspaceMgmt/Infrastructure/Adapter/GitAdapterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace App\WorkspaceMgmt\Infrastructure\Adapter;

use App\WorkspaceMgmt\Infrastructure\Adapter\Dto\RawCommitDto;

/**
* Interface for git operations.
* Implementations handle the actual git CLI commands.
Expand Down Expand Up @@ -70,4 +72,32 @@ public function push(string $workspacePath, string $branchName, string $token):
* @return bool true if the branch has commits that differ from the base branch, false otherwise
*/
public function hasBranchDifferences(string $workspacePath, string $branchName, string $baseBranch = 'main'): bool;

/**
* Get the name of the currently checked-out branch.
*
* @param string $workspacePath the workspace directory
*
* @return string the current branch name
*/
public function getCurrentBranch(string $workspacePath): string;

/**
* Get the N most recent commits on the current branch.
*
* @param string $workspacePath the workspace directory
* @param int $limit the maximum number of commits to return
*
* @return list<RawCommitDto>
*/
public function getRecentCommits(string $workspacePath, int $limit = 10): array;

/**
* Get all local branch names.
*
* @param string $workspacePath the workspace directory
*
* @return list<string>
*/
public function getBranches(string $workspacePath): array;
}
86 changes: 86 additions & 0 deletions src/WorkspaceMgmt/Infrastructure/Adapter/GitCliAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\WorkspaceMgmt\Infrastructure\Adapter;

use App\WorkspaceMgmt\Infrastructure\Adapter\Dto\RawCommitDto;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

Expand Down Expand Up @@ -218,6 +219,91 @@ private function ensureCleanRemoteUrl(string $workspacePath): void
}
}

public function getCurrentBranch(string $workspacePath): string
{
$process = new Process(['git', 'rev-parse', '--abbrev-ref', 'HEAD']);
$process->setWorkingDirectory($workspacePath);
$process->setTimeout(self::TIMEOUT_SECONDS);

$this->runProcess($process, 'Failed to get current branch');

return trim($process->getOutput());
}

public function getRecentCommits(string $workspacePath, int $limit = 10): array
{
// Use NUL byte (\x00) as field separator to safely handle multiline bodies
// Format: hash\x00subject\x00body\x00timestamp\x00
$process = new Process([
'git',
'log',
'-n', (string) $limit,
'--pretty=format:%H%x00%s%x00%b%x00%aI%x00',
]);
$process->setWorkingDirectory($workspacePath);
$process->setTimeout(self::TIMEOUT_SECONDS);

$process->run();

// Return empty array if git log fails (e.g., no commits yet)
if (!$process->isSuccessful()) {
return [];
}

$output = $process->getOutput();
if (trim($output) === '') {
return [];
}

// Split by NUL+newline (git log adds newlines between commits by default)
$records = explode("\x00\n", rtrim($output, "\x00\n"));
$commits = [];

foreach ($records as $record) {
$fields = explode("\x00", $record);
if (count($fields) < 4) {
continue;
}

[$hash, $subject, $body, $timestamp] = $fields;

$commits[] = new RawCommitDto(
trim($hash),
trim($subject),
trim($body),
trim($timestamp)
);
}

return $commits;
}

public function getBranches(string $workspacePath): array
{
$process = new Process(['git', 'branch', '--format=%(refname:short)']);
$process->setWorkingDirectory($workspacePath);
$process->setTimeout(self::TIMEOUT_SECONDS);

$process->run();

// Return empty array if git branch fails (e.g., no commits yet)
if (!$process->isSuccessful()) {
return [];
}

$output = trim($process->getOutput());
if ($output === '') {
return [];
}

$branches = array_filter(
explode("\n", $output),
static fn (string $branch): bool => trim($branch) !== ''
);

return array_values(array_map('trim', $branches));
}

private function runProcess(Process $process, string $errorMessage): void
{
$process->run();
Expand Down
15 changes: 15 additions & 0 deletions src/WorkspaceMgmt/TestHarness/SimulatedGitAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ public function hasBranchDifferences(string $workspacePath, string $branchName,
return $this->realGitAdapter->hasBranchDifferences($workspacePath, $branchName, $baseBranch);
}

public function getCurrentBranch(string $workspacePath): string
{
return $this->realGitAdapter->getCurrentBranch($workspacePath);
}

public function getRecentCommits(string $workspacePath, int $limit = 10): array
{
return $this->realGitAdapter->getRecentCommits($workspacePath, $limit);
}

public function getBranches(string $workspacePath): array
{
return $this->realGitAdapter->getBranches($workspacePath);
}

private function copyDirectory(string $source, string $target): void
{
if (!is_dir($source)) {
Expand Down
12 changes: 12 additions & 0 deletions src/WorkspaceTooling/Facade/AgentExecutionContextInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace App\WorkspaceTooling\Facade;

use App\WorkspaceMgmt\Facade\Dto\WorkspaceGitInfoDto;

/**
* Interface for setting agent execution context.
*
Expand Down Expand Up @@ -67,4 +69,14 @@ public function setSuggestedCommitMessage(string $message): void;
* @return string|null The suggested message, or null if none was set
*/
public function getSuggestedCommitMessage(): ?string;

/**
* Set git context information for the current agent run.
*/
public function setGitInfo(?WorkspaceGitInfoDto $gitInfo): void;

/**
* Get git context information for the current agent run.
*/
public function getGitInfo(): ?WorkspaceGitInfoDto;
}
Loading
Loading