Skip to content
Merged
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ This is the real Source Control shape Guardex is aiming for: isolated agent bran

![Guarded VS Code Source Control example](https://raw.githubusercontent.com/recodeee/gitguardex/main/docs/images/workflow-source-control-grouped.png)

To install the real companion into local VS Code from a Guardex-wired repo:

```sh
node scripts/install-vscode-active-agents-extension.js
```

It adds an `Active Agents` view to the Source Control container, reads `.omx/state/active-sessions/*.json`, and uses VS Code's native `loading~spin` codicon for the running-state affordance. Reload the VS Code window after install.

---

## Commands
Expand Down
12 changes: 12 additions & 0 deletions bin/multiagent-safety.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ const TEMPLATE_FILES = [
'scripts/agent-branch-start.sh',
'scripts/agent-branch-finish.sh',
'scripts/agent-branch-merge.sh',
'scripts/agent-session-state.js',
'scripts/codex-agent.sh',
'scripts/guardex-docker-loader.sh',
'scripts/install-vscode-active-agents-extension.js',
'scripts/review-bot-watch.sh',
'scripts/agent-worktree-prune.sh',
'scripts/agent-file-locks.py',
Expand All @@ -108,12 +110,17 @@ const TEMPLATE_FILES = [
'claude/commands/gitguardex.md',
'github/pull.yml.example',
'github/workflows/cr.yml',
'vscode/guardex-active-agents/package.json',
'vscode/guardex-active-agents/extension.js',
'vscode/guardex-active-agents/session-schema.js',
'vscode/guardex-active-agents/README.md',
];

const REQUIRED_WORKFLOW_FILES = [
'scripts/agent-branch-start.sh',
'scripts/agent-branch-finish.sh',
'scripts/agent-branch-merge.sh',
'scripts/agent-session-state.js',
'scripts/guardex-docker-loader.sh',
'scripts/agent-worktree-prune.sh',
'scripts/agent-file-locks.py',
Expand Down Expand Up @@ -153,8 +160,10 @@ const EXECUTABLE_RELATIVE_PATHS = new Set([
'scripts/agent-branch-start.sh',
'scripts/agent-branch-finish.sh',
'scripts/agent-branch-merge.sh',
'scripts/agent-session-state.js',
'scripts/codex-agent.sh',
'scripts/guardex-docker-loader.sh',
'scripts/install-vscode-active-agents-extension.js',
'scripts/review-bot-watch.sh',
'scripts/agent-worktree-prune.sh',
'scripts/agent-file-locks.py',
Expand Down Expand Up @@ -814,6 +823,9 @@ function toDestinationPath(relativeTemplatePath) {
if (relativeTemplatePath.startsWith('github/')) {
return `.${relativeTemplatePath}`;
}
if (relativeTemplatePath.startsWith('vscode/')) {
return relativeTemplatePath;
}
throw new Error(`Unsupported template path: ${relativeTemplatePath}`);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-21
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## Why

- Guardex already documents a polished VS Code Source Control view, but the repo does not ship a real companion that surfaces live agent lanes inside VS Code.
- Users need a concrete way to see running Guardex/Codex sandboxes without reading lock JSON or switching to terminal status output.

## What Changes

- Add repo-local active-session state that `scripts/codex-agent.sh` writes while a sandbox session is running and removes on exit.
- Add a lightweight VS Code companion extension that contributes an `Active Agents` view inside the Source Control container and renders one spinning row per live session.
- Add a local install path for the companion extension so users can enable it in their real VS Code without publishing to the Marketplace first.

## Impact

- Affected surfaces: `scripts/codex-agent.sh`, new session-state/install helpers, README, tests, and a new `vscode/guardex-active-agents` companion directory.
- Primary risk is stale or misleading session rows if lifecycle cleanup fails, so the companion must ignore dead PIDs and the writer must remove state on wrapper exit.
- The companion should stay native to VS Code constraints by using built-in spinning codicons instead of custom animated SVGs.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## ADDED Requirements

### Requirement: Guardex writes active session presence for sandboxed Codex runs
The system SHALL write repo-local active session presence records while `scripts/codex-agent.sh` is running an interactive sandbox session.

#### Scenario: Session start records live metadata
- **WHEN** `scripts/codex-agent.sh` launches Codex in a sandbox worktree
- **THEN** Guardex writes a JSON record under `.omx/state/active-sessions/`
- **AND** the record includes the repo root, sandbox branch, task name, agent name, worktree path, launch PID, CLI name, and start timestamp.

#### Scenario: Session exit removes presence record
- **WHEN** the wrapper exits after Codex finishes
- **THEN** the corresponding `.omx/state/active-sessions/` record is removed
- **AND** later launches for the same branch can recreate it cleanly.

### Requirement: VS Code companion shows active Guardex lanes in Source Control
The system SHALL provide a VS Code companion extension that surfaces live Guardex sessions in the Source Control container.

#### Scenario: Live sessions render with native spinner
- **WHEN** the companion finds live session records for the current workspace
- **THEN** it shows an `Active Agents` view in the Source Control container
- **AND** each live session renders with a native animated VS Code icon equivalent to `loading~spin`
- **AND** the row includes the branch identity plus an elapsed-time description.

#### Scenario: Dead or stale sessions are ignored
- **WHEN** a session record references a PID that is no longer running or contains invalid JSON
- **THEN** the companion does not render it as an active agent row
- **AND** valid rows continue to render.

### Requirement: Local install path enables the companion without Marketplace publishing
The system SHALL provide a local install path for the companion extension from the repo checkout.

#### Scenario: Local install copies the extension to the VS Code extensions directory
- **WHEN** the local install helper is run
- **THEN** it copies the companion extension into the target VS Code extensions directory using the extension package version
- **AND** it replaces older local installs for the same extension identifier so reload picks up the newest sources.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Definition of Done

This change is complete only when **all** of the following are true:

- Every checkbox below is checked.
- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff.
- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline.

## 1. Specification

- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-vscode-active-agents-extension-2026-04-21-17-38`.
- [x] 1.2 Define normative requirements in `specs/vscode-active-agents-extension/spec.md`.

## 2. Implementation

- [x] 2.1 Add repo-local active-session state writing/cleanup around `scripts/codex-agent.sh`.
- [x] 2.2 Add the VS Code Source Control companion view and local install path.
- [x] 2.3 Add/update focused regression coverage for session-state parsing and install behavior.
- [x] 2.4 Update README guidance for the real VS Code companion flow.

## 3. Verification

- [x] 3.1 Run targeted project verification commands for the session-state helper, extension sources, and install path.
- [x] 3.2 Run `openspec validate agent-codex-vscode-active-agents-extension-2026-04-21-17-38 --type change --strict`.
- [x] 3.3 Run `openspec validate --specs`.

## 4. Cleanup (mandatory; run before claiming completion)

- [ ] 4.1 Run the cleanup pipeline: `bash scripts/agent-branch-finish.sh --branch agent/<your-name>/<branch-slug> --base main --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation.
- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff.
- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch).
110 changes: 110 additions & 0 deletions scripts/agent-session-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env node

const fs = require('node:fs');
const path = require('node:path');

function resolveSessionSchemaModule() {
const candidates = [
path.resolve(__dirname, '..', 'vscode', 'guardex-active-agents', 'session-schema.js'),
path.resolve(__dirname, '..', 'templates', 'vscode', 'guardex-active-agents', 'session-schema.js'),
];

for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return require(candidate);
}
}

throw new Error('Could not resolve Guardex active-agent session schema module.');
}

const sessionSchema = resolveSessionSchemaModule();

function usage() {
return (
'Usage:\n' +
' node scripts/agent-session-state.js start --repo <path> --branch <name> --task <task> --agent <agent> --worktree <path> --pid <pid> --cli <name>\n' +
' node scripts/agent-session-state.js stop --repo <path> --branch <name>\n'
);
}

function parseOptions(argv) {
const options = {};
for (let index = 0; index < argv.length; index += 1) {
const token = argv[index];
if (!token.startsWith('--')) {
throw new Error(`Unexpected argument: ${token}`);
}
const key = token.slice(2);
const value = argv[index + 1];
if (!value || value.startsWith('--')) {
throw new Error(`Missing value for --${key}`);
}
options[key] = value;
index += 1;
}
return options;
}

function requireOption(options, key) {
const value = options[key];
if (!value) {
throw new Error(`Missing required option --${key}`);
}
return value;
}

function writeSessionRecord(options) {
const repoRoot = requireOption(options, 'repo');
const branch = requireOption(options, 'branch');
const record = sessionSchema.buildSessionRecord({
repoRoot,
branch,
taskName: requireOption(options, 'task'),
agentName: requireOption(options, 'agent'),
worktreePath: requireOption(options, 'worktree'),
pid: requireOption(options, 'pid'),
cliName: requireOption(options, 'cli'),
});

const targetPath = sessionSchema.sessionFilePathForBranch(repoRoot, branch);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
}

function removeSessionRecord(options) {
const repoRoot = requireOption(options, 'repo');
const branch = requireOption(options, 'branch');
const targetPath = sessionSchema.sessionFilePathForBranch(repoRoot, branch);
if (fs.existsSync(targetPath)) {
fs.unlinkSync(targetPath);
}
}

function main() {
const [command, ...rest] = process.argv.slice(2);
if (!command || ['-h', '--help', 'help'].includes(command)) {
process.stdout.write(usage());
return;
}

const options = parseOptions(rest);
if (command === 'start') {
writeSessionRecord(options);
return;
}
if (command === 'stop') {
removeSessionRecord(options);
return;
}

throw new Error(`Unknown subcommand: ${command}`);
}

try {
main();
} catch (error) {
process.stderr.write(`[guardex-active-session] ${error.message}\n`);
process.stderr.write(usage());
process.exitCode = 1;
}
51 changes: 51 additions & 0 deletions scripts/codex-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ AGENT_NAME="${GUARDEX_AGENT_NAME:-agent}"
BASE_BRANCH="${GUARDEX_BASE_BRANCH:-}"
BASE_BRANCH_EXPLICIT=0
CODEX_BIN="${GUARDEX_CODEX_BIN:-codex}"
NODE_BIN="${GUARDEX_NODE_BIN:-node}"
AUTO_FINISH_RAW="${GUARDEX_CODEX_AUTO_FINISH:-true}"
AUTO_REVIEW_ON_CONFLICT_RAW="${GUARDEX_CODEX_AUTO_REVIEW_ON_CONFLICT:-true}"
AUTO_CLEANUP_RAW="${GUARDEX_CODEX_AUTO_CLEANUP:-true}"
Expand Down Expand Up @@ -143,6 +144,7 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
exit 1
fi
repo_root="$(git rev-parse --show-toplevel)"
active_session_state_script="${repo_root}/scripts/agent-session-state.js"

guardex_env_helper="${repo_root}/scripts/guardex-env.sh"
if [[ -f "$guardex_env_helper" ]]; then
Expand Down Expand Up @@ -446,6 +448,40 @@ has_origin_remote() {
git -C "$repo_root" remote get-url origin >/dev/null 2>&1
}

run_active_session_state() {
local action="$1"
shift

if [[ ! -f "$active_session_state_script" ]]; then
return 0
fi
if ! command -v "$NODE_BIN" >/dev/null 2>&1; then
return 0
fi

"$NODE_BIN" "$active_session_state_script" "$action" "$@" >/dev/null 2>&1 || true
}

record_active_session_state() {
local wt="$1"
local branch="$2"

run_active_session_state \
start \
--repo "$repo_root" \
--branch "$branch" \
--task "$TASK_NAME" \
--agent "$AGENT_NAME" \
--worktree "$wt" \
--pid "$$" \
--cli "$CODEX_BIN"
}

clear_active_session_state() {
local branch="$1"
run_active_session_state stop --repo "$repo_root" --branch "$branch"
}

origin_remote_supports_pr_finish() {
local origin_url
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
Expand Down Expand Up @@ -833,6 +869,19 @@ if ! ensure_openspec_plan_workspace "$worktree_path" "$worktree_branch"; then
exit 1
fi

active_session_recorded=0
cleanup_active_session_state_on_exit() {
set +e
if [[ "${active_session_recorded:-0}" -eq 1 && -n "${worktree_branch:-}" && "${worktree_branch:-}" != "HEAD" ]]; then
clear_active_session_state "$worktree_branch"
active_session_recorded=0
fi
}

record_active_session_state "$worktree_path" "$worktree_branch"
active_session_recorded=1
trap cleanup_active_session_state_on_exit EXIT INT TERM

echo "[codex-agent] Launching ${CODEX_BIN} in sandbox: $worktree_path"
cd "$worktree_path"
set +e
Expand All @@ -841,6 +890,8 @@ codex_exit="$?"
set -e

cd "$repo_root"
cleanup_active_session_state_on_exit
trap - EXIT INT TERM
final_exit="$codex_exit"
auto_finish_completed=0

Expand Down
Loading