From 04a496538791ca9e610ad30562811bc55977fc59 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Fri, 15 May 2026 14:39:55 +0200 Subject: [PATCH] refactor(cli): drop unused VS Code active-agents extension The bundled `Recodee.gitguardex-active-agents` extension and its `agent-session-state.js` heartbeat plumbing were never adopted but kept adding a `code --install-extension` prompt to `gx setup`, running a background heartbeat per `codex-agent.sh` launch, and shipping ~10k LOC of dead extension source on every npm publish. Removes: - `vscode/guardex-active-agents/` + `templates/vscode/` mirror - `scripts/agent-session-state.js` (+ template) - `scripts/install-vscode-active-agents-extension.js` (+ template) - `maybePromptInstallVscodeExtension()` from `gx setup` - `gx internal heartbeat` / `gx internal stop-session` subcommands - `active_session_state_*` helpers + heartbeat loop + EXIT trap in `templates/scripts/codex-agent.sh` - `sessionState` from `PACKAGE_SCRIPT_ASSETS`, the `vscode/` branch in `toDestinationPath`, and every `agent-session-state.js` entry across `TEMPLATE_FILES`, `PACKAGE_ROOT_SOURCE_OVERRIDES`, `LEGACY_MANAGED_REPO_FILES`, and `MANAGED_GITIGNORE_PATHS` The `.vscode/settings.json` managed IDE settings (separate from the extension) are untouched. --- .../.openspec.yaml | 2 + .../proposal.md | 22 + .../spec.md | 10 + .../tasks.md | 34 + scripts/agent-session-state.js | 171 - .../install-vscode-active-agents-extension.js | 135 - src/cli/main.js | 80 - src/context.js | 43 +- src/hooks/index.js | 64 - templates/scripts/agent-session-state.js | 171 - templates/scripts/codex-agent.sh | 91 - .../install-vscode-active-agents-extension.js | 135 - .../vscode/guardex-active-agents/README.md | 34 - .../vscode/guardex-active-agents/extension.js | 3892 ---------------- .../fileicons/gitguardex-fileicons.json | 54 - .../fileicons/icons/agent.svg | 5 - .../fileicons/icons/branch.svg | 7 - .../fileicons/icons/config.svg | 4 - .../fileicons/icons/hook.svg | 4 - .../fileicons/icons/openspec.svg | 5 - .../fileicons/icons/plan.svg | 4 - .../fileicons/icons/spec.svg | 5 - .../vscode/guardex-active-agents/icon.png | Bin 230734 -> 0 bytes .../media/active-agents-hivemind.svg | 14 - .../vscode/guardex-active-agents/package.json | 169 - .../guardex-active-agents/session-schema.js | 1348 ------ test/helpers/install-test-helpers.js | 2 - test/metadata.test.js | 17 +- test/setup.test.js | 35 +- ...vscode-active-agents-session-state.test.js | 4079 ----------------- vscode/guardex-active-agents/README.md | 34 - vscode/guardex-active-agents/extension.js | 3892 ---------------- .../fileicons/gitguardex-fileicons.json | 54 - .../fileicons/icons/agent.svg | 5 - .../fileicons/icons/branch.svg | 7 - .../fileicons/icons/config.svg | 4 - .../fileicons/icons/hook.svg | 4 - .../fileicons/icons/openspec.svg | 5 - .../fileicons/icons/plan.svg | 4 - .../fileicons/icons/spec.svg | 5 - vscode/guardex-active-agents/icon.png | Bin 230734 -> 0 bytes .../media/active-agents-hivemind.svg | 14 - vscode/guardex-active-agents/package.json | 169 - .../guardex-active-agents/session-schema.js | 1348 ------ 44 files changed, 73 insertions(+), 16108 deletions(-) create mode 100644 openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/.openspec.yaml create mode 100644 openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/proposal.md create mode 100644 openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/specs/remove-vscode-active-agents-extension/spec.md create mode 100644 openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/tasks.md delete mode 100755 scripts/agent-session-state.js delete mode 100755 scripts/install-vscode-active-agents-extension.js delete mode 100755 templates/scripts/agent-session-state.js delete mode 100755 templates/scripts/install-vscode-active-agents-extension.js delete mode 100644 templates/vscode/guardex-active-agents/README.md delete mode 100644 templates/vscode/guardex-active-agents/extension.js delete mode 100644 templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json delete mode 100644 templates/vscode/guardex-active-agents/fileicons/icons/agent.svg delete mode 100644 templates/vscode/guardex-active-agents/fileicons/icons/branch.svg delete mode 100644 templates/vscode/guardex-active-agents/fileicons/icons/config.svg delete mode 100644 templates/vscode/guardex-active-agents/fileicons/icons/hook.svg delete mode 100644 templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg delete mode 100644 templates/vscode/guardex-active-agents/fileicons/icons/plan.svg delete mode 100644 templates/vscode/guardex-active-agents/fileicons/icons/spec.svg delete mode 100644 templates/vscode/guardex-active-agents/icon.png delete mode 100644 templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg delete mode 100644 templates/vscode/guardex-active-agents/package.json delete mode 100644 templates/vscode/guardex-active-agents/session-schema.js delete mode 100644 test/vscode-active-agents-session-state.test.js delete mode 100644 vscode/guardex-active-agents/README.md delete mode 100644 vscode/guardex-active-agents/extension.js delete mode 100644 vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json delete mode 100644 vscode/guardex-active-agents/fileicons/icons/agent.svg delete mode 100644 vscode/guardex-active-agents/fileicons/icons/branch.svg delete mode 100644 vscode/guardex-active-agents/fileicons/icons/config.svg delete mode 100644 vscode/guardex-active-agents/fileicons/icons/hook.svg delete mode 100644 vscode/guardex-active-agents/fileicons/icons/openspec.svg delete mode 100644 vscode/guardex-active-agents/fileicons/icons/plan.svg delete mode 100644 vscode/guardex-active-agents/fileicons/icons/spec.svg delete mode 100644 vscode/guardex-active-agents/icon.png delete mode 100644 vscode/guardex-active-agents/media/active-agents-hivemind.svg delete mode 100644 vscode/guardex-active-agents/package.json delete mode 100644 vscode/guardex-active-agents/session-schema.js diff --git a/openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/.openspec.yaml b/openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/.openspec.yaml new file mode 100644 index 00000000..9f708669 --- /dev/null +++ b/openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-15 diff --git a/openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/proposal.md b/openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/proposal.md new file mode 100644 index 00000000..2a55a88d --- /dev/null +++ b/openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/proposal.md @@ -0,0 +1,22 @@ +## Why + +The bundled VS Code extension `Recodee.gitguardex-active-agents` and its supporting `agent-session-state.js` heartbeat plumbing were never adopted in practice. They added an install prompt to `gx setup`, kept the active-agent session writer running on every codex-agent launch, and shipped ~10k lines of extension source on every npm publish. None of it is required for the core gx guardrail flow. + +## What Changes + +- Remove the `vscode/guardex-active-agents/` extension source and its `templates/vscode/` mirror. +- Remove `scripts/agent-session-state.js` and its template (it only fed the deleted extension). +- Remove `scripts/install-vscode-active-agents-extension.js` and its template. +- Remove `maybePromptInstallVscodeExtension()` from `gx setup` (no install prompt, no `GUARDEX_SKIP_VSCODE_EXT_PROMPT` env). +- Remove `gx internal heartbeat` and `gx internal stop-session` subcommands (only the extension called them). +- Strip the `active_session_state_*` helper functions, heartbeat loop, and exit trap from `templates/scripts/codex-agent.sh`. +- Drop the `vscode/` template-path branch, `sessionState` package asset, and all `agent-session-state.js` entries from `TEMPLATE_FILES`, `PACKAGE_ROOT_SOURCE_OVERRIDES`, `LEGACY_MANAGED_REPO_FILES`, and `MANAGED_GITIGNORE_PATHS` in `src/context.js`. +- Update `test/setup.test.js`, `test/metadata.test.js`, and `test/helpers/install-test-helpers.js` to stop asserting on the removed files. + +## Impact + +- Smaller npm payload; no dead extension code on disk. +- `gx setup` and `gx doctor` no longer touch `code --install-extension`. +- `codex-agent.sh` no longer spawns a background heartbeat process per launch. +- Consumer repos that previously had `scripts/agent-session-state.js` scaffolded as gitignored will see it stop being re-materialized on the next `gx setup` — safe to delete locally. +- The `.vscode/settings.json` IDE settings (separate from the extension) are unchanged. diff --git a/openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/specs/remove-vscode-active-agents-extension/spec.md b/openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/specs/remove-vscode-active-agents-extension/spec.md new file mode 100644 index 00000000..0f2a4f71 --- /dev/null +++ b/openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/specs/remove-vscode-active-agents-extension/spec.md @@ -0,0 +1,10 @@ +## REMOVED Requirements + +### Requirement: Optional VS Code active-agents extension install prompt +`gx setup` MUST NOT prompt to install the `Recodee.gitguardex-active-agents` VS Code extension, MUST NOT honor `GUARDEX_SKIP_VSCODE_EXT_PROMPT`, and MUST NOT shell out to `code --install-extension` as part of setup. The extension source under `vscode/guardex-active-agents/` and its `templates/vscode/` mirror SHALL NOT ship with the package. + +### Requirement: Agent session-state heartbeat helper +The CLI MUST NOT expose `gx internal heartbeat` or `gx internal stop-session` subcommands, MUST NOT register a `sessionState` entry in `PACKAGE_SCRIPT_ASSETS`, and MUST NOT scaffold `scripts/agent-session-state.js` into managed repos. The `templates/scripts/codex-agent.sh` launcher MUST NOT record, refresh, or terminate active-session state and MUST NOT run a background heartbeat loop for the duration of the codex CLI subprocess. + +### Requirement: VS Code extension destination path mapping +`src/context.js#toDestinationPath` MUST NOT accept relative template paths beginning with `vscode/`. Any caller passing such a path is treated as an unsupported template and the function throws. diff --git a/openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/tasks.md b/openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/tasks.md new file mode 100644 index 00000000..ca486256 --- /dev/null +++ b/openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/tasks.md @@ -0,0 +1,34 @@ +## 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. + +## Handoff + +- Handoff: change=`agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19`; branch=`agent//`; scope=`TODO`; action=`continue this sandbox or finish cleanup after a usage-limit/manual takeover`. +- Copy prompt: Continue `agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19` on branch `agent//`. Work inside the existing sandbox, review `openspec/changes/agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent// --base dev --via-pr --wait-for-merge --cleanup`. + +## 1. Specification + +- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19`. +- [x] 1.2 Define normative requirements in `specs/remove-vscode-active-agents-extension/spec.md`. + +## 2. Implementation + +- [x] 2.1 Implement scoped behavior changes. +- [x] 2.2 Add/update focused regression coverage. + +## 3. Verification + +- [x] 3.1 Run targeted project verification commands. +- [x] 3.2 Run `openspec validate agent-claude-remove-vscode-active-agents-extension-2026-05-15-14-19 --type change --strict`. +- [x] 3.3 Run `openspec validate --specs`. + +## 4. Cleanup (mandatory; run before claiming completion) + +- [x] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent// --base dev --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation. +- [x] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff. +- [x] 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). diff --git a/scripts/agent-session-state.js b/scripts/agent-session-state.js deleted file mode 100755 index e6fe2f43..00000000 --- a/scripts/agent-session-state.js +++ /dev/null @@ -1,171 +0,0 @@ -#!/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 --branch --task --agent --worktree --pid --cli [--task-mode ] [--openspec-tier ] [--routing-reason ] [--state ]\n' + - ' node scripts/agent-session-state.js heartbeat --repo --branch [--state ]\n' + - ' node scripts/agent-session-state.js terminate --repo --branch \n' + - ' node scripts/agent-session-state.js stop --repo --branch \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'), - taskMode: options['task-mode'], - openspecTier: options['openspec-tier'], - taskRoutingReason: options['routing-reason'], - state: options.state, - }); - - const targetPath = sessionSchema.sessionFilePathForBranch(repoRoot, branch); - fs.mkdirSync(path.dirname(targetPath), { recursive: true }); - fs.writeFileSync(targetPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8'); -} - -function refreshSessionRecord(options) { - const repoRoot = requireOption(options, 'repo'); - const branch = requireOption(options, 'branch'); - const targetPath = sessionSchema.sessionFilePathForBranch(repoRoot, branch); - if (!fs.existsSync(targetPath)) { - return; - } - - const parsed = JSON.parse(fs.readFileSync(targetPath, 'utf8')); - const nextRecord = { - ...parsed, - lastHeartbeatAt: new Date().toISOString(), - }; - if (options.state) { - nextRecord.state = options.state; - } - - fs.writeFileSync(targetPath, `${JSON.stringify(nextRecord, null, 2)}\n`, 'utf8'); -} - -function readSessionRecord(options) { - const repoRoot = requireOption(options, 'repo'); - const branch = requireOption(options, 'branch'); - const targetPath = sessionSchema.sessionFilePathForBranch(repoRoot, branch); - if (!fs.existsSync(targetPath)) { - return null; - } - return JSON.parse(fs.readFileSync(targetPath, 'utf8')); -} - -function terminateSessionProcess(options) { - const record = readSessionRecord(options); - const pid = Number(record?.pid); - if (!Number.isInteger(pid) || pid <= 0) { - throw new Error('No live pid recorded for branch.'); - } - - try { - process.kill(pid, 'SIGTERM'); - } catch (error) { - if (error?.code === 'ESRCH') { - return; - } - throw error; - } -} - -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 === 'heartbeat') { - refreshSessionRecord(options); - return; - } - if (command === 'terminate') { - terminateSessionProcess(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; -} diff --git a/scripts/install-vscode-active-agents-extension.js b/scripts/install-vscode-active-agents-extension.js deleted file mode 100755 index 6253cdb6..00000000 --- a/scripts/install-vscode-active-agents-extension.js +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env node - -const fs = require('node:fs'); -const os = require('node:os'); -const path = require('node:path'); - -const PATCH_COMPATIBILITY_WINDOW = 20; -const RETIRED_EXTENSION_IDS = [ - 'recodeee.gitguardex-active-agents', -]; - -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 resolveExtensionSource(repoRoot) { - const candidates = [ - path.join(repoRoot, 'vscode', 'guardex-active-agents'), - path.join(repoRoot, 'templates', 'vscode', 'guardex-active-agents'), - ]; - - for (const candidate of candidates) { - if (fs.existsSync(path.join(candidate, 'package.json'))) { - return candidate; - } - } - - throw new Error('Could not find the Guardex VS Code companion sources.'); -} - -function removeIfExists(targetPath) { - if (fs.existsSync(targetPath)) { - fs.rmSync(targetPath, { recursive: true, force: true }); - } -} - -function parseSimpleSemver(version) { - const parts = String(version || '').trim().split('.').map((part) => Number.parseInt(part, 10)); - if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) { - throw new Error(`Expected simple semver for the Active Agents companion, received "${version}".`); - } - return parts; -} - -function buildInstallTargets(extensionId, version, extensionsDir) { - const [major, minor, patch] = parseSimpleSemver(version); - const firstCompatiblePatch = Math.max(0, patch - PATCH_COMPATIBILITY_WINDOW); - const targets = [path.join(extensionsDir, extensionId)]; - - for (let compatiblePatch = firstCompatiblePatch; compatiblePatch <= patch; compatiblePatch += 1) { - targets.push(path.join(extensionsDir, `${extensionId}-${major}.${minor}.${compatiblePatch}`)); - } - - return targets; -} - -function isRetiredExtensionInstall(entryName, currentExtensionId) { - return RETIRED_EXTENSION_IDS - .filter((extensionId) => extensionId !== currentExtensionId) - .some((extensionId) => entryName === extensionId || entryName.startsWith(`${extensionId}-`)); -} - -function main() { - const repoRoot = path.resolve(__dirname, '..'); - const options = parseOptions(process.argv.slice(2)); - const sourceDir = resolveExtensionSource(repoRoot); - const manifest = JSON.parse(fs.readFileSync(path.join(sourceDir, 'package.json'), 'utf8')); - const extensionId = `${manifest.publisher}.${manifest.name}`; - const extensionsDir = path.resolve( - options['extensions-dir'] || - process.env.GUARDEX_VSCODE_EXTENSIONS_DIR || - process.env.VSCODE_EXTENSIONS_DIR || - path.join(os.homedir(), '.vscode', 'extensions'), - ); - - fs.mkdirSync(extensionsDir, { recursive: true }); - const targetDirs = buildInstallTargets(extensionId, manifest.version, extensionsDir); - const canonicalTargetDir = targetDirs[0]; - const keepDirNames = new Set(targetDirs.map((targetDir) => path.basename(targetDir))); - let retiredInstallCount = 0; - - for (const entry of fs.readdirSync(extensionsDir, { withFileTypes: true })) { - if (!entry.isDirectory()) { - continue; - } - if (isRetiredExtensionInstall(entry.name, extensionId)) { - removeIfExists(path.join(extensionsDir, entry.name)); - retiredInstallCount += 1; - continue; - } - if (keepDirNames.has(entry.name)) { - continue; - } - if (entry.name === extensionId || entry.name.startsWith(`${extensionId}-`)) { - removeIfExists(path.join(extensionsDir, entry.name)); - } - } - - for (const targetDir of targetDirs) { - removeIfExists(targetDir); - fs.cpSync(sourceDir, targetDir, { - recursive: true, - force: true, - preserveTimestamps: true, - }); - } - - process.stdout.write( - `[guardex-active-agents] Installed ${extensionId}@${manifest.version} to ${canonicalTargetDir}\n` + - `[guardex-active-agents] Refreshed ${targetDirs.length - 1} recent patch compatibility path(s) for already-open windows.\n` + - `[guardex-active-agents] Removed ${retiredInstallCount} retired extension install path(s).\n` + - '[guardex-active-agents] Reload each already-open VS Code window to activate the newest Source Control companion.\n', - ); -} - -try { - main(); -} catch (error) { - process.stderr.write(`[guardex-active-agents] ${error.message}\n`); - process.exitCode = 1; -} diff --git a/src/cli/main.js b/src/cli/main.js index 67f48b00..de1990fa 100755 --- a/src/cli/main.js +++ b/src/cli/main.js @@ -1268,84 +1268,6 @@ function promptYesNoStrict(question) { } } -const VSCODE_EXTENSION_ID = 'Recodee.gitguardex-active-agents'; -const VSCODE_EXTENSION_DISPLAY_NAME = 'GitGuardex Active Agents'; - -function maybePromptInstallVscodeExtension(options) { - if (options.dryRun) { - console.log( - `[${TOOL_NAME}] (dry-run) Would offer to install VS Code extension '${VSCODE_EXTENSION_ID}'.`, - ); - return; - } - - if (envFlagIsTruthy(process.env.GUARDEX_SKIP_VSCODE_EXT_PROMPT)) { - return; - } - - const codeProbe = cp.spawnSync('code', ['--version'], { stdio: 'ignore' }); - if (codeProbe.error || codeProbe.status !== 0) { - return; - } - - const listProbe = cp.spawnSync('code', ['--list-extensions'], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }); - if (!listProbe.error && listProbe.status === 0) { - const alreadyInstalled = String(listProbe.stdout || '') - .split('\n') - .some((line) => line.trim().toLowerCase() === VSCODE_EXTENSION_ID.toLowerCase()); - if (alreadyInstalled) { - console.log( - `[${TOOL_NAME}] ✅ VS Code extension '${VSCODE_EXTENSION_ID}' already installed.`, - ); - return; - } - } - - let approved; - if (options.yesGlobalInstall) { - approved = true; - } else if (options.noGlobalInstall) { - approved = false; - } else if (!isInteractiveTerminal()) { - console.log( - `[${TOOL_NAME}] Optional VS Code extension '${VSCODE_EXTENSION_ID}' ` + - `(${VSCODE_EXTENSION_DISPLAY_NAME}) not installed. ` + - `Install later: code --install-extension ${VSCODE_EXTENSION_ID}`, - ); - return; - } else { - approved = promptYesNoStrict( - `Install VS Code extension '${VSCODE_EXTENSION_ID}' (${VSCODE_EXTENSION_DISPLAY_NAME}) now?`, - ); - } - - if (!approved) { - console.log( - `[${TOOL_NAME}] ⚠️ VS Code extension skipped. ` + - `Set GUARDEX_SKIP_VSCODE_EXT_PROMPT=1 to silence or run 'code --install-extension ${VSCODE_EXTENSION_ID}' later.`, - ); - return; - } - - const install = cp.spawnSync('code', ['--install-extension', VSCODE_EXTENSION_ID], { - stdio: 'inherit', - }); - if (install.error || install.status !== 0) { - console.log( - `[${TOOL_NAME}] ⚠️ VS Code extension install failed. ` + - `Retry manually: code --install-extension ${VSCODE_EXTENSION_ID}`, - ); - return; - } - console.log( - `[${TOOL_NAME}] ✅ VS Code extension '${VSCODE_EXTENSION_ID}' installed. ` + - `Reload the VS Code window to activate it.`, - ); -} - function resolveGlobalInstallApproval(options) { if (options.yesGlobalInstall && options.noGlobalInstall) { throw new Error('Cannot use both --yes-global-install and --no-global-install'); @@ -3046,8 +2968,6 @@ function setup(rawArgs) { } } - maybePromptInstallVscodeExtension(options); - printRequiredSystemToolStatus(); const topRepoRoot = resolveRepoRoot(options.target); diff --git a/src/context.js b/src/context.js index 93d137f0..ed5b7a93 100644 --- a/src/context.js +++ b/src/context.js @@ -138,9 +138,6 @@ function toDestinationPath(relativeTemplatePath) { if (relativeTemplatePath.startsWith('github/')) { return `.${relativeTemplatePath}`; } - if (relativeTemplatePath.startsWith('vscode/')) { - return relativeTemplatePath; - } throw new Error(`Unsupported template path: ${relativeTemplatePath}`); } @@ -153,7 +150,7 @@ function toDestinationPath(relativeTemplatePath) { // replaced with a regular file. Edit only the templates/scripts/ copy; // the symlink propagates. // -// 2. SCAFFOLD-ONLY files (the 4 below + workflows + vscode extension): +// 2. SCAFFOLD-ONLY files (the 3 below + workflows): // tracked only under templates/; scaffolded into gitignored // scripts/ (or .githooks/, etc.) by `gx setup`. Consumer // repos receive a regular file copy at the destination; gitguardex @@ -165,48 +162,17 @@ function toDestinationPath(relativeTemplatePath) { // pattern (2), append the destination path to .gitignore's multiagent- // safety block (auto-managed by syncManagedGitignoreLines below). const TEMPLATE_FILES = [ - 'scripts/agent-session-state.js', 'scripts/agent-preflight.sh', 'scripts/guardex-docker-loader.sh', 'scripts/guardex-env.sh', - 'scripts/install-vscode-active-agents-extension.js', 'github/pull.yml.example', 'github/workflows/ci.yml', 'github/workflows/ci-full.yml', 'github/workflows/cr.yml', 'github/workflows/README.md', - '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', - 'vscode/guardex-active-agents/icon.png', - 'vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json', - 'vscode/guardex-active-agents/fileicons/icons/agent.svg', - 'vscode/guardex-active-agents/fileicons/icons/branch.svg', - 'vscode/guardex-active-agents/fileicons/icons/config.svg', - 'vscode/guardex-active-agents/fileicons/icons/hook.svg', - 'vscode/guardex-active-agents/fileicons/icons/openspec.svg', - 'vscode/guardex-active-agents/fileicons/icons/plan.svg', - 'vscode/guardex-active-agents/fileicons/icons/spec.svg', ]; -const PACKAGE_ROOT_SOURCE_OVERRIDES = new Set([ - 'scripts/agent-session-state.js', - 'scripts/install-vscode-active-agents-extension.js', - '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', - 'vscode/guardex-active-agents/icon.png', - 'vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json', - 'vscode/guardex-active-agents/fileicons/icons/agent.svg', - 'vscode/guardex-active-agents/fileicons/icons/branch.svg', - 'vscode/guardex-active-agents/fileicons/icons/config.svg', - 'vscode/guardex-active-agents/fileicons/icons/hook.svg', - 'vscode/guardex-active-agents/fileicons/icons/openspec.svg', - 'vscode/guardex-active-agents/fileicons/icons/plan.svg', - 'vscode/guardex-active-agents/fileicons/icons/spec.svg', -]); +const PACKAGE_ROOT_SOURCE_OVERRIDES = new Set(); const LEGACY_WORKFLOW_SHIM_SPECS = [ { relativePath: 'scripts/agent-branch-start.sh', kind: 'shell', command: ['branch', 'start'] }, @@ -229,9 +195,7 @@ const MANAGED_TEMPLATE_SCRIPT_FILES = MANAGED_TEMPLATE_DESTINATIONS.filter((entr const LEGACY_MANAGED_REPO_FILES = [ ...LEGACY_WORKFLOW_SHIMS, - 'scripts/agent-session-state.js', 'scripts/guardex-docker-loader.sh', - 'scripts/install-vscode-active-agents-extension.js', 'scripts/guardex-env.sh', 'scripts/install-agent-git-hooks.sh', '.githooks/pre-commit', @@ -280,7 +244,6 @@ const PACKAGE_SCRIPT_ASSETS = { branchMerge: path.join(TEMPLATE_ROOT, 'scripts', 'agent-branch-merge.sh'), codexAgent: path.join(TEMPLATE_ROOT, 'scripts', 'codex-agent.sh'), reviewBot: path.join(TEMPLATE_ROOT, 'scripts', 'review-bot-watch.sh'), - sessionState: path.join(TEMPLATE_ROOT, 'scripts', 'agent-session-state.js'), worktreePrune: path.join(TEMPLATE_ROOT, 'scripts', 'agent-worktree-prune.sh'), lockTool: path.join(TEMPLATE_ROOT, 'scripts', 'agent-file-locks.py'), planInit: path.join(TEMPLATE_ROOT, 'scripts', 'openspec', 'init-plan-workspace.sh'), @@ -344,10 +307,8 @@ const MANAGED_GITIGNORE_PATHS = [ '!.vscode/', '.vscode/*', '!.vscode/settings.json', - 'scripts/agent-session-state.js', 'scripts/guardex-docker-loader.sh', 'scripts/guardex-env.sh', - 'scripts/install-vscode-active-agents-extension.js', '.githooks', 'oh-my-codex/', LOCK_FILE_RELATIVE, diff --git a/src/hooks/index.js b/src/hooks/index.js index 691b469c..52ad3a9c 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -1,39 +1,5 @@ const path = require('node:path'); -function requireFlagValue(rawArgs, index, flagName) { - const value = rawArgs[index + 1]; - if (!value || value.startsWith('--')) { - throw new Error(`${flagName} requires a value`); - } - return value; -} - -function parseHeartbeatArgs(rawArgs) { - let branch = ''; - let state = ''; - - for (let index = 0; index < rawArgs.length; index += 1) { - const arg = rawArgs[index]; - if (arg === '--branch') { - branch = requireFlagValue(rawArgs, index, '--branch'); - index += 1; - continue; - } - if (arg === '--state') { - state = requireFlagValue(rawArgs, index, '--state'); - index += 1; - continue; - } - throw new Error(`Unknown heartbeat option: ${arg}`); - } - - if (!branch) { - throw new Error('heartbeat requires --branch '); - } - - return { branch, state }; -} - function hook(rawArgs, deps) { const { extractTargetedArgs, @@ -89,36 +55,6 @@ function internal(rawArgs, deps) { } = deps; const [subcommand, assetKey, ...rest] = rawArgs; - if (subcommand === 'heartbeat') { - const { target, passthrough } = extractTargetedArgs([assetKey, ...rest].filter(Boolean)); - const repoRoot = resolveRepoRoot(target); - const options = parseHeartbeatArgs(passthrough); - const heartbeatArgs = ['heartbeat', '--repo', repoRoot, '--branch', options.branch]; - if (options.state) { - heartbeatArgs.push('--state', options.state); - } - const result = runPackageAsset('sessionState', heartbeatArgs, { cwd: repoRoot }); - if (result.stdout) process.stdout.write(result.stdout); - if (result.stderr) process.stderr.write(result.stderr); - process.exitCode = result.status; - return; - } - if (subcommand === 'stop-session') { - const { target, passthrough } = extractTargetedArgs([assetKey, ...rest].filter(Boolean)); - const repoRoot = resolveRepoRoot(target); - const options = parseHeartbeatArgs(passthrough); - const result = runPackageAsset('sessionState', [ - 'terminate', - '--repo', - repoRoot, - '--branch', - options.branch, - ], { cwd: repoRoot }); - if (result.stdout) process.stdout.write(result.stdout); - if (result.stderr) process.stderr.write(result.stderr); - process.exitCode = result.status; - return; - } if (subcommand !== 'run-shell') { throw new Error(`Unknown internal command: ${subcommand || '(missing)'}`); } diff --git a/templates/scripts/agent-session-state.js b/templates/scripts/agent-session-state.js deleted file mode 100755 index e6fe2f43..00000000 --- a/templates/scripts/agent-session-state.js +++ /dev/null @@ -1,171 +0,0 @@ -#!/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 --branch --task --agent --worktree --pid --cli [--task-mode ] [--openspec-tier ] [--routing-reason ] [--state ]\n' + - ' node scripts/agent-session-state.js heartbeat --repo --branch [--state ]\n' + - ' node scripts/agent-session-state.js terminate --repo --branch \n' + - ' node scripts/agent-session-state.js stop --repo --branch \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'), - taskMode: options['task-mode'], - openspecTier: options['openspec-tier'], - taskRoutingReason: options['routing-reason'], - state: options.state, - }); - - const targetPath = sessionSchema.sessionFilePathForBranch(repoRoot, branch); - fs.mkdirSync(path.dirname(targetPath), { recursive: true }); - fs.writeFileSync(targetPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8'); -} - -function refreshSessionRecord(options) { - const repoRoot = requireOption(options, 'repo'); - const branch = requireOption(options, 'branch'); - const targetPath = sessionSchema.sessionFilePathForBranch(repoRoot, branch); - if (!fs.existsSync(targetPath)) { - return; - } - - const parsed = JSON.parse(fs.readFileSync(targetPath, 'utf8')); - const nextRecord = { - ...parsed, - lastHeartbeatAt: new Date().toISOString(), - }; - if (options.state) { - nextRecord.state = options.state; - } - - fs.writeFileSync(targetPath, `${JSON.stringify(nextRecord, null, 2)}\n`, 'utf8'); -} - -function readSessionRecord(options) { - const repoRoot = requireOption(options, 'repo'); - const branch = requireOption(options, 'branch'); - const targetPath = sessionSchema.sessionFilePathForBranch(repoRoot, branch); - if (!fs.existsSync(targetPath)) { - return null; - } - return JSON.parse(fs.readFileSync(targetPath, 'utf8')); -} - -function terminateSessionProcess(options) { - const record = readSessionRecord(options); - const pid = Number(record?.pid); - if (!Number.isInteger(pid) || pid <= 0) { - throw new Error('No live pid recorded for branch.'); - } - - try { - process.kill(pid, 'SIGTERM'); - } catch (error) { - if (error?.code === 'ESRCH') { - return; - } - throw error; - } -} - -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 === 'heartbeat') { - refreshSessionRecord(options); - return; - } - if (command === 'terminate') { - terminateSessionProcess(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; -} diff --git a/templates/scripts/codex-agent.sh b/templates/scripts/codex-agent.sh index 89687707..66f2805f 100755 --- a/templates/scripts/codex-agent.sh +++ b/templates/scripts/codex-agent.sh @@ -324,7 +324,6 @@ 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 @@ -638,78 +637,6 @@ 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" \ - --task-mode "$TASK_MODE" \ - --openspec-tier "$OPENSPEC_TIER" \ - --routing-reason "$TASK_ROUTING_REASON" -} - -clear_active_session_state() { - local branch="$1" - run_active_session_state stop --repo "$repo_root" --branch "$branch" -} - -heartbeat_active_session_state() { - local branch="$1" - run_active_session_state heartbeat --repo "$repo_root" --branch "$branch" --state working -} - -normalize_heartbeat_interval_seconds() { - local raw="${GUARDEX_ACTIVE_SESSION_HEARTBEAT_INTERVAL_SECONDS:-15}" - if [[ "$raw" =~ ^[0-9]+$ ]] && [[ "$raw" -ge 1 ]]; then - printf '%s' "$raw" - return 0 - fi - printf '15' -} - -start_active_session_heartbeat() { - local branch="$1" - local interval - interval="$(normalize_heartbeat_interval_seconds)" - ( - while true; do - sleep "$interval" || break - heartbeat_active_session_state "$branch" - done - ) & - active_session_heartbeat_pid="$!" -} - -stop_active_session_heartbeat() { - if [[ -n "${active_session_heartbeat_pid:-}" ]]; then - kill "$active_session_heartbeat_pid" >/dev/null 2>&1 || true - wait "$active_session_heartbeat_pid" >/dev/null 2>&1 || true - active_session_heartbeat_pid="" - fi -} - origin_remote_supports_pr_finish() { local origin_url origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)" @@ -1103,22 +1030,6 @@ fi echo "[codex-agent] Task routing: $(describe_task_routing) (${TASK_ROUTING_REASON})" -active_session_recorded=0 -active_session_heartbeat_pid="" -cleanup_active_session_state_on_exit() { - set +e - if [[ "${active_session_recorded:-0}" -eq 1 && -n "${worktree_branch:-}" && "${worktree_branch:-}" != "HEAD" ]]; then - stop_active_session_heartbeat - clear_active_session_state "$worktree_branch" - active_session_recorded=0 - fi -} - -record_active_session_state "$worktree_path" "$worktree_branch" -active_session_recorded=1 -start_active_session_heartbeat "$worktree_branch" -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 @@ -1131,8 +1042,6 @@ 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 diff --git a/templates/scripts/install-vscode-active-agents-extension.js b/templates/scripts/install-vscode-active-agents-extension.js deleted file mode 100755 index 6253cdb6..00000000 --- a/templates/scripts/install-vscode-active-agents-extension.js +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env node - -const fs = require('node:fs'); -const os = require('node:os'); -const path = require('node:path'); - -const PATCH_COMPATIBILITY_WINDOW = 20; -const RETIRED_EXTENSION_IDS = [ - 'recodeee.gitguardex-active-agents', -]; - -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 resolveExtensionSource(repoRoot) { - const candidates = [ - path.join(repoRoot, 'vscode', 'guardex-active-agents'), - path.join(repoRoot, 'templates', 'vscode', 'guardex-active-agents'), - ]; - - for (const candidate of candidates) { - if (fs.existsSync(path.join(candidate, 'package.json'))) { - return candidate; - } - } - - throw new Error('Could not find the Guardex VS Code companion sources.'); -} - -function removeIfExists(targetPath) { - if (fs.existsSync(targetPath)) { - fs.rmSync(targetPath, { recursive: true, force: true }); - } -} - -function parseSimpleSemver(version) { - const parts = String(version || '').trim().split('.').map((part) => Number.parseInt(part, 10)); - if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) { - throw new Error(`Expected simple semver for the Active Agents companion, received "${version}".`); - } - return parts; -} - -function buildInstallTargets(extensionId, version, extensionsDir) { - const [major, minor, patch] = parseSimpleSemver(version); - const firstCompatiblePatch = Math.max(0, patch - PATCH_COMPATIBILITY_WINDOW); - const targets = [path.join(extensionsDir, extensionId)]; - - for (let compatiblePatch = firstCompatiblePatch; compatiblePatch <= patch; compatiblePatch += 1) { - targets.push(path.join(extensionsDir, `${extensionId}-${major}.${minor}.${compatiblePatch}`)); - } - - return targets; -} - -function isRetiredExtensionInstall(entryName, currentExtensionId) { - return RETIRED_EXTENSION_IDS - .filter((extensionId) => extensionId !== currentExtensionId) - .some((extensionId) => entryName === extensionId || entryName.startsWith(`${extensionId}-`)); -} - -function main() { - const repoRoot = path.resolve(__dirname, '..'); - const options = parseOptions(process.argv.slice(2)); - const sourceDir = resolveExtensionSource(repoRoot); - const manifest = JSON.parse(fs.readFileSync(path.join(sourceDir, 'package.json'), 'utf8')); - const extensionId = `${manifest.publisher}.${manifest.name}`; - const extensionsDir = path.resolve( - options['extensions-dir'] || - process.env.GUARDEX_VSCODE_EXTENSIONS_DIR || - process.env.VSCODE_EXTENSIONS_DIR || - path.join(os.homedir(), '.vscode', 'extensions'), - ); - - fs.mkdirSync(extensionsDir, { recursive: true }); - const targetDirs = buildInstallTargets(extensionId, manifest.version, extensionsDir); - const canonicalTargetDir = targetDirs[0]; - const keepDirNames = new Set(targetDirs.map((targetDir) => path.basename(targetDir))); - let retiredInstallCount = 0; - - for (const entry of fs.readdirSync(extensionsDir, { withFileTypes: true })) { - if (!entry.isDirectory()) { - continue; - } - if (isRetiredExtensionInstall(entry.name, extensionId)) { - removeIfExists(path.join(extensionsDir, entry.name)); - retiredInstallCount += 1; - continue; - } - if (keepDirNames.has(entry.name)) { - continue; - } - if (entry.name === extensionId || entry.name.startsWith(`${extensionId}-`)) { - removeIfExists(path.join(extensionsDir, entry.name)); - } - } - - for (const targetDir of targetDirs) { - removeIfExists(targetDir); - fs.cpSync(sourceDir, targetDir, { - recursive: true, - force: true, - preserveTimestamps: true, - }); - } - - process.stdout.write( - `[guardex-active-agents] Installed ${extensionId}@${manifest.version} to ${canonicalTargetDir}\n` + - `[guardex-active-agents] Refreshed ${targetDirs.length - 1} recent patch compatibility path(s) for already-open windows.\n` + - `[guardex-active-agents] Removed ${retiredInstallCount} retired extension install path(s).\n` + - '[guardex-active-agents] Reload each already-open VS Code window to activate the newest Source Control companion.\n', - ); -} - -try { - main(); -} catch (error) { - process.stderr.write(`[guardex-active-agents] ${error.message}\n`); - process.exitCode = 1; -} diff --git a/templates/vscode/guardex-active-agents/README.md b/templates/vscode/guardex-active-agents/README.md deleted file mode 100644 index ea5ff11a..00000000 --- a/templates/vscode/guardex-active-agents/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# GitGuardex Active Agents - -Local VS Code companion for Guardex-managed repos. - -## Quick Start - -Use the dedicated Active Agents sidebar icon to create or inspect Guardex sandboxes quickly. - -1. Install from a Guardex-wired repo: - -```sh -node scripts/install-vscode-active-agents-extension.js -``` - -2. Reload the VS Code window. -3. In the Activity Bar, open the dedicated `Active Agents` hive icon. Use `Start agent` to enter a task + agent name and launch the repo Guardex agent runner. The companion prefers `bash scripts/codex-agent.sh` when present, falls back to `npm run agent:codex --`, and only uses `gx branch start` as a last resort. - -What it does: - -- Bundles a local GitGuardex icon so repo installs show branded extension metadata inside VS Code. -- Bundles the optional `GitGuardex File Icons` theme for OpenSpec, agent worktree, and hook files in Explorer. -- Adds a dedicated `Active Agents` Activity Bar container with a hive icon and live badge count for active sessions. -- Renders one repo node per live Guardex workspace with grouped `ACTIVE AGENTS` and `CHANGES` sections. -- Splits live sessions inside `ACTIVE AGENTS` into `BLOCKED`, `WORKING NOW`, `THINKING`, `STALLED`, and `DEAD` groups so stuck, active, and inactive lanes stand out immediately. -- Mirrors the same live state in the VS Code status bar so the selected session or active-agent count stays visible outside the tree. -- Keeps the built-in Source Control view focused on real Git repositories; the Active Agents commit command prompts for a message from its own toolbar action. -- Shows one row per live Guardex sandbox session inside those activity groups, with changed-file rows nested under sessions that are touching files. -- Labels session rows with provider identity and snapshot context; snapshot-backed rows use a one-letter snapshot badge such as `N` for `nagyviktor@edixa.com`. -- Shows raw agent branch groups with the `git-branch` icon instead of the generic folder icon. -- Shows repo-root git changes in a sibling `CHANGES` section when the guarded repo itself is dirty. -- Derives session state from dirty worktree status, git conflict markers, heartbeat freshness, PID liveness, and recent file mtimes, surfaces working/dead/conflict counts in the repo/header summary, and shows changed-file counts for active edits. -- Uses distinct VS Code codicons for each session state, including animated `loading~spin` for `WORKING NOW`. -- Reads repo-local presence files from `.omx/state/active-sessions/`, expects `lastHeartbeatAt` freshness, and falls back to managed worktree-root `AGENT.lock` telemetry when the launcher session file is absent. -- Publishes `guardex.hasAgents` and `guardex.hasConflicts` context keys for other VS Code contributions. diff --git a/templates/vscode/guardex-active-agents/extension.js b/templates/vscode/guardex-active-agents/extension.js deleted file mode 100644 index 6154f838..00000000 --- a/templates/vscode/guardex-active-agents/extension.js +++ /dev/null @@ -1,3892 +0,0 @@ -const fs = require('node:fs'); -const path = require('node:path'); -const cp = require('node:child_process'); -const http = require('node:http'); -const os = require('node:os'); -const vscode = require('vscode'); -const { - clearWorktreeActivityCache, - formatElapsedFrom, - readActiveSessions, - readRepoChanges, - readSessionInspectData, - sanitizeBranchForFile, - sessionFilePathForBranch, -} = require('./session-schema.js'); - -const SESSION_DECORATION_SCHEME = 'gitguardex-agent'; -const IDLE_WARNING_MS = 10 * 60 * 1000; -const IDLE_ERROR_MS = 30 * 60 * 1000; -const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json'); -const ACTIVE_SESSION_FILES_GLOB = '**/.omx/state/active-sessions/*.json'; -const AGENT_FILE_LOCKS_GLOB = '**/.omx/state/agent-file-locks.json'; -const WORKTREE_AGENT_LOCKS_GLOB = '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock'; -const MANAGED_WORKTREE_GIT_FILES_GLOB = '**/{.omx,.omc}/agent-worktrees/*/.git'; -const MANAGED_WORKTREE_RELATIVE_ROOTS = [ - path.join('.omx', 'agent-worktrees'), - path.join('.omc', 'agent-worktrees'), -]; -const AGENT_LOG_FILES_GLOB = '**/.omx/logs/*.log'; -const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.omc/agent-worktrees}/**'; -const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**'; -const MANAGED_WORKTREE_GIT_SCAN_EXCLUDE_GLOB = '**/node_modules/**'; -const SESSION_SCAN_LIMIT = 200; -const REFRESH_DEBOUNCE_MS = 250; -const RECENTLY_ACTIVE_WINDOW_MS = 10 * 60 * 1000; -const SESSION_TOP_FILE_COUNT = 3; -const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agents', 'package.json'); -const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js'); -const RELOAD_WINDOW_ACTION = 'Reload Window'; -const UPDATE_LATER_ACTION = 'Later'; -const ACTIVE_AGENTS_EXTENSION_ID = 'Recodee.gitguardex-active-agents'; -const RESTART_EXTENSION_HOST_COMMAND = 'workbench.action.restartExtensionHost'; -const REFRESH_POLL_INTERVAL_MS = 30_000; -const INSPECT_PANEL_VIEW_TYPE = 'gitguardex.activeAgents.inspect'; -const COLONY_DEFAULT_PORT = 37777; -const COLONY_SNAPSHOT_TTL_MS = 5_000; -const COLONY_FETCH_TIMEOUT_MS = 800; - -function colonyDataDir() { - return process.env.COLONY_HOME - || process.env.CAVEMEM_HOME - || path.join(os.homedir(), '.colony'); -} - -function readColonyPort() { - try { - const raw = fs.readFileSync(path.join(colonyDataDir(), 'settings.json'), 'utf8'); - const parsed = JSON.parse(raw); - const port = Number(parsed?.workerPort); - return Number.isFinite(port) && port > 0 ? port : COLONY_DEFAULT_PORT; - } catch (_error) { - return COLONY_DEFAULT_PORT; - } -} - -function fetchColonyJson(urlPath) { - return new Promise((resolve) => { - const req = http.get( - { - hostname: '127.0.0.1', - port: readColonyPort(), - path: urlPath, - timeout: COLONY_FETCH_TIMEOUT_MS, - }, - (res) => { - if (res.statusCode !== 200) { - res.resume(); - resolve(null); - return; - } - let body = ''; - res.setEncoding('utf8'); - res.on('data', (chunk) => { - body += chunk; - }); - res.on('end', () => { - try { - resolve(JSON.parse(body)); - } catch (_error) { - resolve(null); - } - }); - }, - ); - req.on('error', () => resolve(null)); - req.on('timeout', () => { - req.destroy(); - resolve(null); - }); - }); -} - -const colonyTasksCache = new Map(); - -async function readColonyTasksForRepo(repoRoot) { - const cached = colonyTasksCache.get(repoRoot); - if (cached && Date.now() - cached.at < COLONY_SNAPSHOT_TTL_MS) { - return cached.tasks; - } - const tasks = await fetchColonyJson( - `/api/colony/tasks?repo_root=${encodeURIComponent(repoRoot)}`, - ); - const resolved = Array.isArray(tasks) ? tasks : []; - colonyTasksCache.set(repoRoot, { at: Date.now(), tasks: resolved }); - return resolved; -} - -function compactColonyBranchLabel(branch) { - if (typeof branch !== 'string' || !branch) return 'unknown'; - const parts = branch.split('/').filter(Boolean); - return parts.length > 2 ? parts.slice(-2).join('/') : branch; -} -const GIT_CONFIGURATION_SECTION = 'git'; -const REPO_SCAN_IGNORED_FOLDERS_SETTING = 'repositoryScanIgnoredFolders'; -const BUNDLED_FILE_ICONS_MANIFEST_RELATIVE = path.join('fileicons', 'gitguardex-fileicons.json'); -const MANAGED_REPO_SCAN_IGNORED_FOLDERS = [ - '.omx/agent-worktrees', - '**/.omx/agent-worktrees', - '.omx/.tmp-worktrees', - '**/.omx/.tmp-worktrees', - '.omc/agent-worktrees', - '**/.omc/agent-worktrees', - '.omc/.tmp-worktrees', - '**/.omc/.tmp-worktrees', -]; -const SESSION_ACTIVITY_GROUPS = [ - { kind: 'blocked', label: 'BLOCKED' }, - { kind: 'working', label: 'WORKING NOW' }, - { kind: 'finished', label: 'NEEDS CLEANUP' }, - { kind: 'idle', label: 'THINKING' }, - { kind: 'stalled', label: 'STALLED' }, - { kind: 'dead', label: 'DEAD' }, -]; -const SESSION_ACTIVITY_ICON_IDS = { - blocked: 'warning', - working: 'loading~spin', - finished: 'pass-filled', - idle: 'comment-discussion', - stalled: 'clock', - dead: 'error', -}; -const DISMISSABLE_SESSION_ACTIVITY_KINDS = new Set(['stalled', 'dead']); -const SESSION_PROVIDER_BRANDS = { - openai: { - id: 'openai', - label: 'OpenAI', - badge: 'AI', - }, - claude: { - id: 'claude', - label: 'Claude', - badge: 'CL', - }, -}; -let bundledTreeIconThemeCache = null; - -function iconColorId(iconId) { - switch (iconId) { - case 'warning': - case 'clock': - return 'list.warningForeground'; - case 'error': - return 'list.errorForeground'; - case 'loading~spin': - return 'gitDecoration.addedResourceForeground'; - case 'comment-discussion': - case 'info': - case 'repo': - case 'folder': - case 'graph': - case 'history': - case 'dashboard': - case 'inbox': - case 'file-directory': - case 'settings-gear': - case 'folder-library': - return 'textLink.foreground'; - case 'git-branch': - return 'gitDecoration.modifiedResourceForeground'; - case 'account': - return 'terminal.ansiYellow'; - case 'debug-pause': - return 'terminal.ansiYellow'; - case 'sparkle': - case 'rocket': - return 'terminal.ansiMagenta'; - case 'list-flat': - case 'device-camera': - return 'terminal.ansiCyan'; - case 'list-tree': - case 'telescope': - return 'terminal.ansiBlue'; - case 'organization': - return 'terminal.ansiGreen'; - case 'pass-filled': - case 'pass': - case 'check': - return 'testing.iconPassed'; - default: - return ''; - } -} - -function themeIcon(iconId, colorId = iconColorId(iconId)) { - if (!iconId) { - return undefined; - } - return colorId - ? new vscode.ThemeIcon(iconId, new vscode.ThemeColor(colorId)) - : new vscode.ThemeIcon(iconId); -} - -function sessionDecorationUri(branch) { - return vscode.Uri.parse(`${SESSION_DECORATION_SCHEME}://${sanitizeBranchForFile(branch)}`); -} - -function emptyBundledTreeIconTheme() { - return { - iconPathById: new Map(), - fileNames: {}, - folderNames: {}, - fileExtensions: {}, - }; -} - -function loadBundledTreeIconTheme() { - if (bundledTreeIconThemeCache) { - return bundledTreeIconThemeCache; - } - - const manifestPath = path.join(__dirname, BUNDLED_FILE_ICONS_MANIFEST_RELATIVE); - try { - const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); - const manifestDir = path.dirname(manifestPath); - const iconPathById = new Map(); - for (const [iconId, definition] of Object.entries(parsed?.iconDefinitions || {})) { - if (typeof definition?.iconPath !== 'string' || !definition.iconPath.trim()) { - continue; - } - const iconUri = vscode.Uri.file(path.resolve(manifestDir, definition.iconPath)); - iconPathById.set(iconId, { - light: iconUri, - dark: iconUri, - }); - } - bundledTreeIconThemeCache = { - iconPathById, - fileNames: parsed?.fileNames || {}, - folderNames: parsed?.folderNames || {}, - fileExtensions: parsed?.fileExtensions || {}, - }; - } catch (_error) { - bundledTreeIconThemeCache = emptyBundledTreeIconTheme(); - } - - return bundledTreeIconThemeCache; -} - -function resolveBundledTreeItemIconId(relativePath, kind = 'file') { - const normalizedRelativePath = normalizeRelativePath(relativePath); - const entryName = path.posix.basename(normalizedRelativePath || ''); - if (!entryName) { - return ''; - } - - const bundledTheme = loadBundledTreeIconTheme(); - if (kind === 'folder') { - return bundledTheme.folderNames[entryName] || ''; - } - - if (bundledTheme.fileNames[entryName]) { - return bundledTheme.fileNames[entryName]; - } - - const matchingExtension = Object.keys(bundledTheme.fileExtensions) - .sort((left, right) => right.length - left.length) - .find((extension) => entryName === extension || entryName.endsWith(`.${extension}`)); - return matchingExtension ? bundledTheme.fileExtensions[matchingExtension] : ''; -} - -function resolveBundledTreeItemIcon(relativePath, kind = 'file') { - const bundledTheme = loadBundledTreeIconTheme(); - const iconId = resolveBundledTreeItemIconId(relativePath, kind); - return iconId ? bundledTheme.iconPathById.get(iconId) : undefined; -} - -function sessionIdleDecoration(session, now = Date.now()) { - if (!session) { - return undefined; - } - - if (session.activityKind === 'blocked') { - return { - badge: '!', - tooltip: 'blocked', - color: new vscode.ThemeColor('list.warningForeground'), - }; - } - if (session.activityKind === 'dead') { - return { - badge: 'x', - tooltip: 'dead', - color: new vscode.ThemeColor('list.errorForeground'), - }; - } - if (session.activityKind === 'stalled') { - return { - badge: '!', - tooltip: 'stalled', - color: new vscode.ThemeColor('list.errorForeground'), - }; - } - if (session.activityKind === 'working') { - return undefined; - } - - const startedAtMs = Date.parse(session.startedAt); - if (!Number.isFinite(startedAtMs)) { - return undefined; - } - - const elapsedMs = now - startedAtMs; - if (elapsedMs > IDLE_ERROR_MS) { - return { - badge: '30m+', - tooltip: 'idle 30m+', - color: new vscode.ThemeColor('list.errorForeground'), - }; - } - if (elapsedMs > IDLE_WARNING_MS) { - return { - badge: '10m+', - tooltip: 'idle 10m+', - color: new vscode.ThemeColor('list.warningForeground'), - }; - } - - return undefined; -} - -function formatCountLabel(count, singular, plural = `${singular}s`) { - return `${count} ${count === 1 ? singular : plural}`; -} - -function branchSegments(branch) { - return String(branch || '') - .split('/') - .map((segment) => segment.trim()) - .filter(Boolean); -} - -function compactBranchLabel(branch) { - const segments = branchSegments(branch); - if (segments.length >= 3 && segments[0] === 'agent') { - return `${segments[1]}/${segments.slice(2).join('/')}`; - } - return segments.join('/'); -} - -function sessionFileCountLabel(session) { - const activityCountLabel = typeof session?.activityCountLabel === 'string' - ? session.activityCountLabel.trim() - : ''; - if (activityCountLabel) { - return activityCountLabel; - } - if ((session?.changeCount || 0) > 0) { - return formatCountLabel(session.changeCount, 'file'); - } - return ''; -} - -function uniqueStringList(values) { - const seen = new Set(); - const result = []; - - for (const value of values) { - if (typeof value !== 'string' || seen.has(value)) { - continue; - } - seen.add(value); - result.push(value); - } - - return result; -} - -function normalizeSessionProviderToken(value) { - return typeof value === 'string' ? value.trim().toLowerCase() : ''; -} - -function resolveSessionProvider(session) { - const signals = [ - session?.cliName, - session?.agentName, - session?.branch, - ] - .map(normalizeSessionProviderToken) - .filter(Boolean); - - if (signals.some((value) => value.includes('claude'))) { - return { - ...SESSION_PROVIDER_BRANDS.claude, - cliName: typeof session?.cliName === 'string' ? session.cliName.trim() : '', - }; - } - if (signals.some((value) => value.includes('codex') || value.includes('openai'))) { - return { - ...SESSION_PROVIDER_BRANDS.openai, - cliName: typeof session?.cliName === 'string' ? session.cliName.trim() : '', - }; - } - return null; -} - -function sessionProviderDecoration(session) { - const provider = resolveSessionProvider(session); - if (!provider) { - return undefined; - } - - const cliName = provider.cliName || provider.id; - return { - badge: provider.badge, - tooltip: `${provider.label} session via ${cliName}`, - }; -} - -function normalizeSnapshotIdentityValue(value) { - return typeof value === 'string' ? value.trim() : ''; -} - -function sessionSnapshotDisplayName(session) { - return normalizeSnapshotIdentityValue(session?.snapshotName) - || normalizeSnapshotIdentityValue(session?.snapshotEmail); -} - -function sessionSnapshotBadge(session) { - const displayName = sessionSnapshotDisplayName(session); - const match = displayName.match(/[a-z0-9]/i); - return match ? match[0].toUpperCase() : ''; -} - -function sessionSnapshotDescription(session) { - const displayName = sessionSnapshotDisplayName(session); - return displayName ? `snapshot ${displayName}` : ''; -} - -function sessionSnapshotDecoration(session) { - const badge = sessionSnapshotBadge(session); - const displayName = sessionSnapshotDisplayName(session); - if (!badge || !displayName) { - return undefined; - } - - return { - badge, - tooltip: `Snapshot ${displayName}`, - }; -} - -function sessionIdentityDecoration(session) { - return sessionSnapshotDecoration(session) || sessionProviderDecoration(session); -} - -function stringListsEqual(left, right) { - if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) { - return false; - } - - return left.every((value, index) => value === right[index]); -} - -async function ensureManagedRepoScanIgnores() { - if (typeof vscode.workspace.getConfiguration !== 'function') { - return; - } - - const workspaceFolders = vscode.workspace.workspaceFolders || []; - if (workspaceFolders.length === 0) { - return; - } - - const workspaceFolderTarget = workspaceFolders.length > 1 - ? vscode.ConfigurationTarget?.WorkspaceFolder - : vscode.ConfigurationTarget?.Workspace; - if (workspaceFolderTarget === undefined) { - return; - } - - for (const workspaceFolder of workspaceFolders) { - const gitConfig = vscode.workspace.getConfiguration(GIT_CONFIGURATION_SECTION, workspaceFolder); - const configuredIgnoredFolders = gitConfig.get(REPO_SCAN_IGNORED_FOLDERS_SETTING); - const existingIgnoredFolders = Array.isArray(configuredIgnoredFolders) - ? configuredIgnoredFolders - : []; - const nextIgnoredFolders = uniqueStringList([ - ...existingIgnoredFolders, - ...MANAGED_REPO_SCAN_IGNORED_FOLDERS, - ]); - - if (stringListsEqual(existingIgnoredFolders, nextIgnoredFolders)) { - continue; - } - - try { - await gitConfig.update( - REPO_SCAN_IGNORED_FOLDERS_SETTING, - nextIgnoredFolders, - workspaceFolderTarget, - ); - } catch { - // Leave the extension usable even when the current workspace settings cannot be updated. - } - } -} - -function sessionIdentityLabel(session) { - const agentName = typeof session?.agentName === 'string' ? session.agentName.trim() : ''; - const taskName = sessionDisplayLabel(session); - const label = typeof session?.label === 'string' ? session.label.trim() : ''; - - if (agentName && taskName) { - return `${agentName} · ${taskName}`; - } - if (agentName && label) { - return `${agentName} · ${label}`; - } - - return agentName || taskName || label || 'session'; -} - -function sessionCommitPlaceholder(session) { - if (!session?.branch) { - return 'Pick an Active Agents session to commit its worktree.'; - } - - return `Commit ${sessionIdentityLabel(session)} on ${session.branch} · ${formatCountLabel(session.lockCount || 0, 'lock')}`; -} - -function agentNameFromBranch(branch) { - const segments = String(branch || '') - .split('/') - .map((segment) => segment.trim()) - .filter(Boolean); - if (segments[0] === 'agent' && segments[1]) { - return segments[1]; - } - return segments[0] || 'lock'; -} - -function agentBadgeFromBranch(branch) { - const normalized = agentNameFromBranch(branch).toUpperCase().replace(/[^A-Z0-9]/g, ''); - return normalized.slice(0, 2) || 'LK'; -} - -function buildActiveAgentsStatusSummary(summary) { - const workingCount = summary?.workingCount || 0; - const finishedCount = summary?.finishedCount || 0; - const idleCount = summary?.idleCount || 0; - if (workingCount > 0 || finishedCount > 0 || idleCount > 0) { - const parts = [`${workingCount} working`]; - if (finishedCount > 0) { - parts.push(`${finishedCount} needs cleanup`); - } - parts.push(`${idleCount} idle`); - return `$(git-branch) ${parts.join(' · ')}`; - } - return `$(git-branch) ${formatCountLabel(summary?.sessionCount || 0, 'tracked session')}`; -} - -function buildActiveAgentsStatusTooltip(selectedSession, summary) { - if (selectedSession?.branch) { - return [ - selectedSession.branch, - sessionIdentityLabel(selectedSession), - formatCountLabel(selectedSession.lockCount || 0, 'lock'), - selectedSession.worktreePath, - 'Click to open Active Agents.', - ].filter(Boolean).join('\n'); - } - - const activeCount = Math.max(0, (summary?.sessionCount || 0) - (summary?.deadCount || 0)); - return [ - formatCountLabel(activeCount, 'active agent'), - formatCountLabel(summary?.workingCount || 0, 'working now session', 'working now sessions'), - formatCountLabel(summary?.finishedCount || 0, 'needs cleanup session'), - formatCountLabel(summary?.idleCount || 0, 'idle session'), - formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'), - formatCountLabel(summary?.lockedFileCount || 0, 'locked file'), - summary?.deadCount ? formatCountLabel(summary.deadCount, 'dead session') : '', - 'Click to open Active Agents.', - ].filter(Boolean).join('\n'); -} - -function compactRelativePath(relativePath) { - const normalized = normalizeRelativePath(relativePath); - if (!normalized) { - return ''; - } - - const segments = normalized.split('/').filter(Boolean); - if (segments.length <= 2) { - return normalized; - } - - return `${segments[0]}/.../${segments[segments.length - 1]}`; -} - -function summarizeCompactPaths(paths, maxCount = SESSION_TOP_FILE_COUNT) { - const compactPaths = uniqueStringList((paths || []) - .map(normalizeRelativePath) - .filter(Boolean) - .map((relativePath) => compactRelativePath(relativePath))) - .slice(0, maxCount); - if (compactPaths.length === 0) { - return ''; - } - return compactPaths.join(', '); -} - -function isProtectedBranchName(branch) { - return branch === 'main' || branch === 'dev'; -} - -function countWorkingSessions(sessions) { - return sessions.filter((session) => ( - session.activityKind === 'working' || session.activityKind === 'blocked' - )).length; -} - -function countFinishedSessions(sessions) { - return sessions.filter((session) => session.activityKind === 'finished').length; -} - -function countIdleSessions(sessions) { - return sessions.filter((session) => ( - session.activityKind === 'idle' || session.activityKind === 'stalled' - )).length; -} - -function sessionLastActiveAt(session) { - return [ - session?.lastHeartbeatAt, - session?.lastFileActivityAt, - session?.telemetryUpdatedAt, - session?.startedAt, - ].find((value) => typeof value === 'string' && value.trim().length > 0) || ''; -} - -function sessionLastActiveLabel(session) { - const lastActiveAt = sessionLastActiveAt(session); - if (!lastActiveAt) { - return ''; - } - return formatElapsedFrom(lastActiveAt); -} - -function sessionLastActiveAgeMs(session, now = Date.now()) { - const lastActiveAt = sessionLastActiveAt(session); - const timestamp = Date.parse(lastActiveAt); - if (!Number.isFinite(timestamp)) { - return null; - } - return Math.max(0, now - timestamp); -} - -function sessionFreshnessLabel(session, now = Date.now()) { - const ageMs = sessionLastActiveAgeMs(session, now); - if (session.activityKind === 'blocked') { - return 'Needs attention'; - } - if (session.activityKind === 'finished') { - return 'Needs cleanup'; - } - if (session.activityKind === 'stalled') { - return 'Possibly stale'; - } - if (session.activityKind === 'dead') { - return 'Stopped'; - } - if (ageMs === null) { - return ''; - } - if (ageMs <= IDLE_WARNING_MS) { - return 'Fresh'; - } - if (ageMs <= RECENTLY_ACTIVE_WINDOW_MS) { - return 'Recently active'; - } - if (session.activityKind === 'idle') { - return 'Idle'; - } - return 'Recently active'; -} - -function sessionStatusLabel(session) { - switch (session.activityKind) { - case 'blocked': - return 'Blocked'; - case 'working': - return 'Working'; - case 'finished': - return 'Needs cleanup'; - case 'idle': - return 'Idle'; - case 'stalled': - return 'Stale'; - case 'dead': - return 'Dead'; - default: - return 'Thinking'; - } -} - -function sessionHealthScore(session) { - return Number.isInteger(session?.sessionHealth?.score) ? session.sessionHealth.score : null; -} - -function buildSessionHealthCompactLabel(session) { - const score = sessionHealthScore(session); - return score === null ? '' : `${score}/100`; -} - -function buildSessionHealthSummary(session) { - const compactLabel = buildSessionHealthCompactLabel(session); - if (!compactLabel) { - return ''; - } - - const label = typeof session?.sessionHealth?.label === 'string' - ? session.sessionHealth.label.trim() - : ''; - return label ? `${compactLabel} · ${label}` : compactLabel; -} - -function buildSessionHealthDriversSummary(session) { - const primaryDriver = typeof session?.sessionHealth?.primaryDriver === 'string' - ? session.sessionHealth.primaryDriver.trim() - : ''; - const secondaries = uniqueStringList(Array.isArray(session?.sessionHealth?.secondaries) - ? session.sessionHealth.secondaries.map((value) => String(value || '').trim()) - : []); - return [ - primaryDriver ? `Primary: ${primaryDriver}` : '', - secondaries.length > 0 ? `Secondary: ${secondaries.join(', ')}` : '', - ].filter(Boolean).join(' | '); -} - -function buildSessionHealthTooltip(session) { - const outputLine = typeof session?.sessionHealth?.outputLine === 'string' - ? session.sessionHealth.outputLine.trim() - : ''; - if (outputLine) { - return outputLine; - } - - return [ - buildSessionHealthSummary(session), - buildSessionHealthDriversSummary(session), - ].filter(Boolean).join('\n'); -} - -function buildSessionTopFiles(session) { - return uniqueStringList((session?.worktreeChangedPaths || []) - .map(normalizeRelativePath) - .filter(Boolean)) - .slice(0, SESSION_TOP_FILE_COUNT); -} - -function buildSessionRecentChangeSummary(session) { - if (session?.latestTaskPreview && session.latestTaskPreview !== session.taskName) { - return session.latestTaskPreview; - } - const topFiles = summarizeCompactPaths(session?.worktreeChangedPaths || []); - if (topFiles) { - return `Changed ${topFiles}`; - } - if (session?.activitySummary) { - return session.activitySummary; - } - return 'No recent change summary.'; -} - -function sessionRiskBadges(session) { - return uniqueStringList([ - session?.activityKind === 'blocked' ? 'Blocked' : '', - session?.activityKind === 'stalled' ? 'Stale' : '', - session?.conflictCount > 0 ? 'Conflict' : '', - session?.lockCount > 0 ? 'Locked' : '', - ].filter(Boolean)); -} - -function changeRiskBadges(change) { - return uniqueStringList([ - change?.protectedBranch ? 'Protected branch' : '', - change?.hasForeignLock ? 'Conflict' : '', - !change?.hasForeignLock && change?.lockOwnerBranch ? 'Locked' : '', - change?.deltaLabel || '', - ].filter(Boolean)); -} - -function changeNeedsWarningIcon(change) { - return Boolean( - change?.protectedBranch - || change?.hasForeignLock - || (!change?.hasForeignLock && change?.lockOwnerBranch), - ); -} - -function buildSessionCardDescription(session) { - const provider = resolveSessionProvider(session); - const statusAgentLabel = `${sessionStatusLabel(session)}: ${session.agentName || 'agent'}`; - const descriptionParts = [ - statusAgentLabel, - provider?.label ? `via ${provider.label}` : '', - sessionSnapshotDescription(session), - session.deltaLabel || '', - session.changeCount > 0 ? formatCountLabel(session.changeCount, 'changed file') : '', - session.lockCount > 0 ? formatCountLabel(session.lockCount, 'lock') : '', - buildSessionHealthCompactLabel(session), - session.freshnessLabel || '', - session.lastActiveLabel ? `${session.lastActiveLabel} ago` : '', - ].filter(Boolean); - return descriptionParts.join(' · '); -} - -function buildRawSessionDescription(session) { - const provider = resolveSessionProvider(session); - const descriptionParts = [sessionStatusLabel(session)]; - const fileCountLabel = sessionFileCountLabel(session); - if (fileCountLabel) { - descriptionParts.push(fileCountLabel); - } - if (provider?.label) { - descriptionParts.push(provider.label); - } - const snapshot = sessionSnapshotDescription(session); - if (snapshot) { - descriptionParts.push(snapshot); - } - descriptionParts.push(session.elapsedLabel || formatElapsedFrom(session.startedAt)); - const sessionHealthLabel = buildSessionHealthCompactLabel(session); - if (sessionHealthLabel) { - descriptionParts.push(sessionHealthLabel); - } - if (session.lockCount > 0) { - descriptionParts.push(formatCountLabel(session.lockCount, 'lock')); - } - return descriptionParts.join(' · '); -} - -function buildSessionTooltip(session, description) { - const provider = resolveSessionProvider(session); - const riskSummary = uniqueStringList([ - ...(session?.riskBadges || []), - session?.deltaLabel || '', - ].filter(Boolean)).join(', '); - const topFiles = session?.topChangedFilesLabel || summarizeCompactPaths(session?.worktreeChangedPaths || []); - const sessionHealthSummary = buildSessionHealthSummary(session); - const sessionHealthDrivers = buildSessionHealthDriversSummary(session); - return [ - session.branch, - provider?.label - ? `Provider ${provider.label}${provider.cliName ? ` (${provider.cliName})` : ''}` - : '', - sessionSnapshotDisplayName(session) ? `Snapshot ${sessionSnapshotDisplayName(session)}` : '', - `${session.agentName} · ${sessionDisplayLabel(session)}`, - `Status ${description}`, - sessionHealthSummary ? `Session health ${sessionHealthSummary}` : '', - sessionHealthDrivers ? `Drivers ${sessionHealthDrivers}` : '', - session.recentChangeSummary ? `Recent ${session.recentChangeSummary}` : '', - topFiles ? `Top files ${topFiles}` : '', - riskSummary ? `Signals ${riskSummary}` : '', - session.conflictCount > 0 ? `Conflicts ${session.conflictCount}` : '', - session.lastActiveAt ? `Last active ${session.lastActiveAt}` : '', - session.sourceKind === 'worktree-lock' - ? `Telemetry updated ${session.telemetryUpdatedAt || session.startedAt}` - : `Started ${session.startedAt}`, - session.worktreePath, - ].filter(Boolean).join('\n'); -} - -function buildUnassignedChangeDescription(change) { - return [ - change.statusLabel, - ...changeRiskBadges(change), - ].filter(Boolean).join(' · '); -} - -function buildWorktreeBranchDescription(sessions) { - const sessionList = Array.isArray(sessions) ? sessions : []; - const primarySession = sessionList[0] || null; - if (!primarySession) { - return ''; - } - - const descriptionParts = [ - `${sessionStatusLabel(primarySession).toLowerCase()}: ${primarySession.agentName || 'agent'}`, - sessionSnapshotDescription(primarySession), - ]; - if (sessionList.length > 1) { - descriptionParts.push(formatCountLabel(sessionList.length, 'agent')); - } - return descriptionParts.filter(Boolean).join(' · '); -} - -function buildOverviewDescription(summary) { - return [ - formatCountLabel(summary?.workingCount || 0, 'working agent'), - formatCountLabel(summary?.finishedCount || 0, 'needs cleanup agent'), - formatCountLabel(summary?.idleCount || 0, 'idle agent'), - summary?.colonyTaskCount - ? formatCountLabel(summary.colonyTaskCount, 'colony task') - : '', - summary?.pendingHandoffCount - ? formatCountLabel(summary.pendingHandoffCount, 'pending handoff') - : '', - formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'), - formatCountLabel(summary?.lockedFileCount || 0, 'locked file'), - formatCountLabel(summary?.conflictCount || 0, 'conflict'), - ] - .filter(Boolean) - .join(' · '); -} - -function buildRepoDescription(summary) { - return buildOverviewDescription(summary); -} - -function buildRepoTooltip(repoRoot, summary) { - return [ - repoRoot, - buildOverviewDescription(summary), - ].join('\n'); -} - -function repoRootDisplayLabel(repoRoot) { - const normalizedRepoRoot = path.resolve(repoRoot); - const matchingWorkspaceRoots = (vscode.workspace.workspaceFolders || []) - .map((folder) => (typeof folder?.uri?.fsPath === 'string' ? path.resolve(folder.uri.fsPath) : '')) - .filter((workspaceRoot) => workspaceRoot && isPathWithin(workspaceRoot, normalizedRepoRoot)) - .sort((left, right) => right.length - left.length); - - const workspaceRoot = matchingWorkspaceRoots[0]; - if (!workspaceRoot) { - return path.basename(normalizedRepoRoot); - } - - const workspaceLabel = path.basename(workspaceRoot); - const relativePath = normalizeRelativePath(path.relative(workspaceRoot, normalizedRepoRoot)); - if (!relativePath) { - return workspaceLabel; - } - - return [ - workspaceLabel, - ...relativePath.split('/').filter(Boolean), - ].join('/'); -} - -function sessionSnapshotKey(session) { - return `${session?.repoRoot || ''}::${session?.branch || ''}`; -} - -function changeSnapshotKey(repoRoot, change) { - return `${repoRoot || ''}::${normalizeRelativePath(change?.relativePath)}`; -} - -function buildSessionSnapshot(session) { - return { - activityKind: session.activityKind, - changeCount: session.changeCount || 0, - conflictCount: session.conflictCount || 0, - lockCount: session.lockCount || 0, - changedPaths: [...(session.changedPaths || [])], - }; -} - -function buildChangeSnapshot(change) { - return { - statusLabel: change.statusLabel, - hasForeignLock: Boolean(change.hasForeignLock), - lockOwnerBranch: change.lockOwnerBranch || '', - }; -} - -function deriveSessionDelta(previousSnapshot, currentSession) { - if (!previousSnapshot) { - return ''; - } - if (currentSession.conflictCount > previousSnapshot.conflictCount) { - return 'Conflict'; - } - if (currentSession.activityKind !== previousSnapshot.activityKind) { - return sessionStatusLabel(currentSession); - } - if ( - currentSession.changeCount !== previousSnapshot.changeCount - || !stringListsEqual(currentSession.changedPaths || [], previousSnapshot.changedPaths || []) - ) { - return 'New'; - } - if (currentSession.lockCount !== previousSnapshot.lockCount) { - return 'Updated'; - } - return ''; -} - -function deriveChangeDelta(previousSnapshot, currentChange) { - if (!previousSnapshot) { - return ''; - } - if (currentChange.hasForeignLock && !previousSnapshot.hasForeignLock) { - return 'Conflict'; - } - if ( - currentChange.statusLabel !== previousSnapshot.statusLabel - || currentChange.lockOwnerBranch !== previousSnapshot.lockOwnerBranch - ) { - return 'Updated'; - } - return ''; -} - -function workingSessionSortKey(session) { - if (session.activityKind === 'blocked') { - return 0; - } - if (session.conflictCount > 0) { - return 1; - } - if (session.deltaLabel === 'Conflict') { - return 2; - } - if (session.deltaLabel === 'New') { - return 3; - } - if (session.activityKind === 'finished') { - return 5; - } - return 4; -} - -function idleSessionSortKey(session) { - if (session.activityKind === 'stalled') { - return 0; - } - if (session.activityKind === 'idle') { - return 1; - } - if (session.activityKind === 'dead') { - return 2; - } - return 3; -} - -function sortSessionsForWorkingNow(sessions) { - return [...sessions].sort((left, right) => { - const keyDelta = workingSessionSortKey(left) - workingSessionSortKey(right); - if (keyDelta !== 0) { - return keyDelta; - } - const timeDelta = sessionLastActiveAgeMs(left) - sessionLastActiveAgeMs(right); - if (Number.isFinite(timeDelta) && timeDelta !== 0) { - return timeDelta; - } - const changeDelta = (right.changeCount || 0) - (left.changeCount || 0); - if (changeDelta !== 0) { - return changeDelta; - } - return sessionDisplayLabel(left).localeCompare(sessionDisplayLabel(right)); - }); -} - -function sortSessionsForIdleThinking(sessions) { - return [...sessions].sort((left, right) => { - const keyDelta = idleSessionSortKey(left) - idleSessionSortKey(right); - if (keyDelta !== 0) { - return keyDelta; - } - const timeDelta = sessionLastActiveAgeMs(right) - sessionLastActiveAgeMs(left); - if (Number.isFinite(timeDelta) && timeDelta !== 0) { - return timeDelta; - } - return sessionDisplayLabel(left).localeCompare(sessionDisplayLabel(right)); - }); -} - -function sortUnassignedChanges(changes) { - return [...changes].sort((left, right) => { - const leftBadges = changeRiskBadges(left).length; - const rightBadges = changeRiskBadges(right).length; - if (leftBadges !== rightBadges) { - return rightBadges - leftBadges; - } - return normalizeRelativePath(left.relativePath).localeCompare(normalizeRelativePath(right.relativePath)); - }); -} - -function escapeHtml(value) { - return String(value || '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -function formatInspectBranchSummary(inspectData) { - if (Number.isInteger(inspectData?.aheadCount) && Number.isInteger(inspectData?.behindCount)) { - return `${inspectData.aheadCount} ahead · ${inspectData.behindCount} behind vs ${inspectData.compareRef}`; - } - return `Branch comparison unavailable vs ${inspectData?.compareRef || 'origin/dev'}`; -} - -function inspectPanelTitle(session) { - return `Inspect ${sessionDisplayLabel(session)}`; -} - -function renderInspectPanelHtml(session, inspectData) { - const heldLocksMarkup = Array.isArray(inspectData?.heldLocks) && inspectData.heldLocks.length > 0 - ? `
    ${inspectData.heldLocks.map((entry) => ( - `
  • ${escapeHtml(entry.relativePath)}${entry.allowDelete ? ' delete ok' : ''}${entry.claimedAt ? ` ${escapeHtml(entry.claimedAt)}` : ''}
  • ` - )).join('')}
` - : '

No held locks recorded for this session.

'; - const logContent = inspectData?.logTailText - ? escapeHtml(inspectData.logTailText) - : 'No log output available.'; - - return ` - - - - - - - -

${escapeHtml(sessionIdentityLabel(session))}

-
-
Branch
-
${escapeHtml(session.branch)}
-
Worktree
-
${escapeHtml(session.worktreePath)}
-
Base branch
-
${escapeHtml(inspectData?.baseBranch || 'dev')}
-
Divergence
-
${escapeHtml(formatInspectBranchSummary(inspectData))}
-
Held locks
-
${Array.isArray(inspectData?.heldLocks) ? inspectData.heldLocks.length : 0}
-
Log file
-
${escapeHtml(inspectData?.logPath || 'Unavailable')}
-
-

Held Locks

- ${heldLocksMarkup} -

Agent Log Tail

-
${logContent}
- -`; -} - -class SessionDecorationProvider { - constructor(nowProvider = () => Date.now()) { - this.nowProvider = nowProvider; - this.sessionsByUri = new Map(); - this.lockEntriesByFileUri = new Map(); - this.selectedBranch = ''; - this.onDidChangeFileDecorationsEmitter = new vscode.EventEmitter(); - this.onDidChangeFileDecorations = this.onDidChangeFileDecorationsEmitter.event; - } - - updateSessions(sessions) { - this.sessionsByUri = new Map( - sessions.map((session) => [sessionDecorationUri(session.branch).toString(), session]), - ); - } - - updateLockEntries(repoEntries) { - const nextEntriesByUri = new Map(); - for (const entry of repoEntries || []) { - for (const [relativePath, lockEntry] of entry.lockEntries || []) { - nextEntriesByUri.set( - vscode.Uri.file(path.join(entry.repoRoot, relativePath)).toString(), - { branch: lockEntry.branch }, - ); - } - } - this.lockEntriesByFileUri = nextEntriesByUri; - } - - setSelectedBranch(branch) { - this.selectedBranch = typeof branch === 'string' ? branch.trim() : ''; - } - - refresh() { - this.onDidChangeFileDecorationsEmitter.fire(); - } - - provideFileDecoration(uri) { - if (!uri || uri.scheme !== SESSION_DECORATION_SCHEME) { - if (!uri || uri.scheme !== 'file') { - return undefined; - } - - const lockEntry = this.lockEntriesByFileUri.get(uri.toString()); - if (!lockEntry?.branch) { - return undefined; - } - - const ownsSelectedSession = Boolean(this.selectedBranch) && lockEntry.branch === this.selectedBranch; - return { - badge: agentBadgeFromBranch(lockEntry.branch), - tooltip: ownsSelectedSession - ? `Locked by selected session ${lockEntry.branch}` - : this.selectedBranch - ? `Locked by ${lockEntry.branch} (selected session: ${this.selectedBranch})` - : `Locked by ${lockEntry.branch}`, - color: new vscode.ThemeColor( - ownsSelectedSession - ? 'gitDecoration.modifiedResourceForeground' - : this.selectedBranch - ? 'list.errorForeground' - : 'list.warningForeground', - ), - }; - } - - const session = this.sessionsByUri.get(uri.toString()); - const idleDecoration = sessionIdleDecoration(session, this.nowProvider()); - if (idleDecoration) { - return idleDecoration; - } - return sessionIdentityDecoration(session); - } -} - -class InfoItem extends vscode.TreeItem { - constructor(label, description = '') { - super(label, vscode.TreeItemCollapsibleState.None); - this.description = description; - this.iconPath = themeIcon('info'); - this.tooltip = [label, description].filter(Boolean).join('\n'); - } -} - -class DetailItem extends vscode.TreeItem { - constructor(label, description = '', options = {}) { - super(label, vscode.TreeItemCollapsibleState.None); - this.description = description; - this.tooltip = options.tooltip || [label, description].filter(Boolean).join('\n'); - this.iconPath = options.iconId ? themeIcon(options.iconId, options.iconColorId) : undefined; - } -} - -class RepoItem extends vscode.TreeItem { - constructor(repoRoot, sessions, changes, options = {}) { - const label = typeof options.label === 'string' && options.label.trim() - ? options.label.trim() - : repoRootDisplayLabel(repoRoot); - super(label, vscode.TreeItemCollapsibleState.Expanded); - this.repoRoot = repoRoot; - this.sessions = sessions; - this.changes = changes; - this.unassignedChanges = options.unassignedChanges || []; - this.lockEntries = options.lockEntries || []; - this.colonyTasks = Array.isArray(options.colonyTasks) ? options.colonyTasks : []; - this.overview = options.overview - || buildRepoOverview(sessions, this.unassignedChanges, this.lockEntries, this.colonyTasks); - this.description = buildRepoDescription(this.overview); - this.tooltip = buildRepoTooltip(repoRoot, this.overview); - this.iconPath = themeIcon('repo'); - this.contextValue = 'gitguardex.repo'; - } -} - -class SectionItem extends vscode.TreeItem { - constructor(label, items, options = {}) { - const collapsibleState = items.length > 0 - ? (options.collapsedState ?? vscode.TreeItemCollapsibleState.Expanded) - : vscode.TreeItemCollapsibleState.None; - super(label, collapsibleState); - this.items = items; - this.description = options.description - || (items.length > 0 ? String(items.length) : ''); - this.tooltip = options.tooltip || [label, this.description].filter(Boolean).join('\n'); - this.iconPath = options.iconId ? themeIcon(options.iconId, options.iconColorId) : undefined; - this.contextValue = 'gitguardex.section'; - } -} - -class WorktreeItem extends vscode.TreeItem { - constructor(worktreePath, sessions, items = [], options = {}) { - const normalizedWorktreePath = typeof worktreePath === 'string' ? worktreePath.trim() : ''; - const sessionList = Array.isArray(sessions) ? sessions : []; - const primarySession = options.resourceSession || sessionList[0] || null; - const changedCount = Number.isInteger(options.changedCount) - ? options.changedCount - : sessionList.reduce((total, session) => total + (session.changeCount || 0), 0); - const label = typeof options.label === 'string' && options.label.trim() - ? options.label.trim() - : worktreeDisplayLabel(normalizedWorktreePath, sessionList); - super( - label, - items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None, - ); - this.worktreePath = normalizedWorktreePath; - this.sessions = sessionList; - this.items = items; - this.description = options.description || buildWorktreeDescription(sessionList, changedCount); - this.tooltip = [ - normalizedWorktreePath, - ...sessionList.map((session) => session.branch).filter(Boolean), - ].filter(Boolean).join('\n'); - this.iconPath = themeIcon(options.iconId || 'folder', options.iconColorId); - if (options.useSessionDecoration && primarySession?.branch) { - this.resourceUri = sessionDecorationUri(primarySession.branch); - } - this.contextValue = 'gitguardex.worktree'; - if (primarySession?.worktreePath) { - this.command = { - command: 'gitguardex.activeAgents.openWorktree', - title: 'Open Agent Worktree', - arguments: [primarySession], - }; - } - } -} - -class SessionItem extends vscode.TreeItem { - constructor(session, items = [], options = {}) { - const variant = options.variant === 'raw' ? 'raw' : 'card'; - const label = typeof options.label === 'string' && options.label.trim() - ? options.label.trim() - : (variant === 'raw' ? session.label : sessionDisplayLabel(session)); - const collapsibleState = items.length > 0 - ? (options.collapsedState ?? ( - variant === 'raw' - ? vscode.TreeItemCollapsibleState.Expanded - : vscode.TreeItemCollapsibleState.Collapsed - )) - : vscode.TreeItemCollapsibleState.None; - super( - label, - collapsibleState, - ); - this.session = session; - this.items = items; - this.resourceUri = sessionDecorationUri(session.branch); - this.description = variant === 'raw' - ? buildRawSessionDescription(session) - : buildSessionCardDescription(session); - this.tooltip = buildSessionTooltip(session, this.description); - this.iconPath = themeIcon(resolveSessionActivityIconId(session.activityKind)); - this.contextValue = sessionContextValue(session); - this.command = { - command: 'gitguardex.activeAgents.openWorktree', - title: 'Open Agent Worktree', - arguments: [session], - }; - } -} - -function sessionContextValue(session) { - const activityKind = typeof session?.activityKind === 'string' ? session.activityKind.trim() : ''; - return activityKind - ? `gitguardex.session.${activityKind}` - : 'gitguardex.session'; -} - -function canDismissSession(session) { - return DISMISSABLE_SESSION_ACTIVITY_KINDS.has(session?.activityKind); -} - -function buildDismissSessionDetail(session, statePath) { - const repoRoot = typeof session?.repoRoot === 'string' ? session.repoRoot.trim() : ''; - const relativeStatePath = repoRoot - ? path.relative(repoRoot, statePath) || path.basename(statePath) - : path.basename(statePath); - const detailParts = [ - `Remove ${relativeStatePath} and hide this session from Active Agents.`, - ]; - - if (session?.activityKind === 'stalled') { - detailParts.push('This dismisses the stale sidebar row only; use Stop if you want to interrupt a live agent.'); - } else { - detailParts.push('This clears the stale session record from the sidebar.'); - } - - return detailParts.join(' '); -} - -class FolderItem extends vscode.TreeItem { - constructor(label, relativePath, items, options = {}) { - super( - label, - items.length > 0 - ? (options.collapsedState ?? vscode.TreeItemCollapsibleState.Expanded) - : vscode.TreeItemCollapsibleState.None, - ); - this.relativePath = relativePath; - this.items = items; - this.description = typeof options.description === 'string' ? options.description : ''; - this.tooltip = options.tooltip || relativePath || label; - this.iconPath = options.iconPath - || (!options.iconId ? resolveBundledTreeItemIcon(relativePath || label, 'folder') : undefined) - || themeIcon(options.iconId || 'folder', options.iconColorId); - this.contextValue = options.contextValue || 'gitguardex.folder'; - } -} - -class ChangeItem extends vscode.TreeItem { - constructor(change, options = {}) { - const label = typeof options.label === 'string' && options.label.trim() - ? options.label.trim() - : path.basename(change.relativePath); - super(label, vscode.TreeItemCollapsibleState.None); - this.change = change; - this.description = typeof options.description === 'string' - ? options.description - : change.statusLabel; - this.tooltip = [ - change.relativePath, - `Summary ${this.description}`, - `Status ${change.statusText}`, - change.originalPath ? `Renamed from ${change.originalPath}` : '', - change.hasForeignLock ? `Locked by ${change.lockOwnerBranch}` : '', - change.absolutePath, - ].filter(Boolean).join('\n'); - this.resourceUri = vscode.Uri.file(change.absolutePath); - if (options.iconId || change.hasForeignLock) { - this.iconPath = themeIcon(options.iconId || 'warning', options.iconColorId || 'list.warningForeground'); - } else { - this.iconPath = options.iconPath || resolveBundledTreeItemIcon(change.relativePath || label, 'file'); - } - this.contextValue = 'gitguardex.change'; - this.command = { - command: 'gitguardex.activeAgents.openChange', - title: 'Open Changed File', - arguments: [change], - }; - } -} - -function shellQuote(value) { - const normalized = String(value || ''); - return `'${normalized.replace(/'/g, "'\"'\"'")}'`; -} - - -function hasGitMarker(dirPath) { - return fs.existsSync(path.join(dirPath, '.git')); -} - -function shouldSkipRepoDiscoveryDir(dirName) { - return new Set([ - '.git', - '.omx', - '.omc', - 'node_modules', - 'dist', - 'build', - '.next', - ]).has(dirName); -} - -function discoverNestedGitRepoRoots(rootPath, maxDepth = 3) { - const discovered = []; - - function visit(dirPath, depth) { - if (depth > maxDepth) return; - let entries = []; - try { - entries = fs.readdirSync(dirPath, { withFileTypes: true }); - } catch (_error) { - return; - } - - for (const entry of entries) { - if (!entry.isDirectory() || shouldSkipRepoDiscoveryDir(entry.name)) { - continue; - } - const childPath = path.join(dirPath, entry.name); - if (hasGitMarker(childPath)) { - discovered.push(childPath); - continue; - } - visit(childPath, depth + 1); - } - } - - visit(rootPath, 1); - return discovered; -} - -function discoverWorkspaceRepoRoots() { - const workspaceFolders = vscode.workspace.workspaceFolders || []; - const seen = new Set(); - const roots = []; - - for (const folder of workspaceFolders) { - const rootPath = folder?.uri?.fsPath; - if (!rootPath || seen.has(rootPath)) { - continue; - } - seen.add(rootPath); - roots.push(rootPath); - - for (const nestedRoot of discoverNestedGitRepoRoots(rootPath)) { - if (seen.has(nestedRoot)) { - continue; - } - seen.add(nestedRoot); - roots.push(nestedRoot); - } - } - - return roots; -} - -function repoPickLabel(repoRoot) { - const parent = path.basename(path.dirname(repoRoot)); - const base = path.basename(repoRoot); - return parent ? `${parent}/${base}` : base; -} - -function readGitOutput(repoRoot, args) { - try { - return cp.execFileSync('git', ['-C', repoRoot, ...args], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }).trim(); - } catch { - return null; - } -} - -function repoGitSummary(repoRoot) { - const branch = readGitOutput(repoRoot, ['branch', '--show-current']) || 'unknown'; - const status = readGitOutput(repoRoot, ['status', '--porcelain']); - return { - branch, - dirty: status === null ? 'unknown' : status.length > 0 ? 'dirty' : 'clean', - }; -} - -function repoPickDescription(repoRoot) { - const summary = repoGitSummary(repoRoot); - return `${summary.branch} · ${summary.dirty}`; -} - -function findRepoRootForPath(repoRoots, candidatePath) { - const normalizedCandidatePath = normalizeAbsolutePath(candidatePath); - if (!normalizedCandidatePath) { - return null; - } - - return repoRoots - .filter((repoRoot) => isPathWithin(repoRoot, normalizedCandidatePath)) - .sort((left, right) => right.length - left.length)[0] || null; -} - -function activeScmRootPath() { - const sourceControl = vscode.scm?.activeSourceControl; - return sourceControl?.rootUri?.fsPath || sourceControl?.rootUri?.path || ''; -} - -function preferredRepoRoot(repoRoots) { - return findRepoRootForPath(repoRoots, activeScmRootPath()) - || findRepoRootForPath(repoRoots, vscode.window.activeTextEditor?.document?.uri?.fsPath); -} - -function resolveStartAgentCommand(repoRoot, details) { - const taskArg = shellQuote(details.taskName); - const agentArg = shellQuote(details.agentName); - return `gx agents start ${taskArg} --agent ${agentArg} --target ${shellQuote(repoRoot)}`; -} - -function sessionTaskLabel(session) { - const latestTaskPreview = typeof session?.latestTaskPreview === 'string' - ? session.latestTaskPreview.trim() - : ''; - if (latestTaskPreview) { - return latestTaskPreview; - } - - const taskName = typeof session?.taskName === 'string' ? session.taskName.trim() : ''; - if (taskName) { - return taskName; - } - - return ''; -} - -function sessionDisplayLabel(session) { - return sessionTaskLabel(session) - || session?.label - || compactBranchLabel(session?.branch) - || session?.branch - || path.basename(session?.worktreePath || '') - || 'session'; -} - -function sessionTreeLabel(session) { - return sessionTaskLabel(session) || compactBranchLabel(session?.branch) || sessionDisplayLabel(session); -} - -function worktreeDisplayLabel(worktreePath, sessions) { - const sessionList = Array.isArray(sessions) - ? sessions.filter(Boolean) - : []; - if (sessionList.length === 1) { - return sessionDisplayLabel(sessionList[0]); - } - - return path.basename(String(worktreePath || '').trim()) || 'worktree'; -} - -function buildWorktreeDescription(sessions, changedCount) { - const sessionList = Array.isArray(sessions) - ? sessions.filter(Boolean) - : []; - const primarySession = sessionList.length === 1 ? sessionList[0] : null; - const totalLocks = sessionList.reduce((total, session) => total + (session.lockCount || 0), 0); - const descriptionParts = []; - - if (primarySession?.agentName) { - descriptionParts.push(primarySession.agentName); - } else { - descriptionParts.push(formatCountLabel(sessionList.length, 'agent')); - } - - const fileCountLabel = primarySession - ? sessionFileCountLabel(primarySession) - : changedCount > 0 - ? formatCountLabel(changedCount, 'file') - : ''; - if (fileCountLabel) { - descriptionParts.push(fileCountLabel); - } - if (totalLocks > 0) { - descriptionParts.push(formatCountLabel(totalLocks, 'lock')); - } - - return descriptionParts.join(' · '); -} - -function sessionWorktreePath(session) { - return typeof session?.worktreePath === 'string' ? session.worktreePath.trim() : ''; -} - -function resolveSessionProjectRelativePath(session) { - const repoRoot = typeof session?.repoRoot === 'string' ? session.repoRoot.trim() : ''; - if (!repoRoot) { - return ''; - } - - const resolveCandidate = (candidatePath) => { - const normalizedCandidate = typeof candidatePath === 'string' ? candidatePath.trim() : ''; - if (!normalizedCandidate) { - return ''; - } - - const absolutePath = path.isAbsolute(normalizedCandidate) - ? path.resolve(normalizedCandidate) - : path.resolve(repoRoot, normalizedCandidate); - if (!isPathWithin(repoRoot, absolutePath) || !fs.existsSync(absolutePath)) { - return ''; - } - - return normalizeRelativePath(path.relative(repoRoot, absolutePath)); - }; - - const isManagedWorktreeRelativePath = (relativePath) => { - const normalizedRelativePath = normalizeRelativePath(relativePath); - return MANAGED_WORKTREE_RELATIVE_ROOTS.some((managedRoot) => { - const normalizedManagedRoot = normalizeRelativePath(managedRoot); - return normalizedRelativePath === normalizedManagedRoot - || normalizedRelativePath.startsWith(`${normalizedManagedRoot}/`); - }); - }; - - const explicitProjectPath = resolveCandidate(session?.projectPath); - if (explicitProjectPath && !isManagedWorktreeRelativePath(explicitProjectPath)) { - return explicitProjectPath; - } - - const namedProjectPath = resolveCandidate(session?.projectName); - if (namedProjectPath && !isManagedWorktreeRelativePath(namedProjectPath)) { - return namedProjectPath; - } - return ''; -} - -function worktreeProjectRelativePath(sessions) { - const projectPaths = uniqueStringList((sessions || []) - .map((session) => resolveSessionProjectRelativePath(session)) - .filter(Boolean)); - return projectPaths.length === 1 ? projectPaths[0] : ''; -} - -function repoEntryDisplayLabel(repoRoot, sessions) { - const repoLabel = repoRootDisplayLabel(repoRoot); - const projectPaths = uniqueStringList((sessions || []) - .map((session) => resolveSessionProjectRelativePath(session)) - .filter(Boolean)); - if (projectPaths.length !== 1) { - return repoLabel; - } - - const [projectRelativePath] = projectPaths; - const hasRootScopedSession = (sessions || []).some( - (session) => !resolveSessionProjectRelativePath(session), - ); - if (!projectRelativePath || hasRootScopedSession) { - return repoLabel; - } - if (repoLabel.endsWith(`/${projectRelativePath}`)) { - return repoLabel; - } - return `${repoLabel}/${projectRelativePath}`; -} - -function buildProjectScopedDescription(entries) { - const sessions = (entries || []).flatMap((entry) => Array.isArray(entry?.sessions) ? entry.sessions : []); - if (sessions.length === 0) { - return ''; - } - - const changedCount = sessions.reduce((total, session) => total + (session.changeCount || 0), 0); - const lockCount = sessions.reduce((total, session) => total + (session.lockCount || 0), 0); - const descriptionParts = [formatCountLabel(sessions.length, 'agent')]; - if (changedCount > 0) { - descriptionParts.push(formatCountLabel(changedCount, 'file')); - } - if (lockCount > 0) { - descriptionParts.push(formatCountLabel(lockCount, 'lock')); - } - return descriptionParts.join(' · '); -} - -function buildProjectScopedItems(entries, options = {}) { - const normalizedEntries = Array.isArray(entries) - ? entries.filter((entry) => entry?.item) - : []; - const projectRoots = []; - const rootEntries = []; - let hasProjectFolders = false; - - function sortFolders(nodes) { - nodes.sort((left, right) => left.label.localeCompare(right.label)); - for (const node of nodes) { - sortFolders(node.children); - } - } - - for (const entry of normalizedEntries) { - const projectRelativePath = normalizeRelativePath(entry.projectRelativePath); - if (!projectRelativePath) { - rootEntries.push(entry); - continue; - } - - hasProjectFolders = true; - let nodes = projectRoots; - let folderPath = ''; - let parentNode = null; - for (const segment of projectRelativePath.split('/').filter(Boolean)) { - folderPath = folderPath ? path.posix.join(folderPath, segment) : segment; - let folderNode = nodes.find((node) => node.relativePath === folderPath); - if (!folderNode) { - folderNode = { - label: segment, - relativePath: folderPath, - children: [], - entries: [], - directEntries: [], - }; - nodes.push(folderNode); - } - folderNode.entries.push(entry); - parentNode = folderNode; - nodes = folderNode.children; - } - - if (parentNode) { - parentNode.directEntries.push(entry); - } else { - rootEntries.push(entry); - } - } - - if (!hasProjectFolders) { - return rootEntries.map((entry) => entry.item); - } - - sortFolders(projectRoots); - - function materialize(nodes) { - return nodes.map((node) => new FolderItem( - node.label, - node.relativePath, - [ - ...materialize(node.children), - ...node.directEntries.map((entry) => entry.item), - ], - { - description: buildProjectScopedDescription(node.entries), - tooltip: [node.relativePath, buildProjectScopedDescription(node.entries)].filter(Boolean).join('\n'), - }, - )); - } - - const items = materialize(projectRoots); - if (rootEntries.length === 0) { - return items; - } - - const rootLabel = typeof options.rootLabel === 'string' ? options.rootLabel.trim() : ''; - if (!rootLabel) { - items.push(...rootEntries.map((entry) => entry.item)); - return items; - } - - items.push(new FolderItem( - rootLabel, - '', - rootEntries.map((entry) => entry.item), - { - description: buildProjectScopedDescription(rootEntries), - tooltip: rootLabel, - }, - )); - return items; -} - -function showSessionMessage(message) { - vscode.window.showInformationMessage?.(message); -} - -function ensureSessionWorktree(session, actionLabel) { - const worktreePath = sessionWorktreePath(session); - if (!worktreePath) { - showSessionMessage(`Cannot ${actionLabel}: missing worktree path.`); - return ''; - } - if (!fs.existsSync(worktreePath)) { - showSessionMessage(`Cannot ${actionLabel}: worktree is no longer on disk: ${worktreePath}`); - return ''; - } - return worktreePath; -} - -function runSessionTerminalCommand(session, actionLabel, iconId, commandText) { - const worktreePath = ensureSessionWorktree(session, actionLabel.toLowerCase()); - if (!worktreePath) { - return; - } - - const terminal = vscode.window.createTerminal({ - name: `GitGuardex ${actionLabel}: ${sessionDisplayLabel(session)}`, - cwd: worktreePath, - iconPath: new vscode.ThemeIcon(iconId), - }); - terminal.show(); - terminal.sendText(commandText, true); -} - -function sessionTerminalLabel(session) { - return `GitGuardex Terminal: ${sessionDisplayLabel(session)}`; -} - -function listWindowTerminals() { - return Array.isArray(vscode.window.terminals) ? vscode.window.terminals : []; -} - -function focusTerminal(terminal) { - terminal?.show?.(false); -} - -async function terminalProcessId(terminal) { - if (!terminal?.processId) { - return null; - } - - try { - const pid = await terminal.processId; - return Number.isInteger(pid) && pid > 0 ? pid : null; - } catch (_error) { - return null; - } -} - -function findFallbackSessionTerminal(session) { - const label = sessionTerminalLabel(session); - return listWindowTerminals().find((terminal) => terminal?.name === label) || null; -} - -async function findSessionTerminal(session) { - const pid = Number(session?.pid); - if (!Number.isInteger(pid) || pid <= 0) { - return null; - } - - for (const terminal of listWindowTerminals()) { - if (await terminalProcessId(terminal) === pid) { - return terminal; - } - } - - return null; -} - -function openFallbackSessionTerminal(session, worktreePath) { - const existingTerminal = findFallbackSessionTerminal(session); - if (existingTerminal) { - focusTerminal(existingTerminal); - return existingTerminal; - } - - const terminal = vscode.window.createTerminal({ - name: sessionTerminalLabel(session), - cwd: worktreePath, - iconPath: new vscode.ThemeIcon('terminal'), - }); - focusTerminal(terminal); - return terminal; -} - -async function showSessionTerminal(session) { - const worktreePath = ensureSessionWorktree(session, 'show terminal'); - if (!worktreePath) { - return; - } - - const terminal = await findSessionTerminal(session); - if (terminal) { - focusTerminal(terminal); - return; - } - - openFallbackSessionTerminal(session, worktreePath); -} - -function finishSession(session) { - if (!session?.branch) { - showSessionMessage('Cannot finish session: missing branch name.'); - return; - } - runSessionTerminalCommand( - session, - 'Finish', - 'check', - `gx branch finish --branch ${shellQuote(session.branch)}`, - ); -} - -function syncSession(session) { - runSessionTerminalCommand(session, 'Sync', 'sync', 'gx sync'); -} - -async function restartActiveAgents(extensionId) { - if (extensionId && extensionId !== ACTIVE_AGENTS_EXTENSION_ID) { - return; - } - await vscode.commands.executeCommand(RESTART_EXTENSION_HOST_COMMAND); -} - -function execFileAsync(command, args, options = {}) { - return new Promise((resolve, reject) => { - cp.execFile(command, args, options, (error, stdout = '', stderr = '') => { - if (error) { - error.stdout = stdout; - error.stderr = stderr; - reject(error); - return; - } - resolve({ stdout, stderr }); - }); - }); -} - -function buildStopSessionCommandText(session, pid) { - const parts = ['gx', 'agents', 'stop', '--pid', String(pid)]; - if (session?.repoRoot) { - parts.push('--target', session.repoRoot); - } - return parts.map(shellQuote).join(' '); -} - -async function stopSession(session, refresh) { - const pid = Number(session?.pid); - if (!Number.isInteger(pid) || pid <= 0) { - showSessionMessage('Cannot stop session: missing pid.'); - return; - } - if (!session?.branch) { - showSessionMessage('Cannot stop session: missing branch name.'); - return; - } - - const sessionTerminal = await findSessionTerminal(session); - const stopCommandText = buildStopSessionCommandText(session, pid); - const confirmed = await vscode.window.showWarningMessage( - `Stop ${sessionDisplayLabel(session)}?`, - { - modal: true, - detail: sessionTerminal - ? 'Send Ctrl+C to the live session terminal.' - : `No live session terminal found. Run ${stopCommandText}.`, - }, - 'Stop', - ); - if (confirmed !== 'Stop') { - return; - } - - if (sessionTerminal) { - focusTerminal(sessionTerminal); - sessionTerminal.sendText('\u0003', false); - refresh(); - return; - } - - try { - const commandCwd = session?.repoRoot || sessionWorktreePath(session) || process.cwd(); - const args = ['agents', 'stop', '--pid', String(pid)]; - if (session?.repoRoot) { - args.push('--target', session.repoRoot); - } - await execFileAsync('gx', args, { - cwd: commandCwd, - encoding: 'utf8', - maxBuffer: 1024 * 1024, - }); - refresh(); - } catch (error) { - showSessionMessage( - `Failed to stop session ${sessionDisplayLabel(session)}: ${formatGitCommandFailure(error)}`, - ); - } -} - -async function dismissSession(session, refresh) { - if (!canDismissSession(session)) { - showSessionMessage('Only stalled or dead sessions can be dismissed.'); - return; - } - - const repoRoot = typeof session?.repoRoot === 'string' ? session.repoRoot.trim() : ''; - if (!repoRoot) { - showSessionMessage('Cannot dismiss session: missing repo root.'); - return; - } - if (!session?.branch) { - showSessionMessage('Cannot dismiss session: missing branch name.'); - return; - } - - const statePath = sessionFilePathForBranch(repoRoot, session.branch); - if (!fs.existsSync(statePath)) { - clearWorktreeActivityCache(session.worktreePath); - refresh(); - showSessionMessage(`Session record already gone for ${sessionDisplayLabel(session)}.`); - return; - } - - const confirmed = await vscode.window.showWarningMessage( - `Dismiss ${sessionDisplayLabel(session)}?`, - { - modal: true, - detail: buildDismissSessionDetail(session, statePath), - }, - 'Dismiss', - ); - if (confirmed !== 'Dismiss') { - return; - } - - try { - fs.unlinkSync(statePath); - clearWorktreeActivityCache(session.worktreePath); - refresh(); - } catch (error) { - showSessionMessage(`Failed to dismiss session ${sessionDisplayLabel(session)}: ${error.message}`); - } -} - -function readGitDirPath(targetPath) { - const normalizedTargetPath = typeof targetPath === 'string' ? targetPath.trim() : ''; - if (!normalizedTargetPath) { - return ''; - } - - const gitPath = path.join(path.resolve(normalizedTargetPath), '.git'); - try { - if (fs.statSync(gitPath).isDirectory()) { - return gitPath; - } - } catch (_error) { - return ''; - } - - try { - const gitPointer = fs.readFileSync(gitPath, 'utf8'); - const match = gitPointer.match(/^gitdir:\s*(.+)$/m); - if (match?.[1]) { - return path.resolve(path.dirname(gitPath), match[1].trim()); - } - } catch (_error) { - return ''; - } - - return ''; -} - -function resolveRepoRootFromGitDir(targetPath) { - const gitDir = readGitDirPath(targetPath); - if (!gitDir) { - return ''; - } - - let commonDir = gitDir; - try { - const commonDirPath = path.join(gitDir, 'commondir'); - if (fs.existsSync(commonDirPath)) { - const rawCommonDir = fs.readFileSync(commonDirPath, 'utf8').trim(); - if (rawCommonDir) { - commonDir = path.resolve(gitDir, rawCommonDir); - } - } - } catch (_error) { - // Fall back to the direct git dir when commondir is unreadable. - } - - return path.basename(commonDir) === '.git' - ? path.resolve(path.dirname(commonDir)) - : ''; -} - -function readGitTopLevel(targetPath) { - try { - return cp.execFileSync('git', ['-C', targetPath, 'rev-parse', '--show-toplevel'], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }).trim(); - } catch (_error) { - return ''; - } -} - -function resolveWorkspaceFolderRepoRoot(workspacePath) { - const normalizedWorkspacePath = typeof workspacePath === 'string' ? workspacePath.trim() : ''; - if (!normalizedWorkspacePath) { - return ''; - } - - const absoluteWorkspacePath = path.resolve(normalizedWorkspacePath); - const directRepoRoot = resolveRepoRootFromGitDir(absoluteWorkspacePath); - if (directRepoRoot) { - return directRepoRoot; - } - - const gitTopLevel = readGitTopLevel(absoluteWorkspacePath); - if (!gitTopLevel) { - return absoluteWorkspacePath; - } - - return resolveRepoRootFromGitDir(gitTopLevel) || path.resolve(gitTopLevel); -} - -function repoRootFromSessionFile(filePath) { - return path.resolve(path.dirname(filePath), '..', '..', '..'); -} - -function repoRootFromWorktreeLockFile(filePath) { - return path.resolve(path.dirname(filePath), '..', '..', '..'); -} - -function repoRootFromManagedWorktreeGitFile(filePath) { - return path.resolve(path.dirname(filePath), '..', '..', '..'); -} - -function repoRootFromLockFile(filePath) { - return path.resolve(path.dirname(filePath), '..', '..'); -} - -function normalizeRelativePath(relativePath) { - return String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, ''); -} - -function emptyLockRegistry() { - return { - entriesByPath: new Map(), - countsByBranch: new Map(), - }; -} - -function readLockRegistry(repoRoot) { - const lockPath = path.join(repoRoot, LOCK_FILE_RELATIVE); - if (!fs.existsSync(lockPath)) { - return emptyLockRegistry(); - } - - let parsed; - try { - parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8')); - } catch (_error) { - return emptyLockRegistry(); - } - - const locks = parsed?.locks; - if (!locks || typeof locks !== 'object' || Array.isArray(locks)) { - return emptyLockRegistry(); - } - - const entriesByPath = new Map(); - const countsByBranch = new Map(); - for (const [rawRelativePath, entry] of Object.entries(locks)) { - if (!entry || typeof entry !== 'object') { - continue; - } - - const relativePath = normalizeRelativePath(rawRelativePath); - const branch = typeof entry.branch === 'string' ? entry.branch.trim() : ''; - if (!relativePath || !branch) { - continue; - } - - entriesByPath.set(relativePath, { - branch, - claimedAt: typeof entry.claimed_at === 'string' ? entry.claimed_at : '', - allowDelete: Boolean(entry.allow_delete), - }); - countsByBranch.set(branch, (countsByBranch.get(branch) || 0) + 1); - } - - return { - entriesByPath, - countsByBranch, - }; -} - -function readCurrentBranch(repoRoot) { - try { - return cp.execFileSync('git', ['-C', repoRoot, 'rev-parse', '--abbrev-ref', 'HEAD'], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }).trim(); - } catch (_error) { - return ''; - } -} - -function parseSimpleSemver(version) { - const parts = String(version || '') - .split('.') - .map((part) => Number.parseInt(part, 10)); - if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) { - return null; - } - return parts; -} - -function compareSimpleSemver(left, right) { - const leftParts = parseSimpleSemver(left); - const rightParts = parseSimpleSemver(right); - if (!leftParts || !rightParts) { - return 0; - } - - for (let index = 0; index < leftParts.length; index += 1) { - if (leftParts[index] !== rightParts[index]) { - return leftParts[index] - rightParts[index]; - } - } - - return 0; -} - -function readJsonFile(filePath) { - try { - return JSON.parse(fs.readFileSync(filePath, 'utf8')); - } catch (_error) { - return null; - } -} - -function resolveActiveAgentsAutoUpdateCandidate(installedVersion) { - const candidates = []; - - for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { - const repoRoot = workspaceFolder?.uri?.fsPath; - if (!repoRoot) { - continue; - } - - const manifestPath = path.join(repoRoot, ACTIVE_AGENTS_MANIFEST_RELATIVE); - const installScriptPath = path.join(repoRoot, ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE); - if (!fs.existsSync(manifestPath) || !fs.existsSync(installScriptPath)) { - continue; - } - - const manifest = readJsonFile(manifestPath); - const nextVersion = typeof manifest?.version === 'string' ? manifest.version.trim() : ''; - if (!nextVersion || compareSimpleSemver(nextVersion, installedVersion) <= 0) { - continue; - } - - candidates.push({ repoRoot, installScriptPath, version: nextVersion }); - } - - candidates.sort((left, right) => compareSimpleSemver(right.version, left.version)); - return candidates[0] || null; -} - -function runActiveAgentsInstallScript(repoRoot, installScriptPath) { - return new Promise((resolve, reject) => { - cp.execFile( - process.execPath, - [installScriptPath], - { cwd: repoRoot, encoding: 'utf8' }, - (error, stdout, stderr) => { - if (error) { - reject(new Error(String(stderr || stdout || error.message || '').trim() || 'install failed')); - return; - } - resolve({ stdout, stderr }); - }, - ); - }); -} - -async function maybeAutoUpdateActiveAgentsExtension(context) { - const installedVersion = typeof context?.extension?.packageJSON?.version === 'string' - ? context.extension.packageJSON.version.trim() - : ''; - if (!installedVersion) { - return; - } - - const candidate = resolveActiveAgentsAutoUpdateCandidate(installedVersion); - if (!candidate) { - return; - } - - try { - await runActiveAgentsInstallScript(candidate.repoRoot, candidate.installScriptPath); - } catch (error) { - const failure = typeof error?.message === 'string' && error.message.trim() - ? error.message.trim() - : 'install failed'; - vscode.window.showWarningMessage?.( - `GitGuardex Active Agents could not auto-update to ${candidate.version}: ${failure}`, - ); - return; - } - - const selection = await vscode.window.showInformationMessage?.( - `GitGuardex Active Agents updated to ${candidate.version}. Reload this window now, then reload any other already-open VS Code windows to use the newest companion.`, - RELOAD_WINDOW_ACTION, - UPDATE_LATER_ACTION, - ); - if (selection === RELOAD_WINDOW_ACTION) { - await vscode.commands.executeCommand('workbench.action.reloadWindow'); - } -} - -function decorateSession(session, lockRegistry) { - const touchedChanges = buildSessionTouchedChanges(session, lockRegistry); - const decorated = { - ...session, - lockCount: lockRegistry.countsByBranch.get(session.branch) || 0, - touchedChanges, - conflictCount: touchedChanges.filter((change) => change.hasForeignLock).length, - }; - decorated.lastActiveAt = sessionLastActiveAt(decorated); - decorated.lastActiveLabel = sessionLastActiveLabel(decorated); - decorated.freshnessLabel = sessionFreshnessLabel(decorated); - decorated.topChangedFiles = buildSessionTopFiles(decorated); - decorated.topChangedFilesLabel = summarizeCompactPaths(decorated.topChangedFiles); - decorated.recentChangeSummary = buildSessionRecentChangeSummary(decorated); - decorated.riskBadges = sessionRiskBadges(decorated); - return decorated; -} - -function decorateChange(change, lockRegistry, owningBranch) { - const lockEntry = lockRegistry.entriesByPath.get(normalizeRelativePath(change.relativePath)); - const lockOwnerBranch = lockEntry?.branch || ''; - const decorated = { - ...change, - lockOwnerBranch, - hasForeignLock: Boolean(lockOwnerBranch) && (!owningBranch || lockOwnerBranch !== owningBranch), - protectedBranch: isProtectedBranchName(owningBranch), - }; - decorated.riskBadges = changeRiskBadges(decorated); - return decorated; -} - -function buildSessionTouchedChanges(session, lockRegistry) { - const changedPaths = Array.isArray(session.worktreeChangedPaths) - ? session.worktreeChangedPaths - : []; - return [...new Set(changedPaths.map(normalizeRelativePath).filter(Boolean))] - .map((relativePath) => { - const lockEntry = lockRegistry.entriesByPath.get(relativePath); - const lockOwnerBranch = lockEntry?.branch || ''; - return { - relativePath, - absolutePath: path.join(session.worktreePath, relativePath), - originalPath: '', - statusCode: 'M', - statusLabel: 'M', - statusText: 'Touched', - lockOwnerBranch, - hasForeignLock: Boolean(lockOwnerBranch) && lockOwnerBranch !== session.branch, - }; - }); -} - -function isPathWithin(parentPath, targetPath) { - const relativePath = path.relative(parentPath, targetPath); - return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)); -} - -function normalizeAbsolutePath(value) { - return typeof value === 'string' && value.trim() ? path.resolve(value) : ''; -} - -function isManagedWorktreePath(worktreePath) { - const normalizedWorktreePath = normalizeAbsolutePath(worktreePath); - if (!normalizedWorktreePath) { - return false; - } - - return MANAGED_WORKTREE_RELATIVE_ROOTS.some((relativeRoot) => { - const normalizedRelativeRoot = path.normalize(relativeRoot); - const marker = `${path.sep}${normalizedRelativeRoot}${path.sep}`; - return normalizedWorktreePath.includes(marker); - }); -} - -function removeDeletedWorktreeWorkspaceFolder(worktreePath) { - if (typeof vscode.workspace.updateWorkspaceFolders !== 'function') { - return false; - } - - const normalizedWorktreePath = normalizeAbsolutePath(worktreePath); - if (!normalizedWorktreePath) { - return false; - } - - const workspaceFolders = vscode.workspace.workspaceFolders || []; - const folderIndex = workspaceFolders.findIndex((folder) => ( - normalizeAbsolutePath(folder?.uri?.fsPath) === normalizedWorktreePath - )); - if (folderIndex < 0) { - return false; - } - - try { - return vscode.workspace.updateWorkspaceFolders(folderIndex, 1) === true; - } catch (_error) { - return false; - } -} - -async function closeDeletedWorktreeRepository(worktreePath) { - const normalizedWorktreePath = normalizeAbsolutePath(worktreePath); - if (!normalizedWorktreePath || fs.existsSync(normalizedWorktreePath)) { - return false; - } - - try { - await vscode.commands.executeCommand('git.close', vscode.Uri.file(normalizedWorktreePath)); - } catch (_error) { - // The Git extension may have already removed this repository. - } - - removeDeletedWorktreeWorkspaceFolder(normalizedWorktreePath); - return true; -} - -function findDeletedManagedWorkspaceFolders() { - return (vscode.workspace.workspaceFolders || []) - .map((folder) => normalizeAbsolutePath(folder?.uri?.fsPath)) - .filter((workspacePath) => ( - workspacePath - && !fs.existsSync(workspacePath) - && isManagedWorktreePath(workspacePath) - )); -} - -function localizeChangeForSession(session, change) { - if (!change?.absolutePath || !isPathWithin(session.worktreePath, change.absolutePath)) { - return null; - } - - let originalPath = change.originalPath; - if (originalPath) { - const originalAbsolutePath = path.join(session.repoRoot, originalPath); - if (isPathWithin(session.worktreePath, originalAbsolutePath)) { - originalPath = normalizeRelativePath(path.relative(session.worktreePath, originalAbsolutePath)); - } - } - - return { - ...change, - relativePath: normalizeRelativePath(path.relative(session.worktreePath, change.absolutePath)), - originalPath, - }; -} - -async function findRepoSessionEntries() { - const [sessionFiles, worktreeLockFiles, managedWorktreeGitFiles] = await Promise.all([ - vscode.workspace.findFiles( - ACTIVE_SESSION_FILES_GLOB, - SESSION_SCAN_EXCLUDE_GLOB, - SESSION_SCAN_LIMIT, - ), - vscode.workspace.findFiles( - WORKTREE_AGENT_LOCKS_GLOB, - WORKTREE_LOCK_SCAN_EXCLUDE_GLOB, - SESSION_SCAN_LIMIT, - ), - vscode.workspace.findFiles( - MANAGED_WORKTREE_GIT_FILES_GLOB, - MANAGED_WORKTREE_GIT_SCAN_EXCLUDE_GLOB, - SESSION_SCAN_LIMIT, - ), - ]); - - const repoRoots = new Set(); - const addRepoRootCandidate = (repoRoot) => { - if (typeof repoRoot !== 'string' || !repoRoot.trim()) { - return; - } - - const normalizedRepoRoot = path.resolve(repoRoot); - const isInsideWorkspaceManagedWorktree = (vscode.workspace.workspaceFolders || []) - .map((folder) => (typeof folder?.uri?.fsPath === 'string' ? path.resolve(folder.uri.fsPath) : '')) - .filter(Boolean) - .some((workspaceRoot) => MANAGED_WORKTREE_RELATIVE_ROOTS.some((relativeRoot) => ( - isPathWithin(path.join(workspaceRoot, relativeRoot), normalizedRepoRoot) - ))); - if (!isInsideWorkspaceManagedWorktree) { - repoRoots.add(normalizedRepoRoot); - } - }; - - for (const uri of sessionFiles) { - addRepoRootCandidate(repoRootFromSessionFile(uri.fsPath)); - } - for (const uri of worktreeLockFiles) { - if (path.basename(uri.fsPath) !== 'AGENT.lock') { - continue; - } - addRepoRootCandidate(repoRootFromWorktreeLockFile(uri.fsPath)); - } - for (const uri of managedWorktreeGitFiles) { - if (path.basename(uri.fsPath) !== '.git') { - continue; - } - addRepoRootCandidate(repoRootFromManagedWorktreeGitFile(uri.fsPath)); - } - for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { - if (workspaceFolder?.uri?.fsPath) { - addRepoRootCandidate(resolveWorkspaceFolderRepoRoot(workspaceFolder.uri.fsPath)); - } - } - - const repoEntries = []; - for (const repoRoot of repoRoots) { - const sessions = readActiveSessions(repoRoot, { includeStale: true }); - if (sessions.length > 0) { - repoEntries.push({ repoRoot, sessions }); - } - } - - repoEntries.sort((left, right) => left.repoRoot.localeCompare(right.repoRoot)); - return repoEntries; -} - -function resolveSessionWatcherKey(session) { - return `${path.resolve(session.repoRoot)}::${session.branch}::${path.resolve(session.worktreePath)}`; -} - -function resolveSessionGitIndexPath(worktreePath) { - const gitPath = path.join(worktreePath, '.git'); - const defaultIndexPath = path.join(gitPath, 'index'); - - try { - if (fs.statSync(gitPath).isDirectory()) { - return defaultIndexPath; - } - } catch (_error) { - return defaultIndexPath; - } - - try { - const gitPointer = fs.readFileSync(gitPath, 'utf8'); - const match = gitPointer.match(/^gitdir:\s*(.+)$/m); - if (match?.[1]) { - return path.resolve(worktreePath, match[1].trim(), 'index'); - } - } catch (_error) { - return defaultIndexPath; - } - - return defaultIndexPath; -} - -function bindRefreshWatcher(watcher, refresh) { - return [ - watcher.onDidCreate(refresh), - watcher.onDidChange(refresh), - watcher.onDidDelete(refresh), - ]; -} - -function disposeAll(disposables) { - for (const disposable of disposables) { - disposable?.dispose?.(); - } -} - -function buildChangeTreeNodes(changes) { - const root = []; - - function sortNodes(nodes) { - nodes.sort((left, right) => { - const leftIsFolder = left.kind === 'folder'; - const rightIsFolder = right.kind === 'folder'; - if (leftIsFolder !== rightIsFolder) { - return leftIsFolder ? -1 : 1; - } - return left.label.localeCompare(right.label); - }); - - for (const node of nodes) { - if (node.kind === 'folder') { - sortNodes(node.children); - } - } - } - - for (const change of changes) { - const segments = change.relativePath.split(/[\\/]+/).filter(Boolean); - if (segments.length <= 1) { - root.push({ kind: 'change', label: change.relativePath, change }); - continue; - } - - let nodes = root; - let folderPath = ''; - for (const segment of segments.slice(0, -1)) { - folderPath = folderPath ? path.posix.join(folderPath, segment) : segment; - let folderNode = nodes.find((node) => node.kind === 'folder' && node.relativePath === folderPath); - if (!folderNode) { - folderNode = { - kind: 'folder', - label: segment, - relativePath: folderPath, - children: [], - }; - nodes.push(folderNode); - } - nodes = folderNode.children; - } - - nodes.push({ kind: 'change', label: change.relativePath, change }); - } - - sortNodes(root); - - function materialize(nodes) { - return nodes.map((node) => { - if (node.kind === 'folder') { - return new FolderItem(node.label, node.relativePath, materialize(node.children)); - } - return new ChangeItem(node.change); - }); - } - - return materialize(root); -} - -function countChangedPaths(repoRoot, sessions, changes) { - const changedKeys = new Set(); - - for (const change of changes || []) { - if (change?.relativePath) { - changedKeys.add(normalizeRelativePath(change.relativePath)); - } - } - - for (const session of sessions || []) { - for (const change of session.touchedChanges || []) { - const absolutePath = change?.absolutePath - || path.join(session.worktreePath || '', change?.relativePath || ''); - const normalizedRelativePath = absolutePath && isPathWithin(repoRoot, absolutePath) - ? normalizeRelativePath(path.relative(repoRoot, absolutePath)) - : `${session.branch}:${normalizeRelativePath(change?.relativePath)}`; - if (normalizedRelativePath) { - changedKeys.add(normalizedRelativePath); - } - } - } - - return changedKeys.size; -} - -function buildRepoOverview(sessions, unassignedChanges, lockEntries, colonyTasks = []) { - const colonyTaskList = Array.isArray(colonyTasks) ? colonyTasks : []; - return { - sessionCount: sessions.length, - workingCount: countWorkingSessions(sessions), - finishedCount: countFinishedSessions(sessions), - idleCount: countIdleSessions(sessions), - unassignedChangeCount: (unassignedChanges || []).length, - lockedFileCount: Array.isArray(lockEntries) ? lockEntries.length : 0, - conflictCount: sessions.reduce( - (total, session) => total + (session.conflictCount || 0), - 0, - ) + (unassignedChanges || []).filter((change) => change.hasForeignLock).length, - colonyTaskCount: colonyTaskList.length, - pendingHandoffCount: colonyTaskList.reduce( - (total, task) => total + (task.pending_handoff_count || 0), - 0, - ), - }; -} - -function groupSessionsByWorktree(sessions) { - const sessionsByWorktree = new Map(); - - for (const session of sessions || []) { - const worktreePath = sessionWorktreePath(session); - const key = worktreePath || session?.branch || `session-${sessionsByWorktree.size + 1}`; - if (!sessionsByWorktree.has(key)) { - sessionsByWorktree.set(key, { - worktreePath, - sessions: [], - }); - } - sessionsByWorktree.get(key).sessions.push(session); - } - - return [...sessionsByWorktree.values()] - .map((entry) => ({ - ...entry, - sessions: entry.sessions.sort((left, right) => ( - sessionTreeLabel(left).localeCompare(sessionTreeLabel(right)) - )), - })) - .sort((left, right) => { - const leftLabel = path.basename(left.worktreePath || '') || ''; - const rightLabel = path.basename(right.worktreePath || '') || ''; - return leftLabel.localeCompare(rightLabel) - || (left.worktreePath || '').localeCompare(right.worktreePath || ''); - }); -} - -function partitionChangesByOwnership(sessions, changes) { - const changesBySession = new Map(); - const sessionByChangedPath = new Map(); - const repoRootChanges = []; - - for (const session of sessions) { - changesBySession.set(session.branch, []); - for (const changedPath of session.changedPaths || []) { - if (!sessionByChangedPath.has(changedPath)) { - sessionByChangedPath.set(changedPath, session); - } - } - } - - for (const change of changes) { - const normalizedRelativePath = normalizeRelativePath(change.relativePath); - const session = sessionByChangedPath.get(normalizedRelativePath) - || sessions.find((candidate) => isPathWithin(candidate.worktreePath, change.absolutePath)); - if (!session) { - repoRootChanges.push(change); - continue; - } - - const localizedChange = localizeChangeForSession(session, change); - if (!localizedChange) { - repoRootChanges.push(change); - continue; - } - - changesBySession.get(session.branch).push(localizedChange); - } - - return { - changesBySession, - repoRootChanges, - }; -} - -function buildGroupedChangeTreeNodes(sessions, changes) { - const { changesBySession, repoRootChanges } = partitionChangesByOwnership(sessions, changes); - - const items = buildProjectScopedItems( - groupSessionsByWorktree( - sessions.filter((session) => (changesBySession.get(session.branch) || []).length > 0), - ).map(({ worktreePath, sessions: worktreeSessions }) => { - const sessionItems = worktreeSessions.map((session) => ( - new SessionItem( - session, - buildChangeTreeNodes(changesBySession.get(session.branch) || []), - { - label: sessionTreeLabel(session), - variant: 'raw', - }, - ) - )); - const changedCount = worktreeSessions.reduce( - (total, session) => total + ((changesBySession.get(session.branch) || []).length), - 0, - ); - return { - projectRelativePath: worktreeProjectRelativePath(worktreeSessions), - sessions: worktreeSessions, - item: new WorktreeItem(worktreePath, worktreeSessions, sessionItems, { changedCount }), - }; - }), - ); - - if (repoRootChanges.length > 0) { - items.push(new SectionItem('Repo root', buildChangeTreeNodes(repoRootChanges), { - description: String(repoRootChanges.length), - })); - } - - return items; -} - -function countActiveSessions(sessions) { - return sessions.filter((session) => session.activityKind !== 'dead').length; -} - -function countSessionsByActivityKind(sessions, activityKind) { - return sessions.filter((session) => session.activityKind === activityKind).length; -} - -function resolveSessionActivityIconId(activityKind) { - return SESSION_ACTIVITY_ICON_IDS[activityKind] || 'loading~spin'; -} - -async function pickRepoRoot() { - const repoRoots = discoverWorkspaceRepoRoots(); - if (repoRoots.length === 0) { - vscode.window.showInformationMessage?.('Open a Guardex workspace folder to start an agent.'); - return null; - } - - if (repoRoots.length === 1) { - return repoRoots[0]; - } - - const selectedRepoRoot = preferredRepoRoot(repoRoots); - if (selectedRepoRoot) { - return selectedRepoRoot; - } - - const picks = repoRoots.map((repoRoot) => ({ - label: repoPickLabel(repoRoot), - description: repoPickDescription(repoRoot), - detail: repoRoot, - repoRoot, - })); - const selection = await vscode.window.showQuickPick?.(picks, { - placeHolder: 'Select the Git repo where the Start agent launcher should run.', - }); - return selection?.repoRoot || null; -} - -async function promptStartAgentDetails() { - const taskName = await vscode.window.showInputBox?.({ - prompt: 'Task for the Guardex agent launcher', - placeHolder: 'vscode active agents welcome view', - ignoreFocusOut: true, - validateInput: (value) => value.trim() ? undefined : 'Task is required.', - }); - if (!taskName) { - return null; - } - - const agentName = await vscode.window.showInputBox?.({ - prompt: 'Agent name for the Guardex agent launcher', - placeHolder: 'codex', - value: 'codex', - ignoreFocusOut: true, - validateInput: (value) => value.trim() ? undefined : 'Agent name is required.', - }); - if (!agentName) { - return null; - } - - return { - taskName: taskName.trim(), - agentName: agentName.trim(), - }; -} - -async function startAgentFromPrompt(refresh) { - const repoRoot = await pickRepoRoot(); - if (!repoRoot) { - return; - } - - const details = await promptStartAgentDetails(); - if (!details) { - return; - } - - const terminal = vscode.window.createTerminal?.({ - name: `GitGuardex: ${path.basename(repoRoot)}`, - cwd: repoRoot, - }); - terminal?.show(true); - terminal?.sendText(resolveStartAgentCommand(repoRoot, details), true); - refresh(); -} - -function sessionSelectionKey(session) { - if (!session?.repoRoot || !session?.branch) { - return ''; - } - - return `${session.repoRoot}::${session.branch}`; -} - -function formatGitCommandFailure(error) { - for (const value of [error?.stderr, error?.stdout, error?.message]) { - if (typeof value === 'string' && value.trim().length > 0) { - return value.trim(); - } - } - return 'Git command failed.'; -} - -function runGitCommand(worktreePath, args) { - return cp.execFileSync('git', ['-C', worktreePath, ...args], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - }); -} - -function stageWorktreeForCommit(worktreePath) { - runGitCommand(worktreePath, ['add', '-A', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`]); -} - -function commitWorktree(worktreePath, message) { - runGitCommand(worktreePath, ['commit', '-m', message]); -} - -function buildSessionDetailItems(session) { - const provider = resolveSessionProvider(session); - const snapshot = sessionSnapshotDisplayName(session); - const projectRelativePath = resolveSessionProjectRelativePath(session); - const badgeSummary = uniqueStringList([ - ...(session.riskBadges || []), - session.deltaLabel || '', - ].filter(Boolean)).join(', '); - const sessionHealthSummary = buildSessionHealthSummary(session); - const items = [ - new DetailItem('Recent change', session.recentChangeSummary || 'No recent change summary.', { - iconId: 'history', - }), - new DetailItem('Top files', session.topChangedFilesLabel || 'No tracked file edits.', { - iconId: 'list-flat', - }), - ]; - if (badgeSummary) { - items.push(new DetailItem('Signals', badgeSummary, { - iconId: 'warning', - })); - } - if (sessionHealthSummary) { - items.push(new DetailItem('Session health', sessionHealthSummary, { - iconId: 'pulse', - tooltip: buildSessionHealthTooltip(session) || sessionHealthSummary, - })); - } - if (provider?.label) { - items.push(new DetailItem('Provider', provider.label, { - iconId: 'rocket', - })); - } - if (snapshot) { - items.push(new DetailItem('Snapshot', snapshot, { - iconId: 'device-camera', - })); - } - if (projectRelativePath) { - items.push(new DetailItem('Project', projectRelativePath, { - iconId: 'folder', - tooltip: projectRelativePath, - })); - } - items.push(new DetailItem('Branch', session.branch, { - iconId: 'git-branch', - })); - items.push(new DetailItem('Worktree', session.worktreePath, { - iconId: 'folder-library', - tooltip: session.worktreePath, - })); - return items; -} - -function buildWorkingNowNodes(sessions) { - const sessionEntries = sortSessionsForWorkingNow( - sessions.filter((session) => ( - session.activityKind === 'working' || session.activityKind === 'blocked' - )), - ).map((session) => ({ - projectRelativePath: resolveSessionProjectRelativePath(session), - sessions: [session], - item: new SessionItem(session, buildSessionDetailItems(session)), - })); - return buildProjectScopedItems(sessionEntries, { rootLabel: 'Repo root' }); -} - -function buildIdleThinkingNodes(sessions) { - const sessionEntries = sortSessionsForIdleThinking( - sessions.filter((session) => !( - session.activityKind === 'working' - || session.activityKind === 'blocked' - || session.activityKind === 'finished' - )), - ).map((session) => ({ - projectRelativePath: resolveSessionProjectRelativePath(session), - sessions: [session], - item: new SessionItem(session, buildSessionDetailItems(session)), - })); - return buildProjectScopedItems(sessionEntries, { rootLabel: 'Repo root' }); -} - -function buildNeedsCleanupNodes(sessions) { - const sessionEntries = sessions - .filter((session) => session.activityKind === 'finished') - .map((session) => ({ - projectRelativePath: resolveSessionProjectRelativePath(session), - sessions: [session], - item: new SessionItem(session, buildSessionDetailItems(session)), - })); - return buildProjectScopedItems(sessionEntries, { rootLabel: 'Repo root' }); -} - -function buildUnassignedChangeNodes(changes) { - return sortUnassignedChanges(changes).map((change) => new ChangeItem(change, { - label: compactRelativePath(change.relativePath), - description: buildUnassignedChangeDescription(change), - iconId: changeNeedsWarningIcon(change) ? 'warning' : undefined, - })); -} - -function buildRawActiveAgentGroupNodes(sessions) { - const groups = []; - for (const group of SESSION_ACTIVITY_GROUPS) { - const groupSessions = sessions.filter((session) => session.activityKind === group.kind); - const worktreeItems = buildProjectScopedItems( - groupSessionsByWorktree(groupSessions).map(({ worktreePath, sessions: worktreeSessions }) => ({ - projectRelativePath: worktreeProjectRelativePath(worktreeSessions), - sessions: worktreeSessions, - item: new WorktreeItem( - worktreePath, - worktreeSessions, - worktreeSessions.map((session) => new SessionItem( - session, - buildChangeTreeNodes(session.touchedChanges || []), - { - label: sessionTreeLabel(session), - variant: 'raw', - }, - )), - { - description: buildWorktreeBranchDescription(worktreeSessions), - iconId: 'git-branch', - resourceSession: worktreeSessions[0], - useSessionDecoration: true, - }, - ), - })), - { rootLabel: 'Repo root' }, - ); - if (worktreeItems.length > 0) { - groups.push(new SectionItem(group.label, worktreeItems, { - iconId: resolveSessionActivityIconId(group.kind), - })); - } - } - - return groups; -} - -class ActiveAgentsProvider { - constructor(decorationProvider) { - this.decorationProvider = decorationProvider; - this.onDidChangeTreeDataEmitter = new vscode.EventEmitter(); - this.onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event; - this.onDidChangeSelectedSessionEmitter = new vscode.EventEmitter(); - this.onDidChangeSelectedSession = this.onDidChangeSelectedSessionEmitter.event; - this.treeView = null; - this.lockRegistryByRepoRoot = new Map(); - this.selectedSession = null; - this.viewSummary = { - sessionCount: 0, - workingCount: 0, - finishedCount: 0, - idleCount: 0, - unassignedChangeCount: 0, - lockedFileCount: 0, - deadCount: 0, - conflictCount: 0, - }; - this.previousSnapshot = null; - } - - getTreeItem(element) { - return element; - } - - attachTreeView(treeView) { - this.treeView = treeView; - this.updateViewState({ - sessionCount: 0, - workingCount: 0, - finishedCount: 0, - idleCount: 0, - unassignedChangeCount: 0, - lockedFileCount: 0, - deadCount: 0, - conflictCount: 0, - }); - treeView.onDidChangeSelection?.((event) => { - const sessionItem = event.selection.find((item) => item instanceof SessionItem); - this.setSelectedSession(sessionItem?.session || null); - }); - } - - setSelectedSession(session) { - const nextSession = session?.worktreePath ? { ...session } : null; - const currentKey = sessionSelectionKey(this.selectedSession); - const nextKey = sessionSelectionKey(nextSession); - this.selectedSession = nextSession; - this.decorationProvider?.setSelectedBranch(nextSession?.branch || ''); - if (currentKey !== nextKey) { - this.onDidChangeSelectedSessionEmitter.fire(this.selectedSession); - } - } - - getSelectedSession() { - return this.selectedSession ? { ...this.selectedSession } : null; - } - - getViewSummary() { - return { ...this.viewSummary }; - } - - syncSelectedSession(repoEntries) { - if (!this.selectedSession) { - return; - } - - const nextSession = repoEntries - .flatMap((entry) => entry.sessions) - .find((session) => sessionSelectionKey(session) === sessionSelectionKey(this.selectedSession)); - this.setSelectedSession(nextSession || null); - } - - updateViewState(summary) { - if (!this.treeView) { - return; - } - - const sessionCount = summary?.sessionCount || 0; - const conflictCount = summary?.conflictCount || 0; - this.viewSummary = { ...summary }; - void vscode.commands.executeCommand('setContext', 'guardex.hasAgents', sessionCount > 0); - void vscode.commands.executeCommand('setContext', 'guardex.hasConflicts', conflictCount > 0); - - this.treeView.badge = sessionCount > 0 - ? { - value: sessionCount, - tooltip: buildOverviewDescription(summary), - } - : undefined; - this.treeView.message = undefined; - } - - annotateRepoEntries(repoEntries) { - const hasPreviousSnapshot = Boolean(this.previousSnapshot); - const nextSnapshot = { - sessions: new Map(), - changes: new Map(), - }; - - const annotatedEntries = repoEntries.map((entry) => { - const sessions = entry.sessions.map((session) => { - const snapshotKey = sessionSnapshotKey(session); - nextSnapshot.sessions.set(snapshotKey, buildSessionSnapshot(session)); - const deltaLabel = hasPreviousSnapshot - ? deriveSessionDelta(this.previousSnapshot.sessions.get(snapshotKey), session) - : ''; - return { - ...session, - deltaLabel, - riskBadges: uniqueStringList([ - ...(session.riskBadges || []), - deltaLabel, - ].filter(Boolean)), - }; - }); - - const changes = entry.changes.map((change) => { - const snapshotKey = changeSnapshotKey(entry.repoRoot, change); - nextSnapshot.changes.set(snapshotKey, buildChangeSnapshot(change)); - const deltaLabel = hasPreviousSnapshot - ? deriveChangeDelta(this.previousSnapshot.changes.get(snapshotKey), change) - : ''; - return { - ...change, - deltaLabel, - riskBadges: changeRiskBadges({ - ...change, - deltaLabel, - }), - }; - }); - - const { repoRootChanges } = partitionChangesByOwnership(sessions, changes); - const unassignedChanges = sortUnassignedChanges(repoRootChanges); - const colonyTasks = Array.isArray(entry.colonyTasks) ? entry.colonyTasks : []; - return { - ...entry, - sessions, - changes, - unassignedChanges, - colonyTasks, - overview: buildRepoOverview(sessions, unassignedChanges, entry.lockEntries, colonyTasks), - }; - }); - - this.previousSnapshot = nextSnapshot; - return annotatedEntries; - } - - async syncRepoEntries() { - const repoEntries = this.annotateRepoEntries(await this.loadRepoEntries()); - const summary = { - sessionCount: repoEntries.reduce((total, entry) => total + entry.sessions.length, 0), - workingCount: repoEntries.reduce((total, entry) => total + entry.overview.workingCount, 0), - finishedCount: repoEntries.reduce( - (total, entry) => total + (entry.overview.finishedCount || 0), - 0, - ), - idleCount: repoEntries.reduce((total, entry) => total + entry.overview.idleCount, 0), - unassignedChangeCount: repoEntries.reduce( - (total, entry) => total + entry.overview.unassignedChangeCount, - 0, - ), - lockedFileCount: repoEntries.reduce((total, entry) => total + entry.overview.lockedFileCount, 0), - deadCount: repoEntries.reduce( - (total, entry) => total + countSessionsByActivityKind(entry.sessions, 'dead'), - 0, - ), - conflictCount: repoEntries.reduce((total, entry) => total + entry.overview.conflictCount, 0), - }; - - this.updateViewState(summary); - this.decorationProvider?.updateSessions(repoEntries.flatMap((entry) => entry.sessions)); - this.decorationProvider?.updateLockEntries(repoEntries); - return repoEntries; - } - - async refresh() { - await this.syncRepoEntries(); - this.onDidChangeTreeDataEmitter.fire(); - this.decorationProvider?.refresh(); - } - - readLockRegistryForRepo(repoRoot) { - const lockRegistry = readLockRegistry(repoRoot); - this.lockRegistryByRepoRoot.set(repoRoot, lockRegistry); - return lockRegistry; - } - - getLockRegistryForRepo(repoRoot) { - return this.lockRegistryByRepoRoot.get(repoRoot) || this.readLockRegistryForRepo(repoRoot); - } - - refreshLockRegistryForFile(filePath) { - this.readLockRegistryForRepo(repoRootFromLockFile(filePath)); - } - - async getChildren(element) { - if (element instanceof RepoItem) { - const sectionItems = [ - new SectionItem('Overview', [ - new DetailItem('Summary', buildOverviewDescription(element.overview), { - iconId: 'dashboard', - tooltip: buildRepoTooltip(element.repoRoot, element.overview), - }), - ], { - description: '1', - iconId: 'telescope', - }), - ]; - - const workingNowItems = buildWorkingNowNodes(element.sessions); - if (workingNowItems.length > 0) { - sectionItems.push(new SectionItem('Working now', workingNowItems, { - description: String(workingNowItems.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'loading~spin', - })); - } - - const needsCleanupItems = buildNeedsCleanupNodes(element.sessions); - if (needsCleanupItems.length > 0) { - sectionItems.push(new SectionItem('Needs cleanup', needsCleanupItems, { - description: String(needsCleanupItems.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'pass-filled', - })); - } - - const idleThinkingItems = buildIdleThinkingNodes(element.sessions); - if (idleThinkingItems.length > 0) { - sectionItems.push(new SectionItem('Idle / thinking', idleThinkingItems, { - description: String(idleThinkingItems.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'debug-pause', - })); - } - - if (element.unassignedChanges.length > 0) { - sectionItems.push(new SectionItem('Unassigned changes', buildUnassignedChangeNodes(element.unassignedChanges), { - description: String(element.unassignedChanges.length), - iconId: 'inbox', - })); - } - - const advancedItems = []; - const rawActiveAgents = buildRawActiveAgentGroupNodes(element.sessions); - if (rawActiveAgents.length > 0) { - advancedItems.push(new SectionItem('Active agent tree', rawActiveAgents, { - description: String(element.sessions.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'organization', - })); - } - const rawChangeTree = buildGroupedChangeTreeNodes(element.sessions, element.changes); - if (rawChangeTree.length > 0) { - advancedItems.push(new SectionItem('Raw path tree', rawChangeTree, { - description: String(element.changes.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'file-directory', - })); - } - const colonyTaskList = Array.isArray(element.colonyTasks) ? element.colonyTasks : []; - if (colonyTaskList.length > 0) { - const colonyItems = colonyTaskList.map((task) => { - const pendingLabel = task.pending_handoff_count > 0 - ? formatCountLabel(task.pending_handoff_count, 'pending handoff') - : 'quiet'; - const participantLabel = - (task.participants || []).map((p) => p.agent).filter(Boolean).join(', ') - || 'no participants'; - return new DetailItem( - `#${task.id} · ${compactColonyBranchLabel(task.branch)}`, - `${participantLabel} · ${pendingLabel}`, - { - iconId: task.pending_handoff_count > 0 ? 'warning' : 'comment-discussion', - tooltip: [ - task.branch, - `task #${task.id}`, - participantLabel, - task.pending_handoff_count > 0 - ? formatCountLabel(task.pending_handoff_count, 'pending handoff') - : '', - ].filter(Boolean).join('\n'), - }, - ); - }); - advancedItems.push(new SectionItem('Colony tasks', colonyItems, { - description: String(colonyItems.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'organization', - })); - } - if (advancedItems.length > 0) { - sectionItems.push(new SectionItem('Advanced details', advancedItems, { - description: String(advancedItems.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'settings-gear', - })); - } - return sectionItems; - } - - if (element instanceof SectionItem || element instanceof FolderItem || element instanceof WorktreeItem || element instanceof SessionItem) { - return element.items; - } - - const repoEntries = await this.syncRepoEntries(); - this.syncSelectedSession(repoEntries); - - if (repoEntries.length === 0) { - return [new InfoItem('No active Guardex agents', 'Open or start a sandbox session.')]; - } - - return repoEntries.map((entry) => new RepoItem(entry.repoRoot, entry.sessions, entry.changes, { - label: repoEntryDisplayLabel(entry.repoRoot, entry.sessions), - overview: entry.overview, - unassignedChanges: entry.unassignedChanges, - lockEntries: entry.lockEntries, - colonyTasks: entry.colonyTasks, - })); - } - - async loadRepoEntries() { - const repoEntries = await findRepoSessionEntries(); - return Promise.all( - repoEntries.map(async (entry) => { - const repoRoot = entry.repoRoot; - const lockRegistry = this.getLockRegistryForRepo(repoRoot); - const currentBranch = readCurrentBranch(repoRoot); - const colonyTasks = await readColonyTasksForRepo(repoRoot); - return { - repoRoot, - sessions: entry.sessions.map((session) => decorateSession(session, lockRegistry)), - changes: readRepoChanges(repoRoot).map((change) => ( - decorateChange(change, lockRegistry, currentBranch) - )), - lockEntries: Array.from(lockRegistry.entriesByPath.entries()), - colonyTasks, - }; - }), - ); - } -} - -function countEntryConflicts(entry) { - const sessionConflicts = entry.sessions.reduce( - (total, session) => total + (session.conflictCount || 0), - 0, - ); - const changeConflicts = entry.changes.filter((change) => change.hasForeignLock).length; - return sessionConflicts + changeConflicts; -} - -class SessionInspectPanelManager { - constructor() { - this.panel = null; - this.session = null; - } - - open(session) { - const targetSession = session?.branch ? { ...session } : null; - if (!targetSession?.repoRoot || !targetSession?.branch) { - showSessionMessage('Pick an Active Agents session first.'); - return; - } - if (!vscode.window.createWebviewPanel) { - showSessionMessage('Inspect panel is unavailable in this VS Code build.'); - return; - } - - this.session = targetSession; - if (!this.panel) { - this.panel = vscode.window.createWebviewPanel( - INSPECT_PANEL_VIEW_TYPE, - inspectPanelTitle(targetSession), - vscode.ViewColumn?.Beside, - { - enableFindWidget: true, - enableScripts: false, - retainContextWhenHidden: true, - }, - ); - this.panel.onDidDispose(() => { - this.panel = null; - this.session = null; - }); - } else { - this.panel.reveal?.(vscode.ViewColumn?.Beside); - } - - this.render(); - } - - resolveSession() { - if (!this.session?.repoRoot || !this.session?.branch) { - return this.session ? { ...this.session } : null; - } - - return readActiveSessions(this.session.repoRoot, { includeStale: true }) - .find((entry) => sessionSelectionKey(entry) === sessionSelectionKey(this.session)) - || { ...this.session }; - } - - render() { - if (!this.panel || !this.session) { - return; - } - - const session = this.resolveSession(); - if (!session) { - return; - } - - this.session = { ...session }; - this.panel.title = inspectPanelTitle(session); - this.panel.webview.html = renderInspectPanelHtml(session, readSessionInspectData(session)); - } - - refresh() { - this.render(); - } - - dispose() { - this.panel?.dispose(); - this.panel = null; - this.session = null; - } -} - -class ActiveAgentsRefreshController { - constructor(provider, inspectPanelManager = null) { - this.provider = provider; - this.inspectPanelManager = inspectPanelManager; - this.refreshTimer = null; - this.sessionWatchers = new Map(); - this.closedMissingWorktreeRepositories = new Set(); - this.observedWorktreePaths = new Set(); - } - - scheduleRefresh() { - if (this.refreshTimer) { - clearTimeout(this.refreshTimer); - } - this.refreshTimer = setTimeout(() => { - this.refreshTimer = null; - void this.refreshNow(); - }, REFRESH_DEBOUNCE_MS); - } - - async refreshNow() { - await this.syncSessionWatchers(); - await this.provider.refresh(); - this.inspectPanelManager?.refresh(); - } - - async syncSessionWatchers() { - const repoEntries = await findRepoSessionEntries(); - const liveSessionKeys = new Set(); - - for (const workspacePath of findDeletedManagedWorkspaceFolders()) { - await this.closeMissingWorktreeRepository(workspacePath); - } - - for (const entry of repoEntries) { - for (const session of entry.sessions) { - const worktreePath = sessionWorktreePath(session); - const normalizedWorktreePath = normalizeAbsolutePath(worktreePath); - if (normalizedWorktreePath && !fs.existsSync(normalizedWorktreePath)) { - await this.closeMissingWorktreeRepository(normalizedWorktreePath); - continue; - } - if (normalizedWorktreePath) { - this.closedMissingWorktreeRepositories.delete(normalizedWorktreePath); - this.observedWorktreePaths.add(normalizedWorktreePath); - } - - const sessionKey = resolveSessionWatcherKey(session); - liveSessionKeys.add(sessionKey); - if (this.sessionWatchers.has(sessionKey)) { - continue; - } - - const watcher = vscode.workspace.createFileSystemWatcher( - resolveSessionGitIndexPath(session.worktreePath), - ); - const disposables = bindRefreshWatcher(watcher, () => this.scheduleRefresh()); - this.sessionWatchers.set(sessionKey, { - watcher, - disposables, - worktreePath: normalizedWorktreePath, - }); - } - } - - for (const observedWorktreePath of this.observedWorktreePaths) { - if (fs.existsSync(observedWorktreePath)) { - this.closedMissingWorktreeRepositories.delete(observedWorktreePath); - continue; - } - await this.closeMissingWorktreeRepository(observedWorktreePath); - } - - for (const [sessionKey, entry] of this.sessionWatchers) { - if (liveSessionKeys.has(sessionKey)) { - continue; - } - - if (entry.worktreePath && !fs.existsSync(entry.worktreePath)) { - await this.closeMissingWorktreeRepository(entry.worktreePath); - } - disposeAll(entry.disposables); - entry.watcher.dispose(); - this.sessionWatchers.delete(sessionKey); - } - } - - async closeMissingWorktreeRepository(worktreePath) { - const normalizedWorktreePath = normalizeAbsolutePath(worktreePath); - if (!normalizedWorktreePath || this.closedMissingWorktreeRepositories.has(normalizedWorktreePath)) { - return; - } - - this.closedMissingWorktreeRepositories.add(normalizedWorktreePath); - await closeDeletedWorktreeRepository(normalizedWorktreePath); - } - - dispose() { - if (this.refreshTimer) { - clearTimeout(this.refreshTimer); - this.refreshTimer = null; - } - - for (const entry of this.sessionWatchers.values()) { - disposeAll(entry.disposables); - entry.watcher.dispose(); - } - this.sessionWatchers.clear(); - } -} - -function activate(context) { - const decorationProvider = new SessionDecorationProvider(); - const provider = new ActiveAgentsProvider(decorationProvider); - const inspectPanelManager = new SessionInspectPanelManager(); - const refreshController = new ActiveAgentsRefreshController(provider, inspectPanelManager); - const treeView = vscode.window.createTreeView('gitguardex.activeAgents', { - treeDataProvider: provider, - showCollapseAll: true, - }); - const activeAgentsStatusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10); - activeAgentsStatusItem.name = 'GitGuardex Active Agents'; - activeAgentsStatusItem.command = 'gitguardex.activeAgents.focus'; - provider.attachTreeView(treeView); - const scheduleRefresh = () => refreshController.scheduleRefresh(); - const handleWorkspaceFoldersChanged = () => { - scheduleRefresh(); - void ensureManagedRepoScanIgnores(); - }; - const refresh = () => void refreshController.refreshNow(); - const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB); - const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB); - const worktreeLockWatcher = vscode.workspace.createFileSystemWatcher(WORKTREE_AGENT_LOCKS_GLOB); - const managedWorktreeGitWatcher = vscode.workspace.createFileSystemWatcher(MANAGED_WORKTREE_GIT_FILES_GLOB); - const logWatcher = vscode.workspace.createFileSystemWatcher(AGENT_LOG_FILES_GLOB); - const updateStatusBar = () => { - const selectedSession = provider.getSelectedSession(); - const summary = provider.getViewSummary(); - if ((summary.sessionCount || 0) <= 0) { - activeAgentsStatusItem.hide(); - return; - } - - activeAgentsStatusItem.text = selectedSession?.branch - ? `$(git-branch) ${sessionIdentityLabel(selectedSession)} · ${formatCountLabel(selectedSession.lockCount || 0, 'lock')}` - : buildActiveAgentsStatusSummary(summary); - activeAgentsStatusItem.tooltip = buildActiveAgentsStatusTooltip(selectedSession, summary); - activeAgentsStatusItem.show(); - }; - updateStatusBar(); - const readCommitMessageForSession = async (session) => { - const rawMessage = await vscode.window.showInputBox?.({ - prompt: `Commit ${sessionIdentityLabel(session)} worktree`, - placeHolder: sessionCommitPlaceholder(session), - ignoreFocusOut: true, - }); - if (rawMessage === undefined) { - return undefined; - } - return String(rawMessage).trim(); - }; - const commitSelectedSession = async () => { - const selectedSession = provider.getSelectedSession(); - if (!selectedSession?.worktreePath) { - vscode.window.showInformationMessage?.('Pick an Active Agents session first.'); - return; - } - - if (!fs.existsSync(selectedSession.worktreePath)) { - vscode.window.showInformationMessage?.( - `Selected session worktree is no longer on disk: ${selectedSession.worktreePath}`, - ); - return; - } - - const message = await readCommitMessageForSession(selectedSession); - if (message === undefined) { - return; - } - if (!message) { - vscode.window.showInformationMessage?.('Enter a commit message first.'); - return; - } - - try { - stageWorktreeForCommit(selectedSession.worktreePath); - commitWorktree(selectedSession.worktreePath, message); - refresh(); - } catch (error) { - const failure = formatGitCommandFailure(error); - if (/nothing to commit|no changes added to commit/i.test(failure)) { - vscode.window.showInformationMessage?.(`No changes to commit in ${selectedSession.label}.`); - return; - } - vscode.window.showErrorMessage?.(`Active Agents commit failed: ${failure}`); - } - }; - const interval = setInterval(refresh, REFRESH_POLL_INTERVAL_MS); - const refreshLockRegistry = (uri) => { - if (uri?.fsPath) { - provider.refreshLockRegistryForFile(uri.fsPath); - } - scheduleRefresh(); - }; - - provider.onDidChangeSelectedSession((session) => { - updateStatusBar(); - decorationProvider.refresh(); - }); - provider.onDidChangeTreeData(() => { - updateStatusBar(); - }); - - context.subscriptions.push( - treeView, - activeAgentsStatusItem, - inspectPanelManager, - refreshController, - vscode.window.registerFileDecorationProvider(decorationProvider), - vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)), - vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh), - vscode.commands.registerCommand('gitguardex.activeAgents.restart', restartActiveAgents), - vscode.commands.registerCommand('gitguardex.activeAgents.focus', async () => { - await vscode.commands.executeCommand('workbench.view.extension.gitguardex-active-agents-container'); - }), - vscode.commands.registerCommand('gitguardex.activeAgents.commitSelectedSession', commitSelectedSession), - vscode.commands.registerCommand('gitguardex.activeAgents.openWorktree', async (session) => { - if (!session?.worktreePath) { - return; - } - - await vscode.commands.executeCommand( - 'vscode.openFolder', - vscode.Uri.file(session.worktreePath), - { forceNewWindow: true }, - ); - }), - vscode.commands.registerCommand('gitguardex.activeAgents.openChange', async (change) => { - if (!change?.absolutePath) { - return; - } - - if (!fs.existsSync(change.absolutePath)) { - vscode.window.showInformationMessage?.(`Changed path is no longer on disk: ${change.relativePath}`); - return; - } - - await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(change.absolutePath)); - }), - vscode.commands.registerCommand('gitguardex.activeAgents.inspect', (session) => { - inspectPanelManager.open(session || provider.getSelectedSession()); - }), - vscode.commands.registerCommand('gitguardex.activeAgents.showSessionTerminal', showSessionTerminal), - vscode.commands.registerCommand('gitguardex.activeAgents.finishSession', finishSession), - vscode.commands.registerCommand('gitguardex.activeAgents.syncSession', syncSession), - vscode.commands.registerCommand('gitguardex.activeAgents.stopSession', (session) => stopSession(session, refresh)), - vscode.commands.registerCommand('gitguardex.activeAgents.dismissSession', (session) => dismissSession(session, refresh)), - vscode.workspace.onDidChangeWorkspaceFolders(handleWorkspaceFoldersChanged), - activeSessionsWatcher, - lockWatcher, - worktreeLockWatcher, - managedWorktreeGitWatcher, - logWatcher, - { dispose: () => clearInterval(interval) }, - ); - - context.subscriptions.push( - ...bindRefreshWatcher(activeSessionsWatcher, scheduleRefresh), - ...bindRefreshWatcher(lockWatcher, refreshLockRegistry), - ...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh), - ...bindRefreshWatcher(managedWorktreeGitWatcher, scheduleRefresh), - ...bindRefreshWatcher(logWatcher, scheduleRefresh), - ); - void ensureManagedRepoScanIgnores(); - void refreshController.refreshNow(); - void maybeAutoUpdateActiveAgentsExtension(context); -} - -function deactivate() {} - -module.exports = { - activate, - deactivate, -}; diff --git a/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json b/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json deleted file mode 100644 index e8e59681..00000000 --- a/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "iconDefinitions": { - "_gitguardex_agent": { - "iconPath": "./icons/agent.svg" - }, - "_gitguardex_branch": { - "iconPath": "./icons/branch.svg" - }, - "_gitguardex_config": { - "iconPath": "./icons/config.svg" - }, - "_gitguardex_hook": { - "iconPath": "./icons/hook.svg" - }, - "_gitguardex_openspec": { - "iconPath": "./icons/openspec.svg" - }, - "_gitguardex_plan": { - "iconPath": "./icons/plan.svg" - }, - "_gitguardex_spec": { - "iconPath": "./icons/spec.svg" - } - }, - "folderNames": { - ".agents": "_gitguardex_agent", - ".githooks": "_gitguardex_hook", - ".omc": "_gitguardex_agent", - ".omx": "_gitguardex_agent", - "agent-worktrees": "_gitguardex_branch", - "changes": "_gitguardex_openspec", - "plan": "_gitguardex_plan", - "rules": "_gitguardex_spec", - "specs": "_gitguardex_spec" - }, - "fileNames": { - ".openspec.yaml": "_gitguardex_config", - "AGENT.lock": "_gitguardex_agent", - "AGENTS.md": "_gitguardex_agent", - "CLAUDE.md": "_gitguardex_agent", - "config.yaml": "_gitguardex_config", - "context-docs-cue.md": "_gitguardex_spec", - "post-checkout": "_gitguardex_hook", - "pre-commit": "_gitguardex_hook", - "pre-push": "_gitguardex_hook", - "proposal.md": "_gitguardex_openspec", - "spec.md": "_gitguardex_spec", - "tasks.md": "_gitguardex_plan", - "plan.md": "_gitguardex_plan" - }, - "fileExtensions": { - "openspec.yaml": "_gitguardex_config" - } -} diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg b/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg deleted file mode 100644 index f29ca828..00000000 --- a/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg b/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg deleted file mode 100644 index 62242793..00000000 --- a/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/config.svg b/templates/vscode/guardex-active-agents/fileicons/icons/config.svg deleted file mode 100644 index 6b4e2f9c..00000000 --- a/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg b/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg deleted file mode 100644 index 384987c2..00000000 --- a/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg b/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg deleted file mode 100644 index 8cc93ff9..00000000 --- a/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg b/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg deleted file mode 100644 index 15255686..00000000 --- a/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg b/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg deleted file mode 100644 index 7b3da2be..00000000 --- a/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/templates/vscode/guardex-active-agents/icon.png b/templates/vscode/guardex-active-agents/icon.png deleted file mode 100644 index e0b975050f2e20a4488e6b862032105788c759e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 230734 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%0D1ka6XOgP*K{uskB#3pHcU5n?dV;SM5wfsnQuP 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∋;Q+{ik~%-*iqKk1jc) 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 $~sbD?k1VxZjHc;rEs9Y{p>{r=kR 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

1u&F@vsxZHg+f?J-+at`!9#1iIKcyj3 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 - - - diff --git a/templates/vscode/guardex-active-agents/package.json b/templates/vscode/guardex-active-agents/package.json deleted file mode 100644 index c725ca6a..00000000 --- a/templates/vscode/guardex-active-agents/package.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "name": "gitguardex-active-agents", - "displayName": "GitGuardex Active Agents", - "description": "Shows live Guardex sandbox sessions and repo changes in a dedicated VS Code Active Agents sidebar.", - "publisher": "Recodee", - "version": "0.0.21", - "license": "MIT", - "icon": "icon.png", - "engines": { - "vscode": "^1.88.0" - }, - "categories": [ - "SCM Providers", - "Other" - ], - "activationEvents": [ - "onStartupFinished", - "workspaceContains:.omx/state/active-sessions", - "workspaceContains:.omx/agent-worktrees", - "workspaceContains:.omc/agent-worktrees", - "onView:gitguardex.activeAgents" - ], - "main": "./extension.js", - "contributes": { - "commands": [ - { - "command": "gitguardex.activeAgents.startAgent", - "title": "Start Guardex Agent" - }, - { - "command": "gitguardex.activeAgents.refresh", - "title": "Refresh Active Agents" - }, - { - "command": "gitguardex.activeAgents.restart", - "title": "Restart Active Agents", - "icon": "$(debug-restart)" - }, - { - "command": "gitguardex.activeAgents.commitSelectedSession", - "title": "Commit Selected Session", - "icon": "$(check)" - }, - { - "command": "gitguardex.activeAgents.inspect", - "title": "Inspect Session", - "icon": "$(info)" - }, - { - "command": "gitguardex.activeAgents.openWorktree", - "title": "Open Agent Worktree" - }, - { - "command": "gitguardex.activeAgents.finishSession", - "title": "Finish", - "icon": "$(check)" - }, - { - "command": "gitguardex.activeAgents.syncSession", - "title": "Sync", - "icon": "$(sync)" - }, - { - "command": "gitguardex.activeAgents.stopSession", - "title": "Stop", - "icon": "$(debug-stop)" - }, - { - "command": "gitguardex.activeAgents.dismissSession", - "title": "Dismiss", - "icon": "$(trash)" - }, - { - "command": "gitguardex.activeAgents.showSessionTerminal", - "title": "Show Terminal", - "icon": "$(terminal)" - } - ], - "viewsContainers": { - "activitybar": [ - { - "id": "gitguardex-active-agents-container", - "title": "Active Agents", - "icon": "media/active-agents-hivemind.svg" - } - ] - }, - "views": { - "gitguardex-active-agents-container": [ - { - "id": "gitguardex.activeAgents", - "name": "Active Agents", - "contextualTitle": "Active Agents", - "icon": "media/active-agents-hivemind.svg", - "visibility": "visible" - } - ] - }, - "viewsWelcome": [ - { - "view": "gitguardex.activeAgents", - "contents": "No live Guardex agents are visible in this workspace yet.\n\nThis sidebar tracks Guardex session files and managed worktree telemetry without taking over Source Control.\n\n[Start agent](command:gitguardex.activeAgents.startAgent)\n[Open guide](https://github.com/recodeee/gitguardex/blob/main/vscode/guardex-active-agents/README.md#quick-start)\n[Refresh](command:gitguardex.activeAgents.refresh)" - } - ], - "menus": { - "view/title": [ - { - "command": "gitguardex.activeAgents.commitSelectedSession", - "when": "view == gitguardex.activeAgents && guardex.hasAgents", - "group": "navigation@1" - }, - { - "command": "gitguardex.activeAgents.restart", - "when": "view == gitguardex.activeAgents", - "group": "navigation@8" - }, - { - "command": "gitguardex.activeAgents.refresh", - "when": "view == gitguardex.activeAgents", - "group": "navigation@9" - } - ], - "extension/context": [ - { - "command": "gitguardex.activeAgents.restart", - "when": "extension == Recodee.gitguardex-active-agents && extensionStatus == installed", - "group": "2_configure@2" - } - ], - "view/item/context": [ - { - "command": "gitguardex.activeAgents.openWorktree", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/", - "group": "inline" - }, - { - "command": "gitguardex.activeAgents.inspect", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/", - "group": "inline" - }, - { - "command": "gitguardex.activeAgents.showSessionTerminal", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/", - "group": "inline" - }, - { - "command": "gitguardex.activeAgents.finishSession", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/", - "group": "inline" - }, - { - "command": "gitguardex.activeAgents.syncSession", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/", - "group": "inline" - }, - { - "command": "gitguardex.activeAgents.stopSession", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/", - "group": "inline" - }, - { - "command": "gitguardex.activeAgents.dismissSession", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session\\.(stalled|dead)$/", - "group": "inline" - } - ] - } - } -} diff --git a/templates/vscode/guardex-active-agents/session-schema.js b/templates/vscode/guardex-active-agents/session-schema.js deleted file mode 100644 index 5d2b22c0..00000000 --- a/templates/vscode/guardex-active-agents/session-schema.js +++ /dev/null @@ -1,1348 +0,0 @@ -const fs = require('node:fs'); -const path = require('node:path'); -const cp = require('node:child_process'); - -const ACTIVE_SESSIONS_RELATIVE_DIR = path.join('.omx', 'state', 'active-sessions'); -const SESSION_SCHEMA_VERSION = 1; -const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json'); -const LOGS_RELATIVE_DIR = path.join('.omx', 'logs'); -const AGENT_WORKTREE_LOCK_FILE = 'AGENT.lock'; -const MANAGED_WORKTREE_ROOTS = [ - path.join('.omx', 'agent-worktrees'), - path.join('.omc', 'agent-worktrees'), -]; -const MAX_CHANGED_PATH_PREVIEW = 3; -const ACTIVE_SESSIONS_FILTER_PREFIX = ACTIVE_SESSIONS_RELATIVE_DIR.split(path.sep).join('/'); -const LOCK_FILE_FILTER_PATH = LOCK_FILE_RELATIVE.split(path.sep).join('/'); -const MANAGED_WORKTREE_FILTER_PREFIXES = MANAGED_WORKTREE_ROOTS - .map((relativeRoot) => relativeRoot.split(path.sep).join('/').replace(/\/+$/, '')); -const IDLE_ACTIVITY_WINDOW_MS = 2 * 60 * 1000; -const STALLED_ACTIVITY_WINDOW_MS = 15 * 60 * 1000; -const HEARTBEAT_STALE_MS = 5 * 60 * 1000; -const DEFAULT_BASE_BRANCH = 'dev'; -const DEFAULT_LOG_TAIL_LINE_COUNT = 200; -const ADVISORY_SESSION_STATES = new Set(['working', 'thinking', 'idle']); -const WORKTREE_ACTIVITY_CACHE_TTL_MS = 3_000; -const MAX_WORKTREE_ACTIVITY_STAT_PATHS = 200; -const WORKTREE_ACTIVITY_SKIP_PREFIXES = [ - '.git/', - '.omx/', - '.omc/', - 'node_modules/', - 'dist/', - 'build/', - 'coverage/', - '.next/', - 'out/', - 'vendor/', -]; -const WORKTREE_ACTIVITY_PRIORITY_PREFIXES = [ - 'src/', - 'app/', - 'apps/', - 'lib/', - 'packages/', - 'scripts/', - 'test/', - 'tests/', - 'vscode/', - 'templates/', - 'openspec/', - 'docs/', -]; -const BLOCKING_GIT_STATES = [ - { - label: 'Rebase in progress.', - markers: ['REBASE_HEAD', 'rebase-apply', 'rebase-merge'], - }, - { - label: 'Merge in progress.', - markers: ['MERGE_HEAD'], - }, - { - label: 'Cherry-pick in progress.', - markers: ['CHERRY_PICK_HEAD'], - }, -]; -const worktreeActivityCache = new Map(); - -function toNonEmptyString(value, fallback = '') { - const normalized = typeof value === 'string' ? value.trim() : String(value || '').trim(); - return normalized || fallback; -} - -function toPositiveInteger(value) { - const normalized = Number.parseInt(String(value || ''), 10); - return Number.isInteger(normalized) && normalized > 0 ? normalized : null; -} - -function toBoundedInteger(value, min, max) { - const normalized = Number.parseInt(String(value ?? ''), 10); - if (!Number.isInteger(normalized) || normalized < min || normalized > max) { - return null; - } - return normalized; -} - -function normalizeStringList(values) { - if (!Array.isArray(values)) { - return []; - } - - return values - .map((value) => toNonEmptyString(value)) - .filter(Boolean); -} - -function normalizeSessionHealthPayload(input) { - if (!input || typeof input !== 'object' || Array.isArray(input)) { - return null; - } - - const rawScores = input.scores && typeof input.scores === 'object' && !Array.isArray(input.scores) - ? input.scores - : null; - const score = toBoundedInteger(input.score ?? input.total ?? rawScores?.total, 0, 100); - if (score === null) { - return null; - } - - return { - score, - label: toNonEmptyString(input.label), - primaryDriver: toNonEmptyString(input.primaryDriver), - secondaries: normalizeStringList(input.secondaries), - outputLine: toNonEmptyString(input.outputLine), - }; -} - -function normalizeTaskMode(value) { - const normalized = toNonEmptyString(value).toLowerCase(); - return normalized === 'caveman' || normalized === 'omx' ? normalized : ''; -} - -function normalizeOpenSpecTier(value) { - const normalized = toNonEmptyString(value).toUpperCase(); - return ['T0', 'T1', 'T2', 'T3'].includes(normalized) ? normalized : ''; -} - -function normalizeAdvisoryState(value, fallback = 'working') { - const normalized = toNonEmptyString(value).toLowerCase(); - return ADVISORY_SESSION_STATES.has(normalized) ? normalized : fallback; -} - -function sanitizeBranchForFile(branch) { - const normalized = toNonEmptyString(branch, 'session'); - return normalized.replace(/[^a-zA-Z0-9._-]+/g, '__').replace(/^_+|_+$/g, '') || 'session'; -} - -function sessionFileNameForBranch(branch) { - return `${sanitizeBranchForFile(branch)}.json`; -} - -function activeSessionsDirForRepo(repoRoot) { - return path.join(path.resolve(repoRoot), ACTIVE_SESSIONS_RELATIVE_DIR); -} - -function sessionFilePathForBranch(repoRoot, branch) { - return path.join(activeSessionsDirForRepo(repoRoot), sessionFileNameForBranch(branch)); -} - -function resolveManagedWorktreeRoots(repoRoot) { - return MANAGED_WORKTREE_ROOTS.map((relativeRoot) => path.join(path.resolve(repoRoot), relativeRoot)); -} - -function splitOutputLines(output) { - if (typeof output !== 'string') { - return null; - } - - return output - .split(/\r?\n/) - .filter((line) => line.trim().length > 0); -} - -function normalizeRelativePath(value) { - return toNonEmptyString(value).replace(/\\/g, '/').replace(/^\.\//, ''); -} - -function normalizeProjectPath(value) { - const normalized = toNonEmptyString(value); - if (!normalized) { - return ''; - } - - return path.isAbsolute(normalized) - ? path.resolve(normalized) - : normalizeRelativePath(normalized); -} - -function readJsonFile(filePath) { - try { - return JSON.parse(fs.readFileSync(filePath, 'utf8')); - } catch (_error) { - return null; - } -} - -function readConfiguredBaseBranch(repoRoot) { - const lines = runGitLines(path.resolve(repoRoot), ['config', '--get', 'multiagent.baseBranch']); - if (Array.isArray(lines) && typeof lines[0] === 'string' && lines[0].trim()) { - return lines[0].trim(); - } - return DEFAULT_BASE_BRANCH; -} - -function readAheadBehindCounts(worktreePath, branch, baseBranch) { - const normalizedWorktreePath = toNonEmptyString(worktreePath); - const normalizedBranch = toNonEmptyString(branch); - const normalizedBaseBranch = toNonEmptyString(baseBranch, DEFAULT_BASE_BRANCH); - const compareRef = `origin/${normalizedBaseBranch}`; - - if (!normalizedWorktreePath || !normalizedBranch) { - return { - compareRef, - aheadCount: null, - behindCount: null, - }; - } - - const lines = runGitLines(normalizedWorktreePath, [ - 'rev-list', - '--left-right', - '--count', - `${normalizedBranch}...${compareRef}`, - ]); - const match = Array.isArray(lines) && typeof lines[0] === 'string' - ? lines[0].trim().match(/^(\d+)\s+(\d+)$/) - : null; - if (!match) { - return { - compareRef, - aheadCount: null, - behindCount: null, - }; - } - - return { - compareRef, - aheadCount: Number.parseInt(match[1], 10), - behindCount: Number.parseInt(match[2], 10), - }; -} - -function sessionLogPath(repoRoot, branch) { - const normalizedRepoRoot = toNonEmptyString(repoRoot); - const normalizedBranch = toNonEmptyString(branch); - if (!normalizedRepoRoot || !normalizedBranch) { - return ''; - } - - return path.join( - path.resolve(normalizedRepoRoot), - LOGS_RELATIVE_DIR, - `agent-${sanitizeBranchForFile(normalizedBranch)}.log`, - ); -} - -function readLogTail(filePath, maxLines = DEFAULT_LOG_TAIL_LINE_COUNT) { - const normalizedFilePath = toNonEmptyString(filePath); - const normalizedMaxLines = toPositiveInteger(maxLines) || DEFAULT_LOG_TAIL_LINE_COUNT; - if (!normalizedFilePath || !fs.existsSync(normalizedFilePath)) { - return []; - } - - try { - const lines = fs.readFileSync(normalizedFilePath, 'utf8').split(/\r?\n/); - while (lines.length > 0 && lines[lines.length - 1] === '') { - lines.pop(); - } - return lines.slice(-normalizedMaxLines); - } catch (_error) { - return []; - } -} - -function readSessionHeldLocks(repoRoot, branch) { - const normalizedRepoRoot = toNonEmptyString(repoRoot); - const normalizedBranch = toNonEmptyString(branch); - if (!normalizedRepoRoot || !normalizedBranch) { - return []; - } - - const parsed = readJsonFile(path.join(path.resolve(normalizedRepoRoot), LOCK_FILE_RELATIVE)); - const locks = parsed?.locks; - if (!locks || typeof locks !== 'object' || Array.isArray(locks)) { - return []; - } - - return Object.entries(locks) - .map(([rawRelativePath, entry]) => { - if (!entry || typeof entry !== 'object') { - return null; - } - - const relativePath = normalizeRelativePath(rawRelativePath); - const ownerBranch = toNonEmptyString(entry.branch); - if (!relativePath || ownerBranch !== normalizedBranch) { - return null; - } - - return { - relativePath, - claimedAt: toNonEmptyString(entry.claimed_at), - allowDelete: Boolean(entry.allow_delete), - }; - }) - .filter(Boolean) - .sort((left, right) => left.relativePath.localeCompare(right.relativePath)); -} - -function readSessionInspectData(session, options = {}) { - const repoRoot = toNonEmptyString(session?.repoRoot); - const branch = toNonEmptyString(session?.branch); - const worktreePath = toNonEmptyString(session?.worktreePath); - const baseBranch = readConfiguredBaseBranch(repoRoot); - const logPath = sessionLogPath(repoRoot, branch); - const logTailLines = readLogTail(logPath, options.logLines); - - return { - baseBranch, - logPath, - logExists: Boolean(logPath) && fs.existsSync(logPath), - logTailLines, - logTailText: logTailLines.join('\n'), - heldLocks: readSessionHeldLocks(repoRoot, branch), - ...readAheadBehindCounts(worktreePath, branch, baseBranch), - }; -} - -function normalizeIsoString(value, fallback = '') { - const normalized = toNonEmptyString(value); - if (!normalized) { - return fallback; - } - - const timestamp = Date.parse(normalized); - return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback; -} - -function runGitLines(worktreePath, args) { - try { - const output = cp.execFileSync('git', ['-C', worktreePath, ...args], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }); - return splitOutputLines(output); - } catch (_error) { - return null; - } -} - -function unquoteGitPath(value) { - if (typeof value !== 'string') { - return ''; - } - - const trimmed = value.trim(); - if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) { - return trimmed; - } - - try { - return JSON.parse(trimmed); - } catch (_error) { - return trimmed.slice(1, -1); - } -} - -function formatFileCount(count) { - return `${count} file${count === 1 ? '' : 's'}`; -} - -function previewChangedPaths(paths) { - if (!Array.isArray(paths) || paths.length === 0) { - return ''; - } - - if (paths.length <= MAX_CHANGED_PATH_PREVIEW) { - return paths.join(', '); - } - - const preview = paths.slice(0, MAX_CHANGED_PATH_PREVIEW).join(', '); - return `${preview}, +${paths.length - MAX_CHANGED_PATH_PREVIEW} more`; -} - -function deriveRepoChangeStatus(statusPair) { - if (statusPair === '??') { - return { - statusCode: '??', - statusLabel: 'U', - statusText: 'Untracked', - }; - } - - const code = [statusPair[1], statusPair[0]].find((value) => value && value !== ' ') || 'M'; - const statusTextByCode = { - A: 'Added', - C: 'Copied', - D: 'Deleted', - M: 'Modified', - R: 'Renamed', - T: 'Type changed', - U: 'Conflicted', - }; - - return { - statusCode: code, - statusLabel: code, - statusText: statusTextByCode[code] || 'Changed', - }; -} - -function parseRepoChangeLine(repoRoot, line) { - if (typeof line !== 'string' || line.length < 4) { - return null; - } - - const statusPair = line.slice(0, 2); - if (statusPair === '!!') { - return null; - } - - const rawPath = line.slice(3).trim(); - if (!rawPath) { - return null; - } - - let relativePath = rawPath; - let originalPath = ''; - if (rawPath.includes(' -> ')) { - const parts = rawPath.split(' -> '); - if (parts.length === 2) { - originalPath = unquoteGitPath(parts[0]); - relativePath = parts[1]; - } - } - - relativePath = unquoteGitPath(relativePath); - if (!relativePath) { - return null; - } - - const normalizedRelativePath = relativePath.split(path.sep).join('/'); - if ( - normalizedRelativePath === LOCK_FILE_FILTER_PATH - || normalizedRelativePath.startsWith(`${LOCK_FILE_FILTER_PATH}/`) - || normalizedRelativePath === ACTIVE_SESSIONS_FILTER_PREFIX - || normalizedRelativePath.startsWith(`${ACTIVE_SESSIONS_FILTER_PREFIX}/`) - || MANAGED_WORKTREE_FILTER_PREFIXES.some((prefix) => ( - normalizedRelativePath === prefix || normalizedRelativePath.startsWith(`${prefix}/`) - )) - ) { - return null; - } - - const status = deriveRepoChangeStatus(statusPair); - return { - ...status, - originalPath, - relativePath, - absolutePath: path.join(path.resolve(repoRoot), relativePath), - }; -} - -function collectWorktreeChangedPaths(worktreePath) { - const changedGroups = [ - runGitLines(worktreePath, ['diff', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`, `:(exclude)${AGENT_WORKTREE_LOCK_FILE}`]), - runGitLines(worktreePath, ['diff', '--cached', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`, `:(exclude)${AGENT_WORKTREE_LOCK_FILE}`]), - runGitLines(worktreePath, ['ls-files', '--others', '--exclude-standard']), - ]; - - if (changedGroups.some((group) => group === null)) { - return null; - } - - return [...new Set(changedGroups.flat())] - .filter((relativePath) => ( - relativePath - && relativePath !== LOCK_FILE_RELATIVE - && relativePath !== AGENT_WORKTREE_LOCK_FILE - )) - .sort((left, right) => left.localeCompare(right)); -} - -function resolveWorktreeGitDir(worktreePath) { - const gitPath = path.join(path.resolve(worktreePath), '.git'); - try { - if (fs.statSync(gitPath).isDirectory()) { - return gitPath; - } - } catch (_error) { - return null; - } - - try { - const gitPointer = fs.readFileSync(gitPath, 'utf8'); - const match = gitPointer.match(/^gitdir:\s*(.+)$/m); - if (match?.[1]) { - return path.resolve(worktreePath, match[1].trim()); - } - } catch (_error) { - return null; - } - - return null; -} - -function deriveBlockingGitLabel(worktreePath) { - const gitDir = resolveWorktreeGitDir(worktreePath); - if (!gitDir) { - return ''; - } - - for (const blockingState of BLOCKING_GIT_STATES) { - if (blockingState.markers.some((marker) => fs.existsSync(path.join(gitDir, marker)))) { - return blockingState.label; - } - } - - return ''; -} - -function collectWorktreeTrackedPaths(worktreePath) { - const trackedPaths = runGitLines(worktreePath, ['ls-files', '--cached', '--others', '--exclude-standard']); - if (!trackedPaths) { - return null; - } - - return [...new Set(trackedPaths)] - .filter(Boolean) - .sort((left, right) => left.localeCompare(right)); -} - -function shouldSkipWorktreeActivityPath(relativePath) { - const normalized = normalizeRelativePath(relativePath); - if (!normalized || normalized === LOCK_FILE_RELATIVE || normalized === AGENT_WORKTREE_LOCK_FILE) { - return true; - } - - return WORKTREE_ACTIVITY_SKIP_PREFIXES.some((prefix) => ( - normalized === prefix.slice(0, -1) || normalized.startsWith(prefix) - )); -} - -function worktreeActivityPathPriority(relativePath, recentPathsSet) { - if (recentPathsSet.has(relativePath)) { - return 0; - } - if (!relativePath.includes('/')) { - return 1; - } - if (WORKTREE_ACTIVITY_PRIORITY_PREFIXES.some((prefix) => relativePath.startsWith(prefix))) { - return 2; - } - return 3; -} - -function collectWorktreeActivityCandidatePaths(worktreePath, trackedPaths) { - const recentPaths = runGitLines(worktreePath, ['log', '-1', '--name-only', '--pretty=format:', '--', '.']) || []; - const filteredRecentPaths = [...new Set(recentPaths.map(normalizeRelativePath).filter(Boolean))] - .filter((relativePath) => !shouldSkipWorktreeActivityPath(relativePath)); - const recentPathSet = new Set(filteredRecentPaths); - const prioritizedTrackedPaths = trackedPaths - .map(normalizeRelativePath) - .filter(Boolean) - .filter((relativePath) => !shouldSkipWorktreeActivityPath(relativePath)) - .sort((left, right) => { - const priorityDelta = worktreeActivityPathPriority(left, recentPathSet) - - worktreeActivityPathPriority(right, recentPathSet); - if (priorityDelta !== 0) { - return priorityDelta; - } - return left.localeCompare(right); - }); - - return [...new Set([...filteredRecentPaths, ...prioritizedTrackedPaths])] - .slice(0, MAX_WORKTREE_ACTIVITY_STAT_PATHS); -} - -function clearWorktreeActivityCache(worktreePath = '') { - const normalizedWorktreePath = toNonEmptyString(worktreePath); - if (!normalizedWorktreePath) { - worktreeActivityCache.clear(); - return; - } - worktreeActivityCache.delete(path.resolve(normalizedWorktreePath)); -} - -function deriveLatestWorktreeFileActivity(worktreePath, options = {}) { - const now = Number.isFinite(options.now) ? options.now : Date.now(); - const useCache = options.useCache !== false; - const cacheKey = path.resolve(worktreePath); - if (useCache) { - const cached = worktreeActivityCache.get(cacheKey); - if (cached && (now - cached.checkedAtMs) < WORKTREE_ACTIVITY_CACHE_TTL_MS) { - return cached.latestMtimeMs; - } - } - - const trackedPaths = collectWorktreeTrackedPaths(worktreePath); - if (!trackedPaths) { - return null; - } - - const candidatePaths = collectWorktreeActivityCandidatePaths(worktreePath, trackedPaths); - let latestMtimeMs = null; - for (const relativePath of candidatePaths) { - const absolutePath = path.join(worktreePath, relativePath); - try { - const stats = fs.statSync(absolutePath); - if (!stats.isFile() || !Number.isFinite(stats.mtimeMs)) { - continue; - } - latestMtimeMs = latestMtimeMs === null - ? stats.mtimeMs - : Math.max(latestMtimeMs, stats.mtimeMs); - } catch (_error) { - continue; - } - } - - if (useCache) { - worktreeActivityCache.set(cacheKey, { - checkedAtMs: now, - latestMtimeMs, - }); - } - - return latestMtimeMs; -} - -function deriveSessionActivity(session, options = {}) { - const now = Number.isFinite(options.now) ? options.now : Date.now(); - const pid = toPositiveInteger(session?.pid); - const pidAlive = pid ? isPidAlive(pid) : null; - const heartbeatAt = normalizeIsoString(session?.lastHeartbeatAt); - const heartbeatMs = Date.parse(heartbeatAt); - if (heartbeatAt && Number.isFinite(heartbeatMs) && now - heartbeatMs > HEARTBEAT_STALE_MS) { - return { - activityKind: 'dead', - activityLabel: 'dead', - activityCountLabel: '', - activitySummary: `Heartbeat stale for ${formatElapsedFrom(heartbeatAt, now)}.`, - changeCount: 0, - changedPaths: [], - worktreeChangedPaths: [], - pidAlive, - lastFileActivityAt: '', - lastFileActivityLabel: '', - }; - } - - const blockingLabel = deriveBlockingGitLabel(session.worktreePath); - if (blockingLabel) { - return { - activityKind: 'blocked', - activityLabel: 'blocked', - activityCountLabel: '', - activitySummary: blockingLabel, - changeCount: 0, - changedPaths: [], - worktreeChangedPaths: [], - pidAlive, - lastFileActivityAt: '', - lastFileActivityLabel: '', - }; - } - - if (pid && !pidAlive) { - return { - activityKind: 'dead', - activityLabel: 'dead', - activityCountLabel: '', - activitySummary: 'Recorded PID is not alive.', - changeCount: 0, - changedPaths: [], - worktreeChangedPaths: [], - pidAlive, - lastFileActivityAt: '', - lastFileActivityLabel: '', - }; - } - - const worktreeChangedPaths = collectWorktreeChangedPaths(session.worktreePath); - if (!worktreeChangedPaths) { - return { - activityKind: 'idle', - activityLabel: 'idle', - activityCountLabel: '', - activitySummary: 'Worktree activity unavailable.', - changeCount: 0, - changedPaths: [], - worktreeChangedPaths: [], - pidAlive, - lastFileActivityAt: '', - lastFileActivityLabel: '', - }; - } - - if (worktreeChangedPaths.length > 0) { - const worktreeRelativePaths = [...new Set(worktreeChangedPaths - .map((relativePath) => normalizeRelativePath(relativePath)) - .filter(Boolean))] - .sort((left, right) => left.localeCompare(right)); - clearWorktreeActivityCache(session.worktreePath); - const changedPaths = [...new Set(worktreeChangedPaths - .map((relativePath) => normalizeRelativePath( - path.relative(session.repoRoot, path.resolve(session.worktreePath, relativePath)), - )) - .filter(Boolean))] - .sort((left, right) => left.localeCompare(right)); - - const workingLatestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath, { - now, - useCache: options.useCache, - }); - const workingLastFileActivityAt = Number.isFinite(workingLatestFileActivityMs) - ? new Date(workingLatestFileActivityMs).toISOString() - : ''; - const workingLastFileActivityLabel = workingLastFileActivityAt - ? formatElapsedFrom(workingLastFileActivityAt, now) - : ''; - const workingFileActivityAgeMs = Number.isFinite(workingLatestFileActivityMs) - ? Math.max(0, now - workingLatestFileActivityMs) - : null; - const isFinishedUncommitted = workingFileActivityAgeMs !== null - && workingFileActivityAgeMs > IDLE_ACTIVITY_WINDOW_MS; - - return { - activityKind: isFinishedUncommitted ? 'finished' : 'working', - activityLabel: isFinishedUncommitted ? 'finished' : 'working', - activityCountLabel: formatFileCount(worktreeChangedPaths.length), - activitySummary: isFinishedUncommitted && workingLastFileActivityLabel - ? `${previewChangedPaths(worktreeChangedPaths)} · idle ${workingLastFileActivityLabel}` - : previewChangedPaths(worktreeChangedPaths), - changeCount: worktreeChangedPaths.length, - changedPaths, - worktreeChangedPaths: worktreeRelativePaths, - pidAlive, - lastFileActivityAt: workingLastFileActivityAt, - lastFileActivityLabel: workingLastFileActivityLabel, - }; - } - - const latestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath, { - now, - useCache: options.useCache, - }); - const lastFileActivityAt = Number.isFinite(latestFileActivityMs) - ? new Date(latestFileActivityMs).toISOString() - : ''; - const lastFileActivityLabel = lastFileActivityAt - ? formatElapsedFrom(lastFileActivityAt, now) - : ''; - const lastFileActivityAgeMs = Number.isFinite(latestFileActivityMs) - ? Math.max(0, now - latestFileActivityMs) - : null; - - if (lastFileActivityAgeMs !== null && lastFileActivityAgeMs > STALLED_ACTIVITY_WINDOW_MS) { - return { - activityKind: 'stalled', - activityLabel: 'stalled', - activityCountLabel: '', - activitySummary: `Worktree clean. No file activity for ${lastFileActivityLabel}.`, - changeCount: 0, - changedPaths: [], - worktreeChangedPaths: [], - pidAlive, - lastFileActivityAt, - lastFileActivityLabel, - }; - } - - return { - activityKind: 'idle', - activityLabel: 'idle', - activityCountLabel: '', - activitySummary: lastFileActivityAgeMs !== null && lastFileActivityAgeMs <= IDLE_ACTIVITY_WINDOW_MS - ? `Worktree clean. Recent file activity ${lastFileActivityLabel} ago.` - : lastFileActivityLabel - ? `Worktree clean. Last file activity ${lastFileActivityLabel} ago.` - : 'Worktree clean.', - changeCount: 0, - changedPaths: [], - worktreeChangedPaths: [], - pidAlive, - lastFileActivityAt, - lastFileActivityLabel, - }; -} - -function buildSessionRecord(input) { - const repoRoot = path.resolve(toNonEmptyString(input.repoRoot)); - const worktreePath = path.resolve(toNonEmptyString(input.worktreePath)); - const branch = toNonEmptyString(input.branch); - const pid = toPositiveInteger(input.pid); - const startedAt = input.startedAt ? new Date(input.startedAt) : new Date(); - const lastHeartbeatAt = input.lastHeartbeatAt ? new Date(input.lastHeartbeatAt) : new Date(); - - if (!branch) { - throw new Error('branch is required'); - } - if (!repoRoot) { - throw new Error('repoRoot is required'); - } - if (!worktreePath) { - throw new Error('worktreePath is required'); - } - if (!pid) { - throw new Error('pid must be a positive integer'); - } - if (Number.isNaN(startedAt.getTime())) { - throw new Error('startedAt must be a valid date'); - } - if (Number.isNaN(lastHeartbeatAt.getTime())) { - throw new Error('lastHeartbeatAt must be a valid date'); - } - - return { - schemaVersion: SESSION_SCHEMA_VERSION, - repoRoot, - branch, - taskName: toNonEmptyString(input.taskName, 'task'), - latestTaskPreview: toNonEmptyString(input.latestTaskPreview), - agentName: toNonEmptyString(input.agentName, 'agent'), - projectName: toNonEmptyString(input.projectName), - projectPath: normalizeProjectPath(input.projectPath), - snapshotName: toNonEmptyString(input.snapshotName), - snapshotEmail: toNonEmptyString(input.snapshotEmail || input.email), - worktreePath, - pid, - cliName: toNonEmptyString(input.cliName, 'codex'), - taskMode: normalizeTaskMode(input.taskMode), - openspecTier: normalizeOpenSpecTier(input.openspecTier), - taskRoutingReason: toNonEmptyString(input.taskRoutingReason), - startedAt: startedAt.toISOString(), - lastHeartbeatAt: lastHeartbeatAt.toISOString(), - state: normalizeAdvisoryState(input.state), - sessionHealth: normalizeSessionHealthPayload(input.sessionHealth || input.sessionSeverity), - }; -} - -function deriveSessionLabel(branch, worktreePath) { - const worktreeLeaf = toNonEmptyString(path.basename(worktreePath || '')); - if (worktreeLeaf) { - return worktreeLeaf; - } - return toNonEmptyString(branch).replace(/[\\/]+/g, '-') || 'unknown-agent'; -} - -function normalizeSessionRecord(input, options = {}) { - if (!input || typeof input !== 'object') { - return null; - } - - const repoRoot = toNonEmptyString(input.repoRoot); - const branch = toNonEmptyString(input.branch); - const worktreePath = toNonEmptyString(input.worktreePath); - const startedAt = new Date(input.startedAt); - const lastHeartbeatAt = new Date(input.lastHeartbeatAt || input.startedAt); - const pid = toPositiveInteger(input.pid); - - if ( - !repoRoot - || !branch - || !worktreePath - || !pid - || Number.isNaN(startedAt.getTime()) - || Number.isNaN(lastHeartbeatAt.getTime()) - ) { - return null; - } - - return { - schemaVersion: toPositiveInteger(input.schemaVersion) || SESSION_SCHEMA_VERSION, - repoRoot: path.resolve(repoRoot), - branch, - taskName: toNonEmptyString(input.taskName, 'task'), - latestTaskPreview: toNonEmptyString(input.latestTaskPreview), - agentName: toNonEmptyString(input.agentName, 'agent'), - projectName: toNonEmptyString(input.projectName), - projectPath: normalizeProjectPath(input.projectPath), - snapshotName: toNonEmptyString(input.snapshotName), - snapshotEmail: toNonEmptyString(input.snapshotEmail || input.email), - worktreePath: path.resolve(worktreePath), - pid, - cliName: toNonEmptyString(input.cliName, 'codex'), - taskMode: normalizeTaskMode(input.taskMode), - openspecTier: normalizeOpenSpecTier(input.openspecTier), - taskRoutingReason: toNonEmptyString(input.taskRoutingReason), - startedAt: startedAt.toISOString(), - lastHeartbeatAt: lastHeartbeatAt.toISOString(), - state: normalizeAdvisoryState(input.state, 'idle'), - filePath: toNonEmptyString(options.filePath), - label: deriveSessionLabel(branch, worktreePath), - changedPaths: [], - worktreeChangedPaths: [], - sourceKind: 'active-session', - telemetryUpdatedAt: '', - telemetrySource: '', - lockSnapshotCount: 0, - lockSessionCount: 0, - collaboration: false, - sessionHealth: normalizeSessionHealthPayload(input.sessionHealth || input.sessionSeverity), - }; -} - -function formatElapsedFrom(startedAt, now = Date.now()) { - const startedAtMs = startedAt instanceof Date ? startedAt.getTime() : Date.parse(startedAt); - if (!Number.isFinite(startedAtMs)) { - return '0s'; - } - - const totalSeconds = Math.max(0, Math.floor((now - startedAtMs) / 1000)); - const days = Math.floor(totalSeconds / 86_400); - const hours = Math.floor((totalSeconds % 86_400) / 3_600); - const minutes = Math.floor((totalSeconds % 3_600) / 60); - const seconds = totalSeconds % 60; - - if (days > 0) { - return `${days}d ${hours}h`; - } - if (hours > 0) { - return `${hours}h ${minutes}m`; - } - if (minutes > 0) { - return `${minutes}m ${seconds}s`; - } - return `${seconds}s`; -} - -function isPidAlive(pid) { - const normalizedPid = toPositiveInteger(pid); - if (!normalizedPid) { - return false; - } - - try { - process.kill(normalizedPid, 0); - return true; - } catch (_error) { - return false; - } -} - -function readWorktreeBranch(worktreePath) { - const lines = runGitLines(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']); - return Array.isArray(lines) && typeof lines[0] === 'string' ? lines[0].trim() : ''; -} - -function deriveAgentNameFromBranch(branch) { - const parts = toNonEmptyString(branch).split('/').filter(Boolean); - if (parts.length >= 2 && parts[0] === 'agent') { - return parts[1]; - } - return 'agent'; -} - -function isManagedAgentBranch(branch) { - return toNonEmptyString(branch).startsWith('agent/'); -} - -function deriveManagedWorktreeStartedAt(worktreePath, now = Date.now()) { - try { - const stats = fs.statSync(worktreePath); - if (Number.isFinite(stats.mtimeMs)) { - return new Date(stats.mtimeMs).toISOString(); - } - } catch (_error) { - // Directory mtime is best-effort context only; fall back to current scan time. - } - - return new Date(now).toISOString(); -} - -function flattenTelemetrySnapshotSessions(lockPayload) { - const flattened = []; - const snapshots = Array.isArray(lockPayload?.snapshots) ? lockPayload.snapshots : []; - for (const snapshot of snapshots) { - const snapshotSessions = Array.isArray(snapshot?.sessions) ? snapshot.sessions : []; - for (const session of snapshotSessions) { - flattened.push({ - taskPreview: toNonEmptyString(session?.taskPreview), - taskUpdatedAt: normalizeIsoString(session?.taskUpdatedAt), - projectName: toNonEmptyString(session?.projectName), - projectPath: toNonEmptyString(session?.projectPath), - snapshotName: toNonEmptyString(snapshot?.snapshotName), - email: toNonEmptyString(snapshot?.email), - sessionHealth: normalizeSessionHealthPayload( - session?.sessionHealth || session?.sessionSeverity || snapshot?.sessionHealth || snapshot?.sessionSeverity, - ), - }); - } - } - return flattened; -} - -function sortSessionsByTimestamp(sessions) { - sessions.sort((left, right) => { - const timeDelta = Date.parse(right.startedAt) - Date.parse(left.startedAt); - if (timeDelta !== 0) { - return timeDelta; - } - return left.label.localeCompare(right.label); - }); - return sessions; -} - -function deriveLockTaskAnchor(entries, fallbackTaskName, fallbackTimestamp) { - const sortedEntries = sortTelemetryEntriesForAnchor(entries); - - const latestEntry = sortedEntries[0] || null; - return { - taskName: latestEntry?.taskPreview || fallbackTaskName || 'task', - latestTaskPreview: latestEntry?.taskPreview || '', - timestamp: latestEntry?.taskUpdatedAt || fallbackTimestamp || '', - sessionHealth: latestEntry?.sessionHealth || null, - }; -} - -function sortTelemetryEntriesForAnchor(entries) { - return [...entries].sort((left, right) => { - const timeDelta = Date.parse(right.taskUpdatedAt || '') - Date.parse(left.taskUpdatedAt || ''); - if (timeDelta !== 0) { - return timeDelta; - } - if (Boolean(right.taskPreview) !== Boolean(left.taskPreview)) { - return Number(Boolean(right.taskPreview)) - Number(Boolean(left.taskPreview)); - } - return (right.projectPath || '').localeCompare(left.projectPath || ''); - }); -} - -function deriveLockSnapshotIdentity(entries) { - const latestEntry = sortTelemetryEntriesForAnchor(entries) - .find((entry) => entry?.snapshotName || entry?.email) || null; - return { - snapshotName: toNonEmptyString(latestEntry?.snapshotName), - snapshotEmail: toNonEmptyString(latestEntry?.email), - }; -} - -function deriveLockProjectMetadata(entries) { - const latestEntry = sortTelemetryEntriesForAnchor(entries) - .find((entry) => entry?.projectPath || entry?.projectName) || null; - return { - projectName: toNonEmptyString(latestEntry?.projectName), - projectPath: normalizeProjectPath(latestEntry?.projectPath), - }; -} - -function buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options = {}) { - const now = options.now || Date.now(); - const telemetryEntries = flattenTelemetrySnapshotSessions(lockPayload); - const telemetryUpdatedAt = normalizeIsoString(lockPayload?.updatedAt); - const branch = readWorktreeBranch(worktreePath); - const effectiveBranch = branch && branch !== 'HEAD' - ? branch - : `agent/telemetry/${path.basename(worktreePath)}`; - const label = deriveSessionLabel(effectiveBranch, worktreePath); - const taskAnchor = deriveLockTaskAnchor(telemetryEntries, label, telemetryUpdatedAt); - const snapshotIdentity = deriveLockSnapshotIdentity(telemetryEntries); - const projectMetadata = deriveLockProjectMetadata(telemetryEntries); - const startedAt = taskAnchor.timestamp || telemetryUpdatedAt || new Date(now).toISOString(); - - const session = { - schemaVersion: toPositiveInteger(lockPayload?.schemaVersion) || SESSION_SCHEMA_VERSION, - repoRoot: path.resolve(repoRoot), - branch: effectiveBranch, - taskName: taskAnchor.taskName, - latestTaskPreview: taskAnchor.latestTaskPreview, - agentName: deriveAgentNameFromBranch(effectiveBranch), - projectName: projectMetadata.projectName, - projectPath: projectMetadata.projectPath, - snapshotName: snapshotIdentity.snapshotName, - snapshotEmail: snapshotIdentity.snapshotEmail, - worktreePath: path.resolve(worktreePath), - pid: null, - cliName: 'codex', - taskMode: '', - openspecTier: '', - taskRoutingReason: '', - startedAt, - lastHeartbeatAt: '', - state: '', - filePath: path.join(worktreePath, AGENT_WORKTREE_LOCK_FILE), - label, - changedPaths: [], - worktreeChangedPaths: [], - sourceKind: 'worktree-lock', - telemetryUpdatedAt: telemetryUpdatedAt || startedAt, - telemetrySource: toNonEmptyString(lockPayload?.source, 'worktree-lock'), - lockSnapshotCount: toPositiveInteger(lockPayload?.snapshotCount) || 0, - lockSessionCount: toPositiveInteger(lockPayload?.sessionCount) || telemetryEntries.length, - collaboration: Boolean(lockPayload?.collaboration), - sessionHealth: taskAnchor.sessionHealth || normalizeSessionHealthPayload( - lockPayload?.sessionHealth || lockPayload?.sessionSeverity, - ), - }; - - session.elapsedLabel = formatElapsedFrom(session.startedAt, now); - Object.assign(session, deriveSessionActivity(session, { now })); - return session; -} - -function buildManagedWorktreeSession(repoRoot, worktreePath, options = {}) { - const now = options.now || Date.now(); - const branch = readWorktreeBranch(worktreePath); - if (!branch || branch === 'HEAD' || !isManagedAgentBranch(branch)) { - return null; - } - - const label = deriveSessionLabel(branch, worktreePath); - const startedAt = deriveManagedWorktreeStartedAt(worktreePath, now); - const session = { - schemaVersion: SESSION_SCHEMA_VERSION, - repoRoot: path.resolve(repoRoot), - branch, - taskName: label, - latestTaskPreview: '', - agentName: deriveAgentNameFromBranch(branch), - projectName: '', - projectPath: '', - snapshotName: '', - snapshotEmail: '', - worktreePath: path.resolve(worktreePath), - pid: null, - cliName: 'gx', - taskMode: '', - openspecTier: '', - taskRoutingReason: '', - startedAt, - lastHeartbeatAt: '', - state: '', - filePath: path.join(worktreePath, '.git'), - label, - changedPaths: [], - worktreeChangedPaths: [], - sourceKind: 'managed-worktree', - telemetryUpdatedAt: '', - telemetrySource: 'managed-worktree', - lockSnapshotCount: 0, - lockSessionCount: 0, - collaboration: false, - sessionHealth: null, - }; - - session.elapsedLabel = formatElapsedFrom(session.startedAt, now); - Object.assign(session, deriveSessionActivity(session, { now })); - return session; -} - -function readWorktreeLockSessions(repoRoot, options = {}) { - const sessions = []; - for (const managedRoot of resolveManagedWorktreeRoots(repoRoot)) { - if (!fs.existsSync(managedRoot)) { - continue; - } - - let entries; - try { - entries = fs.readdirSync(managedRoot, { withFileTypes: true }); - } catch (_error) { - continue; - } - - for (const entry of entries) { - if (!entry.isDirectory()) { - continue; - } - - const worktreePath = path.join(managedRoot, entry.name); - const lockPath = path.join(worktreePath, AGENT_WORKTREE_LOCK_FILE); - if (!fs.existsSync(lockPath)) { - continue; - } - - const lockPayload = readJsonFile(lockPath); - if (!lockPayload || typeof lockPayload !== 'object' || Array.isArray(lockPayload)) { - continue; - } - - const telemetryEntries = flattenTelemetrySnapshotSessions(lockPayload); - if (telemetryEntries.length === 0 && !toPositiveInteger(lockPayload.sessionCount)) { - continue; - } - - sessions.push(buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options)); - } - } - - return sortSessionsByTimestamp(sessions); -} - -function readManagedWorktreeSessions(repoRoot, options = {}) { - const lockSessions = readWorktreeLockSessions(repoRoot, options); - const lockSessionsByWorktree = new Map( - lockSessions.map((session) => [path.resolve(session.worktreePath), session]), - ); - const sessions = []; - - for (const managedRoot of resolveManagedWorktreeRoots(repoRoot)) { - if (!fs.existsSync(managedRoot)) { - continue; - } - - let entries; - try { - entries = fs.readdirSync(managedRoot, { withFileTypes: true }); - } catch (_error) { - continue; - } - - for (const entry of entries) { - if (!entry.isDirectory()) { - continue; - } - - const worktreePath = path.join(managedRoot, entry.name); - const worktreeKey = path.resolve(worktreePath); - const lockSession = lockSessionsByWorktree.get(worktreeKey); - if (lockSession) { - sessions.push(lockSession); - continue; - } - - const managedSession = buildManagedWorktreeSession(repoRoot, worktreePath, options); - if (managedSession) { - sessions.push(managedSession); - } - } - } - - return sortSessionsByTimestamp(sessions); -} - -function mergeSessionSources(primarySessions, lockSessions) { - const lockSessionsByWorktree = new Map( - lockSessions.map((session) => [path.resolve(session.worktreePath), session]), - ); - const consumedLockWorktrees = new Set(); - const merged = []; - - for (const session of primarySessions) { - const worktreeKey = path.resolve(session.worktreePath); - const lockSession = lockSessionsByWorktree.get(worktreeKey); - if (lockSession && session.activityKind === 'dead') { - continue; - } - if (lockSession) { - consumedLockWorktrees.add(worktreeKey); - merged.push({ - ...session, - latestTaskPreview: session.latestTaskPreview || lockSession.latestTaskPreview, - projectName: session.projectName || lockSession.projectName, - projectPath: session.projectPath || lockSession.projectPath, - snapshotName: session.snapshotName || lockSession.snapshotName, - snapshotEmail: session.snapshotEmail || lockSession.snapshotEmail, - telemetryUpdatedAt: session.telemetryUpdatedAt || lockSession.telemetryUpdatedAt, - telemetrySource: session.telemetrySource || lockSession.telemetrySource, - lockSnapshotCount: session.lockSnapshotCount || lockSession.lockSnapshotCount, - lockSessionCount: session.lockSessionCount || lockSession.lockSessionCount, - collaboration: session.collaboration || lockSession.collaboration, - sessionHealth: session.sessionHealth || lockSession.sessionHealth, - }); - continue; - } - merged.push(session); - } - - for (const lockSession of lockSessions) { - const worktreeKey = path.resolve(lockSession.worktreePath); - if (!consumedLockWorktrees.has(worktreeKey)) { - merged.push(lockSession); - } - } - - return sortSessionsByTimestamp(merged); -} - -function readActiveSessions(repoRoot, options = {}) { - const activeSessionsDir = activeSessionsDirForRepo(repoRoot); - const now = options.now || Date.now(); - const sessionFileSessions = []; - if (fs.existsSync(activeSessionsDir)) { - for (const entry of fs.readdirSync(activeSessionsDir, { withFileTypes: true })) { - if (!entry.isFile() || !entry.name.endsWith('.json')) { - continue; - } - - const filePath = path.join(activeSessionsDir, entry.name); - const parsed = readJsonFile(filePath); - const normalized = normalizeSessionRecord(parsed, { filePath }); - if (!normalized) { - continue; - } - if (!options.includeStale && !isPidAlive(normalized.pid)) { - continue; - } - - normalized.elapsedLabel = formatElapsedFrom(normalized.startedAt, now); - Object.assign(normalized, deriveSessionActivity(normalized, { now })); - sessionFileSessions.push(normalized); - } - } - - return mergeSessionSources( - sortSessionsByTimestamp(sessionFileSessions), - readManagedWorktreeSessions(repoRoot, { now }), - ); -} - -function readRepoChanges(repoRoot) { - const statusLines = runGitLines(repoRoot, ['status', '--porcelain=v1', '--untracked-files=all']); - if (!statusLines) { - return []; - } - - return statusLines - .map((line) => parseRepoChangeLine(repoRoot, line)) - .filter(Boolean) - .sort((left, right) => left.relativePath.localeCompare(right.relativePath)); -} - -module.exports = { - ACTIVE_SESSIONS_RELATIVE_DIR, - SESSION_SCHEMA_VERSION, - activeSessionsDirForRepo, - buildSessionRecord, - clearWorktreeActivityCache, - collectWorktreeChangedPaths, - collectWorktreeTrackedPaths, - deriveBlockingGitLabel, - deriveLatestWorktreeFileActivity, - deriveSessionLabel, - deriveSessionActivity, - formatElapsedFrom, - formatFileCount, - isPidAlive, - normalizeSessionRecord, - parseRepoChangeLine, - previewChangedPaths, - readActiveSessions, - readManagedWorktreeSessions, - readWorktreeLockSessions, - readRepoChanges, - deriveRepoChangeStatus, - readAheadBehindCounts, - readConfiguredBaseBranch, - readLogTail, - resolveWorktreeGitDir, - readSessionHeldLocks, - readSessionInspectData, - sessionLogPath, - sanitizeBranchForFile, - sessionFileNameForBranch, - sessionFilePathForBranch, -}; diff --git a/test/helpers/install-test-helpers.js b/test/helpers/install-test-helpers.js index 4406c41c..ad93f4e9 100644 --- a/test/helpers/install-test-helpers.js +++ b/test/helpers/install-test-helpers.js @@ -140,10 +140,8 @@ function assertZeroCopyManagedGitignore(content) { assert.match(content, /^!\.vscode\/$/m); assert.match(content, /^\.vscode\/\*$/m); assert.match(content, /^!\.vscode\/settings\.json$/m); - assert.match(content, /^scripts\/agent-session-state\.js$/m); assert.match(content, /^scripts\/guardex-docker-loader\.sh$/m); assert.match(content, /^scripts\/guardex-env\.sh$/m); - assert.match(content, /^scripts\/install-vscode-active-agents-extension\.js$/m); assert.doesNotMatch(content, /^scripts\/\*$/m); assert.doesNotMatch(content, /^scripts\/agent-branch-start\.sh$/m); assert.doesNotMatch(content, /^scripts\/agent-file-locks\.py$/m); diff --git a/test/metadata.test.js b/test/metadata.test.js index 080991d1..944458ef 100644 --- a/test/metadata.test.js +++ b/test/metadata.test.js @@ -220,28 +220,13 @@ test('frontend mirror workflow skips cleanly when the mirror PAT is missing', () assert.match(workflow, /if:\s+\$\{\{\s*env\.SYNC_TOKEN != ''\s*\}\}/); }); -test('critical runtime helper scripts and active-agents sources stay in sync with templates', () => { +test('critical runtime helper scripts stay in sync with templates', () => { const pairs = [ ['templates/scripts/agent-branch-start.sh', 'scripts/agent-branch-start.sh'], ['templates/scripts/agent-branch-finish.sh', 'scripts/agent-branch-finish.sh'], ['templates/scripts/codex-agent.sh', 'scripts/codex-agent.sh'], ['templates/scripts/openspec/init-plan-workspace.sh', 'scripts/openspec/init-plan-workspace.sh'], ['templates/scripts/openspec/init-change-workspace.sh', 'scripts/openspec/init-change-workspace.sh'], - ['templates/scripts/agent-session-state.js', 'scripts/agent-session-state.js'], - ['templates/scripts/install-vscode-active-agents-extension.js', 'scripts/install-vscode-active-agents-extension.js'], - ['templates/vscode/guardex-active-agents/package.json', 'vscode/guardex-active-agents/package.json'], - ['templates/vscode/guardex-active-agents/README.md', 'vscode/guardex-active-agents/README.md'], - ['templates/vscode/guardex-active-agents/extension.js', 'vscode/guardex-active-agents/extension.js'], - ['templates/vscode/guardex-active-agents/session-schema.js', 'vscode/guardex-active-agents/session-schema.js'], - ['templates/vscode/guardex-active-agents/icon.png', 'vscode/guardex-active-agents/icon.png'], - ['templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json', 'vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json'], - ['templates/vscode/guardex-active-agents/fileicons/icons/agent.svg', 'vscode/guardex-active-agents/fileicons/icons/agent.svg'], - ['templates/vscode/guardex-active-agents/fileicons/icons/branch.svg', 'vscode/guardex-active-agents/fileicons/icons/branch.svg'], - ['templates/vscode/guardex-active-agents/fileicons/icons/config.svg', 'vscode/guardex-active-agents/fileicons/icons/config.svg'], - ['templates/vscode/guardex-active-agents/fileicons/icons/hook.svg', 'vscode/guardex-active-agents/fileicons/icons/hook.svg'], - ['templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg', 'vscode/guardex-active-agents/fileicons/icons/openspec.svg'], - ['templates/vscode/guardex-active-agents/fileicons/icons/plan.svg', 'vscode/guardex-active-agents/fileicons/icons/plan.svg'], - ['templates/vscode/guardex-active-agents/fileicons/icons/spec.svg', 'vscode/guardex-active-agents/fileicons/icons/spec.svg'], ]; for (const [templatePath, runtimePath] of pairs) { diff --git a/test/setup.test.js b/test/setup.test.js index 3ac9ffde..dcc7921f 100644 --- a/test/setup.test.js +++ b/test/setup.test.js @@ -62,8 +62,6 @@ const { defineSpawnSuite, } = require('./helpers/install-test-helpers'); -const packageRepoRoot = path.resolve(__dirname, '..'); - defineSpawnSuite('setup integration suite', () => { test('setup provisions workflow files and repo config', () => { @@ -84,10 +82,8 @@ test('setup provisions workflow files and repo config', () => { '.omc/agent-worktrees', '.omx/notepad.md', '.omx/project-memory.json', - 'scripts/agent-session-state.js', 'scripts/guardex-docker-loader.sh', 'scripts/guardex-env.sh', - 'scripts/install-vscode-active-agents-extension.js', '.githooks/pre-commit', '.githooks/pre-push', '.githooks/post-merge', @@ -156,10 +152,8 @@ test('setup provisions workflow files and repo config', () => { const gitignoreContent = fs.readFileSync(path.join(repoDir, '.gitignore'), 'utf8'); assert.match(gitignoreContent, /# multiagent-safety:START/); - assert.match(gitignoreContent, /^scripts\/agent-session-state\.js$/m); assert.match(gitignoreContent, /^scripts\/guardex-docker-loader\.sh$/m); assert.match(gitignoreContent, /^scripts\/guardex-env\.sh$/m); - assert.match(gitignoreContent, /^scripts\/install-vscode-active-agents-extension\.js$/m); assert.doesNotMatch(gitignoreContent, /^scripts\/\*$/m); assert.doesNotMatch(gitignoreContent, /^scripts\/agent-branch-start\.sh$/m); assert.doesNotMatch(gitignoreContent, /^scripts\/agent-file-locks\.py$/m); @@ -181,31 +175,6 @@ test('setup provisions workflow files and repo config', () => { const secondRun = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir); assert.equal(secondRun.status, 0, secondRun.stderr || secondRun.stdout); - - const canonicalBundleFiles = [ - 'vscode/guardex-active-agents/package.json', - 'vscode/guardex-active-agents/README.md', - 'vscode/guardex-active-agents/extension.js', - 'vscode/guardex-active-agents/session-schema.js', - 'vscode/guardex-active-agents/icon.png', - 'vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json', - 'vscode/guardex-active-agents/fileicons/icons/agent.svg', - 'vscode/guardex-active-agents/fileicons/icons/branch.svg', - 'vscode/guardex-active-agents/fileicons/icons/config.svg', - 'vscode/guardex-active-agents/fileicons/icons/hook.svg', - 'vscode/guardex-active-agents/fileicons/icons/openspec.svg', - 'vscode/guardex-active-agents/fileicons/icons/plan.svg', - 'vscode/guardex-active-agents/fileicons/icons/spec.svg', - ]; - for (const relativePath of canonicalBundleFiles) { - const installedPath = path.join(repoDir, relativePath); - const expectedPath = path.join(packageRepoRoot, relativePath); - assert.equal( - Buffer.compare(fs.readFileSync(installedPath), fs.readFileSync(expectedPath)), - 0, - `${relativePath} should match the package repo canonical bundle`, - ); - } }); test('setup preserves an existing root CLAUDE.md instead of replacing it', () => { @@ -821,7 +790,7 @@ test('setup refreshes initialized protected main through a sandbox and prunes it const initialGitignore = fs.readFileSync(gitignorePath, 'utf8'); fs.writeFileSync( gitignorePath, - initialGitignore.replace(/^scripts\/agent-session-state\.js\n/m, ''), + initialGitignore.replace(/^scripts\/guardex-docker-loader\.sh\n/m, ''), 'utf8', ); @@ -843,7 +812,7 @@ test('setup refreshes initialized protected main through a sandbox and prunes it assert.equal(sandboxBranchCheck.stdout.trim(), '', 'setup sandbox branch should be pruned'); const refreshedGitignore = fs.readFileSync(gitignorePath, 'utf8'); - assert.match(refreshedGitignore, /^scripts\/agent-session-state\.js$/m); + assert.match(refreshedGitignore, /^scripts\/guardex-docker-loader\.sh$/m); }); diff --git a/test/vscode-active-agents-session-state.test.js b/test/vscode-active-agents-session-state.test.js deleted file mode 100644 index f074c5d1..00000000 --- a/test/vscode-active-agents-session-state.test.js +++ /dev/null @@ -1,4079 +0,0 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const os = require('node:os'); -const path = require('node:path'); -const cp = require('node:child_process'); - -const repoRoot = path.resolve(__dirname, '..'); -const cliEntry = path.join(repoRoot, 'bin', 'multiagent-safety.js'); -const sessionScript = path.join(repoRoot, 'scripts', 'agent-session-state.js'); -const installScript = path.join(repoRoot, 'scripts', 'install-vscode-active-agents-extension.js'); -const extensionManifestPath = path.join( - repoRoot, - 'vscode', - 'guardex-active-agents', - 'package.json', -); -const templateExtensionManifestPath = path.join( - repoRoot, - 'templates', - 'vscode', - 'guardex-active-agents', - 'package.json', -); -const sessionSchema = require(path.join( - repoRoot, - 'vscode', - 'guardex-active-agents', - 'session-schema.js', -)); -const extensionEntry = path.join(repoRoot, 'vscode', 'guardex-active-agents', 'extension.js'); - -function runNode(scriptPath, args, options = {}) { - return cp.spawnSync('node', [scriptPath, ...args], { - encoding: 'utf8', - ...options, - }); -} - -function runGit(repoPath, args, options = {}) { - const result = cp.spawnSync('git', ['-C', repoPath, ...args], { - encoding: 'utf8', - ...options, - }); - assert.equal(result.status, 0, result.stderr || result.stdout); - return result; -} - -function initGitRepo(repoPath) { - fs.mkdirSync(repoPath, { recursive: true }); - runGit(repoPath, ['init']); - runGit(repoPath, ['config', 'user.email', 'guardex-tests@example.com']); - runGit(repoPath, ['config', 'user.name', 'Guardex Tests']); -} - -function readJson(filePath) { - return JSON.parse(fs.readFileSync(filePath, 'utf8')); -} - -function parseSimpleSemver(version) { - const parts = version.split('.').map((part) => Number.parseInt(part, 10)); - assert.equal(parts.length, 3, `Expected simple semver, received ${version}`); - for (const part of parts) { - assert.equal(Number.isNaN(part), false, `Expected numeric semver, received ${version}`); - } - return parts; -} - -function compareSimpleSemver(left, right) { - const leftParts = parseSimpleSemver(left); - const rightParts = parseSimpleSemver(right); - for (let index = 0; index < leftParts.length; index += 1) { - if (leftParts[index] !== rightParts[index]) { - return leftParts[index] - rightParts[index]; - } - } - return 0; -} - -function resolveRepoBaseRef() { - for (const candidate of ['origin/main', 'main']) { - const result = cp.spawnSync('git', ['-C', repoRoot, 'rev-parse', '--verify', candidate], { - encoding: 'utf8', - }); - if (result.status === 0) { - return candidate; - } - } - throw new Error('Could not resolve a base ref for the extension version guard.'); -} - -function readExtensionManifest(filePath = extensionManifestPath) { - return readJson(filePath); -} - -function readBaseExtensionManifest(baseRef) { - const result = cp.spawnSync( - 'git', - ['-C', repoRoot, 'show', `${baseRef}:vscode/guardex-active-agents/package.json`], - { - encoding: 'utf8', - }, - ); - assert.equal(result.status, 0, result.stderr || result.stdout); - return JSON.parse(result.stdout); -} - -function readChangedExtensionPaths(baseRef) { - const result = cp.spawnSync( - 'git', - [ - '-C', - repoRoot, - 'diff', - '--name-only', - `${baseRef}...HEAD`, - '--', - 'vscode/guardex-active-agents', - 'templates/vscode/guardex-active-agents', - 'scripts/install-vscode-active-agents-extension.js', - ], - { - encoding: 'utf8', - }, - ); - assert.equal(result.status, 0, result.stderr || result.stdout); - return result.stdout - .split('\n') - .map((entry) => entry.trim()) - .filter(Boolean); -} - -function setPathMtime(filePath, whenMs) { - const when = new Date(whenMs); - fs.utimesSync(filePath, when, when); -} - -function writeSessionRecord(repoRoot, record) { - const sessionPath = sessionSchema.sessionFilePathForBranch(repoRoot, record.branch); - fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); - fs.writeFileSync(sessionPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8'); - return sessionPath; -} - -function buildWorktreeLockPayload(worktreePath, overrides = {}) { - return { - schemaVersion: 1, - source: 'recodee-live-telemetry', - updatedAt: '2026-04-22T08:56:00.000Z', - worktreePath, - worktreeName: path.basename(worktreePath), - collaboration: false, - snapshotCount: 1, - sessionCount: 1, - snapshots: [ - { - snapshotName: 'snapshot-a', - accountId: 'acct-1', - email: 'agent@example.com', - liveSessionCount: 1, - trackedSessionCount: 1, - compatSessionCount: 1, - sessions: [ - { - sessionKey: 'pid:101', - taskPreview: 'Implement live worktree telemetry', - taskUpdatedAt: '2026-04-22T08:55:00.000Z', - projectName: 'gitguardex', - projectPath: worktreePath, - }, - ], - }, - ], - ...overrides, - }; -} - -function writeWorktreeLock(worktreePath, overrides = {}) { - const lockPath = path.join(worktreePath, 'AGENT.lock'); - fs.mkdirSync(path.dirname(lockPath), { recursive: true }); - fs.writeFileSync( - lockPath, - `${JSON.stringify(buildWorktreeLockPayload(worktreePath, overrides), null, 2)}\n`, - 'utf8', - ); - return lockPath; -} - -async function getOnlyChild(provider, item) { - const children = await provider.getChildren(item); - assert.equal(children.length, 1, `Expected exactly one child for ${item?.label || 'item'}`); - return children[0]; -} - -async function getOnlyWorktreeAndSession(provider, sectionItem) { - const firstItem = await getOnlyChild(provider, sectionItem); - if (firstItem?.session) { - return { worktreeItem: null, sessionItem: firstItem }; - } - const worktreeItem = firstItem; - const sessionItem = await getOnlyChild(provider, firstItem); - return { worktreeItem, sessionItem }; -} - -async function getSectionByLabel(provider, parentItem, label) { - const children = await provider.getChildren(parentItem); - const match = children.find((item) => item.label === label); - assert.ok(match, `Expected section ${label}`); - return match; -} - -async function getChildByLabel(provider, parentItem, label) { - const children = await provider.getChildren(parentItem); - const match = children.find((item) => item.label === label); - assert.ok(match, `Expected child ${label}`); - return match; -} - -function assertBundledIcon(item, iconFileName) { - assert.equal( - item?.iconPath?.light?.fsPath.endsWith(path.join('fileicons', 'icons', iconFileName)), - true, - `Expected ${item?.label || 'item'} to use ${iconFileName}`, - ); - assert.equal(item?.iconPath?.light?.fsPath, item?.iconPath?.dark?.fsPath); -} - -async function getSessionByBranch(provider, sectionItem, branch) { - const children = await provider.getChildren(sectionItem); - const match = children.find((item) => item.session?.branch === branch); - assert.ok(match, `Expected session ${branch}`); - return match; -} - -function loadExtensionWithMockVscode(mockVscode, mockSessionSchema = null) { - const Module = require('node:module'); - const originalLoad = Module._load; - delete require.cache[require.resolve(extensionEntry)]; - - Module._load = function patchedModuleLoad(request, parent, isMain) { - if (request === 'vscode') { - return mockVscode; - } - if (mockSessionSchema && request === './session-schema.js' && parent?.filename === extensionEntry) { - return mockSessionSchema; - } - return originalLoad.call(this, request, parent, isMain); - }; - - try { - return require(extensionEntry); - } finally { - Module._load = originalLoad; - } -} - -function createMockVscode(tempRoot) { - const registrations = { - providers: [], - decorationProviders: [], - treeViews: [], - statusBarItems: [], - commands: new Map(), - executedCommands: [], - sourceControls: [], - terminals: [], - nextTerminalPid: 7000, - openedDocuments: [], - shownDocuments: [], - infoMessages: [], - infoResponses: [], - inputResponses: [], - inputBoxCalls: [], - quickPickCalls: [], - quickPickResponse: undefined, - informationMessages: [], - errorMessages: [], - warningMessages: [], - webviewPanels: [], - fileWatchers: [], - watchers: [], - workspaceFolderListeners: [], - workspaceFolderUpdates: [], - configurationUpdates: [], - workspaceConfigurationValues: new Map(), - }; - - class TreeItem { - constructor(label, collapsibleState) { - this.label = label; - this.collapsibleState = collapsibleState; - } - } - - class ThemeIcon { - constructor(id, color) { - this.id = id; - this.color = color; - } - } - - class ThemeColor { - constructor(id) { - this.id = id; - } - } - - class EventEmitter { - constructor() { - this.fireCount = 0; - this.listeners = []; - this.event = (listener, thisArg, disposables) => { - const boundListener = thisArg ? listener.bind(thisArg) : listener; - this.listeners.push(boundListener); - const registration = { - dispose: () => { - this.listeners = this.listeners.filter((entry) => entry !== boundListener); - }, - }; - if (Array.isArray(disposables)) { - disposables.push(registration); - } - return registration; - }; - } - - fire(event) { - this.fireCount += 1; - for (const listener of [...this.listeners]) { - listener(event); - } - } - } - - const disposable = (onDispose) => ({ dispose: onDispose || (() => {}) }); - const ConfigurationTarget = { - Workspace: 'workspace', - WorkspaceFolder: 'workspaceFolder', - }; - const configurationKey = (section, scopePath, key) => `${section}::${scopePath}::${key}`; - const resolveWorkspaceScopePath = (scope) => scope?.uri?.fsPath || tempRoot; - const readConfigurationValue = (section, scope, key) => { - const scopePath = resolveWorkspaceScopePath(scope); - const scopedKey = configurationKey(section, scopePath, key); - if (registrations.workspaceConfigurationValues.has(scopedKey)) { - return registrations.workspaceConfigurationValues.get(scopedKey); - } - return registrations.workspaceConfigurationValues.get(configurationKey(section, tempRoot, key)); - }; - const writeConfigurationValue = (section, scopePath, key, value) => { - registrations.workspaceConfigurationValues.set(configurationKey(section, scopePath, key), value); - }; - registrations.getConfigurationValue = (section, scopePath, key) => ( - registrations.workspaceConfigurationValues.get(configurationKey(section, scopePath, key)) - ); - registrations.setConfigurationValue = (section, scopePath, key, value) => { - writeConfigurationValue(section, scopePath, key, value); - }; - - function createFileWatcher(pattern) { - const listeners = { - create: [], - change: [], - delete: [], - }; - - const watcher = { - disposed: false, - pattern, - onDidCreate(callback, thisArg) { - listeners.create.push({ callback, thisArg }); - return disposable(); - }, - onDidChange(callback, thisArg) { - listeners.change.push({ callback, thisArg }); - return disposable(); - }, - onDidDelete(callback, thisArg) { - listeners.delete.push({ callback, thisArg }); - return disposable(); - }, - fireCreate(uri) { - for (const listener of listeners.create) { - listener.callback.call(listener.thisArg, uri); - } - }, - fireChange(uri) { - for (const listener of listeners.change) { - listener.callback.call(listener.thisArg, uri); - } - }, - fireDelete(uri) { - for (const listener of listeners.delete) { - listener.callback.call(listener.thisArg, uri); - } - }, - dispose() { - watcher.disposed = true; - }, - }; - registrations.watchers.push(watcher); - registrations.fileWatchers.push(watcher); - return watcher; - } - - return { - registrations, - vscode: { - TreeItem, - ThemeIcon, - EventEmitter, - TreeItemCollapsibleState: { - None: 0, - Expanded: 1, - Collapsed: 2, - }, - StatusBarAlignment: { - Left: 1, - Right: 2, - }, - ViewColumn: { - Beside: 2, - }, - commands: { - executeCommand: async (command, ...args) => { - registrations.executedCommands.push({ command, args }); - if (command === 'setContext') { - return undefined; - } - const handler = registrations.commands.get(command); - if (handler) { - return handler(...args); - } - return undefined; - }, - registerCommand: (command, handler) => { - registrations.commands.set(command, handler); - return disposable(() => registrations.commands.delete(command)); - }, - }, - scm: { - createSourceControl: (id, label) => { - const sourceControl = { - id, - label, - inputBox: { - value: '', - placeholder: '', - enabled: true, - visible: true, - }, - acceptInputCommand: undefined, - dispose() {}, - }; - registrations.sourceControls.push(sourceControl); - return sourceControl; - }, - }, - Uri: { - file: (fsPath) => ({ - scheme: 'file', - fsPath, - path: fsPath, - toString() { - return `file://${fsPath}`; - }, - }), - parse: (value) => { - const parsed = new URL(value); - return { - scheme: parsed.protocol.replace(/:$/, ''), - authority: parsed.host, - path: parsed.pathname, - toString() { - return value; - }, - }; - }, - }, - window: { - terminals: registrations.terminals, - showInformationMessage: async (...args) => { - registrations.infoMessages.push(args); - if (typeof args[0] === 'string') { - registrations.informationMessages.push(args[0]); - } - return registrations.infoResponses.shift(); - }, - showErrorMessage: async (message) => { - registrations.errorMessages.push(message); - return undefined; - }, - showWarningMessage: async (...args) => { - registrations.warningMessages.push(args); - return undefined; - }, - showInputBox: async (options) => { - registrations.inputBoxCalls.push(options); - return registrations.inputResponses.shift(); - }, - showQuickPick: async (items, options) => { - registrations.quickPickCalls.push({ items, options }); - return registrations.quickPickResponse; - }, - createTerminal: (options) => { - const terminal = { - options, - name: options?.name, - processId: Promise.resolve(options?.processId ?? registrations.nextTerminalPid++), - shown: false, - showArgs: [], - sentTexts: [], - show(preserveFocus) { - this.shown = true; - this.showArgs.push(preserveFocus); - }, - sendText(text, addNewLine) { - this.sentTexts.push({ text, addNewLine }); - }, - dispose() {}, - }; - registrations.terminals.push(terminal); - return terminal; - }, - showTextDocument: async (document, options) => { - registrations.shownDocuments.push({ document, options }); - return { document }; - }, - createWebviewPanel: (viewType, title, column, options) => { - const disposeListeners = []; - const panel = { - viewType, - title, - column, - options, - disposed: false, - revealCalls: [], - webview: { - html: '', - }, - onDidDispose(listener) { - disposeListeners.push(listener); - return disposable(() => { - const index = disposeListeners.indexOf(listener); - if (index >= 0) { - disposeListeners.splice(index, 1); - } - }); - }, - reveal(nextColumn) { - panel.revealCalls.push(nextColumn); - }, - dispose() { - if (panel.disposed) { - return; - } - panel.disposed = true; - for (const listener of [...disposeListeners]) { - listener(); - } - }, - }; - registrations.webviewPanels.push(panel); - return panel; - }, - createTreeView: (viewId, options) => { - const selectionListeners = []; - const treeView = { - viewId, - options, - badge: undefined, - message: undefined, - onDidChangeSelection(listener) { - selectionListeners.push(listener); - return disposable(); - }, - fireSelection(selection) { - for (const listener of selectionListeners) { - listener({ selection }); - } - }, - dispose() {}, - }; - registrations.treeViews.push(treeView); - registrations.providers.push({ viewId, provider: options.treeDataProvider }); - return treeView; - }, - createStatusBarItem: (alignment, priority) => { - const statusBarItem = { - alignment, - priority, - text: '', - tooltip: '', - command: undefined, - name: undefined, - visible: false, - show() { - this.visible = true; - }, - hide() { - this.visible = false; - }, - dispose() {}, - }; - registrations.statusBarItems.push(statusBarItem); - return statusBarItem; - }, - registerFileDecorationProvider: (provider) => { - registrations.decorationProviders.push(provider); - return disposable(); - }, - registerTreeDataProvider: (viewId, provider) => { - registrations.providers.push({ viewId, provider }); - return disposable(); - }, - }, - workspace: { - openTextDocument: async (options) => { - const document = { - ...options, - uri: { scheme: 'untitled' }, - }; - registrations.openedDocuments.push(document); - return document; - }, - createFileSystemWatcher: (pattern) => createFileWatcher(pattern), - findFiles: async () => [], - getConfiguration: (section, scope) => ({ - get: (key) => readConfigurationValue(section, scope, key), - update: async (key, value, target) => { - const scopePath = target === ConfigurationTarget.WorkspaceFolder - ? resolveWorkspaceScopePath(scope) - : tempRoot; - registrations.configurationUpdates.push({ section, key, scopePath, target, value }); - writeConfigurationValue(section, scopePath, key, value); - }, - }), - onDidChangeWorkspaceFolders: (listener) => { - registrations.workspaceFolderListeners.push(listener); - return disposable(() => { - const index = registrations.workspaceFolderListeners.indexOf(listener); - if (index >= 0) { - registrations.workspaceFolderListeners.splice(index, 1); - } - }); - }, - updateWorkspaceFolders(start, deleteCount, ...folders) { - registrations.workspaceFolderUpdates.push({ start, deleteCount, folders }); - this.workspaceFolders.splice(start, deleteCount, ...folders); - return true; - }, - workspaceFolders: [{ uri: { fsPath: tempRoot } }], - }, - ConfigurationTarget, - ThemeColor, - }, - }; -} - -async function flushAsyncWork() { - await Promise.resolve(); - await new Promise((resolve) => setImmediate(resolve)); -} - -test('agent-session-state writes and removes active session records', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-')); - const branch = 'agent/codex/demo-task'; - const worktreePath = path.join(tempRoot, '.omx', 'agent-worktrees', 'agent__codex__demo-task'); - fs.mkdirSync(worktreePath, { recursive: true }); - - const start = runNode(sessionScript, [ - 'start', - '--repo', - tempRoot, - '--branch', - branch, - '--task', - 'demo-task', - '--agent', - 'codex', - '--worktree', - worktreePath, - '--pid', - String(process.pid), - '--cli', - 'codex', - '--task-mode', - 'caveman', - '--openspec-tier', - 'T1', - '--routing-reason', - 'explicit lightweight prefix', - ]); - assert.equal(start.status, 0, start.stderr); - - const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, branch); - assert.equal(path.basename(sessionPath), 'agent__codex__demo-task.json'); - assert.equal(fs.existsSync(sessionPath), true); - - const parsed = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); - assert.equal(parsed.branch, branch); - assert.equal(parsed.taskName, 'demo-task'); - assert.equal(parsed.agentName, 'codex'); - assert.equal(parsed.worktreePath, worktreePath); - assert.equal(parsed.taskMode, 'caveman'); - assert.equal(parsed.openspecTier, 'T1'); - assert.equal(parsed.taskRoutingReason, 'explicit lightweight prefix'); - assert.equal(parsed.state, 'working'); - assert.equal(typeof parsed.lastHeartbeatAt, 'string'); - assert.ok(Date.parse(parsed.lastHeartbeatAt) >= Date.parse(parsed.startedAt)); - - const sessions = sessionSchema.readActiveSessions(tempRoot); - assert.equal(sessions.length, 1); - assert.equal(sessions[0].label, 'agent__codex__demo-task'); - assert.equal(sessions[0].taskMode, 'caveman'); - assert.equal(sessions[0].openspecTier, 'T1'); - - const heartbeat = runNode(sessionScript, [ - 'heartbeat', - '--repo', - tempRoot, - '--branch', - branch, - '--state', - 'thinking', - ]); - assert.equal(heartbeat.status, 0, heartbeat.stderr); - const heartbeatParsed = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); - assert.equal(heartbeatParsed.branch, branch); - assert.equal(heartbeatParsed.state, 'thinking'); - assert.ok(Date.parse(heartbeatParsed.lastHeartbeatAt) >= Date.parse(parsed.lastHeartbeatAt)); - - const stop = runNode(sessionScript, [ - 'stop', - '--repo', - tempRoot, - '--branch', - branch, - ]); - assert.equal(stop.status, 0, stop.stderr); - assert.equal(fs.existsSync(sessionPath), false); -}); - -test('gx internal heartbeat refreshes active session records through the CLI', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-cli-heartbeat-')); - initGitRepo(tempRoot); - const branch = 'agent/codex/cli-heartbeat-task'; - const worktreePath = path.join(tempRoot, '.omx', 'agent-worktrees', 'agent__codex__cli-heartbeat-task'); - fs.mkdirSync(worktreePath, { recursive: true }); - const sessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch, - taskName: 'cli-heartbeat-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - state: 'working', - })); - const before = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); - - const heartbeat = runNode(cliEntry, [ - 'internal', - 'heartbeat', - '--target', - tempRoot, - '--branch', - branch, - '--state', - 'idle', - ], { cwd: repoRoot }); - assert.equal(heartbeat.status, 0, heartbeat.stderr); - - const after = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); - assert.equal(after.branch, branch); - assert.equal(after.taskName, before.taskName); - assert.equal(after.state, 'idle'); - assert.ok(Date.parse(after.lastHeartbeatAt) >= Date.parse(before.lastHeartbeatAt)); -}); - -test('session-schema ignores stale or invalid session records', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-stale-')); - const activeSessionsDir = sessionSchema.activeSessionsDirForRepo(tempRoot); - fs.mkdirSync(activeSessionsDir, { recursive: true }); - - const liveRecord = sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/live-task', - taskName: 'live-task', - agentName: 'codex', - worktreePath: path.join(tempRoot, '.omx', 'agent-worktrees', 'live-task'), - pid: process.pid, - cliName: 'codex', - }); - fs.writeFileSync( - sessionSchema.sessionFilePathForBranch(tempRoot, liveRecord.branch), - `${JSON.stringify(liveRecord, null, 2)}\n`, - 'utf8', - ); - - const staleRecord = sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/stale-task', - taskName: 'stale-task', - agentName: 'codex', - worktreePath: path.join(tempRoot, '.omx', 'agent-worktrees', 'stale-task'), - pid: 999999, - cliName: 'codex', - }); - fs.writeFileSync( - sessionSchema.sessionFilePathForBranch(tempRoot, staleRecord.branch), - `${JSON.stringify(staleRecord, null, 2)}\n`, - 'utf8', - ); - fs.writeFileSync(path.join(activeSessionsDir, 'broken.json'), '{broken json', 'utf8'); - - const sessions = sessionSchema.readActiveSessions(tempRoot); - assert.equal(sessions.length, 1); - assert.equal(sessions[0].branch, liveRecord.branch); - - const sessionsIncludingStale = sessionSchema.readActiveSessions(tempRoot, { includeStale: true }); - assert.equal(sessionsIncludingStale.length, 2); - assert.equal( - sessionsIncludingStale.find((session) => session.branch === staleRecord.branch)?.activityKind, - 'dead', - ); -}); - -test('session-schema falls back to managed worktree AGENT.lock telemetry when launcher state is absent', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-lock-fallback-')); - const worktreePath = path.join( - tempRoot, - '.omx', - 'agent-worktrees', - 'agent__codex__live-lock-task', - ); - initGitRepo(worktreePath); - runGit(worktreePath, ['checkout', '-b', 'agent/codex/live-lock-task']); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\nchanged\n', 'utf8'); - writeWorktreeLock(worktreePath); - - const [session] = sessionSchema.readActiveSessions(tempRoot); - assert.equal(session.sourceKind, 'worktree-lock'); - assert.equal(session.branch, 'agent/codex/live-lock-task'); - assert.equal(session.agentName, 'codex'); - assert.equal(session.taskName, 'Implement live worktree telemetry'); - assert.equal(session.activityKind, 'working'); - assert.equal(session.activityCountLabel, '1 file'); - assert.equal(session.telemetrySource, 'recodee-live-telemetry'); - assert.equal(session.telemetryUpdatedAt, '2026-04-22T08:56:00.000Z'); -}); - -test('session-schema falls back to plain managed worktrees when launcher state and AGENT.lock are absent', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-managed-fallback-')); - const worktreePath = path.join( - tempRoot, - '.omx', - 'agent-worktrees', - 'agent__codex__plain-visible-task', - ); - initGitRepo(worktreePath); - runGit(worktreePath, ['checkout', '-b', 'agent/codex/plain-visible-task']); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\nchanged\n', 'utf8'); - - const [session] = sessionSchema.readActiveSessions(tempRoot); - assert.equal(session.sourceKind, 'managed-worktree'); - assert.equal(session.branch, 'agent/codex/plain-visible-task'); - assert.equal(session.agentName, 'codex'); - assert.equal(session.taskName, 'agent__codex__plain-visible-task'); - assert.equal(session.activityKind, 'working'); - assert.equal(session.activityCountLabel, '1 file'); - assert.equal(session.telemetrySource, 'managed-worktree'); -}); - -test('session-schema prefers live worktree telemetry over a dead launcher record for the same worktree', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-lock-prefer-')); - const worktreePath = path.join( - tempRoot, - '.omx', - 'agent-worktrees', - 'agent__codex__replace-dead-session', - ); - initGitRepo(worktreePath); - runGit(worktreePath, ['checkout', '-b', 'agent/codex/replace-dead-session']); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - writeWorktreeLock(worktreePath, { - updatedAt: '2026-04-22T08:57:00.000Z', - }); - writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/replace-dead-session', - taskName: 'replace-dead-session', - agentName: 'codex', - worktreePath, - pid: 999999, - cliName: 'codex', - })); - - const [session] = sessionSchema.readActiveSessions(tempRoot, { includeStale: true }); - assert.equal(session.sourceKind, 'worktree-lock'); - assert.equal(session.activityKind, 'idle'); -}); - -test('session-schema derives working activity from dirty sandbox worktrees', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-working-')); - const worktreePath = path.join(tempRoot, 'sandbox'); - initGitRepo(worktreePath); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\nchanged\n', 'utf8'); - fs.writeFileSync(path.join(worktreePath, 'new-file.txt'), 'new\n', 'utf8'); - - const record = sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/working-task', - taskName: 'working-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - }); - const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, record.branch); - fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); - fs.writeFileSync(sessionPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8'); - - const [session] = sessionSchema.readActiveSessions(tempRoot); - assert.equal(session.activityKind, 'working'); - assert.equal(session.changeCount, 2); - assert.equal(session.activityCountLabel, '2 files'); - assert.deepEqual(session.changedPaths, ['sandbox/new-file.txt', 'sandbox/tracked.txt']); - assert.deepEqual(session.worktreeChangedPaths, ['new-file.txt', 'tracked.txt']); - assert.equal(session.activitySummary, 'new-file.txt, tracked.txt'); -}); - -test('session-schema derives blocked activity from git markers in the worktree git dir', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-blocked-')); - const worktreePath = path.join(tempRoot, 'sandbox'); - initGitRepo(worktreePath); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(worktreePath, '.git', 'MERGE_HEAD'), 'deadbeef\n', 'utf8'); - - const session = sessionSchema.deriveSessionActivity(sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/blocked-task', - taskName: 'blocked-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - })); - - assert.equal(session.activityKind, 'blocked'); - assert.equal(session.activitySummary, 'Merge in progress.'); -}); - -test('session-schema derives idle and stalled activity from clean worktree mtimes', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-idle-')); - const worktreePath = path.join(tempRoot, 'sandbox'); - const trackedPath = path.join(worktreePath, 'tracked.txt'); - initGitRepo(worktreePath); - fs.writeFileSync(trackedPath, 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - - const record = sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/idle-task', - taskName: 'idle-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - }); - const now = Date.parse('2026-04-22T10:00:00.000Z'); - - setPathMtime(trackedPath, now - 45_000); - const idleSession = sessionSchema.deriveSessionActivity(record, { now, useCache: false }); - assert.equal(idleSession.activityKind, 'idle'); - assert.match(idleSession.activitySummary, /Recent file activity 45s ago\./); - assert.equal(idleSession.lastFileActivityAt, new Date(now - 45_000).toISOString()); - - setPathMtime(trackedPath, now - (20 * 60 * 1000)); - const stalledSession = sessionSchema.deriveSessionActivity(record, { now, useCache: false }); - assert.equal(stalledSession.activityKind, 'stalled'); - assert.match(stalledSession.activitySummary, /No file activity for 20m 0s\./); - assert.equal(stalledSession.lastFileActivityAt, new Date(now - (20 * 60 * 1000)).toISOString()); -}); - -test('session-schema caps clean-worktree stat scans and caches activity lookups briefly', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-cache-')); - const worktreePath = path.join(tempRoot, 'sandbox'); - initGitRepo(worktreePath); - - for (let index = 0; index < 205; index += 1) { - const filePath = path.join(worktreePath, 'src', `tracked-${String(index).padStart(3, '0')}.txt`); - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, `file ${index}\n`, 'utf8'); - } - runGit(worktreePath, ['add', '.']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - - const now = Date.parse('2026-04-22T10:00:00.000Z'); - for (let index = 0; index < 205; index += 1) { - setPathMtime( - path.join(worktreePath, 'src', `tracked-${String(index).padStart(3, '0')}.txt`), - now - 90_000, - ); - } - const trackedPath = path.join(worktreePath, 'src', 'tracked-000.txt'); - setPathMtime(trackedPath, now - 30_000); - - const record = sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/cached-activity', - taskName: 'cached-activity', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - }); - - let statCount = 0; - const originalStatSync = fs.statSync; - fs.statSync = (...args) => { - const filePath = String(args[0] || ''); - if (filePath.startsWith(worktreePath) && filePath.endsWith('.txt')) { - statCount += 1; - } - return originalStatSync(...args); - }; - - try { - const firstSession = sessionSchema.deriveSessionActivity(record, { now }); - const firstStatCount = statCount; - const secondSession = sessionSchema.deriveSessionActivity(record, { now: now + 1_000 }); - - assert.equal(firstSession.activityKind, 'idle'); - assert.equal(firstSession.lastFileActivityAt, new Date(now - 30_000).toISOString()); - assert.ok(firstStatCount <= 200, `expected <=200 file stats, saw ${firstStatCount}`); - assert.equal(secondSession.lastFileActivityAt, firstSession.lastFileActivityAt); - assert.equal(statCount, firstStatCount); - } finally { - fs.statSync = originalStatSync; - sessionSchema.clearWorktreeActivityCache(worktreePath); - } -}); - -test('session-schema derives dead activity when the recorded pid is not alive', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-dead-')); - const worktreePath = path.join(tempRoot, 'sandbox'); - initGitRepo(worktreePath); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - - const session = sessionSchema.deriveSessionActivity(sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/dead-task', - taskName: 'dead-task', - agentName: 'codex', - worktreePath, - pid: 999999, - cliName: 'codex', - })); - - assert.equal(session.activityKind, 'dead'); - assert.equal(session.activitySummary, 'Recorded PID is not alive.'); - assert.equal(session.pidAlive, false); -}); - -test('session-schema derives dead activity when launcher heartbeat is stale', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-stale-heartbeat-')); - const worktreePath = path.join(tempRoot, 'sandbox'); - const lastHeartbeatAt = '2026-04-22T10:00:00.000Z'; - const session = sessionSchema.deriveSessionActivity(sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/stale-heartbeat-task', - taskName: 'stale-heartbeat-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - startedAt: lastHeartbeatAt, - lastHeartbeatAt, - }), { now: Date.parse('2026-04-22T10:06:00.000Z') }); - - assert.equal(session.activityKind, 'dead'); - assert.equal(session.activitySummary, 'Heartbeat stale for 6m 0s.'); -}); - -test('session-schema derives repo change rows from root git status', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-root-status-')); - initGitRepo(tempRoot); - fs.writeFileSync(path.join(tempRoot, 'tracked.txt'), 'base\n', 'utf8'); - runGit(tempRoot, ['add', 'tracked.txt']); - runGit(tempRoot, ['commit', '-m', 'baseline']); - - fs.writeFileSync(path.join(tempRoot, 'tracked.txt'), 'base\nchanged\n', 'utf8'); - fs.writeFileSync(path.join(tempRoot, 'new-file.txt'), 'new\n', 'utf8'); - fs.mkdirSync(path.join(tempRoot, '.omx', 'agent-worktrees', 'agent__codex__sandbox'), { recursive: true }); - fs.writeFileSync( - path.join(tempRoot, '.omx', 'agent-worktrees', 'agent__codex__sandbox', 'sandbox.txt'), - 'sandbox\n', - 'utf8', - ); - fs.mkdirSync(path.join(tempRoot, '.omc', 'agent-worktrees', 'agent__claude__sandbox'), { recursive: true }); - fs.writeFileSync( - path.join(tempRoot, '.omc', 'agent-worktrees', 'agent__claude__sandbox', 'sandbox.txt'), - 'sandbox\n', - 'utf8', - ); - fs.mkdirSync(path.join(tempRoot, '.omx', 'state', 'active-sessions'), { recursive: true }); - fs.writeFileSync( - path.join(tempRoot, '.omx', 'state', 'active-sessions', 'agent__codex__sandbox.json'), - '{}\n', - 'utf8', - ); - fs.writeFileSync( - path.join(tempRoot, '.omx', 'state', 'agent-file-locks.json'), - '{"locks":{}}\n', - 'utf8', - ); - - const changes = sessionSchema.readRepoChanges(tempRoot); - assert.deepEqual( - changes.map((change) => [change.relativePath, change.statusLabel]), - [ - ['new-file.txt', 'U'], - ['tracked.txt', 'M'], - ], - ); -}); - -test('session-schema reads inspect data from base-branch config, log tail, and held locks', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-inspect-')); - const remoteRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-inspect-remote-')); - const branch = 'agent/codex/inspect-task'; - - initGitRepo(tempRoot); - runGit(tempRoot, ['checkout', '-b', 'main']); - fs.writeFileSync(path.join(tempRoot, 'tracked.txt'), 'base\n', 'utf8'); - runGit(tempRoot, ['add', 'tracked.txt']); - runGit(tempRoot, ['commit', '-m', 'baseline']); - runGit(remoteRoot, ['init', '--bare']); - runGit(tempRoot, ['remote', 'add', 'origin', remoteRoot]); - runGit(tempRoot, ['push', '-u', 'origin', 'main']); - runGit(tempRoot, ['config', 'multiagent.baseBranch', 'main']); - runGit(tempRoot, ['checkout', '-b', branch]); - fs.writeFileSync(path.join(tempRoot, 'tracked.txt'), 'base\ninspect\n', 'utf8'); - runGit(tempRoot, ['add', 'tracked.txt']); - runGit(tempRoot, ['commit', '-m', 'inspect ahead commit']); - - const logPath = path.join( - tempRoot, - '.omx', - 'logs', - `agent-${sessionSchema.sanitizeBranchForFile(branch)}.log`, - ); - fs.mkdirSync(path.dirname(logPath), { recursive: true }); - fs.writeFileSync(logPath, 'log line 1\nlog line 2\n', 'utf8'); - - const lockPath = path.join(tempRoot, '.omx', 'state', 'agent-file-locks.json'); - fs.mkdirSync(path.dirname(lockPath), { recursive: true }); - fs.writeFileSync(lockPath, `${JSON.stringify({ - locks: { - 'src/alpha.js': { - branch, - claimed_at: '2026-04-22T09:10:00.000Z', - allow_delete: false, - }, - 'src/beta.js': { - branch, - claimed_at: '2026-04-22T09:11:00.000Z', - allow_delete: true, - }, - 'src/foreign.js': { - branch: 'agent/codex/other-task', - claimed_at: '2026-04-22T09:12:00.000Z', - allow_delete: false, - }, - }, - }, null, 2)}\n`, 'utf8'); - - const inspectData = sessionSchema.readSessionInspectData({ - repoRoot: tempRoot, - branch, - worktreePath: tempRoot, - }); - - assert.equal(inspectData.baseBranch, 'main'); - assert.equal(inspectData.compareRef, 'origin/main'); - assert.equal(inspectData.aheadCount, 1); - assert.equal(inspectData.behindCount, 0); - assert.equal(inspectData.logPath, logPath); - assert.equal(inspectData.logExists, true); - assert.match(inspectData.logTailText, /log line 2/); - assert.deepEqual( - inspectData.heldLocks.map((entry) => entry.relativePath), - ['src/alpha.js', 'src/beta.js'], - ); -}); - -test('install-vscode-active-agents-extension installs the current extension into a canonical dir and refreshes recent patch compatibility copies', () => { - const tempExtensionsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-ext-')); - const manifest = readExtensionManifest(); - const extensionId = `${manifest.publisher}.${manifest.name}`; - const [major, minor, patch] = parseSimpleSemver(manifest.version); - const canonicalDir = path.join(tempExtensionsDir, extensionId); - const currentVersionDir = path.join(tempExtensionsDir, `${extensionId}-${manifest.version}`); - const recentCompatDir = patch > 0 - ? path.join(tempExtensionsDir, `${extensionId}-${major}.${minor}.${patch - 1}`) - : currentVersionDir; - const farLegacyDir = path.join(tempExtensionsDir, `${extensionId}-99.99.99`); - const retiredLegacyDir = path.join(tempExtensionsDir, 'recodeee.gitguardex-active-agents'); - const retiredLegacyVersionDir = path.join(tempExtensionsDir, 'recodeee.gitguardex-active-agents-0.0.18'); - - fs.mkdirSync(recentCompatDir, { recursive: true }); - fs.writeFileSync(path.join(recentCompatDir, 'stale.txt'), 'old', 'utf8'); - fs.mkdirSync(farLegacyDir, { recursive: true }); - fs.writeFileSync(path.join(farLegacyDir, 'stale.txt'), 'old', 'utf8'); - fs.mkdirSync(retiredLegacyDir, { recursive: true }); - fs.writeFileSync(path.join(retiredLegacyDir, 'extension.js'), 'vscode.scm.createSourceControl();\n', 'utf8'); - fs.mkdirSync(retiredLegacyVersionDir, { recursive: true }); - fs.writeFileSync(path.join(retiredLegacyVersionDir, 'extension.js'), 'vscode.scm.createSourceControl();\n', 'utf8'); - - const result = runNode(installScript, ['--extensions-dir', tempExtensionsDir], { - cwd: repoRoot, - }); - assert.equal(result.status, 0, result.stderr); - - const installedManifest = readJson(path.join(canonicalDir, 'package.json')); - assert.equal(fs.existsSync(canonicalDir), true); - assert.equal(fs.existsSync(path.join(canonicalDir, 'extension.js')), true); - assert.equal(fs.existsSync(path.join(canonicalDir, 'session-schema.js')), true); - assert.equal(installedManifest.icon, 'icon.png'); - assert.equal(installedManifest.version, manifest.version); - assert.deepEqual(installedManifest.activationEvents, manifest.activationEvents); - assert.equal(installedManifest.contributes.iconThemes, undefined); - assert.equal(installedManifest.activationEvents.includes('onStartupFinished'), true); - assert.equal(fs.existsSync(path.join(canonicalDir, 'icon.png')), true); - assert.equal(fs.existsSync(path.join(canonicalDir, 'fileicons', 'gitguardex-fileicons.json')), true); - assert.equal(fs.existsSync(path.join(canonicalDir, 'fileicons', 'icons', 'openspec.svg')), true); - assert.equal(fs.existsSync(currentVersionDir), true); - assert.equal(fs.existsSync(path.join(recentCompatDir, 'package.json')), true); - assert.equal(fs.existsSync(path.join(recentCompatDir, 'stale.txt')), false); - assert.equal(fs.existsSync(farLegacyDir), false); - assert.equal(fs.existsSync(retiredLegacyDir), false); - assert.equal(fs.existsSync(retiredLegacyVersionDir), false); - assert.match(result.stdout, new RegExp(`Installed ${extensionId}@${manifest.version} to ${canonicalDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)); - assert.match(result.stdout, /Refreshed \d+ recent patch compatibility path\(s\)/); - assert.match(result.stdout, /Removed 2 retired extension install path\(s\)/); - assert.match(result.stdout, /Reload each already-open VS Code window/); -}); - -test('active-agents extension edits require a higher manifest version than the base branch', () => { - const baseRef = resolveRepoBaseRef(); - const changedPaths = readChangedExtensionPaths(baseRef); - - if (changedPaths.length === 0) { - return; - } - - const liveManifest = readExtensionManifest(); - const templateManifest = readExtensionManifest(templateExtensionManifestPath); - const baseManifest = readBaseExtensionManifest(baseRef); - - assert.equal( - liveManifest.version, - templateManifest.version, - 'Live and template Active Agents manifests must stay in sync.', - ); - assert.deepEqual( - liveManifest.activationEvents, - templateManifest.activationEvents, - 'Live and template Active Agents activation events must stay in sync.', - ); - assert.equal( - liveManifest.contributes.iconThemes, - templateManifest.contributes.iconThemes, - 'Live and template Active Agents icon theme contributions must stay in sync.', - ); - assert.deepEqual( - liveManifest.contributes.viewsContainers, - templateManifest.contributes.viewsContainers, - 'Live and template Active Agents view containers must stay in sync.', - ); - assert.equal( - liveManifest.activationEvents.includes('onStartupFinished'), - true, - 'Active Agents manifests must activate on VS Code startup.', - ); - assert.ok( - compareSimpleSemver(liveManifest.version, baseManifest.version) > 0, - [ - `Active Agents extension files changed (${changedPaths.join(', ')})`, - `but version ${liveManifest.version} did not increase above ${baseManifest.version}.`, - ].join(' '), - ); -}); - -test('active-agents manifest uses a dedicated activity bar container with a hive icon', () => { - const manifest = readExtensionManifest(); - const activitybarContainers = manifest.contributes.viewsContainers?.activitybar || []; - const activeAgentsContainer = activitybarContainers.find( - (entry) => entry.id === 'gitguardex-active-agents-container', - ); - assert.ok(activeAgentsContainer, 'Expected the Active Agents activity bar container.'); - assert.match(activeAgentsContainer.id, /^[A-Za-z0-9_-]+$/); - assert.equal(activeAgentsContainer.title, 'Active Agents'); - assert.equal(activeAgentsContainer.icon, 'media/active-agents-hivemind.svg'); - - const activeAgentsViews = manifest.contributes.views?.['gitguardex-active-agents-container'] || []; - assert.deepEqual(activeAgentsViews, [ - { - id: 'gitguardex.activeAgents', - name: 'Active Agents', - contextualTitle: 'Active Agents', - icon: 'media/active-agents-hivemind.svg', - visibility: 'visible', - }, - ]); -}); - -test('active-agents manifest does not contribute a file icon theme', () => { - const manifest = readExtensionManifest(); - assert.equal(manifest.contributes.iconThemes, undefined); -}); - -test('active-agents manifest contributes restart actions for extension management and view title', () => { - const manifest = readExtensionManifest(); - const templateManifest = readExtensionManifest(templateExtensionManifestPath); - - const restartCommand = manifest.contributes.commands.find( - (entry) => entry.command === 'gitguardex.activeAgents.restart', - ); - assert.deepEqual(restartCommand, { - command: 'gitguardex.activeAgents.restart', - title: 'Restart Active Agents', - icon: '$(debug-restart)', - }); - - const restartViewTitleAction = manifest.contributes.menus['view/title'].find( - (entry) => entry.command === 'gitguardex.activeAgents.restart', - ); - assert.deepEqual(restartViewTitleAction, { - command: 'gitguardex.activeAgents.restart', - when: 'view == gitguardex.activeAgents', - group: 'navigation@8', - }); - - const restartExtensionAction = manifest.contributes.menus['extension/context'].find( - (entry) => entry.command === 'gitguardex.activeAgents.restart', - ); - assert.deepEqual(restartExtensionAction, { - command: 'gitguardex.activeAgents.restart', - when: 'extension == Recodee.gitguardex-active-agents && extensionStatus == installed', - group: '2_configure@2', - }); - - assert.deepEqual( - manifest.contributes.menus['extension/context'], - templateManifest.contributes.menus['extension/context'], - ); -}); - -test('active-agents manifest contributes dismiss only for stalled and dead session rows', () => { - const manifest = readExtensionManifest(); - const templateManifest = readExtensionManifest(templateExtensionManifestPath); - - const dismissCommand = manifest.contributes.commands.find( - (entry) => entry.command === 'gitguardex.activeAgents.dismissSession', - ); - assert.deepEqual(dismissCommand, { - command: 'gitguardex.activeAgents.dismissSession', - title: 'Dismiss', - icon: '$(trash)', - }); - - const dismissMenuAction = manifest.contributes.menus['view/item/context'].find( - (entry) => entry.command === 'gitguardex.activeAgents.dismissSession', - ); - assert.deepEqual(dismissMenuAction, { - command: 'gitguardex.activeAgents.dismissSession', - when: 'view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session\\.(stalled|dead)$/', - group: 'inline', - }); - - assert.deepEqual( - manifest.contributes.menus['view/item/context'], - templateManifest.contributes.menus['view/item/context'], - ); -}); - -test('active-agents extension auto-installs a newer workspace build and offers reload', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-autoupdate-')); - const repoManifest = { - ...readExtensionManifest(), - version: '9.9.9', - }; - const repoManifestPath = path.join(tempRoot, 'vscode', 'guardex-active-agents', 'package.json'); - const repoInstallScriptPath = path.join(tempRoot, 'scripts', 'install-vscode-active-agents-extension.js'); - fs.mkdirSync(path.dirname(repoManifestPath), { recursive: true }); - fs.writeFileSync(repoManifestPath, `${JSON.stringify(repoManifest, null, 2)}\n`, 'utf8'); - fs.mkdirSync(path.dirname(repoInstallScriptPath), { recursive: true }); - fs.writeFileSync(repoInstallScriptPath, '#!/usr/bin/env node\n', 'utf8'); - - const execCalls = []; - const originalExecFile = cp.execFile; - let context; - cp.execFile = (file, args, options, callback) => { - execCalls.push({ file, args, options }); - callback(null, '[guardex-active-agents] ok\n', ''); - }; - - try { - const { registrations, vscode } = createMockVscode(tempRoot); - registrations.infoResponses.push('Reload Window'); - const extension = loadExtensionWithMockVscode(vscode); - context = { - subscriptions: [], - extension: { - packageJSON: { - version: '0.0.2', - }, - }, - }; - - extension.activate(context); - await flushAsyncWork(); - - assert.equal(execCalls.length, 1); - assert.equal(execCalls[0].file, process.execPath); - assert.deepEqual(execCalls[0].args, [repoInstallScriptPath]); - assert.equal(execCalls[0].options.cwd, tempRoot); - assert.equal(execCalls[0].options.encoding, 'utf8'); - assert.match( - registrations.informationMessages.at(-1), - /GitGuardex Active Agents updated to 9\.9\.9.*reload any other already-open VS Code windows/i, - ); - assert.deepEqual(registrations.infoMessages.at(-1).slice(1), ['Reload Window', 'Later']); - assert.equal( - registrations.executedCommands.some( - (entry) => entry.command === 'workbench.action.reloadWindow', - ), - true, - ); - } finally { - cp.execFile = originalExecFile; - for (const subscription of context?.subscriptions ?? []) { - subscription.dispose?.(); - } - } -}); - -test('active-agents extension registers tree and decoration providers', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-view-')); - const { registrations, vscode } = createMockVscode(tempRoot); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - assert.equal(registrations.treeViews.length, 1); - assert.equal(registrations.sourceControls.length, 0); - assert.equal(registrations.statusBarItems.length, 1); - assert.equal(registrations.treeViews[0].viewId, 'gitguardex.activeAgents'); - assert.equal(registrations.statusBarItems[0].name, 'GitGuardex Active Agents'); - assert.equal(registrations.statusBarItems[0].command, 'gitguardex.activeAgents.focus'); - assert.equal(registrations.statusBarItems[0].visible, false); - assert.equal(registrations.providers.length, 1); - assert.equal(registrations.providers[0].viewId, 'gitguardex.activeAgents'); - assert.equal(registrations.decorationProviders.length, 1); - assert.equal(registrations.fileWatchers.length, 5); - assert.deepEqual( - registrations.fileWatchers.map((watcher) => watcher.pattern), - [ - '**/.omx/state/active-sessions/*.json', - '**/.omx/state/agent-file-locks.json', - '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock', - '**/{.omx,.omc}/agent-worktrees/*/.git', - '**/.omx/logs/*.log', - ], - ); - assert.equal(registrations.workspaceFolderListeners.length, 1); - - const provider = registrations.providers[0].provider; - assert.equal(typeof provider.getTreeItem, 'function'); - assert.equal(typeof registrations.commands.get('gitguardex.activeAgents.startAgent'), 'function'); - assert.equal(typeof registrations.commands.get('gitguardex.activeAgents.restart'), 'function'); - assert.equal(typeof registrations.commands.get('gitguardex.activeAgents.inspect'), 'function'); - - const rootItems = await provider.getChildren(); - assert.equal(rootItems.length, 1); - assert.equal(rootItems[0].label, 'No active Guardex agents'); - assert.equal(registrations.treeViews[0].badge, undefined); - assert.equal(registrations.treeViews[0].message, undefined); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension closes deleted worktree repositories during refresh', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-close-deleted-')); - const worktreePath = path.join(tempRoot, '.omx', 'agent-worktrees', 'deleted-task'); - fs.mkdirSync(worktreePath, { recursive: true }); - const sessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/deleted-task', - taskName: 'deleted-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - })); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.workspaceFolders = [ - { uri: { fsPath: tempRoot } }, - { uri: { fsPath: worktreePath } }, - ]; - vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const gitCloseCalls = () => registrations.executedCommands.filter((entry) => ( - entry.command === 'git.close' - )); - assert.equal(gitCloseCalls().length, 0); - - fs.rmSync(worktreePath, { recursive: true, force: true }); - await registrations.commands.get('gitguardex.activeAgents.refresh')(); - await flushAsyncWork(); - - assert.equal(gitCloseCalls().length, 1); - assert.equal(gitCloseCalls()[0].args[0].fsPath, path.resolve(worktreePath)); - assert.deepEqual(registrations.workspaceFolderUpdates, [ - { start: 1, deleteCount: 1, folders: [] }, - ]); - assert.deepEqual( - vscode.workspace.workspaceFolders.map((folder) => folder.uri.fsPath), - [tempRoot], - ); - - await registrations.commands.get('gitguardex.activeAgents.refresh')(); - await flushAsyncWork(); - assert.equal(gitCloseCalls().length, 1); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension closes deleted worktrees after session records disappear', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-close-deleted-no-session-')); - const worktreePath = path.join(tempRoot, '.omx', 'agent-worktrees', 'deleted-task'); - fs.mkdirSync(worktreePath, { recursive: true }); - const sessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/deleted-task', - taskName: 'deleted-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - })); - let currentSessionFiles = [{ fsPath: sessionPath }]; - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.workspaceFolders = [ - { uri: { fsPath: tempRoot } }, - { uri: { fsPath: worktreePath } }, - ]; - vscode.workspace.findFiles = async () => currentSessionFiles; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const gitCloseCalls = () => registrations.executedCommands.filter((entry) => ( - entry.command === 'git.close' - )); - assert.equal(gitCloseCalls().length, 0); - - fs.rmSync(worktreePath, { recursive: true, force: true }); - fs.rmSync(sessionPath, { force: true }); - currentSessionFiles = []; - await registrations.commands.get('gitguardex.activeAgents.refresh')(); - await flushAsyncWork(); - - assert.equal(gitCloseCalls().length, 1); - assert.equal(gitCloseCalls()[0].args[0].fsPath, path.resolve(worktreePath)); - assert.deepEqual(registrations.workspaceFolderUpdates, [ - { start: 1, deleteCount: 1, folders: [] }, - ]); - assert.deepEqual( - vscode.workspace.workspaceFolders.map((folder) => folder.uri.fsPath), - [tempRoot], - ); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension closes deleted managed workspace folders without session state', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-close-stale-folder-')); - const worktreePath = path.join(tempRoot, '.omc', 'agent-worktrees', 'deleted-task'); - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.workspaceFolders = [ - { uri: { fsPath: tempRoot } }, - { uri: { fsPath: worktreePath } }, - ]; - vscode.workspace.findFiles = async () => []; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const gitCloseCalls = registrations.executedCommands.filter((entry) => ( - entry.command === 'git.close' - )); - assert.equal(gitCloseCalls.length, 1); - assert.equal(gitCloseCalls[0].args[0].fsPath, path.resolve(worktreePath)); - assert.deepEqual(registrations.workspaceFolderUpdates, [ - { start: 1, deleteCount: 1, folders: [] }, - ]); - assert.deepEqual( - vscode.workspace.workspaceFolders.map((folder) => folder.uri.fsPath), - [tempRoot], - ); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents restart command restarts the extension host for this extension only', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-restart-command-')); - const { registrations, vscode } = createMockVscode(tempRoot); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - await registrations.commands.get('gitguardex.activeAgents.restart')('Recodee.gitguardex-active-agents'); - await registrations.commands.get('gitguardex.activeAgents.restart')('someone.else'); - - const restartCalls = registrations.executedCommands.filter( - (entry) => entry.command === 'workbench.action.restartExtensionHost', - ); - assert.equal(restartCalls.length, 1); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents focus command opens the dedicated sidebar container', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-focus-view-')); - const { registrations, vscode } = createMockVscode(tempRoot); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - await vscode.commands.executeCommand('gitguardex.activeAgents.focus'); - - assert.equal( - registrations.executedCommands.some((entry) => ( - entry.command === 'workbench.view.extension.gitguardex-active-agents-container' - )), - true, - ); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension self-heals managed repo-scan ignores on activation and workspace changes', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-scan-ignores-')); - const secondRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-scan-ignores-second-')); - const { registrations, vscode } = createMockVscode(tempRoot); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - const managedRepoScanIgnoredFolders = [ - '.omx/agent-worktrees', - '**/.omx/agent-worktrees', - '.omx/.tmp-worktrees', - '**/.omx/.tmp-worktrees', - '.omc/agent-worktrees', - '**/.omc/agent-worktrees', - '.omc/.tmp-worktrees', - '**/.omc/.tmp-worktrees', - ]; - const mergeManagedRepoScanIgnores = (values) => Array.from(new Set([ - ...values, - ...managedRepoScanIgnoredFolders, - ])); - - registrations.setConfigurationValue('git', tempRoot, 'repositoryScanIgnoredFolders', [ - 'custom-ignore', - '.omx/agent-worktrees', - '.omx/agent-worktrees', - ]); - - extension.activate(context); - await flushAsyncWork(); - - assert.deepEqual( - registrations.getConfigurationValue('git', tempRoot, 'repositoryScanIgnoredFolders'), - mergeManagedRepoScanIgnores([ - 'custom-ignore', - '.omx/agent-worktrees', - '.omx/agent-worktrees', - ]), - ); - assert.deepEqual(registrations.configurationUpdates, [ - { - section: 'git', - key: 'repositoryScanIgnoredFolders', - scopePath: tempRoot, - target: vscode.ConfigurationTarget.Workspace, - value: mergeManagedRepoScanIgnores([ - 'custom-ignore', - '.omx/agent-worktrees', - '.omx/agent-worktrees', - ]), - }, - ]); - - registrations.setConfigurationValue('git', secondRoot, 'repositoryScanIgnoredFolders', [ - 'second-ignore', - '.omc/agent-worktrees', - ]); - vscode.workspace.workspaceFolders = [ - { uri: { fsPath: tempRoot } }, - { uri: { fsPath: secondRoot } }, - ]; - registrations.workspaceFolderListeners[0]({ - added: [{ uri: { fsPath: secondRoot } }], - removed: [], - }); - await flushAsyncWork(); - - assert.deepEqual( - registrations.getConfigurationValue('git', secondRoot, 'repositoryScanIgnoredFolders'), - mergeManagedRepoScanIgnores([ - 'second-ignore', - '.omc/agent-worktrees', - ]), - ); - assert.deepEqual(registrations.configurationUpdates, [ - { - section: 'git', - key: 'repositoryScanIgnoredFolders', - scopePath: tempRoot, - target: vscode.ConfigurationTarget.Workspace, - value: mergeManagedRepoScanIgnores([ - 'custom-ignore', - '.omx/agent-worktrees', - '.omx/agent-worktrees', - ]), - }, - { - section: 'git', - key: 'repositoryScanIgnoredFolders', - scopePath: secondRoot, - target: vscode.ConfigurationTarget.WorkspaceFolder, - value: mergeManagedRepoScanIgnores([ - 'second-ignore', - '.omc/agent-worktrees', - ]), - }, - ]); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension startAgent command uses gx agents start with a target repo', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-start-agent-')); - const { registrations, vscode } = createMockVscode(tempRoot); - registrations.inputResponses.push('demo task', 'codex'); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - - await registrations.commands.get('gitguardex.activeAgents.startAgent')(); - - assert.equal(registrations.terminals.length, 1); - assert.deepEqual(registrations.terminals[0].options, { - name: `GitGuardex: ${path.basename(tempRoot)}`, - cwd: tempRoot, - }); - assert.equal(registrations.terminals[0].shown, true); - assert.deepEqual(registrations.terminals[0].sentTexts, [ - { - text: `gx agents start 'demo task' --agent 'codex' --target '${tempRoot}'`, - addNewLine: true, - }, - ]); - assert.deepEqual(registrations.quickPickCalls, []); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension startAgent command uses gx agents start for plain repos', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-start-agent-fallback-')); - const { registrations, vscode } = createMockVscode(tempRoot); - registrations.inputResponses.push('demo task', 'codex'); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - - await registrations.commands.get('gitguardex.activeAgents.startAgent')(); - - assert.equal(registrations.terminals.length, 1); - assert.deepEqual(registrations.terminals[0].options, { - name: `GitGuardex: ${path.basename(tempRoot)}`, - cwd: tempRoot, - }); - assert.equal(registrations.terminals[0].shown, true); - assert.deepEqual(registrations.terminals[0].sentTexts, [ - { - text: `gx agents start 'demo task' --agent 'codex' --target '${tempRoot}'`, - addNewLine: true, - }, - ]); - assert.deepEqual(registrations.quickPickCalls, []); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension startAgent can target a nested Git repo', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-start-agent-nested-')); - const storefrontRoot = path.join(tempRoot, 'apps', 'storefront'); - const backendRoot = path.join(tempRoot, 'apps', 'backend'); - initGitRepo(storefrontRoot); - initGitRepo(backendRoot); - fs.writeFileSync(path.join(storefrontRoot, 'dirty.txt'), 'changed\n', 'utf8'); - const { registrations, vscode } = createMockVscode(tempRoot); - registrations.quickPickResponse = { - label: 'apps/storefront', - description: 'main · dirty', - repoRoot: storefrontRoot, - }; - registrations.inputResponses.push('nested task', 'codex'); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - - await registrations.commands.get('gitguardex.activeAgents.startAgent')(); - - assert.equal(registrations.quickPickCalls.length, 1); - assert.deepEqual( - registrations.quickPickCalls[0].items.map((item) => item.repoRoot), - [tempRoot, backendRoot, storefrontRoot], - ); - assert.deepEqual( - registrations.quickPickCalls[0].items.map((item) => item.detail), - [tempRoot, backendRoot, storefrontRoot], - ); - const storefrontPick = registrations.quickPickCalls[0].items.find((item) => item.repoRoot === storefrontRoot); - assert.ok(storefrontPick.description.endsWith(' · dirty')); - assert.equal(registrations.terminals.length, 1); - assert.deepEqual(registrations.terminals[0].options, { - name: `GitGuardex: ${path.basename(storefrontRoot)}`, - cwd: storefrontRoot, - }); - assert.deepEqual(registrations.terminals[0].sentTexts, [ - { - text: `gx agents start 'nested task' --agent 'codex' --target '${storefrontRoot}'`, - addNewLine: true, - }, - ]); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension startAgent defaults to the active editor nested repo', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-start-agent-active-editor-')); - const storefrontRoot = path.join(tempRoot, 'apps', 'storefront'); - const backendRoot = path.join(tempRoot, 'apps', 'backend'); - const editorPath = path.join(storefrontRoot, 'src', 'home.tsx'); - initGitRepo(storefrontRoot); - initGitRepo(backendRoot); - fs.mkdirSync(path.dirname(editorPath), { recursive: true }); - fs.writeFileSync(editorPath, 'export {};\n', 'utf8'); - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.window.activeTextEditor = { document: { uri: vscode.Uri.file(editorPath) } }; - registrations.inputResponses.push('active editor task', 'codex'); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - - await registrations.commands.get('gitguardex.activeAgents.startAgent')(); - - assert.deepEqual(registrations.quickPickCalls, []); - assert.equal(registrations.terminals.length, 1); - assert.deepEqual(registrations.terminals[0].options, { - name: `GitGuardex: ${path.basename(storefrontRoot)}`, - cwd: storefrontRoot, - }); - assert.deepEqual(registrations.terminals[0].sentTexts, [ - { - text: `gx agents start 'active editor task' --agent 'codex' --target '${storefrontRoot}'`, - addNewLine: true, - }, - ]); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension groups live sessions under a repo node', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-live-view-')); - const sessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/live-task', - taskName: 'live-task', - agentName: 'codex', - worktreePath: path.join(tempRoot, '.omx', 'agent-worktrees', 'live-task'), - pid: process.pid, - cliName: 'codex', - })); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.label, path.basename(tempRoot)); - assert.equal(repoItem.description, '0 working agents · 0 needs cleanup agents · 1 idle agent · 0 unassigned changes · 0 locked files · 0 conflicts'); - - assert.deepEqual((await provider.getChildren(repoItem)).map((item) => item.label), [ - 'Overview', - 'Idle / thinking', - 'Advanced details', - ]); - const overviewSection = await getSectionByLabel(provider, repoItem, 'Overview'); - const [summaryItem] = await provider.getChildren(overviewSection); - assert.equal(summaryItem.label, 'Summary'); - assert.equal(summaryItem.description, repoItem.description); - - const idleSection = await getSectionByLabel(provider, repoItem, 'Idle / thinking'); - assert.equal(idleSection.description, '1'); - - const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, idleSection); - assert.equal(worktreeItem, null); - assert.equal(sessionItem.label, 'live-task'); - assert.equal(sessionItem.session.branch, 'agent/codex/live-task'); - assert.match(sessionItem.description, /^Idle: codex · via OpenAI/); - assert.equal(sessionItem.iconPath.id, 'comment-discussion'); - assert.equal(sessionItem.resourceUri.scheme, 'gitguardex-agent'); - assert.equal( - sessionItem.resourceUri.toString(), - `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/codex/live-task')}`, - ); - assert.deepEqual(registrations.treeViews[0].badge, { - value: 1, - tooltip: repoItem.description, - }); - assert.equal(registrations.treeViews[0].message, undefined); - assert.equal( - registrations.executedCommands.some((entry) => ( - entry.command === 'setContext' - && entry.args[0] === 'guardex.hasAgents' - && entry.args[1] === true - )), - true, - ); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension labels idle dirty finished worktrees as needing cleanup', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-cleanup-label-')); - initGitRepo(tempRoot); - fs.writeFileSync(path.join(tempRoot, 'tracked.txt'), 'base\n', 'utf8'); - runGit(tempRoot, ['add', 'tracked.txt']); - runGit(tempRoot, ['commit', '-m', 'baseline']); - - const worktreePath = path.join(tempRoot, '.omx', 'agent-worktrees', 'finished-task'); - fs.mkdirSync(path.dirname(worktreePath), { recursive: true }); - runGit(tempRoot, [ - 'worktree', - 'add', - '-b', - 'agent/codex/finished-task', - worktreePath, - 'HEAD', - ]); - const changedPath = path.join(worktreePath, 'tracked.txt'); - fs.writeFileSync(changedPath, 'base\nleftover cleanup\n', 'utf8'); - setPathMtime(changedPath, Date.now() - (20 * 60 * 1000)); - - const sessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/finished-task', - taskName: 'finished-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - })); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.description, '0 working agents · 1 needs cleanup agent · 0 idle agents · 0 unassigned changes · 0 locked files · 0 conflicts'); - assert.deepEqual((await provider.getChildren(repoItem)).map((item) => item.label), [ - 'Overview', - 'Needs cleanup', - 'Advanced details', - ]); - - const cleanupSection = await getSectionByLabel(provider, repoItem, 'Needs cleanup'); - const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, cleanupSection); - assert.equal(worktreeItem, null); - assert.equal(sessionItem.label, 'finished-task'); - assert.match(sessionItem.description, /^Needs cleanup: codex · via OpenAI · 1 changed file/); - assert.equal(sessionItem.iconPath.id, 'pass-filled'); - assert.deepEqual(registrations.treeViews[0].badge, { - value: 1, - tooltip: repoItem.description, - }); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension discovers nested managed-worktree subprojects under workspace roots', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-subprojects-')); - const nestedRepoRoot = path.join(tempRoot, 'gitguardex'); - initGitRepo(nestedRepoRoot); - fs.writeFileSync(path.join(nestedRepoRoot, 'tracked.txt'), 'base\n', 'utf8'); - runGit(nestedRepoRoot, ['add', 'tracked.txt']); - runGit(nestedRepoRoot, ['commit', '-m', 'baseline']); - - const worktreePath = path.join( - nestedRepoRoot, - '.omx', - 'agent-worktrees', - 'agent__codex__nested-visible-task', - ); - fs.mkdirSync(path.dirname(worktreePath), { recursive: true }); - runGit(nestedRepoRoot, [ - 'worktree', - 'add', - '-b', - 'agent/codex/nested-visible-task', - worktreePath, - ]); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\nchanged\n', 'utf8'); - - const managedWorktreeGitFile = path.join(worktreePath, '.git'); - assert.equal(fs.statSync(managedWorktreeGitFile).isFile(), true); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async (pattern) => { - if (pattern === '**/{.omx,.omc}/agent-worktrees/*/.git') { - return [{ fsPath: managedWorktreeGitFile }]; - } - return []; - }; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.label, `${path.basename(tempRoot)}/gitguardex`); - assert.equal(repoItem.repoRoot, nestedRepoRoot); - assert.equal(repoItem.description, '1 working agent · 0 needs cleanup agents · 0 idle agents · 0 unassigned changes · 0 locked files · 0 conflicts'); - - const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); - const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); - assert.equal(worktreeItem, null); - assert.equal(sessionItem.session.repoRoot, nestedRepoRoot); - assert.equal(sessionItem.session.worktreePath, worktreePath); - assert.equal(sessionItem.session.branch, 'agent/codex/nested-visible-task'); - assert.match(sessionItem.description, /^Working: codex · via OpenAI · 1 changed file/); - assert.deepEqual(registrations.treeViews[0].badge, { - value: 1, - tooltip: repoItem.description, - }); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension shows provider and snapshot identity badges', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-provider-badges-')); - const codexWorktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-provider-codex-')); - const claudeWorktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-provider-claude-')); - initGitRepo(codexWorktreePath); - initGitRepo(claudeWorktreePath); - - const codexSessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/provider-task', - taskName: 'provider-task', - agentName: 'codex', - snapshotName: 'nagyviktor@edixa.com', - worktreePath: codexWorktreePath, - pid: process.pid, - cliName: 'codex', - })); - const claudeSessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/claude/provider-task', - taskName: 'provider-task', - agentName: 'claude', - worktreePath: claudeWorktreePath, - pid: process.pid, - cliName: 'claude', - })); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [ - { fsPath: codexSessionPath }, - { fsPath: claudeSessionPath }, - ]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - const idleSection = await getSectionByLabel(provider, repoItem, 'Idle / thinking'); - const codexItem = await getSessionByBranch(provider, idleSection, 'agent/codex/provider-task'); - const claudeItem = await getSessionByBranch(provider, idleSection, 'agent/claude/provider-task'); - assert.match(codexItem.description, /^Idle: codex · via OpenAI · snapshot nagyviktor@edixa\.com/); - assert.match(claudeItem.description, /^Idle: claude · via Claude/); - - const decorationProvider = registrations.decorationProviders[0]; - const codexDecoration = decorationProvider.provideFileDecoration(vscode.Uri.parse( - `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/codex/provider-task')}`, - )); - const claudeDecoration = decorationProvider.provideFileDecoration(vscode.Uri.parse( - `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/claude/provider-task')}`, - )); - assert.equal(codexDecoration.badge, 'N'); - assert.equal(codexDecoration.tooltip, 'Snapshot nagyviktor@edixa.com'); - assert.equal(claudeDecoration.badge, 'CL'); - assert.equal(claudeDecoration.tooltip, 'Claude session via claude'); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension decorates idle clean sessions without overriding working rows', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-idle-decorations-')); - - const idleWarningPath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-idle-warning-')); - initGitRepo(idleWarningPath); - fs.writeFileSync(path.join(idleWarningPath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(idleWarningPath, ['add', 'tracked.txt']); - runGit(idleWarningPath, ['commit', '-m', 'baseline']); - - const idleErrorPath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-idle-error-')); - initGitRepo(idleErrorPath); - fs.writeFileSync(path.join(idleErrorPath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(idleErrorPath, ['add', 'tracked.txt']); - runGit(idleErrorPath, ['commit', '-m', 'baseline']); - - const workingPath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-idle-working-')); - initGitRepo(workingPath); - fs.writeFileSync(path.join(workingPath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(workingPath, ['add', 'tracked.txt']); - runGit(workingPath, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(workingPath, 'tracked.txt'), 'base\nchanged\n', 'utf8'); - - const sessionRecords = [ - { - branch: 'agent/codex/idle-warning', - worktreePath: idleWarningPath, - startedAt: new Date(Date.now() - (11 * 60 * 1000)).toISOString(), - }, - { - branch: 'agent/codex/idle-error', - worktreePath: idleErrorPath, - startedAt: new Date(Date.now() - (31 * 60 * 1000)).toISOString(), - }, - { - branch: 'agent/codex/working-now', - worktreePath: workingPath, - startedAt: new Date(Date.now() - (31 * 60 * 1000)).toISOString(), - }, - ]; - - for (const record of sessionRecords) { - const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, record.branch); - fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); - fs.writeFileSync( - sessionPath, - `${JSON.stringify(sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: record.branch, - taskName: path.basename(record.worktreePath), - agentName: 'codex', - worktreePath: record.worktreePath, - pid: process.pid, - cliName: 'codex', - startedAt: record.startedAt, - }), null, 2)}\n`, - 'utf8', - ); - } - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => sessionRecords.map((record) => ({ - fsPath: sessionSchema.sessionFilePathForBranch(tempRoot, record.branch), - })); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - await provider.getChildren(); - const decorationProvider = registrations.decorationProviders[0]; - - const warningDecoration = decorationProvider.provideFileDecoration(vscode.Uri.parse( - `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/codex/idle-warning')}`, - )); - assert.equal(warningDecoration.badge, '10m+'); - assert.equal(warningDecoration.tooltip, 'idle 10m+'); - assert.equal(warningDecoration.color.id, 'list.warningForeground'); - - const errorDecoration = decorationProvider.provideFileDecoration(vscode.Uri.parse( - `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/codex/idle-error')}`, - )); - assert.equal(errorDecoration.badge, '30m+'); - assert.equal(errorDecoration.tooltip, 'idle 30m+'); - assert.equal(errorDecoration.color.id, 'list.errorForeground'); - - const workingDecoration = decorationProvider.provideFileDecoration(vscode.Uri.parse( - `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/codex/working-now')}`, - )); - assert.equal(workingDecoration.badge, 'AI'); - assert.equal(workingDecoration.tooltip, 'OpenAI session via codex'); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents refresh also invalidates session decorations', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-decoration-refresh-')); - const worktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-decoration-refresh-session-')); - initGitRepo(worktreePath); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - - const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, 'agent/codex/idle-refresh'); - fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); - fs.writeFileSync( - sessionPath, - `${JSON.stringify(sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/idle-refresh', - taskName: 'idle-refresh', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - startedAt: new Date(Date.now() - (11 * 60 * 1000)).toISOString(), - }), null, 2)}\n`, - 'utf8', - ); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - - const provider = registrations.providers[0].provider; - await provider.getChildren(); - await flushAsyncWork(); - - let decorationRefreshCount = 0; - registrations.decorationProviders[0].onDidChangeFileDecorations(() => { - decorationRefreshCount += 1; - }); - - await provider.refresh(); - assert.ok(decorationRefreshCount >= 1); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension shows grouped repo changes beside active agents', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-working-view-')); - initGitRepo(tempRoot); - fs.writeFileSync(path.join(tempRoot, 'root-file.txt'), 'base\n', 'utf8'); - runGit(tempRoot, ['add', 'root-file.txt']); - runGit(tempRoot, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(tempRoot, 'root-file.txt'), 'base\nchanged\n', 'utf8'); - - const worktreePath = path.join(tempRoot, 'sandbox'); - initGitRepo(worktreePath); - fs.mkdirSync(path.join(worktreePath, 'src'), { recursive: true }); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - fs.writeFileSync(path.join(worktreePath, 'src', 'nested.js'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['add', 'src/nested.js']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\nchanged\n', 'utf8'); - fs.writeFileSync(path.join(worktreePath, 'src', 'nested.js'), 'base\nchanged\n', 'utf8'); - - const latestTaskPreview = 'Fix cave hivemind hero layout'; - const liveSessionRecord = sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/live-task', - taskName: 'live-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - }); - liveSessionRecord.latestTaskPreview = latestTaskPreview; - const sessionPath = writeSessionRecord(tempRoot, liveSessionRecord); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }]; - const mockSessionSchema = { - ...sessionSchema, - readActiveSessions: () => sessionSchema.readActiveSessions(tempRoot, { includeStale: true }), - readRepoChanges: () => [ - { - relativePath: 'sandbox/src/nested.js', - absolutePath: path.join(worktreePath, 'src', 'nested.js'), - statusLabel: 'M', - statusText: 'Modified', - }, - { - relativePath: 'sandbox/tracked.txt', - absolutePath: path.join(worktreePath, 'tracked.txt'), - statusLabel: 'M', - statusText: 'Modified', - }, - { - relativePath: 'root-file.txt', - absolutePath: path.join(tempRoot, 'root-file.txt'), - statusLabel: 'M', - statusText: 'Modified', - }, - ], - }; - const extension = loadExtensionWithMockVscode(vscode, mockSessionSchema); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.description, '1 working agent · 0 needs cleanup agents · 0 idle agents · 1 unassigned change · 0 locked files · 0 conflicts'); - assert.deepEqual((await provider.getChildren(repoItem)).map((item) => item.label), [ - 'Overview', - 'Working now', - 'Unassigned changes', - 'Advanced details', - ]); - - const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); - const unassignedSection = await getSectionByLabel(provider, repoItem, 'Unassigned changes'); - const advancedSection = await getSectionByLabel(provider, repoItem, 'Advanced details'); - const overviewSection = await getSectionByLabel(provider, repoItem, 'Overview'); - - assert.equal(overviewSection.collapsibleState, vscode.TreeItemCollapsibleState.Expanded); - assert.equal(workingSection.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed); - assert.equal(advancedSection.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed); - - const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); - assert.equal(worktreeItem, null); - assert.equal(sessionItem.label, latestTaskPreview); - assert.equal(sessionItem.session.branch, 'agent/codex/live-task'); - assert.match(sessionItem.description, /^Working: codex · via OpenAI · 2 changed files/); - assert.match(sessionItem.tooltip, /Recent Fix cave hivemind hero layout/); - assert.equal(sessionItem.iconPath.id, 'loading~spin'); - assert.equal(sessionItem.iconPath.color.id, 'gitDecoration.addedResourceForeground'); - const sessionDetails = await provider.getChildren(sessionItem); - assert.equal(sessionDetails.find((item) => item.label === 'Top files')?.description, 'src/nested.js, tracked.txt'); - assert.deepEqual(registrations.treeViews[0].badge, { - value: 1, - tooltip: repoItem.description, - }); - - const [unassignedChangeItem] = await provider.getChildren(unassignedSection); - assert.equal(unassignedChangeItem.label, 'root-file.txt'); - assert.equal(unassignedChangeItem.description, 'M · Protected branch'); - assert.equal(unassignedChangeItem.iconPath.id, 'warning'); - assert.equal(unassignedChangeItem.iconPath.color.id, 'list.warningForeground'); - - const activeAgentTree = await getSectionByLabel(provider, advancedSection, 'Active agent tree'); - const rawWorkingSection = await getSectionByLabel(provider, activeAgentTree, 'WORKING NOW'); - const [rawWorktreeItem] = await provider.getChildren(rawWorkingSection); - assert.equal(rawWorktreeItem.label, latestTaskPreview); - assert.equal(rawWorktreeItem.description, 'working: codex'); - const [rawSessionItem] = await provider.getChildren(rawWorktreeItem); - assert.equal(rawSessionItem.label, latestTaskPreview); - assert.match(rawSessionItem.description, /^Working · 2 files · /); - - const rawPathTree = await getSectionByLabel(provider, advancedSection, 'Raw path tree'); - const [worktreeGroup, repoRootGroup] = await provider.getChildren(rawPathTree); - assert.equal(worktreeGroup.label, latestTaskPreview); - assert.equal(worktreeGroup.description, 'codex · 2 files'); - assert.equal(repoRootGroup.label, 'Repo root'); - - const [sessionGroup] = await provider.getChildren(worktreeGroup); - assert.equal(sessionGroup.label, latestTaskPreview); - assert.match(sessionGroup.description, /^Working · 2 files · /); - const [folderItem, trackedItem] = await provider.getChildren(sessionGroup); - assert.equal(folderItem.label, 'src'); - assert.equal(trackedItem.label, 'tracked.txt'); - assert.match(trackedItem.tooltip, /^tracked\.txt\nSummary M\nStatus Modified\n/); - - const [nestedItem] = await provider.getChildren(folderItem); - assert.equal(nestedItem.label, 'nested.js'); - assert.match(nestedItem.tooltip, /^src\/nested\.js\nSummary M\nStatus Modified\n/); - - const [rootItem] = await provider.getChildren(repoRootGroup); - assert.equal(rootItem.label, 'root-file.txt'); - assert.equal(rootItem.description, 'M'); - assert.match(rootItem.tooltip, /^root-file\.txt\nSummary M\nStatus Modified\n/); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension surfaces live managed worktrees from AGENT.lock fallback', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-worktree-lock-view-')); - initGitRepo(tempRoot); - - const worktreePath = path.join( - tempRoot, - '.omx', - 'agent-worktrees', - 'agent__codex__lock-visible-task', - ); - initGitRepo(worktreePath); - runGit(worktreePath, ['checkout', '-b', 'agent/codex/lock-visible-task']); - fs.mkdirSync(path.join(worktreePath, 'src'), { recursive: true }); - fs.writeFileSync(path.join(worktreePath, 'src', 'live.js'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'src/live.js']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(worktreePath, 'src', 'live.js'), 'base\nchanged\n', 'utf8'); - const projectPath = path.join(tempRoot, 'gitguardex'); - fs.mkdirSync(projectPath, { recursive: true }); - const lockPath = writeWorktreeLock(worktreePath, { - updatedAt: '2026-04-22T09:01:00.000Z', - snapshots: [ - { - snapshotName: 'nagyviktor@edixa.com', - accountId: 'acct-1', - email: 'nagyviktor@edixa.com', - liveSessionCount: 1, - trackedSessionCount: 1, - compatSessionCount: 1, - sessions: [ - { - sessionKey: 'pid:101', - taskPreview: 'Implement live worktree telemetry', - taskUpdatedAt: '2026-04-22T08:55:00.000Z', - projectName: 'gitguardex', - projectPath, - }, - ], - }, - ], - }); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async (pattern) => { - if (pattern === '**/.omx/state/active-sessions/*.json') { - return []; - } - if (pattern === '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock') { - return [{ fsPath: lockPath }]; - } - return []; - }; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.label, `${path.basename(tempRoot)}/gitguardex`); - assert.equal(repoItem.description, '1 working agent · 0 needs cleanup agents · 0 idle agents · 0 unassigned changes · 0 locked files · 0 conflicts'); - - assert.deepEqual((await provider.getChildren(repoItem)).map((item) => item.label), [ - 'Overview', - 'Working now', - 'Advanced details', - ]); - const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); - const overviewSection = await getSectionByLabel(provider, repoItem, 'Overview'); - const [projectFolder] = await provider.getChildren(workingSection); - assert.equal(projectFolder.label, 'gitguardex'); - assert.equal(projectFolder.description, '1 agent · 1 file'); - const [sessionItem] = await provider.getChildren(projectFolder); - assert.equal(sessionItem.label, 'Implement live worktree telemetry'); - assert.equal(sessionItem.session.branch, 'agent/codex/lock-visible-task'); - assert.match(sessionItem.description, /^Working: codex · via OpenAI · snapshot nagyviktor@edixa\.com · 1 changed file/); - assert.equal(sessionItem.iconPath.color.id, 'gitDecoration.addedResourceForeground'); - assert.equal(sessionItem.session.snapshotName, 'nagyviktor@edixa.com'); - assert.match(sessionItem.tooltip, /Telemetry updated 2026-04-22T09:01:00.000Z/); - assert.match(sessionItem.tooltip, /Snapshot nagyviktor@edixa\.com/); - - const advancedSection = await getSectionByLabel(provider, repoItem, 'Advanced details'); - assert.equal(overviewSection.collapsibleState, vscode.TreeItemCollapsibleState.Expanded); - assert.equal(workingSection.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed); - assert.equal(advancedSection.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed); - const activeAgentTree = await getSectionByLabel(provider, advancedSection, 'Active agent tree'); - const rawWorkingSection = await getSectionByLabel(provider, activeAgentTree, 'WORKING NOW'); - const [rawProjectFolder] = await provider.getChildren(rawWorkingSection); - assert.equal(rawProjectFolder.label, 'gitguardex'); - assert.equal(rawProjectFolder.description, '1 agent · 1 file'); - const [rawWorktreeItem] = await provider.getChildren(rawProjectFolder); - assert.equal(rawWorktreeItem.label, 'Implement live worktree telemetry'); - assert.equal(rawWorktreeItem.description, 'working: codex · snapshot nagyviktor@edixa.com'); - assert.equal( - rawWorktreeItem.resourceUri.toString(), - `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/codex/lock-visible-task')}`, - ); - const [rawSessionItem] = await provider.getChildren(rawWorktreeItem); - assert.equal(rawSessionItem.label, 'Implement live worktree telemetry'); - assert.match(rawSessionItem.description, /^Working · 1 file · /); - - const snapshotDecoration = registrations.decorationProviders[0].provideFileDecoration(vscode.Uri.parse( - `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/codex/lock-visible-task')}`, - )); - assert.equal(snapshotDecoration.badge, 'N'); - assert.equal(snapshotDecoration.tooltip, 'Snapshot nagyviktor@edixa.com'); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension shows session health from active-session records', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-session-health-active-')); - initGitRepo(tempRoot); - - const worktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-session-health-worktree-')); - initGitRepo(worktreePath); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\nchanged\n', 'utf8'); - - const branch = 'agent/codex/health-task'; - const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, branch); - fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); - const record = sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch, - taskName: 'health-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - state: 'working', - }); - record.sessionHealth = { - score: 45, - label: 'Inefficient', - primaryDriver: 'turn fragmentation', - secondaries: ['write_stdin churn'], - outputLine: 'Score 45/100 — Inefficient. Primary: turn fragmentation. Secondaries: write_stdin churn.', - }; - fs.writeFileSync(sessionPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8'); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); - const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); - assert.equal(worktreeItem, null); - assert.match(sessionItem.description, /^Working: codex · via OpenAI · 1 changed file · 45\/100/); - assert.match(sessionItem.tooltip, /Session health 45\/100 · Inefficient/); - const sessionDetails = await provider.getChildren(sessionItem); - const sessionHealthItem = sessionDetails.find((item) => item.label === 'Session health'); - assert.equal(sessionHealthItem?.description, '45/100 · Inefficient'); - assert.match(sessionHealthItem?.tooltip || '', /Score 45\/100 — Inefficient\./); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension shows session health from AGENT.lock fallback telemetry', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-session-health-lock-')); - initGitRepo(tempRoot); - - const worktreePath = path.join( - tempRoot, - '.omx', - 'agent-worktrees', - 'agent__codex__health-lock-task', - ); - initGitRepo(worktreePath); - runGit(worktreePath, ['checkout', '-b', 'agent/codex/health-lock-task']); - fs.mkdirSync(path.join(worktreePath, 'src'), { recursive: true }); - fs.writeFileSync(path.join(worktreePath, 'src', 'live.js'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'src/live.js']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(worktreePath, 'src', 'live.js'), 'base\nchanged\n', 'utf8'); - const lockPath = writeWorktreeLock(worktreePath, { - updatedAt: '2026-04-22T09:01:00.000Z', - snapshots: [ - { - snapshotName: 'snapshot-a', - accountId: 'acct-1', - email: 'agent@example.com', - liveSessionCount: 1, - trackedSessionCount: 1, - compatSessionCount: 1, - sessions: [ - { - sessionKey: 'pid:101', - taskPreview: 'Implement live worktree telemetry', - taskUpdatedAt: '2026-04-22T08:55:00.000Z', - projectName: 'gitguardex', - projectPath: worktreePath, - sessionHealth: { - score: 45, - label: 'Inefficient', - primaryDriver: 'turn fragmentation', - secondaries: ['write_stdin churn'], - outputLine: 'Score 45/100 — Inefficient. Primary: turn fragmentation. Secondaries: write_stdin churn.', - }, - }, - ], - }, - ], - }); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async (pattern) => { - if (pattern === '**/.omx/state/active-sessions/*.json') { - return []; - } - if (pattern === '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock') { - return [{ fsPath: lockPath }]; - } - return []; - }; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); - const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); - assert.equal(worktreeItem, null); - assert.match(sessionItem.description, /^Working: codex · via OpenAI · snapshot snapshot-a · 1 changed file · 45\/100/); - assert.match(sessionItem.tooltip, /Session health 45\/100 · Inefficient/); - const sessionDetails = await provider.getChildren(sessionItem); - const sessionHealthItem = sessionDetails.find((item) => item.label === 'Session health'); - assert.equal(sessionHealthItem?.description, '45/100 · Inefficient'); - assert.match(sessionHealthItem?.tooltip || '', /Score 45\/100 — Inefficient\./); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension surfaces plain managed worktrees from workspace fallback', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-managed-worktree-view-')); - initGitRepo(tempRoot); - - const worktreePath = path.join( - tempRoot, - '.omx', - 'agent-worktrees', - 'agent__codex__plain-visible-task', - ); - initGitRepo(worktreePath); - runGit(worktreePath, ['checkout', '-b', 'agent/codex/plain-visible-task']); - fs.mkdirSync(path.join(worktreePath, 'src'), { recursive: true }); - fs.writeFileSync(path.join(worktreePath, 'src', 'live.js'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'src/live.js']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(worktreePath, 'src', 'live.js'), 'base\nchanged\n', 'utf8'); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => []; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.description, '1 working agent · 0 needs cleanup agents · 0 idle agents · 0 unassigned changes · 0 locked files · 0 conflicts'); - - const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); - const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); - assert.equal(worktreeItem, null); - assert.equal(sessionItem.session.branch, 'agent/codex/plain-visible-task'); - assert.match(sessionItem.description, /^Working: codex · via OpenAI · 1 changed file/); - assert.match(sessionItem.tooltip, /Started /); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension resolves owning repo sessions when the window is opened on a linked worktree', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-linked-worktree-view-')); - initGitRepo(tempRoot); - fs.writeFileSync(path.join(tempRoot, 'tracked.txt'), 'base\n', 'utf8'); - runGit(tempRoot, ['add', 'tracked.txt']); - runGit(tempRoot, ['commit', '-m', 'baseline']); - - const branch = 'agent/codex/linked-worktree-visible-task'; - const worktreePath = path.join( - tempRoot, - '.omx', - 'agent-worktrees', - 'agent__codex__linked-worktree-visible-task', - ); - fs.mkdirSync(path.dirname(worktreePath), { recursive: true }); - runGit(tempRoot, ['worktree', 'add', '-b', branch, worktreePath]); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\nchanged\n', 'utf8'); - - writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch, - taskName: 'linked-worktree-visible-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - state: 'working', - })); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.workspaceFolders = [{ uri: { fsPath: worktreePath } }]; - vscode.workspace.findFiles = async () => []; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.label, path.basename(tempRoot)); - assert.equal(repoItem.description, '1 working agent · 0 needs cleanup agents · 0 idle agents · 0 unassigned changes · 0 locked files · 0 conflicts'); - - const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); - const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); - assert.equal(worktreeItem, null); - assert.equal(sessionItem.session.repoRoot, tempRoot); - assert.equal(sessionItem.session.worktreePath, worktreePath); - assert.equal(sessionItem.session.branch, branch); - assert.match(sessionItem.description, /^Working: codex · via OpenAI · 1 changed file/); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension decorates sessions and repo changes from the lock registry', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-lock-decorations-')); - initGitRepo(tempRoot); - fs.writeFileSync(path.join(tempRoot, 'root-file.txt'), 'base\n', 'utf8'); - runGit(tempRoot, ['add', 'root-file.txt']); - runGit(tempRoot, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(tempRoot, 'root-file.txt'), 'base\nchanged\n', 'utf8'); - - const worktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-lock-worktree-')); - initGitRepo(worktreePath); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\nchanged\n', 'utf8'); - - const branch = 'agent/codex/live-task'; - const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, branch); - fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); - fs.writeFileSync( - sessionPath, - `${JSON.stringify(sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch, - taskName: 'live-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - }), null, 2)}\n`, - 'utf8', - ); - - const lockPath = path.join(tempRoot, '.omx', 'state', 'agent-file-locks.json'); - fs.mkdirSync(path.dirname(lockPath), { recursive: true }); - fs.writeFileSync(lockPath, `${JSON.stringify({ - locks: { - 'owned-file.txt': { - branch, - claimed_at: '2026-04-22T08:55:00.000Z', - allow_delete: false, - }, - 'root-file.txt': { - branch: 'agent/codex/other-task', - claimed_at: '2026-04-22T08:56:00.000Z', - allow_delete: false, - }, - 'tracked.txt': { - branch: 'agent/codex/other-task', - claimed_at: '2026-04-22T08:57:00.000Z', - allow_delete: false, - }, - }, - }, null, 2)}\n`, 'utf8'); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.description, '1 working agent · 0 needs cleanup agents · 0 idle agents · 1 unassigned change · 3 locked files · 2 conflicts'); - const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); - const unassignedSection = await getSectionByLabel(provider, repoItem, 'Unassigned changes'); - const advancedSection = await getSectionByLabel(provider, repoItem, 'Advanced details'); - const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); - assert.equal(worktreeItem, null); - assert.equal(sessionItem.label, 'live-task'); - assert.equal(sessionItem.session.branch, branch); - assert.match(sessionItem.tooltip, /1 lock/); - assert.match(sessionItem.tooltip, /Conflicts 1/); - - const activeAgentTree = await getSectionByLabel(provider, advancedSection, 'Active agent tree'); - const rawWorkingSection = await getSectionByLabel(provider, activeAgentTree, 'WORKING NOW'); - const worktreeGroup = await getChildByLabel(provider, rawWorkingSection, 'live-task'); - assert.equal(worktreeGroup.iconPath.id, 'git-branch'); - assert.equal(worktreeGroup.description, 'working: codex'); - assert.equal(worktreeGroup.resourceUri.toString(), `gitguardex-agent://${sessionSchema.sanitizeBranchForFile(branch)}`); - const [sessionGroup] = await provider.getChildren(worktreeGroup); - assert.equal(sessionGroup.label, 'live-task'); - assert.match(sessionGroup.description, /^Working · 1 file · /); - const [sessionChangeItem] = await provider.getChildren(sessionGroup); - assert.equal(sessionChangeItem.label, 'tracked.txt'); - assert.equal(sessionChangeItem.iconPath.id, 'warning'); - assert.match(sessionChangeItem.tooltip, /Locked by agent\/codex\/other-task/); - - const [changeItem] = await provider.getChildren(unassignedSection); - assert.equal(changeItem.label, 'root-file.txt'); - assert.equal(changeItem.iconPath.id, 'warning'); - assert.match(changeItem.tooltip, /Locked by agent\/codex\/other-task/); - assert.deepEqual(registrations.treeViews[0].badge, { - value: 1, - tooltip: repoItem.description, - }); - assert.equal( - registrations.executedCommands.some((entry) => ( - entry.command === 'setContext' - && entry.args[0] === 'guardex.hasConflicts' - && entry.args[1] === true - )), - true, - ); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension re-reads lock state on watcher events instead of every tree load', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-lock-watch-')); - const branch = 'agent/codex/live-task'; - const worktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-lock-watch-worktree-')); - initGitRepo(worktreePath); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - - const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, branch); - fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); - fs.writeFileSync( - sessionPath, - `${JSON.stringify(sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch, - taskName: 'live-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - }), null, 2)}\n`, - 'utf8', - ); - - const lockPath = path.join(tempRoot, '.omx', 'state', 'agent-file-locks.json'); - fs.mkdirSync(path.dirname(lockPath), { recursive: true }); - fs.writeFileSync(lockPath, `${JSON.stringify({ - locks: { - 'owned-file.txt': { - branch, - claimed_at: '2026-04-22T08:57:00.000Z', - allow_delete: false, - }, - }, - }, null, 2)}\n`, 'utf8'); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - const originalReadFileSync = fs.readFileSync; - let lockReadCount = 0; - fs.readFileSync = function patchedReadFileSync(filePath, ...args) { - if (path.resolve(String(filePath)) === lockPath) { - lockReadCount += 1; - } - return originalReadFileSync.call(this, filePath, ...args); - }; - - try { - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const lockWatcher = registrations.watchers.find((watcher) => watcher.pattern === '**/.omx/state/agent-file-locks.json'); - assert.ok(lockWatcher, 'expected lock watcher registration'); - - const [repoItem] = await provider.getChildren(); - const idleSection = await getSectionByLabel(provider, repoItem, 'Idle / thinking'); - const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, idleSection); - assert.equal(worktreeItem, null); - assert.equal(sessionItem.label, 'live-task'); - assert.equal(sessionItem.session.branch, branch); - assert.equal(lockReadCount, 1); - - await provider.getChildren(); - assert.equal(lockReadCount, 1); - - fs.writeFileSync(lockPath, `${JSON.stringify({ - locks: { - 'owned-file.txt': { - branch, - claimed_at: '2026-04-22T08:57:00.000Z', - allow_delete: false, - }, - 'second-owned-file.txt': { - branch, - claimed_at: '2026-04-22T08:58:00.000Z', - allow_delete: false, - }, - }, - }, null, 2)}\n`, 'utf8'); - lockWatcher.fireChange({ fsPath: lockPath }); - assert.equal(lockReadCount, 2); - - const [updatedRepoItem] = await provider.getChildren(); - const updatedIdleSection = await getSectionByLabel(provider, updatedRepoItem, 'Idle / thinking'); - const { worktreeItem: updatedWorktreeItem, sessionItem: updatedSessionItem } = await getOnlyWorktreeAndSession(provider, updatedIdleSection); - assert.equal(updatedWorktreeItem, null); - assert.equal(updatedSessionItem.label, 'live-task'); - assert.equal(updatedSessionItem.session.branch, branch); - - await provider.getChildren(); - assert.equal(lockReadCount, 2); - } finally { - fs.readFileSync = originalReadFileSync; - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } - } -}); - -test('active-agents extension groups blocked, working, idle, stalled, and dead sessions in order', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-state-groups-')); - const now = Date.now(); - - const blockedPath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-blocked-')); - initGitRepo(blockedPath); - fs.writeFileSync(path.join(blockedPath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(blockedPath, ['add', 'tracked.txt']); - runGit(blockedPath, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(blockedPath, '.git', 'MERGE_HEAD'), 'deadbeef\n', 'utf8'); - - const workingPath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-working-')); - initGitRepo(workingPath); - fs.writeFileSync(path.join(workingPath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(workingPath, ['add', 'tracked.txt']); - runGit(workingPath, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(workingPath, 'tracked.txt'), 'base\nchanged\n', 'utf8'); - - const idlePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-idle-')); - initGitRepo(idlePath); - fs.writeFileSync(path.join(idlePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(idlePath, ['add', 'tracked.txt']); - runGit(idlePath, ['commit', '-m', 'baseline']); - setPathMtime(path.join(idlePath, 'tracked.txt'), now - 30_000); - - const stalledPath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-stalled-')); - initGitRepo(stalledPath); - fs.writeFileSync(path.join(stalledPath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(stalledPath, ['add', 'tracked.txt']); - runGit(stalledPath, ['commit', '-m', 'baseline']); - setPathMtime(path.join(stalledPath, 'tracked.txt'), now - (20 * 60 * 1000)); - - const deadPath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-dead-')); - initGitRepo(deadPath); - fs.writeFileSync(path.join(deadPath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(deadPath, ['add', 'tracked.txt']); - runGit(deadPath, ['commit', '-m', 'baseline']); - - const blockedSessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/blocked-task', - taskName: 'blocked-task', - agentName: 'codex', - worktreePath: blockedPath, - pid: process.pid, - cliName: 'codex', - })); - const workingSessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/working-task', - taskName: 'working-task', - agentName: 'codex', - worktreePath: workingPath, - pid: process.pid, - cliName: 'codex', - })); - const idleSessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/idle-task', - taskName: 'idle-task', - agentName: 'codex', - worktreePath: idlePath, - pid: process.pid, - cliName: 'codex', - })); - const stalledSessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/stalled-task', - taskName: 'stalled-task', - agentName: 'codex', - worktreePath: stalledPath, - pid: process.pid, - cliName: 'codex', - })); - const deadSessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/dead-task', - taskName: 'dead-task', - agentName: 'codex', - worktreePath: deadPath, - pid: 999999, - cliName: 'codex', - })); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [ - { fsPath: blockedSessionPath }, - { fsPath: workingSessionPath }, - { fsPath: idleSessionPath }, - { fsPath: stalledSessionPath }, - { fsPath: deadSessionPath }, - ]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.description, '2 working agents · 0 needs cleanup agents · 2 idle agents · 0 unassigned changes · 0 locked files · 0 conflicts'); - - assert.deepEqual((await provider.getChildren(repoItem)).map((item) => item.label), [ - 'Overview', - 'Working now', - 'Idle / thinking', - 'Advanced details', - ]); - const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); - const idleThinkingSection = await getSectionByLabel(provider, repoItem, 'Idle / thinking'); - assert.equal(workingSection.description, '2'); - assert.equal(idleThinkingSection.description, '3'); - - const blockedItem = await getSessionByBranch(provider, workingSection, 'agent/codex/blocked-task'); - const workingItem = await getSessionByBranch(provider, workingSection, 'agent/codex/working-task'); - const idleItem = await getSessionByBranch(provider, idleThinkingSection, 'agent/codex/idle-task'); - const stalledItem = await getSessionByBranch(provider, idleThinkingSection, 'agent/codex/stalled-task'); - const deadItem = await getSessionByBranch(provider, idleThinkingSection, 'agent/codex/dead-task'); - assert.match(blockedItem.description, /^Blocked: codex · via OpenAI/); - assert.equal(blockedItem.iconPath.id, 'warning'); - assert.match(workingItem.description, /^Working: codex · via OpenAI · 1 changed file/); - assert.equal(workingItem.iconPath.id, 'loading~spin'); - assert.equal(workingItem.contextValue, 'gitguardex.session.working'); - assert.match(idleItem.description, /^Idle: codex · via OpenAI/); - assert.equal(idleItem.iconPath.id, 'comment-discussion'); - assert.match(stalledItem.description, /^Stale: codex · via OpenAI/); - assert.equal(stalledItem.iconPath.id, 'clock'); - assert.equal(stalledItem.contextValue, 'gitguardex.session.stalled'); - assert.match(deadItem.description, /^Dead: codex · via OpenAI/); - assert.equal(deadItem.iconPath.id, 'error'); - assert.equal(deadItem.contextValue, 'gitguardex.session.dead'); - assert.deepEqual(registrations.treeViews[0].badge, { - value: 5, - tooltip: repoItem.description, - }); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension watches active sessions, lock files, logs, and session git indexes', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-watchers-')); - const worktreePath = path.join(tempRoot, 'sandbox'); - initGitRepo(worktreePath); - - const sessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/watch-task', - taskName: 'watch-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - })); - - const { registrations, vscode } = createMockVscode(tempRoot); - let currentSessionFiles = [{ fsPath: sessionPath }]; - vscode.workspace.findFiles = async () => currentSessionFiles; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - assert.deepEqual( - registrations.fileWatchers.map((watcher) => watcher.pattern), - [ - '**/.omx/state/active-sessions/*.json', - '**/.omx/state/agent-file-locks.json', - '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock', - '**/{.omx,.omc}/agent-worktrees/*/.git', - '**/.omx/logs/*.log', - path.join(worktreePath, '.git', 'index'), - ], - ); - - currentSessionFiles = []; - fs.unlinkSync(sessionPath); - registrations.fileWatchers[0].fireDelete({ fsPath: sessionPath }); - await new Promise((resolve) => setTimeout(resolve, 350)); - await flushAsyncWork(); - - assert.equal(registrations.fileWatchers[5].disposed, true); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension debounces refresh events with a trailing 250ms timer', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-debounce-')); - const { registrations, vscode } = createMockVscode(tempRoot); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - provider.onDidChangeTreeDataEmitter.fireCount = 0; - - registrations.fileWatchers[0].fireChange({ fsPath: path.join(tempRoot, '.omx', 'state', 'active-sessions', 'a.json') }); - registrations.fileWatchers[1].fireChange({ fsPath: path.join(tempRoot, '.omx', 'state', 'agent-file-locks.json') }); - registrations.fileWatchers[2].fireChange({ fsPath: path.join(tempRoot, '.omx', 'agent-worktrees', 'agent__codex__a', 'AGENT.lock') }); - registrations.fileWatchers[3].fireChange({ fsPath: path.join(tempRoot, '.omx', 'agent-worktrees', 'agent__codex__a', '.git') }); - registrations.fileWatchers[4].fireChange({ fsPath: path.join(tempRoot, '.omx', 'logs', 'agent-agent__codex__a.log') }); - assert.equal(provider.onDidChangeTreeDataEmitter.fireCount, 0); - - await new Promise((resolve) => setTimeout(resolve, 300)); - await flushAsyncWork(); - - assert.equal(provider.onDidChangeTreeDataEmitter.fireCount, 1); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension commits the selected session worktree from the header prompt', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-commit-view-')); - const worktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-commit-session-')); - initGitRepo(worktreePath); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\nchanged\n', 'utf8'); - fs.mkdirSync(path.join(worktreePath, '.omx', 'state'), { recursive: true }); - fs.writeFileSync( - path.join(worktreePath, '.omx', 'state', 'agent-file-locks.json'), - '{"owner":"codex"}\n', - 'utf8', - ); - - const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, 'agent/codex/commit-task'); - fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); - fs.writeFileSync( - sessionPath, - `${JSON.stringify(sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/commit-task', - taskName: 'commit-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - }), null, 2)}\n`, - 'utf8', - ); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); - const { sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); - registrations.treeViews[0].fireSelection([sessionItem]); - - registrations.inputResponses.push('Ship the selected sandbox'); - assert.equal(registrations.sourceControls.length, 0); - assert.deepEqual(registrations.inputBoxCalls, []); - - await vscode.commands.executeCommand('gitguardex.activeAgents.commitSelectedSession'); - - assert.equal( - registrations.inputBoxCalls.at(-1).placeHolder, - `Commit ${sessionItem.session.agentName} · ${sessionItem.session.taskName} on ${sessionItem.session.branch} · 0 locks`, - ); - assert.equal(registrations.inputBoxCalls.at(-1).prompt, 'Commit codex · commit-task worktree'); - - const commitMessage = runGit(worktreePath, ['log', '-1', '--pretty=%s']).stdout.trim(); - assert.equal(commitMessage, 'Ship the selected sandbox'); - assert.equal(runGit(worktreePath, ['status', '--short', '--', 'tracked.txt']).stdout.trim(), ''); - assert.equal( - runGit(worktreePath, ['status', '--short', '--', '.omx/state/agent-file-locks.json']).stdout.trim(), - '?? .omx/state/agent-file-locks.json', - ); - assert.deepEqual(registrations.informationMessages, []); - assert.deepEqual(registrations.errorMessages, []); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension asks for a session before committing', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-no-selection-')); - const { registrations, vscode } = createMockVscode(tempRoot); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - - await vscode.commands.executeCommand('gitguardex.activeAgents.commitSelectedSession'); - - assert.deepEqual(registrations.informationMessages, ['Pick an Active Agents session first.']); - assert.deepEqual(registrations.errorMessages, []); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension decorates sessions and repo changes from the lock registry', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-lock-decorations-')); - initGitRepo(tempRoot); - fs.writeFileSync(path.join(tempRoot, 'root-file.txt'), 'base\n', 'utf8'); - runGit(tempRoot, ['add', 'root-file.txt']); - runGit(tempRoot, ['commit', '-m', 'baseline']); - fs.writeFileSync(path.join(tempRoot, 'root-file.txt'), 'base\nchanged\n', 'utf8'); - - const worktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-lock-worktree-')); - initGitRepo(worktreePath); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - - const branch = 'agent/codex/live-task'; - const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, branch); - fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); - fs.writeFileSync( - sessionPath, - `${JSON.stringify(sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch, - taskName: 'live-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - }), null, 2)}\n`, - 'utf8', - ); - - const lockPath = path.join(tempRoot, '.omx', 'state', 'agent-file-locks.json'); - fs.mkdirSync(path.dirname(lockPath), { recursive: true }); - fs.writeFileSync(lockPath, `${JSON.stringify({ - locks: { - 'owned-file.txt': { - branch, - claimed_at: '2026-04-22T08:55:00.000Z', - allow_delete: false, - }, - 'root-file.txt': { - branch: 'agent/codex/other-task', - claimed_at: '2026-04-22T08:56:00.000Z', - allow_delete: false, - }, - }, - }, null, 2)}\n`, 'utf8'); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - const idleSection = await getSectionByLabel(provider, repoItem, 'Idle / thinking'); - const unassignedSection = await getSectionByLabel(provider, repoItem, 'Unassigned changes'); - const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, idleSection); - assert.equal(worktreeItem, null); - assert.equal(sessionItem.label, 'live-task'); - assert.equal(sessionItem.session.branch, branch); - assert.match(sessionItem.tooltip, /1 lock/); - - const [changeItem] = await provider.getChildren(unassignedSection); - assert.equal(changeItem.label, 'root-file.txt'); - assert.equal(changeItem.iconPath.id, 'warning'); - assert.match(changeItem.tooltip, /Locked by agent\/codex\/other-task/); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension re-reads lock state on watcher events instead of every tree load', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-lock-watch-')); - const branch = 'agent/codex/live-task'; - const worktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-lock-watch-worktree-')); - initGitRepo(worktreePath); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - - const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, branch); - fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); - fs.writeFileSync( - sessionPath, - `${JSON.stringify(sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch, - taskName: 'live-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - }), null, 2)}\n`, - 'utf8', - ); - - const lockPath = path.join(tempRoot, '.omx', 'state', 'agent-file-locks.json'); - fs.mkdirSync(path.dirname(lockPath), { recursive: true }); - fs.writeFileSync(lockPath, `${JSON.stringify({ - locks: { - 'owned-file.txt': { - branch, - claimed_at: '2026-04-22T08:57:00.000Z', - allow_delete: false, - }, - }, - }, null, 2)}\n`, 'utf8'); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - const originalReadFileSync = fs.readFileSync; - let lockReadCount = 0; - fs.readFileSync = function patchedReadFileSync(filePath, ...args) { - if (path.resolve(String(filePath)) === lockPath) { - lockReadCount += 1; - } - return originalReadFileSync.call(this, filePath, ...args); - }; - - try { - extension.activate(context); - - const provider = registrations.providers[0].provider; - const lockWatcher = registrations.watchers.find((watcher) => watcher.pattern === '**/.omx/state/agent-file-locks.json'); - assert.ok(lockWatcher, 'expected lock watcher registration'); - - const [repoItem] = await provider.getChildren(); - const thinkingSection = await getSectionByLabel(provider, repoItem, 'Idle / thinking'); - const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, thinkingSection); - assert.equal(worktreeItem, null); - assert.equal(sessionItem.label, 'live-task'); - assert.equal(sessionItem.session.branch, branch); - assert.equal(lockReadCount, 1); - - await provider.getChildren(); - assert.equal(lockReadCount, 1); - - fs.writeFileSync(lockPath, `${JSON.stringify({ - locks: { - 'owned-file.txt': { - branch, - claimed_at: '2026-04-22T08:57:00.000Z', - allow_delete: false, - }, - 'second-owned-file.txt': { - branch, - claimed_at: '2026-04-22T08:58:00.000Z', - allow_delete: false, - }, - }, - }, null, 2)}\n`, 'utf8'); - lockWatcher.fireChange({ fsPath: lockPath }); - assert.equal(lockReadCount, 2); - - const [updatedRepoItem] = await provider.getChildren(); - const updatedThinkingSection = await getSectionByLabel(provider, updatedRepoItem, 'Idle / thinking'); - const { worktreeItem: updatedWorktreeItem, sessionItem: updatedSessionItem } = await getOnlyWorktreeAndSession(provider, updatedThinkingSection); - assert.equal(updatedWorktreeItem, null); - assert.equal(updatedSessionItem.label, 'live-task'); - assert.equal(updatedSessionItem.session.branch, branch); - - await provider.getChildren(); - assert.equal(lockReadCount, 2); - } finally { - fs.readFileSync = originalReadFileSync; - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } - } -}); - -test('active-agents extension launches finish and sync commands in session terminals', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-inline-actions-')); - const worktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-inline-worktree-')); - initGitRepo(worktreePath); - fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); - runGit(worktreePath, ['add', 'tracked.txt']); - runGit(worktreePath, ['commit', '-m', 'baseline']); - - const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, 'agent/codex/live-task'); - fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); - fs.writeFileSync( - sessionPath, - `${JSON.stringify(sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/live-task', - taskName: 'live-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - }), null, 2)}\n`, - 'utf8', - ); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - const idleSection = await getSectionByLabel(provider, repoItem, 'Idle / thinking'); - const { sessionItem } = await getOnlyWorktreeAndSession(provider, idleSection); - - await registrations.commands.get('gitguardex.activeAgents.finishSession')(sessionItem.session); - await registrations.commands.get('gitguardex.activeAgents.syncSession')(sessionItem.session); - - assert.equal(registrations.terminals.length, 2); - assert.equal(registrations.terminals[0].options.cwd, worktreePath); - assert.equal(registrations.terminals[0].options.iconPath.id, 'check'); - assert.match(registrations.terminals[0].options.name, /GitGuardex Finish: live-task/); - assert.deepEqual(registrations.terminals[0].sentTexts, [ - { text: "gx branch finish --branch 'agent/codex/live-task'", addNewLine: true }, - ]); - assert.equal(registrations.terminals[0].shown, true); - - assert.equal(registrations.terminals[1].options.cwd, worktreePath); - assert.equal(registrations.terminals[1].options.iconPath.id, 'sync'); - assert.match(registrations.terminals[1].options.name, /GitGuardex Sync: live-task/); - assert.deepEqual(registrations.terminals[1].sentTexts, [ - { text: 'gx sync', addNewLine: true }, - ]); - assert.equal(registrations.terminals[1].shown, true); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension opens and refreshes the inspect panel from shared watcher events', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-inspect-panel-')); - const remoteRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-inspect-remote-')); - const branch = 'agent/codex/inspect-task'; - - initGitRepo(tempRoot); - runGit(tempRoot, ['checkout', '-b', 'main']); - fs.writeFileSync(path.join(tempRoot, 'tracked.txt'), 'base\n', 'utf8'); - runGit(tempRoot, ['add', 'tracked.txt']); - runGit(tempRoot, ['commit', '-m', 'baseline']); - runGit(remoteRoot, ['init', '--bare']); - runGit(tempRoot, ['remote', 'add', 'origin', remoteRoot]); - runGit(tempRoot, ['push', '-u', 'origin', 'main']); - runGit(tempRoot, ['config', 'multiagent.baseBranch', 'main']); - runGit(tempRoot, ['checkout', '-b', branch]); - fs.writeFileSync(path.join(tempRoot, 'tracked.txt'), 'base\ninspect\n', 'utf8'); - runGit(tempRoot, ['add', 'tracked.txt']); - runGit(tempRoot, ['commit', '-m', 'inspect ahead commit']); - - const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, branch); - fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); - fs.writeFileSync( - sessionPath, - `${JSON.stringify(sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch, - taskName: 'inspect-task', - agentName: 'codex', - worktreePath: tempRoot, - pid: process.pid, - cliName: 'codex', - }), null, 2)}\n`, - 'utf8', - ); - - const lockPath = path.join(tempRoot, '.omx', 'state', 'agent-file-locks.json'); - fs.mkdirSync(path.dirname(lockPath), { recursive: true }); - fs.writeFileSync(lockPath, `${JSON.stringify({ - locks: { - 'src/owned-file.txt': { - branch, - claimed_at: '2026-04-22T09:13:00.000Z', - allow_delete: false, - }, - }, - }, null, 2)}\n`, 'utf8'); - - const logPath = path.join( - tempRoot, - '.omx', - 'logs', - `agent-${sessionSchema.sanitizeBranchForFile(branch)}.log`, - ); - fs.mkdirSync(path.dirname(logPath), { recursive: true }); - fs.writeFileSync(logPath, 'log line 1\n', 'utf8'); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async (pattern) => { - if (pattern === '**/.omx/state/active-sessions/*.json') { - return [{ fsPath: sessionPath }]; - } - return []; - }; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - const groupSection = await getSectionByLabel(provider, repoItem, 'Working now'); - const { sessionItem } = await getOnlyWorktreeAndSession(provider, groupSection); - - await registrations.commands.get('gitguardex.activeAgents.inspect')(sessionItem.session); - - assert.equal(registrations.webviewPanels.length, 1); - const panel = registrations.webviewPanels[0]; - assert.equal(panel.viewType, 'gitguardex.activeAgents.inspect'); - assert.match(panel.title, /Inspect inspect-task/); - assert.match(panel.webview.html, /origin\/main/); - assert.match(panel.webview.html, /1 ahead/); - assert.match(panel.webview.html, /0 behind/); - assert.match(panel.webview.html, /src\/owned-file.txt/); - assert.match(panel.webview.html, /log line 1/); - - fs.writeFileSync(logPath, 'log line 1\nlog line 2\n', 'utf8'); - const logWatcher = registrations.watchers.find((watcher) => watcher.pattern === '**/.omx/logs/*.log'); - assert.ok(logWatcher, 'expected log watcher registration'); - logWatcher.fireChange({ fsPath: logPath }); - await new Promise((resolve) => setTimeout(resolve, 300)); - await flushAsyncWork(); - - assert.match(panel.webview.html, /log line 2/); - - await registrations.commands.get('gitguardex.activeAgents.inspect')(sessionItem.session); - assert.equal(registrations.webviewPanels.length, 1); - assert.equal(panel.revealCalls.length, 1); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension reveals the matching session terminal and opens a fallback worktree terminal when needed', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-show-terminal-')); - const worktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-show-terminal-worktree-')); - const { registrations, vscode } = createMockVscode(tempRoot); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - - const liveTerminal = vscode.window.createTerminal({ - name: `GitGuardex: ${path.basename(tempRoot)}`, - cwd: tempRoot, - processId: 4242, - }); - await registrations.commands.get('gitguardex.activeAgents.showSessionTerminal')({ - label: 'live-task', - branch: 'agent/codex/live-task', - pid: 4242, - repoRoot: tempRoot, - worktreePath, - }); - - assert.equal(registrations.terminals.length, 1); - assert.equal(liveTerminal.shown, true); - assert.deepEqual(liveTerminal.showArgs, [false]); - assert.deepEqual(liveTerminal.sentTexts, []); - - await registrations.commands.get('gitguardex.activeAgents.showSessionTerminal')({ - label: 'fallback-task', - branch: 'agent/codex/fallback-task', - pid: 9001, - repoRoot: tempRoot, - worktreePath, - }); - - assert.equal(registrations.terminals.length, 2); - assert.equal(registrations.terminals[1].options.name, 'GitGuardex Terminal: fallback-task'); - assert.equal(registrations.terminals[1].options.cwd, worktreePath); - assert.equal(registrations.terminals[1].options.iconPath.id, 'terminal'); - assert.equal(registrations.terminals[1].shown, true); - assert.deepEqual(registrations.terminals[1].showArgs, [false]); - assert.deepEqual(registrations.terminals[1].sentTexts, []); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension stops matching session terminals with Ctrl+C before gx fallback', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-stop-session-')); - const worktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-stop-worktree-')); - const { registrations, vscode } = createMockVscode(tempRoot); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - vscode.window.showWarningMessage = async (...args) => { - registrations.warningMessages.push(args); - return 'Stop'; - }; - - extension.activate(context); - const provider = registrations.providers[0].provider; - await flushAsyncWork(); - provider.onDidChangeTreeDataEmitter.fireCount = 0; - - const liveTerminal = vscode.window.createTerminal({ - name: `GitGuardex: ${path.basename(tempRoot)}`, - cwd: tempRoot, - processId: 4242, - }); - - await registrations.commands.get('gitguardex.activeAgents.stopSession')({ - label: 'live-task', - branch: 'agent/codex/live-task', - pid: 4242, - repoRoot: tempRoot, - worktreePath, - }); - await flushAsyncWork(); - - assert.ok(registrations.providers[0].provider.onDidChangeTreeDataEmitter.fireCount >= 1); - assert.equal(registrations.warningMessages.length, 1); - assert.match(registrations.warningMessages[0][0], /Stop live-task\?/); - assert.match(registrations.warningMessages[0][1].detail, /Ctrl\+C/); - assert.equal(liveTerminal.shown, true); - assert.deepEqual(liveTerminal.showArgs, [false]); - assert.deepEqual(liveTerminal.sentTexts, [ - { text: '\u0003', addNewLine: false }, - ]); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension confirms stop and routes through gx agents stop --pid when no live terminal matches', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-stop-session-fallback-')); - const worktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-stop-worktree-fallback-')); - const { registrations, vscode } = createMockVscode(tempRoot); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - let execCall = null; - const originalExecFile = cp.execFile; - - vscode.window.showWarningMessage = async (...args) => { - registrations.warningMessages.push(args); - return 'Stop'; - }; - cp.execFile = (command, args, options, callback) => { - execCall = { command, args, options }; - callback(null, '[gx] Stopped agent pid 4242 (stopped).\n', ''); - }; - - try { - extension.activate(context); - const provider = registrations.providers[0].provider; - await flushAsyncWork(); - provider.onDidChangeTreeDataEmitter.fireCount = 0; - - await registrations.commands.get('gitguardex.activeAgents.stopSession')({ - label: 'live-task', - branch: 'agent/codex/live-task', - pid: 4242, - repoRoot: tempRoot, - worktreePath, - }); - await flushAsyncWork(); - } finally { - cp.execFile = originalExecFile; - } - - assert.deepEqual(execCall, { - command: 'gx', - args: ['agents', 'stop', '--pid', '4242', '--target', tempRoot], - options: { - cwd: tempRoot, - encoding: 'utf8', - maxBuffer: 1024 * 1024, - }, - }); - assert.ok(registrations.providers[0].provider.onDidChangeTreeDataEmitter.fireCount >= 1); - assert.equal(registrations.warningMessages.length, 1); - assert.match(registrations.warningMessages[0][0], /Stop live-task\?/); - assert.match(registrations.warningMessages[0][1].detail, /--pid/); - assert.match(registrations.warningMessages[0][1].detail, /4242/); - assert.match(registrations.warningMessages[0][1].detail, /--target/); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension dismisses stalled session rows by deleting the matching active-session record', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-dismiss-session-')); - const worktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-dismiss-worktree-')); - const sessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch: 'agent/codex/stalled-task', - taskName: 'stalled-task', - agentName: 'codex', - worktreePath, - pid: 4242, - cliName: 'codex', - })); - const { registrations, vscode } = createMockVscode(tempRoot); - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - vscode.window.showWarningMessage = async (...args) => { - registrations.warningMessages.push(args); - return 'Dismiss'; - }; - - extension.activate(context); - const provider = registrations.providers[0].provider; - await flushAsyncWork(); - provider.onDidChangeTreeDataEmitter.fireCount = 0; - - await registrations.commands.get('gitguardex.activeAgents.dismissSession')({ - label: 'stalled-task', - branch: 'agent/codex/stalled-task', - activityKind: 'stalled', - repoRoot: tempRoot, - worktreePath, - }); - await flushAsyncWork(); - - assert.equal(fs.existsSync(sessionPath), false); - assert.ok(registrations.providers[0].provider.onDidChangeTreeDataEmitter.fireCount >= 1); - assert.equal(registrations.warningMessages.length, 1); - assert.match(registrations.warningMessages[0][0], /Dismiss stalled-task\?/); - assert.match(registrations.warningMessages[0][1].detail, /\.omx[\/\\]state[\/\\]active-sessions/); - assert.match(registrations.warningMessages[0][1].detail, /stale sidebar row only/); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension uses bundled OpenSpec icons in Active Agents tree nodes', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-openspec-icons-')); - initGitRepo(tempRoot); - const branch = 'agent/codex/openspec-icons'; - runGit(tempRoot, ['checkout', '-b', branch]); - - const proposalPath = path.join(tempRoot, 'openspec', 'changes', 'icon-pass', 'proposal.md'); - const tasksPath = path.join(tempRoot, 'openspec', 'changes', 'icon-pass', 'tasks.md'); - const specPath = path.join(tempRoot, 'openspec', 'changes', 'icon-pass', 'specs', 'active-agents-icons', 'spec.md'); - fs.mkdirSync(path.dirname(proposalPath), { recursive: true }); - fs.mkdirSync(path.dirname(specPath), { recursive: true }); - fs.writeFileSync(proposalPath, 'proposal base\n', 'utf8'); - fs.writeFileSync(tasksPath, 'tasks base\n', 'utf8'); - fs.writeFileSync(specPath, 'spec base\n', 'utf8'); - runGit(tempRoot, ['add', 'openspec']); - runGit(tempRoot, ['commit', '-m', 'baseline']); - fs.writeFileSync(proposalPath, 'proposal base\nchanged\n', 'utf8'); - fs.writeFileSync(tasksPath, 'tasks base\nchanged\n', 'utf8'); - fs.writeFileSync(specPath, 'spec base\nchanged\n', 'utf8'); - - const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, branch); - fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); - fs.writeFileSync( - sessionPath, - `${JSON.stringify(sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch, - taskName: 'openspec-icons', - agentName: 'codex', - worktreePath: tempRoot, - pid: process.pid, - cliName: 'codex', - }), null, 2)}\n`, - 'utf8', - ); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }]; - const extension = loadExtensionWithMockVscode(vscode); - const context = { subscriptions: [] }; - - extension.activate(context); - await flushAsyncWork(); - - const provider = registrations.providers[0].provider; - const [repoItem] = await provider.getChildren(); - const advancedSection = await getSectionByLabel(provider, repoItem, 'Advanced details'); - const activeAgentTree = await getSectionByLabel(provider, advancedSection, 'Active agent tree'); - const rawWorkingSection = await getSectionByLabel(provider, activeAgentTree, 'WORKING NOW'); - const { sessionItem } = await getOnlyWorktreeAndSession(provider, rawWorkingSection); - - const openspecFolder = await getChildByLabel(provider, sessionItem, 'openspec'); - const changesFolder = await getChildByLabel(provider, openspecFolder, 'changes'); - assertBundledIcon(changesFolder, 'openspec.svg'); - - const iconPassFolder = await getChildByLabel(provider, changesFolder, 'icon-pass'); - const proposalItem = await getChildByLabel(provider, iconPassFolder, 'proposal.md'); - const specsFolder = await getChildByLabel(provider, iconPassFolder, 'specs'); - const tasksItem = await getChildByLabel(provider, iconPassFolder, 'tasks.md'); - assertBundledIcon(proposalItem, 'openspec.svg'); - assertBundledIcon(specsFolder, 'spec.svg'); - assertBundledIcon(tasksItem, 'plan.svg'); - - const activeAgentsIconsFolder = await getChildByLabel(provider, specsFolder, 'active-agents-icons'); - const specItem = await getChildByLabel(provider, activeAgentsIconsFolder, 'spec.md'); - assertBundledIcon(specItem, 'spec.svg'); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); - -test('active-agents extension keeps semantic OpenSpec icons for delta-only unassigned changes', async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-openspec-unassigned-icons-')); - initGitRepo(tempRoot); - fs.writeFileSync(path.join(tempRoot, 'tracked.txt'), 'base\n', 'utf8'); - runGit(tempRoot, ['add', 'tracked.txt']); - runGit(tempRoot, ['commit', '-m', 'baseline']); - runGit(tempRoot, ['checkout', '-b', 'agent/codex/unassigned-root']); - const branch = 'agent/codex/live-task'; - const worktreePath = path.join( - tempRoot, - '.omx', - 'agent-worktrees', - 'agent__codex__live-task', - ); - fs.mkdirSync(path.dirname(worktreePath), { recursive: true }); - runGit(tempRoot, ['worktree', 'add', '-b', branch, worktreePath]); - const changeDir = path.join(tempRoot, 'openspec', 'changes', 'icon-pass'); - const proposalPath = path.join(changeDir, 'proposal.md'); - const tasksPath = path.join(changeDir, 'tasks.md'); - const specPath = path.join(changeDir, 'specs', 'active-agents-icons', 'spec.md'); - fs.mkdirSync(path.dirname(specPath), { recursive: true }); - fs.writeFileSync(proposalPath, 'proposal\n', 'utf8'); - fs.writeFileSync(tasksPath, 'tasks\n', 'utf8'); - fs.writeFileSync(specPath, 'spec\n', 'utf8'); - writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ - repoRoot: tempRoot, - branch, - taskName: 'live-task', - agentName: 'codex', - worktreePath, - pid: process.pid, - cliName: 'codex', - state: 'working', - })); - - const { registrations, vscode } = createMockVscode(tempRoot); - vscode.workspace.findFiles = async () => []; - let repoChanges = [ - { - relativePath: 'openspec/changes/icon-pass/proposal.md', - absolutePath: proposalPath, - statusLabel: 'A', - statusText: 'Added', - }, - { - relativePath: 'openspec/changes/icon-pass/tasks.md', - absolutePath: tasksPath, - statusLabel: 'A', - statusText: 'Added', - }, - { - relativePath: 'openspec/changes/icon-pass/specs/active-agents-icons/spec.md', - absolutePath: specPath, - statusLabel: 'A', - statusText: 'Added', - }, - ]; - const mockSessionSchema = { - ...sessionSchema, - readActiveSessions: () => sessionSchema.readActiveSessions(tempRoot, { includeStale: true }), - readRepoChanges: () => repoChanges, - }; - const extension = loadExtensionWithMockVscode(vscode, mockSessionSchema); - const context = { subscriptions: [] }; - - extension.activate(context); - const provider = registrations.providers[0].provider; - await provider.getChildren(); - await flushAsyncWork(); - - repoChanges = repoChanges.map((change) => ({ - ...change, - statusLabel: 'M', - statusText: 'Modified', - })); - const [repoItem] = await provider.getChildren(); - const unassignedSection = await getSectionByLabel(provider, repoItem, 'Unassigned changes'); - const unassignedItems = await provider.getChildren(unassignedSection); - assert.equal(unassignedItems.length, 3); - - const proposalItem = unassignedItems.find((item) => item.label === 'openspec/.../proposal.md'); - const tasksItem = unassignedItems.find((item) => item.label === 'openspec/.../tasks.md'); - const specItem = unassignedItems.find((item) => item.label === 'openspec/.../spec.md'); - assert.ok(proposalItem); - assert.ok(tasksItem); - assert.ok(specItem); - assert.equal(proposalItem.description, 'M · Updated'); - assert.equal(tasksItem.description, 'M · Updated'); - assert.equal(specItem.description, 'M · Updated'); - assertBundledIcon(proposalItem, 'openspec.svg'); - assertBundledIcon(tasksItem, 'plan.svg'); - assertBundledIcon(specItem, 'spec.svg'); - - for (const subscription of context.subscriptions) { - subscription.dispose?.(); - } -}); diff --git a/vscode/guardex-active-agents/README.md b/vscode/guardex-active-agents/README.md deleted file mode 100644 index ea5ff11a..00000000 --- a/vscode/guardex-active-agents/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# GitGuardex Active Agents - -Local VS Code companion for Guardex-managed repos. - -## Quick Start - -Use the dedicated Active Agents sidebar icon to create or inspect Guardex sandboxes quickly. - -1. Install from a Guardex-wired repo: - -```sh -node scripts/install-vscode-active-agents-extension.js -``` - -2. Reload the VS Code window. -3. In the Activity Bar, open the dedicated `Active Agents` hive icon. Use `Start agent` to enter a task + agent name and launch the repo Guardex agent runner. The companion prefers `bash scripts/codex-agent.sh` when present, falls back to `npm run agent:codex --`, and only uses `gx branch start` as a last resort. - -What it does: - -- Bundles a local GitGuardex icon so repo installs show branded extension metadata inside VS Code. -- Bundles the optional `GitGuardex File Icons` theme for OpenSpec, agent worktree, and hook files in Explorer. -- Adds a dedicated `Active Agents` Activity Bar container with a hive icon and live badge count for active sessions. -- Renders one repo node per live Guardex workspace with grouped `ACTIVE AGENTS` and `CHANGES` sections. -- Splits live sessions inside `ACTIVE AGENTS` into `BLOCKED`, `WORKING NOW`, `THINKING`, `STALLED`, and `DEAD` groups so stuck, active, and inactive lanes stand out immediately. -- Mirrors the same live state in the VS Code status bar so the selected session or active-agent count stays visible outside the tree. -- Keeps the built-in Source Control view focused on real Git repositories; the Active Agents commit command prompts for a message from its own toolbar action. -- Shows one row per live Guardex sandbox session inside those activity groups, with changed-file rows nested under sessions that are touching files. -- Labels session rows with provider identity and snapshot context; snapshot-backed rows use a one-letter snapshot badge such as `N` for `nagyviktor@edixa.com`. -- Shows raw agent branch groups with the `git-branch` icon instead of the generic folder icon. -- Shows repo-root git changes in a sibling `CHANGES` section when the guarded repo itself is dirty. -- Derives session state from dirty worktree status, git conflict markers, heartbeat freshness, PID liveness, and recent file mtimes, surfaces working/dead/conflict counts in the repo/header summary, and shows changed-file counts for active edits. -- Uses distinct VS Code codicons for each session state, including animated `loading~spin` for `WORKING NOW`. -- Reads repo-local presence files from `.omx/state/active-sessions/`, expects `lastHeartbeatAt` freshness, and falls back to managed worktree-root `AGENT.lock` telemetry when the launcher session file is absent. -- Publishes `guardex.hasAgents` and `guardex.hasConflicts` context keys for other VS Code contributions. diff --git a/vscode/guardex-active-agents/extension.js b/vscode/guardex-active-agents/extension.js deleted file mode 100644 index 6154f838..00000000 --- a/vscode/guardex-active-agents/extension.js +++ /dev/null @@ -1,3892 +0,0 @@ -const fs = require('node:fs'); -const path = require('node:path'); -const cp = require('node:child_process'); -const http = require('node:http'); -const os = require('node:os'); -const vscode = require('vscode'); -const { - clearWorktreeActivityCache, - formatElapsedFrom, - readActiveSessions, - readRepoChanges, - readSessionInspectData, - sanitizeBranchForFile, - sessionFilePathForBranch, -} = require('./session-schema.js'); - -const SESSION_DECORATION_SCHEME = 'gitguardex-agent'; -const IDLE_WARNING_MS = 10 * 60 * 1000; -const IDLE_ERROR_MS = 30 * 60 * 1000; -const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json'); -const ACTIVE_SESSION_FILES_GLOB = '**/.omx/state/active-sessions/*.json'; -const AGENT_FILE_LOCKS_GLOB = '**/.omx/state/agent-file-locks.json'; -const WORKTREE_AGENT_LOCKS_GLOB = '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock'; -const MANAGED_WORKTREE_GIT_FILES_GLOB = '**/{.omx,.omc}/agent-worktrees/*/.git'; -const MANAGED_WORKTREE_RELATIVE_ROOTS = [ - path.join('.omx', 'agent-worktrees'), - path.join('.omc', 'agent-worktrees'), -]; -const AGENT_LOG_FILES_GLOB = '**/.omx/logs/*.log'; -const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.omc/agent-worktrees}/**'; -const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**'; -const MANAGED_WORKTREE_GIT_SCAN_EXCLUDE_GLOB = '**/node_modules/**'; -const SESSION_SCAN_LIMIT = 200; -const REFRESH_DEBOUNCE_MS = 250; -const RECENTLY_ACTIVE_WINDOW_MS = 10 * 60 * 1000; -const SESSION_TOP_FILE_COUNT = 3; -const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agents', 'package.json'); -const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js'); -const RELOAD_WINDOW_ACTION = 'Reload Window'; -const UPDATE_LATER_ACTION = 'Later'; -const ACTIVE_AGENTS_EXTENSION_ID = 'Recodee.gitguardex-active-agents'; -const RESTART_EXTENSION_HOST_COMMAND = 'workbench.action.restartExtensionHost'; -const REFRESH_POLL_INTERVAL_MS = 30_000; -const INSPECT_PANEL_VIEW_TYPE = 'gitguardex.activeAgents.inspect'; -const COLONY_DEFAULT_PORT = 37777; -const COLONY_SNAPSHOT_TTL_MS = 5_000; -const COLONY_FETCH_TIMEOUT_MS = 800; - -function colonyDataDir() { - return process.env.COLONY_HOME - || process.env.CAVEMEM_HOME - || path.join(os.homedir(), '.colony'); -} - -function readColonyPort() { - try { - const raw = fs.readFileSync(path.join(colonyDataDir(), 'settings.json'), 'utf8'); - const parsed = JSON.parse(raw); - const port = Number(parsed?.workerPort); - return Number.isFinite(port) && port > 0 ? port : COLONY_DEFAULT_PORT; - } catch (_error) { - return COLONY_DEFAULT_PORT; - } -} - -function fetchColonyJson(urlPath) { - return new Promise((resolve) => { - const req = http.get( - { - hostname: '127.0.0.1', - port: readColonyPort(), - path: urlPath, - timeout: COLONY_FETCH_TIMEOUT_MS, - }, - (res) => { - if (res.statusCode !== 200) { - res.resume(); - resolve(null); - return; - } - let body = ''; - res.setEncoding('utf8'); - res.on('data', (chunk) => { - body += chunk; - }); - res.on('end', () => { - try { - resolve(JSON.parse(body)); - } catch (_error) { - resolve(null); - } - }); - }, - ); - req.on('error', () => resolve(null)); - req.on('timeout', () => { - req.destroy(); - resolve(null); - }); - }); -} - -const colonyTasksCache = new Map(); - -async function readColonyTasksForRepo(repoRoot) { - const cached = colonyTasksCache.get(repoRoot); - if (cached && Date.now() - cached.at < COLONY_SNAPSHOT_TTL_MS) { - return cached.tasks; - } - const tasks = await fetchColonyJson( - `/api/colony/tasks?repo_root=${encodeURIComponent(repoRoot)}`, - ); - const resolved = Array.isArray(tasks) ? tasks : []; - colonyTasksCache.set(repoRoot, { at: Date.now(), tasks: resolved }); - return resolved; -} - -function compactColonyBranchLabel(branch) { - if (typeof branch !== 'string' || !branch) return 'unknown'; - const parts = branch.split('/').filter(Boolean); - return parts.length > 2 ? parts.slice(-2).join('/') : branch; -} -const GIT_CONFIGURATION_SECTION = 'git'; -const REPO_SCAN_IGNORED_FOLDERS_SETTING = 'repositoryScanIgnoredFolders'; -const BUNDLED_FILE_ICONS_MANIFEST_RELATIVE = path.join('fileicons', 'gitguardex-fileicons.json'); -const MANAGED_REPO_SCAN_IGNORED_FOLDERS = [ - '.omx/agent-worktrees', - '**/.omx/agent-worktrees', - '.omx/.tmp-worktrees', - '**/.omx/.tmp-worktrees', - '.omc/agent-worktrees', - '**/.omc/agent-worktrees', - '.omc/.tmp-worktrees', - '**/.omc/.tmp-worktrees', -]; -const SESSION_ACTIVITY_GROUPS = [ - { kind: 'blocked', label: 'BLOCKED' }, - { kind: 'working', label: 'WORKING NOW' }, - { kind: 'finished', label: 'NEEDS CLEANUP' }, - { kind: 'idle', label: 'THINKING' }, - { kind: 'stalled', label: 'STALLED' }, - { kind: 'dead', label: 'DEAD' }, -]; -const SESSION_ACTIVITY_ICON_IDS = { - blocked: 'warning', - working: 'loading~spin', - finished: 'pass-filled', - idle: 'comment-discussion', - stalled: 'clock', - dead: 'error', -}; -const DISMISSABLE_SESSION_ACTIVITY_KINDS = new Set(['stalled', 'dead']); -const SESSION_PROVIDER_BRANDS = { - openai: { - id: 'openai', - label: 'OpenAI', - badge: 'AI', - }, - claude: { - id: 'claude', - label: 'Claude', - badge: 'CL', - }, -}; -let bundledTreeIconThemeCache = null; - -function iconColorId(iconId) { - switch (iconId) { - case 'warning': - case 'clock': - return 'list.warningForeground'; - case 'error': - return 'list.errorForeground'; - case 'loading~spin': - return 'gitDecoration.addedResourceForeground'; - case 'comment-discussion': - case 'info': - case 'repo': - case 'folder': - case 'graph': - case 'history': - case 'dashboard': - case 'inbox': - case 'file-directory': - case 'settings-gear': - case 'folder-library': - return 'textLink.foreground'; - case 'git-branch': - return 'gitDecoration.modifiedResourceForeground'; - case 'account': - return 'terminal.ansiYellow'; - case 'debug-pause': - return 'terminal.ansiYellow'; - case 'sparkle': - case 'rocket': - return 'terminal.ansiMagenta'; - case 'list-flat': - case 'device-camera': - return 'terminal.ansiCyan'; - case 'list-tree': - case 'telescope': - return 'terminal.ansiBlue'; - case 'organization': - return 'terminal.ansiGreen'; - case 'pass-filled': - case 'pass': - case 'check': - return 'testing.iconPassed'; - default: - return ''; - } -} - -function themeIcon(iconId, colorId = iconColorId(iconId)) { - if (!iconId) { - return undefined; - } - return colorId - ? new vscode.ThemeIcon(iconId, new vscode.ThemeColor(colorId)) - : new vscode.ThemeIcon(iconId); -} - -function sessionDecorationUri(branch) { - return vscode.Uri.parse(`${SESSION_DECORATION_SCHEME}://${sanitizeBranchForFile(branch)}`); -} - -function emptyBundledTreeIconTheme() { - return { - iconPathById: new Map(), - fileNames: {}, - folderNames: {}, - fileExtensions: {}, - }; -} - -function loadBundledTreeIconTheme() { - if (bundledTreeIconThemeCache) { - return bundledTreeIconThemeCache; - } - - const manifestPath = path.join(__dirname, BUNDLED_FILE_ICONS_MANIFEST_RELATIVE); - try { - const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); - const manifestDir = path.dirname(manifestPath); - const iconPathById = new Map(); - for (const [iconId, definition] of Object.entries(parsed?.iconDefinitions || {})) { - if (typeof definition?.iconPath !== 'string' || !definition.iconPath.trim()) { - continue; - } - const iconUri = vscode.Uri.file(path.resolve(manifestDir, definition.iconPath)); - iconPathById.set(iconId, { - light: iconUri, - dark: iconUri, - }); - } - bundledTreeIconThemeCache = { - iconPathById, - fileNames: parsed?.fileNames || {}, - folderNames: parsed?.folderNames || {}, - fileExtensions: parsed?.fileExtensions || {}, - }; - } catch (_error) { - bundledTreeIconThemeCache = emptyBundledTreeIconTheme(); - } - - return bundledTreeIconThemeCache; -} - -function resolveBundledTreeItemIconId(relativePath, kind = 'file') { - const normalizedRelativePath = normalizeRelativePath(relativePath); - const entryName = path.posix.basename(normalizedRelativePath || ''); - if (!entryName) { - return ''; - } - - const bundledTheme = loadBundledTreeIconTheme(); - if (kind === 'folder') { - return bundledTheme.folderNames[entryName] || ''; - } - - if (bundledTheme.fileNames[entryName]) { - return bundledTheme.fileNames[entryName]; - } - - const matchingExtension = Object.keys(bundledTheme.fileExtensions) - .sort((left, right) => right.length - left.length) - .find((extension) => entryName === extension || entryName.endsWith(`.${extension}`)); - return matchingExtension ? bundledTheme.fileExtensions[matchingExtension] : ''; -} - -function resolveBundledTreeItemIcon(relativePath, kind = 'file') { - const bundledTheme = loadBundledTreeIconTheme(); - const iconId = resolveBundledTreeItemIconId(relativePath, kind); - return iconId ? bundledTheme.iconPathById.get(iconId) : undefined; -} - -function sessionIdleDecoration(session, now = Date.now()) { - if (!session) { - return undefined; - } - - if (session.activityKind === 'blocked') { - return { - badge: '!', - tooltip: 'blocked', - color: new vscode.ThemeColor('list.warningForeground'), - }; - } - if (session.activityKind === 'dead') { - return { - badge: 'x', - tooltip: 'dead', - color: new vscode.ThemeColor('list.errorForeground'), - }; - } - if (session.activityKind === 'stalled') { - return { - badge: '!', - tooltip: 'stalled', - color: new vscode.ThemeColor('list.errorForeground'), - }; - } - if (session.activityKind === 'working') { - return undefined; - } - - const startedAtMs = Date.parse(session.startedAt); - if (!Number.isFinite(startedAtMs)) { - return undefined; - } - - const elapsedMs = now - startedAtMs; - if (elapsedMs > IDLE_ERROR_MS) { - return { - badge: '30m+', - tooltip: 'idle 30m+', - color: new vscode.ThemeColor('list.errorForeground'), - }; - } - if (elapsedMs > IDLE_WARNING_MS) { - return { - badge: '10m+', - tooltip: 'idle 10m+', - color: new vscode.ThemeColor('list.warningForeground'), - }; - } - - return undefined; -} - -function formatCountLabel(count, singular, plural = `${singular}s`) { - return `${count} ${count === 1 ? singular : plural}`; -} - -function branchSegments(branch) { - return String(branch || '') - .split('/') - .map((segment) => segment.trim()) - .filter(Boolean); -} - -function compactBranchLabel(branch) { - const segments = branchSegments(branch); - if (segments.length >= 3 && segments[0] === 'agent') { - return `${segments[1]}/${segments.slice(2).join('/')}`; - } - return segments.join('/'); -} - -function sessionFileCountLabel(session) { - const activityCountLabel = typeof session?.activityCountLabel === 'string' - ? session.activityCountLabel.trim() - : ''; - if (activityCountLabel) { - return activityCountLabel; - } - if ((session?.changeCount || 0) > 0) { - return formatCountLabel(session.changeCount, 'file'); - } - return ''; -} - -function uniqueStringList(values) { - const seen = new Set(); - const result = []; - - for (const value of values) { - if (typeof value !== 'string' || seen.has(value)) { - continue; - } - seen.add(value); - result.push(value); - } - - return result; -} - -function normalizeSessionProviderToken(value) { - return typeof value === 'string' ? value.trim().toLowerCase() : ''; -} - -function resolveSessionProvider(session) { - const signals = [ - session?.cliName, - session?.agentName, - session?.branch, - ] - .map(normalizeSessionProviderToken) - .filter(Boolean); - - if (signals.some((value) => value.includes('claude'))) { - return { - ...SESSION_PROVIDER_BRANDS.claude, - cliName: typeof session?.cliName === 'string' ? session.cliName.trim() : '', - }; - } - if (signals.some((value) => value.includes('codex') || value.includes('openai'))) { - return { - ...SESSION_PROVIDER_BRANDS.openai, - cliName: typeof session?.cliName === 'string' ? session.cliName.trim() : '', - }; - } - return null; -} - -function sessionProviderDecoration(session) { - const provider = resolveSessionProvider(session); - if (!provider) { - return undefined; - } - - const cliName = provider.cliName || provider.id; - return { - badge: provider.badge, - tooltip: `${provider.label} session via ${cliName}`, - }; -} - -function normalizeSnapshotIdentityValue(value) { - return typeof value === 'string' ? value.trim() : ''; -} - -function sessionSnapshotDisplayName(session) { - return normalizeSnapshotIdentityValue(session?.snapshotName) - || normalizeSnapshotIdentityValue(session?.snapshotEmail); -} - -function sessionSnapshotBadge(session) { - const displayName = sessionSnapshotDisplayName(session); - const match = displayName.match(/[a-z0-9]/i); - return match ? match[0].toUpperCase() : ''; -} - -function sessionSnapshotDescription(session) { - const displayName = sessionSnapshotDisplayName(session); - return displayName ? `snapshot ${displayName}` : ''; -} - -function sessionSnapshotDecoration(session) { - const badge = sessionSnapshotBadge(session); - const displayName = sessionSnapshotDisplayName(session); - if (!badge || !displayName) { - return undefined; - } - - return { - badge, - tooltip: `Snapshot ${displayName}`, - }; -} - -function sessionIdentityDecoration(session) { - return sessionSnapshotDecoration(session) || sessionProviderDecoration(session); -} - -function stringListsEqual(left, right) { - if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) { - return false; - } - - return left.every((value, index) => value === right[index]); -} - -async function ensureManagedRepoScanIgnores() { - if (typeof vscode.workspace.getConfiguration !== 'function') { - return; - } - - const workspaceFolders = vscode.workspace.workspaceFolders || []; - if (workspaceFolders.length === 0) { - return; - } - - const workspaceFolderTarget = workspaceFolders.length > 1 - ? vscode.ConfigurationTarget?.WorkspaceFolder - : vscode.ConfigurationTarget?.Workspace; - if (workspaceFolderTarget === undefined) { - return; - } - - for (const workspaceFolder of workspaceFolders) { - const gitConfig = vscode.workspace.getConfiguration(GIT_CONFIGURATION_SECTION, workspaceFolder); - const configuredIgnoredFolders = gitConfig.get(REPO_SCAN_IGNORED_FOLDERS_SETTING); - const existingIgnoredFolders = Array.isArray(configuredIgnoredFolders) - ? configuredIgnoredFolders - : []; - const nextIgnoredFolders = uniqueStringList([ - ...existingIgnoredFolders, - ...MANAGED_REPO_SCAN_IGNORED_FOLDERS, - ]); - - if (stringListsEqual(existingIgnoredFolders, nextIgnoredFolders)) { - continue; - } - - try { - await gitConfig.update( - REPO_SCAN_IGNORED_FOLDERS_SETTING, - nextIgnoredFolders, - workspaceFolderTarget, - ); - } catch { - // Leave the extension usable even when the current workspace settings cannot be updated. - } - } -} - -function sessionIdentityLabel(session) { - const agentName = typeof session?.agentName === 'string' ? session.agentName.trim() : ''; - const taskName = sessionDisplayLabel(session); - const label = typeof session?.label === 'string' ? session.label.trim() : ''; - - if (agentName && taskName) { - return `${agentName} · ${taskName}`; - } - if (agentName && label) { - return `${agentName} · ${label}`; - } - - return agentName || taskName || label || 'session'; -} - -function sessionCommitPlaceholder(session) { - if (!session?.branch) { - return 'Pick an Active Agents session to commit its worktree.'; - } - - return `Commit ${sessionIdentityLabel(session)} on ${session.branch} · ${formatCountLabel(session.lockCount || 0, 'lock')}`; -} - -function agentNameFromBranch(branch) { - const segments = String(branch || '') - .split('/') - .map((segment) => segment.trim()) - .filter(Boolean); - if (segments[0] === 'agent' && segments[1]) { - return segments[1]; - } - return segments[0] || 'lock'; -} - -function agentBadgeFromBranch(branch) { - const normalized = agentNameFromBranch(branch).toUpperCase().replace(/[^A-Z0-9]/g, ''); - return normalized.slice(0, 2) || 'LK'; -} - -function buildActiveAgentsStatusSummary(summary) { - const workingCount = summary?.workingCount || 0; - const finishedCount = summary?.finishedCount || 0; - const idleCount = summary?.idleCount || 0; - if (workingCount > 0 || finishedCount > 0 || idleCount > 0) { - const parts = [`${workingCount} working`]; - if (finishedCount > 0) { - parts.push(`${finishedCount} needs cleanup`); - } - parts.push(`${idleCount} idle`); - return `$(git-branch) ${parts.join(' · ')}`; - } - return `$(git-branch) ${formatCountLabel(summary?.sessionCount || 0, 'tracked session')}`; -} - -function buildActiveAgentsStatusTooltip(selectedSession, summary) { - if (selectedSession?.branch) { - return [ - selectedSession.branch, - sessionIdentityLabel(selectedSession), - formatCountLabel(selectedSession.lockCount || 0, 'lock'), - selectedSession.worktreePath, - 'Click to open Active Agents.', - ].filter(Boolean).join('\n'); - } - - const activeCount = Math.max(0, (summary?.sessionCount || 0) - (summary?.deadCount || 0)); - return [ - formatCountLabel(activeCount, 'active agent'), - formatCountLabel(summary?.workingCount || 0, 'working now session', 'working now sessions'), - formatCountLabel(summary?.finishedCount || 0, 'needs cleanup session'), - formatCountLabel(summary?.idleCount || 0, 'idle session'), - formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'), - formatCountLabel(summary?.lockedFileCount || 0, 'locked file'), - summary?.deadCount ? formatCountLabel(summary.deadCount, 'dead session') : '', - 'Click to open Active Agents.', - ].filter(Boolean).join('\n'); -} - -function compactRelativePath(relativePath) { - const normalized = normalizeRelativePath(relativePath); - if (!normalized) { - return ''; - } - - const segments = normalized.split('/').filter(Boolean); - if (segments.length <= 2) { - return normalized; - } - - return `${segments[0]}/.../${segments[segments.length - 1]}`; -} - -function summarizeCompactPaths(paths, maxCount = SESSION_TOP_FILE_COUNT) { - const compactPaths = uniqueStringList((paths || []) - .map(normalizeRelativePath) - .filter(Boolean) - .map((relativePath) => compactRelativePath(relativePath))) - .slice(0, maxCount); - if (compactPaths.length === 0) { - return ''; - } - return compactPaths.join(', '); -} - -function isProtectedBranchName(branch) { - return branch === 'main' || branch === 'dev'; -} - -function countWorkingSessions(sessions) { - return sessions.filter((session) => ( - session.activityKind === 'working' || session.activityKind === 'blocked' - )).length; -} - -function countFinishedSessions(sessions) { - return sessions.filter((session) => session.activityKind === 'finished').length; -} - -function countIdleSessions(sessions) { - return sessions.filter((session) => ( - session.activityKind === 'idle' || session.activityKind === 'stalled' - )).length; -} - -function sessionLastActiveAt(session) { - return [ - session?.lastHeartbeatAt, - session?.lastFileActivityAt, - session?.telemetryUpdatedAt, - session?.startedAt, - ].find((value) => typeof value === 'string' && value.trim().length > 0) || ''; -} - -function sessionLastActiveLabel(session) { - const lastActiveAt = sessionLastActiveAt(session); - if (!lastActiveAt) { - return ''; - } - return formatElapsedFrom(lastActiveAt); -} - -function sessionLastActiveAgeMs(session, now = Date.now()) { - const lastActiveAt = sessionLastActiveAt(session); - const timestamp = Date.parse(lastActiveAt); - if (!Number.isFinite(timestamp)) { - return null; - } - return Math.max(0, now - timestamp); -} - -function sessionFreshnessLabel(session, now = Date.now()) { - const ageMs = sessionLastActiveAgeMs(session, now); - if (session.activityKind === 'blocked') { - return 'Needs attention'; - } - if (session.activityKind === 'finished') { - return 'Needs cleanup'; - } - if (session.activityKind === 'stalled') { - return 'Possibly stale'; - } - if (session.activityKind === 'dead') { - return 'Stopped'; - } - if (ageMs === null) { - return ''; - } - if (ageMs <= IDLE_WARNING_MS) { - return 'Fresh'; - } - if (ageMs <= RECENTLY_ACTIVE_WINDOW_MS) { - return 'Recently active'; - } - if (session.activityKind === 'idle') { - return 'Idle'; - } - return 'Recently active'; -} - -function sessionStatusLabel(session) { - switch (session.activityKind) { - case 'blocked': - return 'Blocked'; - case 'working': - return 'Working'; - case 'finished': - return 'Needs cleanup'; - case 'idle': - return 'Idle'; - case 'stalled': - return 'Stale'; - case 'dead': - return 'Dead'; - default: - return 'Thinking'; - } -} - -function sessionHealthScore(session) { - return Number.isInteger(session?.sessionHealth?.score) ? session.sessionHealth.score : null; -} - -function buildSessionHealthCompactLabel(session) { - const score = sessionHealthScore(session); - return score === null ? '' : `${score}/100`; -} - -function buildSessionHealthSummary(session) { - const compactLabel = buildSessionHealthCompactLabel(session); - if (!compactLabel) { - return ''; - } - - const label = typeof session?.sessionHealth?.label === 'string' - ? session.sessionHealth.label.trim() - : ''; - return label ? `${compactLabel} · ${label}` : compactLabel; -} - -function buildSessionHealthDriversSummary(session) { - const primaryDriver = typeof session?.sessionHealth?.primaryDriver === 'string' - ? session.sessionHealth.primaryDriver.trim() - : ''; - const secondaries = uniqueStringList(Array.isArray(session?.sessionHealth?.secondaries) - ? session.sessionHealth.secondaries.map((value) => String(value || '').trim()) - : []); - return [ - primaryDriver ? `Primary: ${primaryDriver}` : '', - secondaries.length > 0 ? `Secondary: ${secondaries.join(', ')}` : '', - ].filter(Boolean).join(' | '); -} - -function buildSessionHealthTooltip(session) { - const outputLine = typeof session?.sessionHealth?.outputLine === 'string' - ? session.sessionHealth.outputLine.trim() - : ''; - if (outputLine) { - return outputLine; - } - - return [ - buildSessionHealthSummary(session), - buildSessionHealthDriversSummary(session), - ].filter(Boolean).join('\n'); -} - -function buildSessionTopFiles(session) { - return uniqueStringList((session?.worktreeChangedPaths || []) - .map(normalizeRelativePath) - .filter(Boolean)) - .slice(0, SESSION_TOP_FILE_COUNT); -} - -function buildSessionRecentChangeSummary(session) { - if (session?.latestTaskPreview && session.latestTaskPreview !== session.taskName) { - return session.latestTaskPreview; - } - const topFiles = summarizeCompactPaths(session?.worktreeChangedPaths || []); - if (topFiles) { - return `Changed ${topFiles}`; - } - if (session?.activitySummary) { - return session.activitySummary; - } - return 'No recent change summary.'; -} - -function sessionRiskBadges(session) { - return uniqueStringList([ - session?.activityKind === 'blocked' ? 'Blocked' : '', - session?.activityKind === 'stalled' ? 'Stale' : '', - session?.conflictCount > 0 ? 'Conflict' : '', - session?.lockCount > 0 ? 'Locked' : '', - ].filter(Boolean)); -} - -function changeRiskBadges(change) { - return uniqueStringList([ - change?.protectedBranch ? 'Protected branch' : '', - change?.hasForeignLock ? 'Conflict' : '', - !change?.hasForeignLock && change?.lockOwnerBranch ? 'Locked' : '', - change?.deltaLabel || '', - ].filter(Boolean)); -} - -function changeNeedsWarningIcon(change) { - return Boolean( - change?.protectedBranch - || change?.hasForeignLock - || (!change?.hasForeignLock && change?.lockOwnerBranch), - ); -} - -function buildSessionCardDescription(session) { - const provider = resolveSessionProvider(session); - const statusAgentLabel = `${sessionStatusLabel(session)}: ${session.agentName || 'agent'}`; - const descriptionParts = [ - statusAgentLabel, - provider?.label ? `via ${provider.label}` : '', - sessionSnapshotDescription(session), - session.deltaLabel || '', - session.changeCount > 0 ? formatCountLabel(session.changeCount, 'changed file') : '', - session.lockCount > 0 ? formatCountLabel(session.lockCount, 'lock') : '', - buildSessionHealthCompactLabel(session), - session.freshnessLabel || '', - session.lastActiveLabel ? `${session.lastActiveLabel} ago` : '', - ].filter(Boolean); - return descriptionParts.join(' · '); -} - -function buildRawSessionDescription(session) { - const provider = resolveSessionProvider(session); - const descriptionParts = [sessionStatusLabel(session)]; - const fileCountLabel = sessionFileCountLabel(session); - if (fileCountLabel) { - descriptionParts.push(fileCountLabel); - } - if (provider?.label) { - descriptionParts.push(provider.label); - } - const snapshot = sessionSnapshotDescription(session); - if (snapshot) { - descriptionParts.push(snapshot); - } - descriptionParts.push(session.elapsedLabel || formatElapsedFrom(session.startedAt)); - const sessionHealthLabel = buildSessionHealthCompactLabel(session); - if (sessionHealthLabel) { - descriptionParts.push(sessionHealthLabel); - } - if (session.lockCount > 0) { - descriptionParts.push(formatCountLabel(session.lockCount, 'lock')); - } - return descriptionParts.join(' · '); -} - -function buildSessionTooltip(session, description) { - const provider = resolveSessionProvider(session); - const riskSummary = uniqueStringList([ - ...(session?.riskBadges || []), - session?.deltaLabel || '', - ].filter(Boolean)).join(', '); - const topFiles = session?.topChangedFilesLabel || summarizeCompactPaths(session?.worktreeChangedPaths || []); - const sessionHealthSummary = buildSessionHealthSummary(session); - const sessionHealthDrivers = buildSessionHealthDriversSummary(session); - return [ - session.branch, - provider?.label - ? `Provider ${provider.label}${provider.cliName ? ` (${provider.cliName})` : ''}` - : '', - sessionSnapshotDisplayName(session) ? `Snapshot ${sessionSnapshotDisplayName(session)}` : '', - `${session.agentName} · ${sessionDisplayLabel(session)}`, - `Status ${description}`, - sessionHealthSummary ? `Session health ${sessionHealthSummary}` : '', - sessionHealthDrivers ? `Drivers ${sessionHealthDrivers}` : '', - session.recentChangeSummary ? `Recent ${session.recentChangeSummary}` : '', - topFiles ? `Top files ${topFiles}` : '', - riskSummary ? `Signals ${riskSummary}` : '', - session.conflictCount > 0 ? `Conflicts ${session.conflictCount}` : '', - session.lastActiveAt ? `Last active ${session.lastActiveAt}` : '', - session.sourceKind === 'worktree-lock' - ? `Telemetry updated ${session.telemetryUpdatedAt || session.startedAt}` - : `Started ${session.startedAt}`, - session.worktreePath, - ].filter(Boolean).join('\n'); -} - -function buildUnassignedChangeDescription(change) { - return [ - change.statusLabel, - ...changeRiskBadges(change), - ].filter(Boolean).join(' · '); -} - -function buildWorktreeBranchDescription(sessions) { - const sessionList = Array.isArray(sessions) ? sessions : []; - const primarySession = sessionList[0] || null; - if (!primarySession) { - return ''; - } - - const descriptionParts = [ - `${sessionStatusLabel(primarySession).toLowerCase()}: ${primarySession.agentName || 'agent'}`, - sessionSnapshotDescription(primarySession), - ]; - if (sessionList.length > 1) { - descriptionParts.push(formatCountLabel(sessionList.length, 'agent')); - } - return descriptionParts.filter(Boolean).join(' · '); -} - -function buildOverviewDescription(summary) { - return [ - formatCountLabel(summary?.workingCount || 0, 'working agent'), - formatCountLabel(summary?.finishedCount || 0, 'needs cleanup agent'), - formatCountLabel(summary?.idleCount || 0, 'idle agent'), - summary?.colonyTaskCount - ? formatCountLabel(summary.colonyTaskCount, 'colony task') - : '', - summary?.pendingHandoffCount - ? formatCountLabel(summary.pendingHandoffCount, 'pending handoff') - : '', - formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'), - formatCountLabel(summary?.lockedFileCount || 0, 'locked file'), - formatCountLabel(summary?.conflictCount || 0, 'conflict'), - ] - .filter(Boolean) - .join(' · '); -} - -function buildRepoDescription(summary) { - return buildOverviewDescription(summary); -} - -function buildRepoTooltip(repoRoot, summary) { - return [ - repoRoot, - buildOverviewDescription(summary), - ].join('\n'); -} - -function repoRootDisplayLabel(repoRoot) { - const normalizedRepoRoot = path.resolve(repoRoot); - const matchingWorkspaceRoots = (vscode.workspace.workspaceFolders || []) - .map((folder) => (typeof folder?.uri?.fsPath === 'string' ? path.resolve(folder.uri.fsPath) : '')) - .filter((workspaceRoot) => workspaceRoot && isPathWithin(workspaceRoot, normalizedRepoRoot)) - .sort((left, right) => right.length - left.length); - - const workspaceRoot = matchingWorkspaceRoots[0]; - if (!workspaceRoot) { - return path.basename(normalizedRepoRoot); - } - - const workspaceLabel = path.basename(workspaceRoot); - const relativePath = normalizeRelativePath(path.relative(workspaceRoot, normalizedRepoRoot)); - if (!relativePath) { - return workspaceLabel; - } - - return [ - workspaceLabel, - ...relativePath.split('/').filter(Boolean), - ].join('/'); -} - -function sessionSnapshotKey(session) { - return `${session?.repoRoot || ''}::${session?.branch || ''}`; -} - -function changeSnapshotKey(repoRoot, change) { - return `${repoRoot || ''}::${normalizeRelativePath(change?.relativePath)}`; -} - -function buildSessionSnapshot(session) { - return { - activityKind: session.activityKind, - changeCount: session.changeCount || 0, - conflictCount: session.conflictCount || 0, - lockCount: session.lockCount || 0, - changedPaths: [...(session.changedPaths || [])], - }; -} - -function buildChangeSnapshot(change) { - return { - statusLabel: change.statusLabel, - hasForeignLock: Boolean(change.hasForeignLock), - lockOwnerBranch: change.lockOwnerBranch || '', - }; -} - -function deriveSessionDelta(previousSnapshot, currentSession) { - if (!previousSnapshot) { - return ''; - } - if (currentSession.conflictCount > previousSnapshot.conflictCount) { - return 'Conflict'; - } - if (currentSession.activityKind !== previousSnapshot.activityKind) { - return sessionStatusLabel(currentSession); - } - if ( - currentSession.changeCount !== previousSnapshot.changeCount - || !stringListsEqual(currentSession.changedPaths || [], previousSnapshot.changedPaths || []) - ) { - return 'New'; - } - if (currentSession.lockCount !== previousSnapshot.lockCount) { - return 'Updated'; - } - return ''; -} - -function deriveChangeDelta(previousSnapshot, currentChange) { - if (!previousSnapshot) { - return ''; - } - if (currentChange.hasForeignLock && !previousSnapshot.hasForeignLock) { - return 'Conflict'; - } - if ( - currentChange.statusLabel !== previousSnapshot.statusLabel - || currentChange.lockOwnerBranch !== previousSnapshot.lockOwnerBranch - ) { - return 'Updated'; - } - return ''; -} - -function workingSessionSortKey(session) { - if (session.activityKind === 'blocked') { - return 0; - } - if (session.conflictCount > 0) { - return 1; - } - if (session.deltaLabel === 'Conflict') { - return 2; - } - if (session.deltaLabel === 'New') { - return 3; - } - if (session.activityKind === 'finished') { - return 5; - } - return 4; -} - -function idleSessionSortKey(session) { - if (session.activityKind === 'stalled') { - return 0; - } - if (session.activityKind === 'idle') { - return 1; - } - if (session.activityKind === 'dead') { - return 2; - } - return 3; -} - -function sortSessionsForWorkingNow(sessions) { - return [...sessions].sort((left, right) => { - const keyDelta = workingSessionSortKey(left) - workingSessionSortKey(right); - if (keyDelta !== 0) { - return keyDelta; - } - const timeDelta = sessionLastActiveAgeMs(left) - sessionLastActiveAgeMs(right); - if (Number.isFinite(timeDelta) && timeDelta !== 0) { - return timeDelta; - } - const changeDelta = (right.changeCount || 0) - (left.changeCount || 0); - if (changeDelta !== 0) { - return changeDelta; - } - return sessionDisplayLabel(left).localeCompare(sessionDisplayLabel(right)); - }); -} - -function sortSessionsForIdleThinking(sessions) { - return [...sessions].sort((left, right) => { - const keyDelta = idleSessionSortKey(left) - idleSessionSortKey(right); - if (keyDelta !== 0) { - return keyDelta; - } - const timeDelta = sessionLastActiveAgeMs(right) - sessionLastActiveAgeMs(left); - if (Number.isFinite(timeDelta) && timeDelta !== 0) { - return timeDelta; - } - return sessionDisplayLabel(left).localeCompare(sessionDisplayLabel(right)); - }); -} - -function sortUnassignedChanges(changes) { - return [...changes].sort((left, right) => { - const leftBadges = changeRiskBadges(left).length; - const rightBadges = changeRiskBadges(right).length; - if (leftBadges !== rightBadges) { - return rightBadges - leftBadges; - } - return normalizeRelativePath(left.relativePath).localeCompare(normalizeRelativePath(right.relativePath)); - }); -} - -function escapeHtml(value) { - return String(value || '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -function formatInspectBranchSummary(inspectData) { - if (Number.isInteger(inspectData?.aheadCount) && Number.isInteger(inspectData?.behindCount)) { - return `${inspectData.aheadCount} ahead · ${inspectData.behindCount} behind vs ${inspectData.compareRef}`; - } - return `Branch comparison unavailable vs ${inspectData?.compareRef || 'origin/dev'}`; -} - -function inspectPanelTitle(session) { - return `Inspect ${sessionDisplayLabel(session)}`; -} - -function renderInspectPanelHtml(session, inspectData) { - const heldLocksMarkup = Array.isArray(inspectData?.heldLocks) && inspectData.heldLocks.length > 0 - ? `

    ${inspectData.heldLocks.map((entry) => ( - `
  • ${escapeHtml(entry.relativePath)}${entry.allowDelete ? ' delete ok' : ''}${entry.claimedAt ? ` ${escapeHtml(entry.claimedAt)}` : ''}
  • ` - )).join('')}
` - : '

No held locks recorded for this session.

'; - const logContent = inspectData?.logTailText - ? escapeHtml(inspectData.logTailText) - : 'No log output available.'; - - return ` - - - - - - - -

${escapeHtml(sessionIdentityLabel(session))}

-
-
Branch
-
${escapeHtml(session.branch)}
-
Worktree
-
${escapeHtml(session.worktreePath)}
-
Base branch
-
${escapeHtml(inspectData?.baseBranch || 'dev')}
-
Divergence
-
${escapeHtml(formatInspectBranchSummary(inspectData))}
-
Held locks
-
${Array.isArray(inspectData?.heldLocks) ? inspectData.heldLocks.length : 0}
-
Log file
-
${escapeHtml(inspectData?.logPath || 'Unavailable')}
-
-

Held Locks

- ${heldLocksMarkup} -

Agent Log Tail

-
${logContent}
- -`; -} - -class SessionDecorationProvider { - constructor(nowProvider = () => Date.now()) { - this.nowProvider = nowProvider; - this.sessionsByUri = new Map(); - this.lockEntriesByFileUri = new Map(); - this.selectedBranch = ''; - this.onDidChangeFileDecorationsEmitter = new vscode.EventEmitter(); - this.onDidChangeFileDecorations = this.onDidChangeFileDecorationsEmitter.event; - } - - updateSessions(sessions) { - this.sessionsByUri = new Map( - sessions.map((session) => [sessionDecorationUri(session.branch).toString(), session]), - ); - } - - updateLockEntries(repoEntries) { - const nextEntriesByUri = new Map(); - for (const entry of repoEntries || []) { - for (const [relativePath, lockEntry] of entry.lockEntries || []) { - nextEntriesByUri.set( - vscode.Uri.file(path.join(entry.repoRoot, relativePath)).toString(), - { branch: lockEntry.branch }, - ); - } - } - this.lockEntriesByFileUri = nextEntriesByUri; - } - - setSelectedBranch(branch) { - this.selectedBranch = typeof branch === 'string' ? branch.trim() : ''; - } - - refresh() { - this.onDidChangeFileDecorationsEmitter.fire(); - } - - provideFileDecoration(uri) { - if (!uri || uri.scheme !== SESSION_DECORATION_SCHEME) { - if (!uri || uri.scheme !== 'file') { - return undefined; - } - - const lockEntry = this.lockEntriesByFileUri.get(uri.toString()); - if (!lockEntry?.branch) { - return undefined; - } - - const ownsSelectedSession = Boolean(this.selectedBranch) && lockEntry.branch === this.selectedBranch; - return { - badge: agentBadgeFromBranch(lockEntry.branch), - tooltip: ownsSelectedSession - ? `Locked by selected session ${lockEntry.branch}` - : this.selectedBranch - ? `Locked by ${lockEntry.branch} (selected session: ${this.selectedBranch})` - : `Locked by ${lockEntry.branch}`, - color: new vscode.ThemeColor( - ownsSelectedSession - ? 'gitDecoration.modifiedResourceForeground' - : this.selectedBranch - ? 'list.errorForeground' - : 'list.warningForeground', - ), - }; - } - - const session = this.sessionsByUri.get(uri.toString()); - const idleDecoration = sessionIdleDecoration(session, this.nowProvider()); - if (idleDecoration) { - return idleDecoration; - } - return sessionIdentityDecoration(session); - } -} - -class InfoItem extends vscode.TreeItem { - constructor(label, description = '') { - super(label, vscode.TreeItemCollapsibleState.None); - this.description = description; - this.iconPath = themeIcon('info'); - this.tooltip = [label, description].filter(Boolean).join('\n'); - } -} - -class DetailItem extends vscode.TreeItem { - constructor(label, description = '', options = {}) { - super(label, vscode.TreeItemCollapsibleState.None); - this.description = description; - this.tooltip = options.tooltip || [label, description].filter(Boolean).join('\n'); - this.iconPath = options.iconId ? themeIcon(options.iconId, options.iconColorId) : undefined; - } -} - -class RepoItem extends vscode.TreeItem { - constructor(repoRoot, sessions, changes, options = {}) { - const label = typeof options.label === 'string' && options.label.trim() - ? options.label.trim() - : repoRootDisplayLabel(repoRoot); - super(label, vscode.TreeItemCollapsibleState.Expanded); - this.repoRoot = repoRoot; - this.sessions = sessions; - this.changes = changes; - this.unassignedChanges = options.unassignedChanges || []; - this.lockEntries = options.lockEntries || []; - this.colonyTasks = Array.isArray(options.colonyTasks) ? options.colonyTasks : []; - this.overview = options.overview - || buildRepoOverview(sessions, this.unassignedChanges, this.lockEntries, this.colonyTasks); - this.description = buildRepoDescription(this.overview); - this.tooltip = buildRepoTooltip(repoRoot, this.overview); - this.iconPath = themeIcon('repo'); - this.contextValue = 'gitguardex.repo'; - } -} - -class SectionItem extends vscode.TreeItem { - constructor(label, items, options = {}) { - const collapsibleState = items.length > 0 - ? (options.collapsedState ?? vscode.TreeItemCollapsibleState.Expanded) - : vscode.TreeItemCollapsibleState.None; - super(label, collapsibleState); - this.items = items; - this.description = options.description - || (items.length > 0 ? String(items.length) : ''); - this.tooltip = options.tooltip || [label, this.description].filter(Boolean).join('\n'); - this.iconPath = options.iconId ? themeIcon(options.iconId, options.iconColorId) : undefined; - this.contextValue = 'gitguardex.section'; - } -} - -class WorktreeItem extends vscode.TreeItem { - constructor(worktreePath, sessions, items = [], options = {}) { - const normalizedWorktreePath = typeof worktreePath === 'string' ? worktreePath.trim() : ''; - const sessionList = Array.isArray(sessions) ? sessions : []; - const primarySession = options.resourceSession || sessionList[0] || null; - const changedCount = Number.isInteger(options.changedCount) - ? options.changedCount - : sessionList.reduce((total, session) => total + (session.changeCount || 0), 0); - const label = typeof options.label === 'string' && options.label.trim() - ? options.label.trim() - : worktreeDisplayLabel(normalizedWorktreePath, sessionList); - super( - label, - items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None, - ); - this.worktreePath = normalizedWorktreePath; - this.sessions = sessionList; - this.items = items; - this.description = options.description || buildWorktreeDescription(sessionList, changedCount); - this.tooltip = [ - normalizedWorktreePath, - ...sessionList.map((session) => session.branch).filter(Boolean), - ].filter(Boolean).join('\n'); - this.iconPath = themeIcon(options.iconId || 'folder', options.iconColorId); - if (options.useSessionDecoration && primarySession?.branch) { - this.resourceUri = sessionDecorationUri(primarySession.branch); - } - this.contextValue = 'gitguardex.worktree'; - if (primarySession?.worktreePath) { - this.command = { - command: 'gitguardex.activeAgents.openWorktree', - title: 'Open Agent Worktree', - arguments: [primarySession], - }; - } - } -} - -class SessionItem extends vscode.TreeItem { - constructor(session, items = [], options = {}) { - const variant = options.variant === 'raw' ? 'raw' : 'card'; - const label = typeof options.label === 'string' && options.label.trim() - ? options.label.trim() - : (variant === 'raw' ? session.label : sessionDisplayLabel(session)); - const collapsibleState = items.length > 0 - ? (options.collapsedState ?? ( - variant === 'raw' - ? vscode.TreeItemCollapsibleState.Expanded - : vscode.TreeItemCollapsibleState.Collapsed - )) - : vscode.TreeItemCollapsibleState.None; - super( - label, - collapsibleState, - ); - this.session = session; - this.items = items; - this.resourceUri = sessionDecorationUri(session.branch); - this.description = variant === 'raw' - ? buildRawSessionDescription(session) - : buildSessionCardDescription(session); - this.tooltip = buildSessionTooltip(session, this.description); - this.iconPath = themeIcon(resolveSessionActivityIconId(session.activityKind)); - this.contextValue = sessionContextValue(session); - this.command = { - command: 'gitguardex.activeAgents.openWorktree', - title: 'Open Agent Worktree', - arguments: [session], - }; - } -} - -function sessionContextValue(session) { - const activityKind = typeof session?.activityKind === 'string' ? session.activityKind.trim() : ''; - return activityKind - ? `gitguardex.session.${activityKind}` - : 'gitguardex.session'; -} - -function canDismissSession(session) { - return DISMISSABLE_SESSION_ACTIVITY_KINDS.has(session?.activityKind); -} - -function buildDismissSessionDetail(session, statePath) { - const repoRoot = typeof session?.repoRoot === 'string' ? session.repoRoot.trim() : ''; - const relativeStatePath = repoRoot - ? path.relative(repoRoot, statePath) || path.basename(statePath) - : path.basename(statePath); - const detailParts = [ - `Remove ${relativeStatePath} and hide this session from Active Agents.`, - ]; - - if (session?.activityKind === 'stalled') { - detailParts.push('This dismisses the stale sidebar row only; use Stop if you want to interrupt a live agent.'); - } else { - detailParts.push('This clears the stale session record from the sidebar.'); - } - - return detailParts.join(' '); -} - -class FolderItem extends vscode.TreeItem { - constructor(label, relativePath, items, options = {}) { - super( - label, - items.length > 0 - ? (options.collapsedState ?? vscode.TreeItemCollapsibleState.Expanded) - : vscode.TreeItemCollapsibleState.None, - ); - this.relativePath = relativePath; - this.items = items; - this.description = typeof options.description === 'string' ? options.description : ''; - this.tooltip = options.tooltip || relativePath || label; - this.iconPath = options.iconPath - || (!options.iconId ? resolveBundledTreeItemIcon(relativePath || label, 'folder') : undefined) - || themeIcon(options.iconId || 'folder', options.iconColorId); - this.contextValue = options.contextValue || 'gitguardex.folder'; - } -} - -class ChangeItem extends vscode.TreeItem { - constructor(change, options = {}) { - const label = typeof options.label === 'string' && options.label.trim() - ? options.label.trim() - : path.basename(change.relativePath); - super(label, vscode.TreeItemCollapsibleState.None); - this.change = change; - this.description = typeof options.description === 'string' - ? options.description - : change.statusLabel; - this.tooltip = [ - change.relativePath, - `Summary ${this.description}`, - `Status ${change.statusText}`, - change.originalPath ? `Renamed from ${change.originalPath}` : '', - change.hasForeignLock ? `Locked by ${change.lockOwnerBranch}` : '', - change.absolutePath, - ].filter(Boolean).join('\n'); - this.resourceUri = vscode.Uri.file(change.absolutePath); - if (options.iconId || change.hasForeignLock) { - this.iconPath = themeIcon(options.iconId || 'warning', options.iconColorId || 'list.warningForeground'); - } else { - this.iconPath = options.iconPath || resolveBundledTreeItemIcon(change.relativePath || label, 'file'); - } - this.contextValue = 'gitguardex.change'; - this.command = { - command: 'gitguardex.activeAgents.openChange', - title: 'Open Changed File', - arguments: [change], - }; - } -} - -function shellQuote(value) { - const normalized = String(value || ''); - return `'${normalized.replace(/'/g, "'\"'\"'")}'`; -} - - -function hasGitMarker(dirPath) { - return fs.existsSync(path.join(dirPath, '.git')); -} - -function shouldSkipRepoDiscoveryDir(dirName) { - return new Set([ - '.git', - '.omx', - '.omc', - 'node_modules', - 'dist', - 'build', - '.next', - ]).has(dirName); -} - -function discoverNestedGitRepoRoots(rootPath, maxDepth = 3) { - const discovered = []; - - function visit(dirPath, depth) { - if (depth > maxDepth) return; - let entries = []; - try { - entries = fs.readdirSync(dirPath, { withFileTypes: true }); - } catch (_error) { - return; - } - - for (const entry of entries) { - if (!entry.isDirectory() || shouldSkipRepoDiscoveryDir(entry.name)) { - continue; - } - const childPath = path.join(dirPath, entry.name); - if (hasGitMarker(childPath)) { - discovered.push(childPath); - continue; - } - visit(childPath, depth + 1); - } - } - - visit(rootPath, 1); - return discovered; -} - -function discoverWorkspaceRepoRoots() { - const workspaceFolders = vscode.workspace.workspaceFolders || []; - const seen = new Set(); - const roots = []; - - for (const folder of workspaceFolders) { - const rootPath = folder?.uri?.fsPath; - if (!rootPath || seen.has(rootPath)) { - continue; - } - seen.add(rootPath); - roots.push(rootPath); - - for (const nestedRoot of discoverNestedGitRepoRoots(rootPath)) { - if (seen.has(nestedRoot)) { - continue; - } - seen.add(nestedRoot); - roots.push(nestedRoot); - } - } - - return roots; -} - -function repoPickLabel(repoRoot) { - const parent = path.basename(path.dirname(repoRoot)); - const base = path.basename(repoRoot); - return parent ? `${parent}/${base}` : base; -} - -function readGitOutput(repoRoot, args) { - try { - return cp.execFileSync('git', ['-C', repoRoot, ...args], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }).trim(); - } catch { - return null; - } -} - -function repoGitSummary(repoRoot) { - const branch = readGitOutput(repoRoot, ['branch', '--show-current']) || 'unknown'; - const status = readGitOutput(repoRoot, ['status', '--porcelain']); - return { - branch, - dirty: status === null ? 'unknown' : status.length > 0 ? 'dirty' : 'clean', - }; -} - -function repoPickDescription(repoRoot) { - const summary = repoGitSummary(repoRoot); - return `${summary.branch} · ${summary.dirty}`; -} - -function findRepoRootForPath(repoRoots, candidatePath) { - const normalizedCandidatePath = normalizeAbsolutePath(candidatePath); - if (!normalizedCandidatePath) { - return null; - } - - return repoRoots - .filter((repoRoot) => isPathWithin(repoRoot, normalizedCandidatePath)) - .sort((left, right) => right.length - left.length)[0] || null; -} - -function activeScmRootPath() { - const sourceControl = vscode.scm?.activeSourceControl; - return sourceControl?.rootUri?.fsPath || sourceControl?.rootUri?.path || ''; -} - -function preferredRepoRoot(repoRoots) { - return findRepoRootForPath(repoRoots, activeScmRootPath()) - || findRepoRootForPath(repoRoots, vscode.window.activeTextEditor?.document?.uri?.fsPath); -} - -function resolveStartAgentCommand(repoRoot, details) { - const taskArg = shellQuote(details.taskName); - const agentArg = shellQuote(details.agentName); - return `gx agents start ${taskArg} --agent ${agentArg} --target ${shellQuote(repoRoot)}`; -} - -function sessionTaskLabel(session) { - const latestTaskPreview = typeof session?.latestTaskPreview === 'string' - ? session.latestTaskPreview.trim() - : ''; - if (latestTaskPreview) { - return latestTaskPreview; - } - - const taskName = typeof session?.taskName === 'string' ? session.taskName.trim() : ''; - if (taskName) { - return taskName; - } - - return ''; -} - -function sessionDisplayLabel(session) { - return sessionTaskLabel(session) - || session?.label - || compactBranchLabel(session?.branch) - || session?.branch - || path.basename(session?.worktreePath || '') - || 'session'; -} - -function sessionTreeLabel(session) { - return sessionTaskLabel(session) || compactBranchLabel(session?.branch) || sessionDisplayLabel(session); -} - -function worktreeDisplayLabel(worktreePath, sessions) { - const sessionList = Array.isArray(sessions) - ? sessions.filter(Boolean) - : []; - if (sessionList.length === 1) { - return sessionDisplayLabel(sessionList[0]); - } - - return path.basename(String(worktreePath || '').trim()) || 'worktree'; -} - -function buildWorktreeDescription(sessions, changedCount) { - const sessionList = Array.isArray(sessions) - ? sessions.filter(Boolean) - : []; - const primarySession = sessionList.length === 1 ? sessionList[0] : null; - const totalLocks = sessionList.reduce((total, session) => total + (session.lockCount || 0), 0); - const descriptionParts = []; - - if (primarySession?.agentName) { - descriptionParts.push(primarySession.agentName); - } else { - descriptionParts.push(formatCountLabel(sessionList.length, 'agent')); - } - - const fileCountLabel = primarySession - ? sessionFileCountLabel(primarySession) - : changedCount > 0 - ? formatCountLabel(changedCount, 'file') - : ''; - if (fileCountLabel) { - descriptionParts.push(fileCountLabel); - } - if (totalLocks > 0) { - descriptionParts.push(formatCountLabel(totalLocks, 'lock')); - } - - return descriptionParts.join(' · '); -} - -function sessionWorktreePath(session) { - return typeof session?.worktreePath === 'string' ? session.worktreePath.trim() : ''; -} - -function resolveSessionProjectRelativePath(session) { - const repoRoot = typeof session?.repoRoot === 'string' ? session.repoRoot.trim() : ''; - if (!repoRoot) { - return ''; - } - - const resolveCandidate = (candidatePath) => { - const normalizedCandidate = typeof candidatePath === 'string' ? candidatePath.trim() : ''; - if (!normalizedCandidate) { - return ''; - } - - const absolutePath = path.isAbsolute(normalizedCandidate) - ? path.resolve(normalizedCandidate) - : path.resolve(repoRoot, normalizedCandidate); - if (!isPathWithin(repoRoot, absolutePath) || !fs.existsSync(absolutePath)) { - return ''; - } - - return normalizeRelativePath(path.relative(repoRoot, absolutePath)); - }; - - const isManagedWorktreeRelativePath = (relativePath) => { - const normalizedRelativePath = normalizeRelativePath(relativePath); - return MANAGED_WORKTREE_RELATIVE_ROOTS.some((managedRoot) => { - const normalizedManagedRoot = normalizeRelativePath(managedRoot); - return normalizedRelativePath === normalizedManagedRoot - || normalizedRelativePath.startsWith(`${normalizedManagedRoot}/`); - }); - }; - - const explicitProjectPath = resolveCandidate(session?.projectPath); - if (explicitProjectPath && !isManagedWorktreeRelativePath(explicitProjectPath)) { - return explicitProjectPath; - } - - const namedProjectPath = resolveCandidate(session?.projectName); - if (namedProjectPath && !isManagedWorktreeRelativePath(namedProjectPath)) { - return namedProjectPath; - } - return ''; -} - -function worktreeProjectRelativePath(sessions) { - const projectPaths = uniqueStringList((sessions || []) - .map((session) => resolveSessionProjectRelativePath(session)) - .filter(Boolean)); - return projectPaths.length === 1 ? projectPaths[0] : ''; -} - -function repoEntryDisplayLabel(repoRoot, sessions) { - const repoLabel = repoRootDisplayLabel(repoRoot); - const projectPaths = uniqueStringList((sessions || []) - .map((session) => resolveSessionProjectRelativePath(session)) - .filter(Boolean)); - if (projectPaths.length !== 1) { - return repoLabel; - } - - const [projectRelativePath] = projectPaths; - const hasRootScopedSession = (sessions || []).some( - (session) => !resolveSessionProjectRelativePath(session), - ); - if (!projectRelativePath || hasRootScopedSession) { - return repoLabel; - } - if (repoLabel.endsWith(`/${projectRelativePath}`)) { - return repoLabel; - } - return `${repoLabel}/${projectRelativePath}`; -} - -function buildProjectScopedDescription(entries) { - const sessions = (entries || []).flatMap((entry) => Array.isArray(entry?.sessions) ? entry.sessions : []); - if (sessions.length === 0) { - return ''; - } - - const changedCount = sessions.reduce((total, session) => total + (session.changeCount || 0), 0); - const lockCount = sessions.reduce((total, session) => total + (session.lockCount || 0), 0); - const descriptionParts = [formatCountLabel(sessions.length, 'agent')]; - if (changedCount > 0) { - descriptionParts.push(formatCountLabel(changedCount, 'file')); - } - if (lockCount > 0) { - descriptionParts.push(formatCountLabel(lockCount, 'lock')); - } - return descriptionParts.join(' · '); -} - -function buildProjectScopedItems(entries, options = {}) { - const normalizedEntries = Array.isArray(entries) - ? entries.filter((entry) => entry?.item) - : []; - const projectRoots = []; - const rootEntries = []; - let hasProjectFolders = false; - - function sortFolders(nodes) { - nodes.sort((left, right) => left.label.localeCompare(right.label)); - for (const node of nodes) { - sortFolders(node.children); - } - } - - for (const entry of normalizedEntries) { - const projectRelativePath = normalizeRelativePath(entry.projectRelativePath); - if (!projectRelativePath) { - rootEntries.push(entry); - continue; - } - - hasProjectFolders = true; - let nodes = projectRoots; - let folderPath = ''; - let parentNode = null; - for (const segment of projectRelativePath.split('/').filter(Boolean)) { - folderPath = folderPath ? path.posix.join(folderPath, segment) : segment; - let folderNode = nodes.find((node) => node.relativePath === folderPath); - if (!folderNode) { - folderNode = { - label: segment, - relativePath: folderPath, - children: [], - entries: [], - directEntries: [], - }; - nodes.push(folderNode); - } - folderNode.entries.push(entry); - parentNode = folderNode; - nodes = folderNode.children; - } - - if (parentNode) { - parentNode.directEntries.push(entry); - } else { - rootEntries.push(entry); - } - } - - if (!hasProjectFolders) { - return rootEntries.map((entry) => entry.item); - } - - sortFolders(projectRoots); - - function materialize(nodes) { - return nodes.map((node) => new FolderItem( - node.label, - node.relativePath, - [ - ...materialize(node.children), - ...node.directEntries.map((entry) => entry.item), - ], - { - description: buildProjectScopedDescription(node.entries), - tooltip: [node.relativePath, buildProjectScopedDescription(node.entries)].filter(Boolean).join('\n'), - }, - )); - } - - const items = materialize(projectRoots); - if (rootEntries.length === 0) { - return items; - } - - const rootLabel = typeof options.rootLabel === 'string' ? options.rootLabel.trim() : ''; - if (!rootLabel) { - items.push(...rootEntries.map((entry) => entry.item)); - return items; - } - - items.push(new FolderItem( - rootLabel, - '', - rootEntries.map((entry) => entry.item), - { - description: buildProjectScopedDescription(rootEntries), - tooltip: rootLabel, - }, - )); - return items; -} - -function showSessionMessage(message) { - vscode.window.showInformationMessage?.(message); -} - -function ensureSessionWorktree(session, actionLabel) { - const worktreePath = sessionWorktreePath(session); - if (!worktreePath) { - showSessionMessage(`Cannot ${actionLabel}: missing worktree path.`); - return ''; - } - if (!fs.existsSync(worktreePath)) { - showSessionMessage(`Cannot ${actionLabel}: worktree is no longer on disk: ${worktreePath}`); - return ''; - } - return worktreePath; -} - -function runSessionTerminalCommand(session, actionLabel, iconId, commandText) { - const worktreePath = ensureSessionWorktree(session, actionLabel.toLowerCase()); - if (!worktreePath) { - return; - } - - const terminal = vscode.window.createTerminal({ - name: `GitGuardex ${actionLabel}: ${sessionDisplayLabel(session)}`, - cwd: worktreePath, - iconPath: new vscode.ThemeIcon(iconId), - }); - terminal.show(); - terminal.sendText(commandText, true); -} - -function sessionTerminalLabel(session) { - return `GitGuardex Terminal: ${sessionDisplayLabel(session)}`; -} - -function listWindowTerminals() { - return Array.isArray(vscode.window.terminals) ? vscode.window.terminals : []; -} - -function focusTerminal(terminal) { - terminal?.show?.(false); -} - -async function terminalProcessId(terminal) { - if (!terminal?.processId) { - return null; - } - - try { - const pid = await terminal.processId; - return Number.isInteger(pid) && pid > 0 ? pid : null; - } catch (_error) { - return null; - } -} - -function findFallbackSessionTerminal(session) { - const label = sessionTerminalLabel(session); - return listWindowTerminals().find((terminal) => terminal?.name === label) || null; -} - -async function findSessionTerminal(session) { - const pid = Number(session?.pid); - if (!Number.isInteger(pid) || pid <= 0) { - return null; - } - - for (const terminal of listWindowTerminals()) { - if (await terminalProcessId(terminal) === pid) { - return terminal; - } - } - - return null; -} - -function openFallbackSessionTerminal(session, worktreePath) { - const existingTerminal = findFallbackSessionTerminal(session); - if (existingTerminal) { - focusTerminal(existingTerminal); - return existingTerminal; - } - - const terminal = vscode.window.createTerminal({ - name: sessionTerminalLabel(session), - cwd: worktreePath, - iconPath: new vscode.ThemeIcon('terminal'), - }); - focusTerminal(terminal); - return terminal; -} - -async function showSessionTerminal(session) { - const worktreePath = ensureSessionWorktree(session, 'show terminal'); - if (!worktreePath) { - return; - } - - const terminal = await findSessionTerminal(session); - if (terminal) { - focusTerminal(terminal); - return; - } - - openFallbackSessionTerminal(session, worktreePath); -} - -function finishSession(session) { - if (!session?.branch) { - showSessionMessage('Cannot finish session: missing branch name.'); - return; - } - runSessionTerminalCommand( - session, - 'Finish', - 'check', - `gx branch finish --branch ${shellQuote(session.branch)}`, - ); -} - -function syncSession(session) { - runSessionTerminalCommand(session, 'Sync', 'sync', 'gx sync'); -} - -async function restartActiveAgents(extensionId) { - if (extensionId && extensionId !== ACTIVE_AGENTS_EXTENSION_ID) { - return; - } - await vscode.commands.executeCommand(RESTART_EXTENSION_HOST_COMMAND); -} - -function execFileAsync(command, args, options = {}) { - return new Promise((resolve, reject) => { - cp.execFile(command, args, options, (error, stdout = '', stderr = '') => { - if (error) { - error.stdout = stdout; - error.stderr = stderr; - reject(error); - return; - } - resolve({ stdout, stderr }); - }); - }); -} - -function buildStopSessionCommandText(session, pid) { - const parts = ['gx', 'agents', 'stop', '--pid', String(pid)]; - if (session?.repoRoot) { - parts.push('--target', session.repoRoot); - } - return parts.map(shellQuote).join(' '); -} - -async function stopSession(session, refresh) { - const pid = Number(session?.pid); - if (!Number.isInteger(pid) || pid <= 0) { - showSessionMessage('Cannot stop session: missing pid.'); - return; - } - if (!session?.branch) { - showSessionMessage('Cannot stop session: missing branch name.'); - return; - } - - const sessionTerminal = await findSessionTerminal(session); - const stopCommandText = buildStopSessionCommandText(session, pid); - const confirmed = await vscode.window.showWarningMessage( - `Stop ${sessionDisplayLabel(session)}?`, - { - modal: true, - detail: sessionTerminal - ? 'Send Ctrl+C to the live session terminal.' - : `No live session terminal found. Run ${stopCommandText}.`, - }, - 'Stop', - ); - if (confirmed !== 'Stop') { - return; - } - - if (sessionTerminal) { - focusTerminal(sessionTerminal); - sessionTerminal.sendText('\u0003', false); - refresh(); - return; - } - - try { - const commandCwd = session?.repoRoot || sessionWorktreePath(session) || process.cwd(); - const args = ['agents', 'stop', '--pid', String(pid)]; - if (session?.repoRoot) { - args.push('--target', session.repoRoot); - } - await execFileAsync('gx', args, { - cwd: commandCwd, - encoding: 'utf8', - maxBuffer: 1024 * 1024, - }); - refresh(); - } catch (error) { - showSessionMessage( - `Failed to stop session ${sessionDisplayLabel(session)}: ${formatGitCommandFailure(error)}`, - ); - } -} - -async function dismissSession(session, refresh) { - if (!canDismissSession(session)) { - showSessionMessage('Only stalled or dead sessions can be dismissed.'); - return; - } - - const repoRoot = typeof session?.repoRoot === 'string' ? session.repoRoot.trim() : ''; - if (!repoRoot) { - showSessionMessage('Cannot dismiss session: missing repo root.'); - return; - } - if (!session?.branch) { - showSessionMessage('Cannot dismiss session: missing branch name.'); - return; - } - - const statePath = sessionFilePathForBranch(repoRoot, session.branch); - if (!fs.existsSync(statePath)) { - clearWorktreeActivityCache(session.worktreePath); - refresh(); - showSessionMessage(`Session record already gone for ${sessionDisplayLabel(session)}.`); - return; - } - - const confirmed = await vscode.window.showWarningMessage( - `Dismiss ${sessionDisplayLabel(session)}?`, - { - modal: true, - detail: buildDismissSessionDetail(session, statePath), - }, - 'Dismiss', - ); - if (confirmed !== 'Dismiss') { - return; - } - - try { - fs.unlinkSync(statePath); - clearWorktreeActivityCache(session.worktreePath); - refresh(); - } catch (error) { - showSessionMessage(`Failed to dismiss session ${sessionDisplayLabel(session)}: ${error.message}`); - } -} - -function readGitDirPath(targetPath) { - const normalizedTargetPath = typeof targetPath === 'string' ? targetPath.trim() : ''; - if (!normalizedTargetPath) { - return ''; - } - - const gitPath = path.join(path.resolve(normalizedTargetPath), '.git'); - try { - if (fs.statSync(gitPath).isDirectory()) { - return gitPath; - } - } catch (_error) { - return ''; - } - - try { - const gitPointer = fs.readFileSync(gitPath, 'utf8'); - const match = gitPointer.match(/^gitdir:\s*(.+)$/m); - if (match?.[1]) { - return path.resolve(path.dirname(gitPath), match[1].trim()); - } - } catch (_error) { - return ''; - } - - return ''; -} - -function resolveRepoRootFromGitDir(targetPath) { - const gitDir = readGitDirPath(targetPath); - if (!gitDir) { - return ''; - } - - let commonDir = gitDir; - try { - const commonDirPath = path.join(gitDir, 'commondir'); - if (fs.existsSync(commonDirPath)) { - const rawCommonDir = fs.readFileSync(commonDirPath, 'utf8').trim(); - if (rawCommonDir) { - commonDir = path.resolve(gitDir, rawCommonDir); - } - } - } catch (_error) { - // Fall back to the direct git dir when commondir is unreadable. - } - - return path.basename(commonDir) === '.git' - ? path.resolve(path.dirname(commonDir)) - : ''; -} - -function readGitTopLevel(targetPath) { - try { - return cp.execFileSync('git', ['-C', targetPath, 'rev-parse', '--show-toplevel'], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }).trim(); - } catch (_error) { - return ''; - } -} - -function resolveWorkspaceFolderRepoRoot(workspacePath) { - const normalizedWorkspacePath = typeof workspacePath === 'string' ? workspacePath.trim() : ''; - if (!normalizedWorkspacePath) { - return ''; - } - - const absoluteWorkspacePath = path.resolve(normalizedWorkspacePath); - const directRepoRoot = resolveRepoRootFromGitDir(absoluteWorkspacePath); - if (directRepoRoot) { - return directRepoRoot; - } - - const gitTopLevel = readGitTopLevel(absoluteWorkspacePath); - if (!gitTopLevel) { - return absoluteWorkspacePath; - } - - return resolveRepoRootFromGitDir(gitTopLevel) || path.resolve(gitTopLevel); -} - -function repoRootFromSessionFile(filePath) { - return path.resolve(path.dirname(filePath), '..', '..', '..'); -} - -function repoRootFromWorktreeLockFile(filePath) { - return path.resolve(path.dirname(filePath), '..', '..', '..'); -} - -function repoRootFromManagedWorktreeGitFile(filePath) { - return path.resolve(path.dirname(filePath), '..', '..', '..'); -} - -function repoRootFromLockFile(filePath) { - return path.resolve(path.dirname(filePath), '..', '..'); -} - -function normalizeRelativePath(relativePath) { - return String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, ''); -} - -function emptyLockRegistry() { - return { - entriesByPath: new Map(), - countsByBranch: new Map(), - }; -} - -function readLockRegistry(repoRoot) { - const lockPath = path.join(repoRoot, LOCK_FILE_RELATIVE); - if (!fs.existsSync(lockPath)) { - return emptyLockRegistry(); - } - - let parsed; - try { - parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8')); - } catch (_error) { - return emptyLockRegistry(); - } - - const locks = parsed?.locks; - if (!locks || typeof locks !== 'object' || Array.isArray(locks)) { - return emptyLockRegistry(); - } - - const entriesByPath = new Map(); - const countsByBranch = new Map(); - for (const [rawRelativePath, entry] of Object.entries(locks)) { - if (!entry || typeof entry !== 'object') { - continue; - } - - const relativePath = normalizeRelativePath(rawRelativePath); - const branch = typeof entry.branch === 'string' ? entry.branch.trim() : ''; - if (!relativePath || !branch) { - continue; - } - - entriesByPath.set(relativePath, { - branch, - claimedAt: typeof entry.claimed_at === 'string' ? entry.claimed_at : '', - allowDelete: Boolean(entry.allow_delete), - }); - countsByBranch.set(branch, (countsByBranch.get(branch) || 0) + 1); - } - - return { - entriesByPath, - countsByBranch, - }; -} - -function readCurrentBranch(repoRoot) { - try { - return cp.execFileSync('git', ['-C', repoRoot, 'rev-parse', '--abbrev-ref', 'HEAD'], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }).trim(); - } catch (_error) { - return ''; - } -} - -function parseSimpleSemver(version) { - const parts = String(version || '') - .split('.') - .map((part) => Number.parseInt(part, 10)); - if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) { - return null; - } - return parts; -} - -function compareSimpleSemver(left, right) { - const leftParts = parseSimpleSemver(left); - const rightParts = parseSimpleSemver(right); - if (!leftParts || !rightParts) { - return 0; - } - - for (let index = 0; index < leftParts.length; index += 1) { - if (leftParts[index] !== rightParts[index]) { - return leftParts[index] - rightParts[index]; - } - } - - return 0; -} - -function readJsonFile(filePath) { - try { - return JSON.parse(fs.readFileSync(filePath, 'utf8')); - } catch (_error) { - return null; - } -} - -function resolveActiveAgentsAutoUpdateCandidate(installedVersion) { - const candidates = []; - - for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { - const repoRoot = workspaceFolder?.uri?.fsPath; - if (!repoRoot) { - continue; - } - - const manifestPath = path.join(repoRoot, ACTIVE_AGENTS_MANIFEST_RELATIVE); - const installScriptPath = path.join(repoRoot, ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE); - if (!fs.existsSync(manifestPath) || !fs.existsSync(installScriptPath)) { - continue; - } - - const manifest = readJsonFile(manifestPath); - const nextVersion = typeof manifest?.version === 'string' ? manifest.version.trim() : ''; - if (!nextVersion || compareSimpleSemver(nextVersion, installedVersion) <= 0) { - continue; - } - - candidates.push({ repoRoot, installScriptPath, version: nextVersion }); - } - - candidates.sort((left, right) => compareSimpleSemver(right.version, left.version)); - return candidates[0] || null; -} - -function runActiveAgentsInstallScript(repoRoot, installScriptPath) { - return new Promise((resolve, reject) => { - cp.execFile( - process.execPath, - [installScriptPath], - { cwd: repoRoot, encoding: 'utf8' }, - (error, stdout, stderr) => { - if (error) { - reject(new Error(String(stderr || stdout || error.message || '').trim() || 'install failed')); - return; - } - resolve({ stdout, stderr }); - }, - ); - }); -} - -async function maybeAutoUpdateActiveAgentsExtension(context) { - const installedVersion = typeof context?.extension?.packageJSON?.version === 'string' - ? context.extension.packageJSON.version.trim() - : ''; - if (!installedVersion) { - return; - } - - const candidate = resolveActiveAgentsAutoUpdateCandidate(installedVersion); - if (!candidate) { - return; - } - - try { - await runActiveAgentsInstallScript(candidate.repoRoot, candidate.installScriptPath); - } catch (error) { - const failure = typeof error?.message === 'string' && error.message.trim() - ? error.message.trim() - : 'install failed'; - vscode.window.showWarningMessage?.( - `GitGuardex Active Agents could not auto-update to ${candidate.version}: ${failure}`, - ); - return; - } - - const selection = await vscode.window.showInformationMessage?.( - `GitGuardex Active Agents updated to ${candidate.version}. Reload this window now, then reload any other already-open VS Code windows to use the newest companion.`, - RELOAD_WINDOW_ACTION, - UPDATE_LATER_ACTION, - ); - if (selection === RELOAD_WINDOW_ACTION) { - await vscode.commands.executeCommand('workbench.action.reloadWindow'); - } -} - -function decorateSession(session, lockRegistry) { - const touchedChanges = buildSessionTouchedChanges(session, lockRegistry); - const decorated = { - ...session, - lockCount: lockRegistry.countsByBranch.get(session.branch) || 0, - touchedChanges, - conflictCount: touchedChanges.filter((change) => change.hasForeignLock).length, - }; - decorated.lastActiveAt = sessionLastActiveAt(decorated); - decorated.lastActiveLabel = sessionLastActiveLabel(decorated); - decorated.freshnessLabel = sessionFreshnessLabel(decorated); - decorated.topChangedFiles = buildSessionTopFiles(decorated); - decorated.topChangedFilesLabel = summarizeCompactPaths(decorated.topChangedFiles); - decorated.recentChangeSummary = buildSessionRecentChangeSummary(decorated); - decorated.riskBadges = sessionRiskBadges(decorated); - return decorated; -} - -function decorateChange(change, lockRegistry, owningBranch) { - const lockEntry = lockRegistry.entriesByPath.get(normalizeRelativePath(change.relativePath)); - const lockOwnerBranch = lockEntry?.branch || ''; - const decorated = { - ...change, - lockOwnerBranch, - hasForeignLock: Boolean(lockOwnerBranch) && (!owningBranch || lockOwnerBranch !== owningBranch), - protectedBranch: isProtectedBranchName(owningBranch), - }; - decorated.riskBadges = changeRiskBadges(decorated); - return decorated; -} - -function buildSessionTouchedChanges(session, lockRegistry) { - const changedPaths = Array.isArray(session.worktreeChangedPaths) - ? session.worktreeChangedPaths - : []; - return [...new Set(changedPaths.map(normalizeRelativePath).filter(Boolean))] - .map((relativePath) => { - const lockEntry = lockRegistry.entriesByPath.get(relativePath); - const lockOwnerBranch = lockEntry?.branch || ''; - return { - relativePath, - absolutePath: path.join(session.worktreePath, relativePath), - originalPath: '', - statusCode: 'M', - statusLabel: 'M', - statusText: 'Touched', - lockOwnerBranch, - hasForeignLock: Boolean(lockOwnerBranch) && lockOwnerBranch !== session.branch, - }; - }); -} - -function isPathWithin(parentPath, targetPath) { - const relativePath = path.relative(parentPath, targetPath); - return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)); -} - -function normalizeAbsolutePath(value) { - return typeof value === 'string' && value.trim() ? path.resolve(value) : ''; -} - -function isManagedWorktreePath(worktreePath) { - const normalizedWorktreePath = normalizeAbsolutePath(worktreePath); - if (!normalizedWorktreePath) { - return false; - } - - return MANAGED_WORKTREE_RELATIVE_ROOTS.some((relativeRoot) => { - const normalizedRelativeRoot = path.normalize(relativeRoot); - const marker = `${path.sep}${normalizedRelativeRoot}${path.sep}`; - return normalizedWorktreePath.includes(marker); - }); -} - -function removeDeletedWorktreeWorkspaceFolder(worktreePath) { - if (typeof vscode.workspace.updateWorkspaceFolders !== 'function') { - return false; - } - - const normalizedWorktreePath = normalizeAbsolutePath(worktreePath); - if (!normalizedWorktreePath) { - return false; - } - - const workspaceFolders = vscode.workspace.workspaceFolders || []; - const folderIndex = workspaceFolders.findIndex((folder) => ( - normalizeAbsolutePath(folder?.uri?.fsPath) === normalizedWorktreePath - )); - if (folderIndex < 0) { - return false; - } - - try { - return vscode.workspace.updateWorkspaceFolders(folderIndex, 1) === true; - } catch (_error) { - return false; - } -} - -async function closeDeletedWorktreeRepository(worktreePath) { - const normalizedWorktreePath = normalizeAbsolutePath(worktreePath); - if (!normalizedWorktreePath || fs.existsSync(normalizedWorktreePath)) { - return false; - } - - try { - await vscode.commands.executeCommand('git.close', vscode.Uri.file(normalizedWorktreePath)); - } catch (_error) { - // The Git extension may have already removed this repository. - } - - removeDeletedWorktreeWorkspaceFolder(normalizedWorktreePath); - return true; -} - -function findDeletedManagedWorkspaceFolders() { - return (vscode.workspace.workspaceFolders || []) - .map((folder) => normalizeAbsolutePath(folder?.uri?.fsPath)) - .filter((workspacePath) => ( - workspacePath - && !fs.existsSync(workspacePath) - && isManagedWorktreePath(workspacePath) - )); -} - -function localizeChangeForSession(session, change) { - if (!change?.absolutePath || !isPathWithin(session.worktreePath, change.absolutePath)) { - return null; - } - - let originalPath = change.originalPath; - if (originalPath) { - const originalAbsolutePath = path.join(session.repoRoot, originalPath); - if (isPathWithin(session.worktreePath, originalAbsolutePath)) { - originalPath = normalizeRelativePath(path.relative(session.worktreePath, originalAbsolutePath)); - } - } - - return { - ...change, - relativePath: normalizeRelativePath(path.relative(session.worktreePath, change.absolutePath)), - originalPath, - }; -} - -async function findRepoSessionEntries() { - const [sessionFiles, worktreeLockFiles, managedWorktreeGitFiles] = await Promise.all([ - vscode.workspace.findFiles( - ACTIVE_SESSION_FILES_GLOB, - SESSION_SCAN_EXCLUDE_GLOB, - SESSION_SCAN_LIMIT, - ), - vscode.workspace.findFiles( - WORKTREE_AGENT_LOCKS_GLOB, - WORKTREE_LOCK_SCAN_EXCLUDE_GLOB, - SESSION_SCAN_LIMIT, - ), - vscode.workspace.findFiles( - MANAGED_WORKTREE_GIT_FILES_GLOB, - MANAGED_WORKTREE_GIT_SCAN_EXCLUDE_GLOB, - SESSION_SCAN_LIMIT, - ), - ]); - - const repoRoots = new Set(); - const addRepoRootCandidate = (repoRoot) => { - if (typeof repoRoot !== 'string' || !repoRoot.trim()) { - return; - } - - const normalizedRepoRoot = path.resolve(repoRoot); - const isInsideWorkspaceManagedWorktree = (vscode.workspace.workspaceFolders || []) - .map((folder) => (typeof folder?.uri?.fsPath === 'string' ? path.resolve(folder.uri.fsPath) : '')) - .filter(Boolean) - .some((workspaceRoot) => MANAGED_WORKTREE_RELATIVE_ROOTS.some((relativeRoot) => ( - isPathWithin(path.join(workspaceRoot, relativeRoot), normalizedRepoRoot) - ))); - if (!isInsideWorkspaceManagedWorktree) { - repoRoots.add(normalizedRepoRoot); - } - }; - - for (const uri of sessionFiles) { - addRepoRootCandidate(repoRootFromSessionFile(uri.fsPath)); - } - for (const uri of worktreeLockFiles) { - if (path.basename(uri.fsPath) !== 'AGENT.lock') { - continue; - } - addRepoRootCandidate(repoRootFromWorktreeLockFile(uri.fsPath)); - } - for (const uri of managedWorktreeGitFiles) { - if (path.basename(uri.fsPath) !== '.git') { - continue; - } - addRepoRootCandidate(repoRootFromManagedWorktreeGitFile(uri.fsPath)); - } - for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { - if (workspaceFolder?.uri?.fsPath) { - addRepoRootCandidate(resolveWorkspaceFolderRepoRoot(workspaceFolder.uri.fsPath)); - } - } - - const repoEntries = []; - for (const repoRoot of repoRoots) { - const sessions = readActiveSessions(repoRoot, { includeStale: true }); - if (sessions.length > 0) { - repoEntries.push({ repoRoot, sessions }); - } - } - - repoEntries.sort((left, right) => left.repoRoot.localeCompare(right.repoRoot)); - return repoEntries; -} - -function resolveSessionWatcherKey(session) { - return `${path.resolve(session.repoRoot)}::${session.branch}::${path.resolve(session.worktreePath)}`; -} - -function resolveSessionGitIndexPath(worktreePath) { - const gitPath = path.join(worktreePath, '.git'); - const defaultIndexPath = path.join(gitPath, 'index'); - - try { - if (fs.statSync(gitPath).isDirectory()) { - return defaultIndexPath; - } - } catch (_error) { - return defaultIndexPath; - } - - try { - const gitPointer = fs.readFileSync(gitPath, 'utf8'); - const match = gitPointer.match(/^gitdir:\s*(.+)$/m); - if (match?.[1]) { - return path.resolve(worktreePath, match[1].trim(), 'index'); - } - } catch (_error) { - return defaultIndexPath; - } - - return defaultIndexPath; -} - -function bindRefreshWatcher(watcher, refresh) { - return [ - watcher.onDidCreate(refresh), - watcher.onDidChange(refresh), - watcher.onDidDelete(refresh), - ]; -} - -function disposeAll(disposables) { - for (const disposable of disposables) { - disposable?.dispose?.(); - } -} - -function buildChangeTreeNodes(changes) { - const root = []; - - function sortNodes(nodes) { - nodes.sort((left, right) => { - const leftIsFolder = left.kind === 'folder'; - const rightIsFolder = right.kind === 'folder'; - if (leftIsFolder !== rightIsFolder) { - return leftIsFolder ? -1 : 1; - } - return left.label.localeCompare(right.label); - }); - - for (const node of nodes) { - if (node.kind === 'folder') { - sortNodes(node.children); - } - } - } - - for (const change of changes) { - const segments = change.relativePath.split(/[\\/]+/).filter(Boolean); - if (segments.length <= 1) { - root.push({ kind: 'change', label: change.relativePath, change }); - continue; - } - - let nodes = root; - let folderPath = ''; - for (const segment of segments.slice(0, -1)) { - folderPath = folderPath ? path.posix.join(folderPath, segment) : segment; - let folderNode = nodes.find((node) => node.kind === 'folder' && node.relativePath === folderPath); - if (!folderNode) { - folderNode = { - kind: 'folder', - label: segment, - relativePath: folderPath, - children: [], - }; - nodes.push(folderNode); - } - nodes = folderNode.children; - } - - nodes.push({ kind: 'change', label: change.relativePath, change }); - } - - sortNodes(root); - - function materialize(nodes) { - return nodes.map((node) => { - if (node.kind === 'folder') { - return new FolderItem(node.label, node.relativePath, materialize(node.children)); - } - return new ChangeItem(node.change); - }); - } - - return materialize(root); -} - -function countChangedPaths(repoRoot, sessions, changes) { - const changedKeys = new Set(); - - for (const change of changes || []) { - if (change?.relativePath) { - changedKeys.add(normalizeRelativePath(change.relativePath)); - } - } - - for (const session of sessions || []) { - for (const change of session.touchedChanges || []) { - const absolutePath = change?.absolutePath - || path.join(session.worktreePath || '', change?.relativePath || ''); - const normalizedRelativePath = absolutePath && isPathWithin(repoRoot, absolutePath) - ? normalizeRelativePath(path.relative(repoRoot, absolutePath)) - : `${session.branch}:${normalizeRelativePath(change?.relativePath)}`; - if (normalizedRelativePath) { - changedKeys.add(normalizedRelativePath); - } - } - } - - return changedKeys.size; -} - -function buildRepoOverview(sessions, unassignedChanges, lockEntries, colonyTasks = []) { - const colonyTaskList = Array.isArray(colonyTasks) ? colonyTasks : []; - return { - sessionCount: sessions.length, - workingCount: countWorkingSessions(sessions), - finishedCount: countFinishedSessions(sessions), - idleCount: countIdleSessions(sessions), - unassignedChangeCount: (unassignedChanges || []).length, - lockedFileCount: Array.isArray(lockEntries) ? lockEntries.length : 0, - conflictCount: sessions.reduce( - (total, session) => total + (session.conflictCount || 0), - 0, - ) + (unassignedChanges || []).filter((change) => change.hasForeignLock).length, - colonyTaskCount: colonyTaskList.length, - pendingHandoffCount: colonyTaskList.reduce( - (total, task) => total + (task.pending_handoff_count || 0), - 0, - ), - }; -} - -function groupSessionsByWorktree(sessions) { - const sessionsByWorktree = new Map(); - - for (const session of sessions || []) { - const worktreePath = sessionWorktreePath(session); - const key = worktreePath || session?.branch || `session-${sessionsByWorktree.size + 1}`; - if (!sessionsByWorktree.has(key)) { - sessionsByWorktree.set(key, { - worktreePath, - sessions: [], - }); - } - sessionsByWorktree.get(key).sessions.push(session); - } - - return [...sessionsByWorktree.values()] - .map((entry) => ({ - ...entry, - sessions: entry.sessions.sort((left, right) => ( - sessionTreeLabel(left).localeCompare(sessionTreeLabel(right)) - )), - })) - .sort((left, right) => { - const leftLabel = path.basename(left.worktreePath || '') || ''; - const rightLabel = path.basename(right.worktreePath || '') || ''; - return leftLabel.localeCompare(rightLabel) - || (left.worktreePath || '').localeCompare(right.worktreePath || ''); - }); -} - -function partitionChangesByOwnership(sessions, changes) { - const changesBySession = new Map(); - const sessionByChangedPath = new Map(); - const repoRootChanges = []; - - for (const session of sessions) { - changesBySession.set(session.branch, []); - for (const changedPath of session.changedPaths || []) { - if (!sessionByChangedPath.has(changedPath)) { - sessionByChangedPath.set(changedPath, session); - } - } - } - - for (const change of changes) { - const normalizedRelativePath = normalizeRelativePath(change.relativePath); - const session = sessionByChangedPath.get(normalizedRelativePath) - || sessions.find((candidate) => isPathWithin(candidate.worktreePath, change.absolutePath)); - if (!session) { - repoRootChanges.push(change); - continue; - } - - const localizedChange = localizeChangeForSession(session, change); - if (!localizedChange) { - repoRootChanges.push(change); - continue; - } - - changesBySession.get(session.branch).push(localizedChange); - } - - return { - changesBySession, - repoRootChanges, - }; -} - -function buildGroupedChangeTreeNodes(sessions, changes) { - const { changesBySession, repoRootChanges } = partitionChangesByOwnership(sessions, changes); - - const items = buildProjectScopedItems( - groupSessionsByWorktree( - sessions.filter((session) => (changesBySession.get(session.branch) || []).length > 0), - ).map(({ worktreePath, sessions: worktreeSessions }) => { - const sessionItems = worktreeSessions.map((session) => ( - new SessionItem( - session, - buildChangeTreeNodes(changesBySession.get(session.branch) || []), - { - label: sessionTreeLabel(session), - variant: 'raw', - }, - ) - )); - const changedCount = worktreeSessions.reduce( - (total, session) => total + ((changesBySession.get(session.branch) || []).length), - 0, - ); - return { - projectRelativePath: worktreeProjectRelativePath(worktreeSessions), - sessions: worktreeSessions, - item: new WorktreeItem(worktreePath, worktreeSessions, sessionItems, { changedCount }), - }; - }), - ); - - if (repoRootChanges.length > 0) { - items.push(new SectionItem('Repo root', buildChangeTreeNodes(repoRootChanges), { - description: String(repoRootChanges.length), - })); - } - - return items; -} - -function countActiveSessions(sessions) { - return sessions.filter((session) => session.activityKind !== 'dead').length; -} - -function countSessionsByActivityKind(sessions, activityKind) { - return sessions.filter((session) => session.activityKind === activityKind).length; -} - -function resolveSessionActivityIconId(activityKind) { - return SESSION_ACTIVITY_ICON_IDS[activityKind] || 'loading~spin'; -} - -async function pickRepoRoot() { - const repoRoots = discoverWorkspaceRepoRoots(); - if (repoRoots.length === 0) { - vscode.window.showInformationMessage?.('Open a Guardex workspace folder to start an agent.'); - return null; - } - - if (repoRoots.length === 1) { - return repoRoots[0]; - } - - const selectedRepoRoot = preferredRepoRoot(repoRoots); - if (selectedRepoRoot) { - return selectedRepoRoot; - } - - const picks = repoRoots.map((repoRoot) => ({ - label: repoPickLabel(repoRoot), - description: repoPickDescription(repoRoot), - detail: repoRoot, - repoRoot, - })); - const selection = await vscode.window.showQuickPick?.(picks, { - placeHolder: 'Select the Git repo where the Start agent launcher should run.', - }); - return selection?.repoRoot || null; -} - -async function promptStartAgentDetails() { - const taskName = await vscode.window.showInputBox?.({ - prompt: 'Task for the Guardex agent launcher', - placeHolder: 'vscode active agents welcome view', - ignoreFocusOut: true, - validateInput: (value) => value.trim() ? undefined : 'Task is required.', - }); - if (!taskName) { - return null; - } - - const agentName = await vscode.window.showInputBox?.({ - prompt: 'Agent name for the Guardex agent launcher', - placeHolder: 'codex', - value: 'codex', - ignoreFocusOut: true, - validateInput: (value) => value.trim() ? undefined : 'Agent name is required.', - }); - if (!agentName) { - return null; - } - - return { - taskName: taskName.trim(), - agentName: agentName.trim(), - }; -} - -async function startAgentFromPrompt(refresh) { - const repoRoot = await pickRepoRoot(); - if (!repoRoot) { - return; - } - - const details = await promptStartAgentDetails(); - if (!details) { - return; - } - - const terminal = vscode.window.createTerminal?.({ - name: `GitGuardex: ${path.basename(repoRoot)}`, - cwd: repoRoot, - }); - terminal?.show(true); - terminal?.sendText(resolveStartAgentCommand(repoRoot, details), true); - refresh(); -} - -function sessionSelectionKey(session) { - if (!session?.repoRoot || !session?.branch) { - return ''; - } - - return `${session.repoRoot}::${session.branch}`; -} - -function formatGitCommandFailure(error) { - for (const value of [error?.stderr, error?.stdout, error?.message]) { - if (typeof value === 'string' && value.trim().length > 0) { - return value.trim(); - } - } - return 'Git command failed.'; -} - -function runGitCommand(worktreePath, args) { - return cp.execFileSync('git', ['-C', worktreePath, ...args], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - }); -} - -function stageWorktreeForCommit(worktreePath) { - runGitCommand(worktreePath, ['add', '-A', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`]); -} - -function commitWorktree(worktreePath, message) { - runGitCommand(worktreePath, ['commit', '-m', message]); -} - -function buildSessionDetailItems(session) { - const provider = resolveSessionProvider(session); - const snapshot = sessionSnapshotDisplayName(session); - const projectRelativePath = resolveSessionProjectRelativePath(session); - const badgeSummary = uniqueStringList([ - ...(session.riskBadges || []), - session.deltaLabel || '', - ].filter(Boolean)).join(', '); - const sessionHealthSummary = buildSessionHealthSummary(session); - const items = [ - new DetailItem('Recent change', session.recentChangeSummary || 'No recent change summary.', { - iconId: 'history', - }), - new DetailItem('Top files', session.topChangedFilesLabel || 'No tracked file edits.', { - iconId: 'list-flat', - }), - ]; - if (badgeSummary) { - items.push(new DetailItem('Signals', badgeSummary, { - iconId: 'warning', - })); - } - if (sessionHealthSummary) { - items.push(new DetailItem('Session health', sessionHealthSummary, { - iconId: 'pulse', - tooltip: buildSessionHealthTooltip(session) || sessionHealthSummary, - })); - } - if (provider?.label) { - items.push(new DetailItem('Provider', provider.label, { - iconId: 'rocket', - })); - } - if (snapshot) { - items.push(new DetailItem('Snapshot', snapshot, { - iconId: 'device-camera', - })); - } - if (projectRelativePath) { - items.push(new DetailItem('Project', projectRelativePath, { - iconId: 'folder', - tooltip: projectRelativePath, - })); - } - items.push(new DetailItem('Branch', session.branch, { - iconId: 'git-branch', - })); - items.push(new DetailItem('Worktree', session.worktreePath, { - iconId: 'folder-library', - tooltip: session.worktreePath, - })); - return items; -} - -function buildWorkingNowNodes(sessions) { - const sessionEntries = sortSessionsForWorkingNow( - sessions.filter((session) => ( - session.activityKind === 'working' || session.activityKind === 'blocked' - )), - ).map((session) => ({ - projectRelativePath: resolveSessionProjectRelativePath(session), - sessions: [session], - item: new SessionItem(session, buildSessionDetailItems(session)), - })); - return buildProjectScopedItems(sessionEntries, { rootLabel: 'Repo root' }); -} - -function buildIdleThinkingNodes(sessions) { - const sessionEntries = sortSessionsForIdleThinking( - sessions.filter((session) => !( - session.activityKind === 'working' - || session.activityKind === 'blocked' - || session.activityKind === 'finished' - )), - ).map((session) => ({ - projectRelativePath: resolveSessionProjectRelativePath(session), - sessions: [session], - item: new SessionItem(session, buildSessionDetailItems(session)), - })); - return buildProjectScopedItems(sessionEntries, { rootLabel: 'Repo root' }); -} - -function buildNeedsCleanupNodes(sessions) { - const sessionEntries = sessions - .filter((session) => session.activityKind === 'finished') - .map((session) => ({ - projectRelativePath: resolveSessionProjectRelativePath(session), - sessions: [session], - item: new SessionItem(session, buildSessionDetailItems(session)), - })); - return buildProjectScopedItems(sessionEntries, { rootLabel: 'Repo root' }); -} - -function buildUnassignedChangeNodes(changes) { - return sortUnassignedChanges(changes).map((change) => new ChangeItem(change, { - label: compactRelativePath(change.relativePath), - description: buildUnassignedChangeDescription(change), - iconId: changeNeedsWarningIcon(change) ? 'warning' : undefined, - })); -} - -function buildRawActiveAgentGroupNodes(sessions) { - const groups = []; - for (const group of SESSION_ACTIVITY_GROUPS) { - const groupSessions = sessions.filter((session) => session.activityKind === group.kind); - const worktreeItems = buildProjectScopedItems( - groupSessionsByWorktree(groupSessions).map(({ worktreePath, sessions: worktreeSessions }) => ({ - projectRelativePath: worktreeProjectRelativePath(worktreeSessions), - sessions: worktreeSessions, - item: new WorktreeItem( - worktreePath, - worktreeSessions, - worktreeSessions.map((session) => new SessionItem( - session, - buildChangeTreeNodes(session.touchedChanges || []), - { - label: sessionTreeLabel(session), - variant: 'raw', - }, - )), - { - description: buildWorktreeBranchDescription(worktreeSessions), - iconId: 'git-branch', - resourceSession: worktreeSessions[0], - useSessionDecoration: true, - }, - ), - })), - { rootLabel: 'Repo root' }, - ); - if (worktreeItems.length > 0) { - groups.push(new SectionItem(group.label, worktreeItems, { - iconId: resolveSessionActivityIconId(group.kind), - })); - } - } - - return groups; -} - -class ActiveAgentsProvider { - constructor(decorationProvider) { - this.decorationProvider = decorationProvider; - this.onDidChangeTreeDataEmitter = new vscode.EventEmitter(); - this.onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event; - this.onDidChangeSelectedSessionEmitter = new vscode.EventEmitter(); - this.onDidChangeSelectedSession = this.onDidChangeSelectedSessionEmitter.event; - this.treeView = null; - this.lockRegistryByRepoRoot = new Map(); - this.selectedSession = null; - this.viewSummary = { - sessionCount: 0, - workingCount: 0, - finishedCount: 0, - idleCount: 0, - unassignedChangeCount: 0, - lockedFileCount: 0, - deadCount: 0, - conflictCount: 0, - }; - this.previousSnapshot = null; - } - - getTreeItem(element) { - return element; - } - - attachTreeView(treeView) { - this.treeView = treeView; - this.updateViewState({ - sessionCount: 0, - workingCount: 0, - finishedCount: 0, - idleCount: 0, - unassignedChangeCount: 0, - lockedFileCount: 0, - deadCount: 0, - conflictCount: 0, - }); - treeView.onDidChangeSelection?.((event) => { - const sessionItem = event.selection.find((item) => item instanceof SessionItem); - this.setSelectedSession(sessionItem?.session || null); - }); - } - - setSelectedSession(session) { - const nextSession = session?.worktreePath ? { ...session } : null; - const currentKey = sessionSelectionKey(this.selectedSession); - const nextKey = sessionSelectionKey(nextSession); - this.selectedSession = nextSession; - this.decorationProvider?.setSelectedBranch(nextSession?.branch || ''); - if (currentKey !== nextKey) { - this.onDidChangeSelectedSessionEmitter.fire(this.selectedSession); - } - } - - getSelectedSession() { - return this.selectedSession ? { ...this.selectedSession } : null; - } - - getViewSummary() { - return { ...this.viewSummary }; - } - - syncSelectedSession(repoEntries) { - if (!this.selectedSession) { - return; - } - - const nextSession = repoEntries - .flatMap((entry) => entry.sessions) - .find((session) => sessionSelectionKey(session) === sessionSelectionKey(this.selectedSession)); - this.setSelectedSession(nextSession || null); - } - - updateViewState(summary) { - if (!this.treeView) { - return; - } - - const sessionCount = summary?.sessionCount || 0; - const conflictCount = summary?.conflictCount || 0; - this.viewSummary = { ...summary }; - void vscode.commands.executeCommand('setContext', 'guardex.hasAgents', sessionCount > 0); - void vscode.commands.executeCommand('setContext', 'guardex.hasConflicts', conflictCount > 0); - - this.treeView.badge = sessionCount > 0 - ? { - value: sessionCount, - tooltip: buildOverviewDescription(summary), - } - : undefined; - this.treeView.message = undefined; - } - - annotateRepoEntries(repoEntries) { - const hasPreviousSnapshot = Boolean(this.previousSnapshot); - const nextSnapshot = { - sessions: new Map(), - changes: new Map(), - }; - - const annotatedEntries = repoEntries.map((entry) => { - const sessions = entry.sessions.map((session) => { - const snapshotKey = sessionSnapshotKey(session); - nextSnapshot.sessions.set(snapshotKey, buildSessionSnapshot(session)); - const deltaLabel = hasPreviousSnapshot - ? deriveSessionDelta(this.previousSnapshot.sessions.get(snapshotKey), session) - : ''; - return { - ...session, - deltaLabel, - riskBadges: uniqueStringList([ - ...(session.riskBadges || []), - deltaLabel, - ].filter(Boolean)), - }; - }); - - const changes = entry.changes.map((change) => { - const snapshotKey = changeSnapshotKey(entry.repoRoot, change); - nextSnapshot.changes.set(snapshotKey, buildChangeSnapshot(change)); - const deltaLabel = hasPreviousSnapshot - ? deriveChangeDelta(this.previousSnapshot.changes.get(snapshotKey), change) - : ''; - return { - ...change, - deltaLabel, - riskBadges: changeRiskBadges({ - ...change, - deltaLabel, - }), - }; - }); - - const { repoRootChanges } = partitionChangesByOwnership(sessions, changes); - const unassignedChanges = sortUnassignedChanges(repoRootChanges); - const colonyTasks = Array.isArray(entry.colonyTasks) ? entry.colonyTasks : []; - return { - ...entry, - sessions, - changes, - unassignedChanges, - colonyTasks, - overview: buildRepoOverview(sessions, unassignedChanges, entry.lockEntries, colonyTasks), - }; - }); - - this.previousSnapshot = nextSnapshot; - return annotatedEntries; - } - - async syncRepoEntries() { - const repoEntries = this.annotateRepoEntries(await this.loadRepoEntries()); - const summary = { - sessionCount: repoEntries.reduce((total, entry) => total + entry.sessions.length, 0), - workingCount: repoEntries.reduce((total, entry) => total + entry.overview.workingCount, 0), - finishedCount: repoEntries.reduce( - (total, entry) => total + (entry.overview.finishedCount || 0), - 0, - ), - idleCount: repoEntries.reduce((total, entry) => total + entry.overview.idleCount, 0), - unassignedChangeCount: repoEntries.reduce( - (total, entry) => total + entry.overview.unassignedChangeCount, - 0, - ), - lockedFileCount: repoEntries.reduce((total, entry) => total + entry.overview.lockedFileCount, 0), - deadCount: repoEntries.reduce( - (total, entry) => total + countSessionsByActivityKind(entry.sessions, 'dead'), - 0, - ), - conflictCount: repoEntries.reduce((total, entry) => total + entry.overview.conflictCount, 0), - }; - - this.updateViewState(summary); - this.decorationProvider?.updateSessions(repoEntries.flatMap((entry) => entry.sessions)); - this.decorationProvider?.updateLockEntries(repoEntries); - return repoEntries; - } - - async refresh() { - await this.syncRepoEntries(); - this.onDidChangeTreeDataEmitter.fire(); - this.decorationProvider?.refresh(); - } - - readLockRegistryForRepo(repoRoot) { - const lockRegistry = readLockRegistry(repoRoot); - this.lockRegistryByRepoRoot.set(repoRoot, lockRegistry); - return lockRegistry; - } - - getLockRegistryForRepo(repoRoot) { - return this.lockRegistryByRepoRoot.get(repoRoot) || this.readLockRegistryForRepo(repoRoot); - } - - refreshLockRegistryForFile(filePath) { - this.readLockRegistryForRepo(repoRootFromLockFile(filePath)); - } - - async getChildren(element) { - if (element instanceof RepoItem) { - const sectionItems = [ - new SectionItem('Overview', [ - new DetailItem('Summary', buildOverviewDescription(element.overview), { - iconId: 'dashboard', - tooltip: buildRepoTooltip(element.repoRoot, element.overview), - }), - ], { - description: '1', - iconId: 'telescope', - }), - ]; - - const workingNowItems = buildWorkingNowNodes(element.sessions); - if (workingNowItems.length > 0) { - sectionItems.push(new SectionItem('Working now', workingNowItems, { - description: String(workingNowItems.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'loading~spin', - })); - } - - const needsCleanupItems = buildNeedsCleanupNodes(element.sessions); - if (needsCleanupItems.length > 0) { - sectionItems.push(new SectionItem('Needs cleanup', needsCleanupItems, { - description: String(needsCleanupItems.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'pass-filled', - })); - } - - const idleThinkingItems = buildIdleThinkingNodes(element.sessions); - if (idleThinkingItems.length > 0) { - sectionItems.push(new SectionItem('Idle / thinking', idleThinkingItems, { - description: String(idleThinkingItems.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'debug-pause', - })); - } - - if (element.unassignedChanges.length > 0) { - sectionItems.push(new SectionItem('Unassigned changes', buildUnassignedChangeNodes(element.unassignedChanges), { - description: String(element.unassignedChanges.length), - iconId: 'inbox', - })); - } - - const advancedItems = []; - const rawActiveAgents = buildRawActiveAgentGroupNodes(element.sessions); - if (rawActiveAgents.length > 0) { - advancedItems.push(new SectionItem('Active agent tree', rawActiveAgents, { - description: String(element.sessions.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'organization', - })); - } - const rawChangeTree = buildGroupedChangeTreeNodes(element.sessions, element.changes); - if (rawChangeTree.length > 0) { - advancedItems.push(new SectionItem('Raw path tree', rawChangeTree, { - description: String(element.changes.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'file-directory', - })); - } - const colonyTaskList = Array.isArray(element.colonyTasks) ? element.colonyTasks : []; - if (colonyTaskList.length > 0) { - const colonyItems = colonyTaskList.map((task) => { - const pendingLabel = task.pending_handoff_count > 0 - ? formatCountLabel(task.pending_handoff_count, 'pending handoff') - : 'quiet'; - const participantLabel = - (task.participants || []).map((p) => p.agent).filter(Boolean).join(', ') - || 'no participants'; - return new DetailItem( - `#${task.id} · ${compactColonyBranchLabel(task.branch)}`, - `${participantLabel} · ${pendingLabel}`, - { - iconId: task.pending_handoff_count > 0 ? 'warning' : 'comment-discussion', - tooltip: [ - task.branch, - `task #${task.id}`, - participantLabel, - task.pending_handoff_count > 0 - ? formatCountLabel(task.pending_handoff_count, 'pending handoff') - : '', - ].filter(Boolean).join('\n'), - }, - ); - }); - advancedItems.push(new SectionItem('Colony tasks', colonyItems, { - description: String(colonyItems.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'organization', - })); - } - if (advancedItems.length > 0) { - sectionItems.push(new SectionItem('Advanced details', advancedItems, { - description: String(advancedItems.length), - collapsedState: vscode.TreeItemCollapsibleState.Collapsed, - iconId: 'settings-gear', - })); - } - return sectionItems; - } - - if (element instanceof SectionItem || element instanceof FolderItem || element instanceof WorktreeItem || element instanceof SessionItem) { - return element.items; - } - - const repoEntries = await this.syncRepoEntries(); - this.syncSelectedSession(repoEntries); - - if (repoEntries.length === 0) { - return [new InfoItem('No active Guardex agents', 'Open or start a sandbox session.')]; - } - - return repoEntries.map((entry) => new RepoItem(entry.repoRoot, entry.sessions, entry.changes, { - label: repoEntryDisplayLabel(entry.repoRoot, entry.sessions), - overview: entry.overview, - unassignedChanges: entry.unassignedChanges, - lockEntries: entry.lockEntries, - colonyTasks: entry.colonyTasks, - })); - } - - async loadRepoEntries() { - const repoEntries = await findRepoSessionEntries(); - return Promise.all( - repoEntries.map(async (entry) => { - const repoRoot = entry.repoRoot; - const lockRegistry = this.getLockRegistryForRepo(repoRoot); - const currentBranch = readCurrentBranch(repoRoot); - const colonyTasks = await readColonyTasksForRepo(repoRoot); - return { - repoRoot, - sessions: entry.sessions.map((session) => decorateSession(session, lockRegistry)), - changes: readRepoChanges(repoRoot).map((change) => ( - decorateChange(change, lockRegistry, currentBranch) - )), - lockEntries: Array.from(lockRegistry.entriesByPath.entries()), - colonyTasks, - }; - }), - ); - } -} - -function countEntryConflicts(entry) { - const sessionConflicts = entry.sessions.reduce( - (total, session) => total + (session.conflictCount || 0), - 0, - ); - const changeConflicts = entry.changes.filter((change) => change.hasForeignLock).length; - return sessionConflicts + changeConflicts; -} - -class SessionInspectPanelManager { - constructor() { - this.panel = null; - this.session = null; - } - - open(session) { - const targetSession = session?.branch ? { ...session } : null; - if (!targetSession?.repoRoot || !targetSession?.branch) { - showSessionMessage('Pick an Active Agents session first.'); - return; - } - if (!vscode.window.createWebviewPanel) { - showSessionMessage('Inspect panel is unavailable in this VS Code build.'); - return; - } - - this.session = targetSession; - if (!this.panel) { - this.panel = vscode.window.createWebviewPanel( - INSPECT_PANEL_VIEW_TYPE, - inspectPanelTitle(targetSession), - vscode.ViewColumn?.Beside, - { - enableFindWidget: true, - enableScripts: false, - retainContextWhenHidden: true, - }, - ); - this.panel.onDidDispose(() => { - this.panel = null; - this.session = null; - }); - } else { - this.panel.reveal?.(vscode.ViewColumn?.Beside); - } - - this.render(); - } - - resolveSession() { - if (!this.session?.repoRoot || !this.session?.branch) { - return this.session ? { ...this.session } : null; - } - - return readActiveSessions(this.session.repoRoot, { includeStale: true }) - .find((entry) => sessionSelectionKey(entry) === sessionSelectionKey(this.session)) - || { ...this.session }; - } - - render() { - if (!this.panel || !this.session) { - return; - } - - const session = this.resolveSession(); - if (!session) { - return; - } - - this.session = { ...session }; - this.panel.title = inspectPanelTitle(session); - this.panel.webview.html = renderInspectPanelHtml(session, readSessionInspectData(session)); - } - - refresh() { - this.render(); - } - - dispose() { - this.panel?.dispose(); - this.panel = null; - this.session = null; - } -} - -class ActiveAgentsRefreshController { - constructor(provider, inspectPanelManager = null) { - this.provider = provider; - this.inspectPanelManager = inspectPanelManager; - this.refreshTimer = null; - this.sessionWatchers = new Map(); - this.closedMissingWorktreeRepositories = new Set(); - this.observedWorktreePaths = new Set(); - } - - scheduleRefresh() { - if (this.refreshTimer) { - clearTimeout(this.refreshTimer); - } - this.refreshTimer = setTimeout(() => { - this.refreshTimer = null; - void this.refreshNow(); - }, REFRESH_DEBOUNCE_MS); - } - - async refreshNow() { - await this.syncSessionWatchers(); - await this.provider.refresh(); - this.inspectPanelManager?.refresh(); - } - - async syncSessionWatchers() { - const repoEntries = await findRepoSessionEntries(); - const liveSessionKeys = new Set(); - - for (const workspacePath of findDeletedManagedWorkspaceFolders()) { - await this.closeMissingWorktreeRepository(workspacePath); - } - - for (const entry of repoEntries) { - for (const session of entry.sessions) { - const worktreePath = sessionWorktreePath(session); - const normalizedWorktreePath = normalizeAbsolutePath(worktreePath); - if (normalizedWorktreePath && !fs.existsSync(normalizedWorktreePath)) { - await this.closeMissingWorktreeRepository(normalizedWorktreePath); - continue; - } - if (normalizedWorktreePath) { - this.closedMissingWorktreeRepositories.delete(normalizedWorktreePath); - this.observedWorktreePaths.add(normalizedWorktreePath); - } - - const sessionKey = resolveSessionWatcherKey(session); - liveSessionKeys.add(sessionKey); - if (this.sessionWatchers.has(sessionKey)) { - continue; - } - - const watcher = vscode.workspace.createFileSystemWatcher( - resolveSessionGitIndexPath(session.worktreePath), - ); - const disposables = bindRefreshWatcher(watcher, () => this.scheduleRefresh()); - this.sessionWatchers.set(sessionKey, { - watcher, - disposables, - worktreePath: normalizedWorktreePath, - }); - } - } - - for (const observedWorktreePath of this.observedWorktreePaths) { - if (fs.existsSync(observedWorktreePath)) { - this.closedMissingWorktreeRepositories.delete(observedWorktreePath); - continue; - } - await this.closeMissingWorktreeRepository(observedWorktreePath); - } - - for (const [sessionKey, entry] of this.sessionWatchers) { - if (liveSessionKeys.has(sessionKey)) { - continue; - } - - if (entry.worktreePath && !fs.existsSync(entry.worktreePath)) { - await this.closeMissingWorktreeRepository(entry.worktreePath); - } - disposeAll(entry.disposables); - entry.watcher.dispose(); - this.sessionWatchers.delete(sessionKey); - } - } - - async closeMissingWorktreeRepository(worktreePath) { - const normalizedWorktreePath = normalizeAbsolutePath(worktreePath); - if (!normalizedWorktreePath || this.closedMissingWorktreeRepositories.has(normalizedWorktreePath)) { - return; - } - - this.closedMissingWorktreeRepositories.add(normalizedWorktreePath); - await closeDeletedWorktreeRepository(normalizedWorktreePath); - } - - dispose() { - if (this.refreshTimer) { - clearTimeout(this.refreshTimer); - this.refreshTimer = null; - } - - for (const entry of this.sessionWatchers.values()) { - disposeAll(entry.disposables); - entry.watcher.dispose(); - } - this.sessionWatchers.clear(); - } -} - -function activate(context) { - const decorationProvider = new SessionDecorationProvider(); - const provider = new ActiveAgentsProvider(decorationProvider); - const inspectPanelManager = new SessionInspectPanelManager(); - const refreshController = new ActiveAgentsRefreshController(provider, inspectPanelManager); - const treeView = vscode.window.createTreeView('gitguardex.activeAgents', { - treeDataProvider: provider, - showCollapseAll: true, - }); - const activeAgentsStatusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10); - activeAgentsStatusItem.name = 'GitGuardex Active Agents'; - activeAgentsStatusItem.command = 'gitguardex.activeAgents.focus'; - provider.attachTreeView(treeView); - const scheduleRefresh = () => refreshController.scheduleRefresh(); - const handleWorkspaceFoldersChanged = () => { - scheduleRefresh(); - void ensureManagedRepoScanIgnores(); - }; - const refresh = () => void refreshController.refreshNow(); - const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB); - const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB); - const worktreeLockWatcher = vscode.workspace.createFileSystemWatcher(WORKTREE_AGENT_LOCKS_GLOB); - const managedWorktreeGitWatcher = vscode.workspace.createFileSystemWatcher(MANAGED_WORKTREE_GIT_FILES_GLOB); - const logWatcher = vscode.workspace.createFileSystemWatcher(AGENT_LOG_FILES_GLOB); - const updateStatusBar = () => { - const selectedSession = provider.getSelectedSession(); - const summary = provider.getViewSummary(); - if ((summary.sessionCount || 0) <= 0) { - activeAgentsStatusItem.hide(); - return; - } - - activeAgentsStatusItem.text = selectedSession?.branch - ? `$(git-branch) ${sessionIdentityLabel(selectedSession)} · ${formatCountLabel(selectedSession.lockCount || 0, 'lock')}` - : buildActiveAgentsStatusSummary(summary); - activeAgentsStatusItem.tooltip = buildActiveAgentsStatusTooltip(selectedSession, summary); - activeAgentsStatusItem.show(); - }; - updateStatusBar(); - const readCommitMessageForSession = async (session) => { - const rawMessage = await vscode.window.showInputBox?.({ - prompt: `Commit ${sessionIdentityLabel(session)} worktree`, - placeHolder: sessionCommitPlaceholder(session), - ignoreFocusOut: true, - }); - if (rawMessage === undefined) { - return undefined; - } - return String(rawMessage).trim(); - }; - const commitSelectedSession = async () => { - const selectedSession = provider.getSelectedSession(); - if (!selectedSession?.worktreePath) { - vscode.window.showInformationMessage?.('Pick an Active Agents session first.'); - return; - } - - if (!fs.existsSync(selectedSession.worktreePath)) { - vscode.window.showInformationMessage?.( - `Selected session worktree is no longer on disk: ${selectedSession.worktreePath}`, - ); - return; - } - - const message = await readCommitMessageForSession(selectedSession); - if (message === undefined) { - return; - } - if (!message) { - vscode.window.showInformationMessage?.('Enter a commit message first.'); - return; - } - - try { - stageWorktreeForCommit(selectedSession.worktreePath); - commitWorktree(selectedSession.worktreePath, message); - refresh(); - } catch (error) { - const failure = formatGitCommandFailure(error); - if (/nothing to commit|no changes added to commit/i.test(failure)) { - vscode.window.showInformationMessage?.(`No changes to commit in ${selectedSession.label}.`); - return; - } - vscode.window.showErrorMessage?.(`Active Agents commit failed: ${failure}`); - } - }; - const interval = setInterval(refresh, REFRESH_POLL_INTERVAL_MS); - const refreshLockRegistry = (uri) => { - if (uri?.fsPath) { - provider.refreshLockRegistryForFile(uri.fsPath); - } - scheduleRefresh(); - }; - - provider.onDidChangeSelectedSession((session) => { - updateStatusBar(); - decorationProvider.refresh(); - }); - provider.onDidChangeTreeData(() => { - updateStatusBar(); - }); - - context.subscriptions.push( - treeView, - activeAgentsStatusItem, - inspectPanelManager, - refreshController, - vscode.window.registerFileDecorationProvider(decorationProvider), - vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)), - vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh), - vscode.commands.registerCommand('gitguardex.activeAgents.restart', restartActiveAgents), - vscode.commands.registerCommand('gitguardex.activeAgents.focus', async () => { - await vscode.commands.executeCommand('workbench.view.extension.gitguardex-active-agents-container'); - }), - vscode.commands.registerCommand('gitguardex.activeAgents.commitSelectedSession', commitSelectedSession), - vscode.commands.registerCommand('gitguardex.activeAgents.openWorktree', async (session) => { - if (!session?.worktreePath) { - return; - } - - await vscode.commands.executeCommand( - 'vscode.openFolder', - vscode.Uri.file(session.worktreePath), - { forceNewWindow: true }, - ); - }), - vscode.commands.registerCommand('gitguardex.activeAgents.openChange', async (change) => { - if (!change?.absolutePath) { - return; - } - - if (!fs.existsSync(change.absolutePath)) { - vscode.window.showInformationMessage?.(`Changed path is no longer on disk: ${change.relativePath}`); - return; - } - - await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(change.absolutePath)); - }), - vscode.commands.registerCommand('gitguardex.activeAgents.inspect', (session) => { - inspectPanelManager.open(session || provider.getSelectedSession()); - }), - vscode.commands.registerCommand('gitguardex.activeAgents.showSessionTerminal', showSessionTerminal), - vscode.commands.registerCommand('gitguardex.activeAgents.finishSession', finishSession), - vscode.commands.registerCommand('gitguardex.activeAgents.syncSession', syncSession), - vscode.commands.registerCommand('gitguardex.activeAgents.stopSession', (session) => stopSession(session, refresh)), - vscode.commands.registerCommand('gitguardex.activeAgents.dismissSession', (session) => dismissSession(session, refresh)), - vscode.workspace.onDidChangeWorkspaceFolders(handleWorkspaceFoldersChanged), - activeSessionsWatcher, - lockWatcher, - worktreeLockWatcher, - managedWorktreeGitWatcher, - logWatcher, - { dispose: () => clearInterval(interval) }, - ); - - context.subscriptions.push( - ...bindRefreshWatcher(activeSessionsWatcher, scheduleRefresh), - ...bindRefreshWatcher(lockWatcher, refreshLockRegistry), - ...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh), - ...bindRefreshWatcher(managedWorktreeGitWatcher, scheduleRefresh), - ...bindRefreshWatcher(logWatcher, scheduleRefresh), - ); - void ensureManagedRepoScanIgnores(); - void refreshController.refreshNow(); - void maybeAutoUpdateActiveAgentsExtension(context); -} - -function deactivate() {} - -module.exports = { - activate, - deactivate, -}; diff --git a/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json b/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json deleted file mode 100644 index e8e59681..00000000 --- a/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "iconDefinitions": { - "_gitguardex_agent": { - "iconPath": "./icons/agent.svg" - }, - "_gitguardex_branch": { - "iconPath": "./icons/branch.svg" - }, - "_gitguardex_config": { - "iconPath": "./icons/config.svg" - }, - "_gitguardex_hook": { - "iconPath": "./icons/hook.svg" - }, - "_gitguardex_openspec": { - "iconPath": "./icons/openspec.svg" - }, - "_gitguardex_plan": { - "iconPath": "./icons/plan.svg" - }, - "_gitguardex_spec": { - "iconPath": "./icons/spec.svg" - } - }, - "folderNames": { - ".agents": "_gitguardex_agent", - ".githooks": "_gitguardex_hook", - ".omc": "_gitguardex_agent", - ".omx": "_gitguardex_agent", - "agent-worktrees": "_gitguardex_branch", - "changes": "_gitguardex_openspec", - "plan": "_gitguardex_plan", - "rules": "_gitguardex_spec", - "specs": "_gitguardex_spec" - }, - "fileNames": { - ".openspec.yaml": "_gitguardex_config", - "AGENT.lock": "_gitguardex_agent", - "AGENTS.md": "_gitguardex_agent", - "CLAUDE.md": "_gitguardex_agent", - "config.yaml": "_gitguardex_config", - "context-docs-cue.md": "_gitguardex_spec", - "post-checkout": "_gitguardex_hook", - "pre-commit": "_gitguardex_hook", - "pre-push": "_gitguardex_hook", - "proposal.md": "_gitguardex_openspec", - "spec.md": "_gitguardex_spec", - "tasks.md": "_gitguardex_plan", - "plan.md": "_gitguardex_plan" - }, - "fileExtensions": { - "openspec.yaml": "_gitguardex_config" - } -} diff --git a/vscode/guardex-active-agents/fileicons/icons/agent.svg b/vscode/guardex-active-agents/fileicons/icons/agent.svg deleted file mode 100644 index f29ca828..00000000 --- a/vscode/guardex-active-agents/fileicons/icons/agent.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/vscode/guardex-active-agents/fileicons/icons/branch.svg b/vscode/guardex-active-agents/fileicons/icons/branch.svg deleted file mode 100644 index 62242793..00000000 --- a/vscode/guardex-active-agents/fileicons/icons/branch.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/vscode/guardex-active-agents/fileicons/icons/config.svg b/vscode/guardex-active-agents/fileicons/icons/config.svg deleted file mode 100644 index 6b4e2f9c..00000000 --- a/vscode/guardex-active-agents/fileicons/icons/config.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/vscode/guardex-active-agents/fileicons/icons/hook.svg b/vscode/guardex-active-agents/fileicons/icons/hook.svg deleted file mode 100644 index 384987c2..00000000 --- a/vscode/guardex-active-agents/fileicons/icons/hook.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/vscode/guardex-active-agents/fileicons/icons/openspec.svg b/vscode/guardex-active-agents/fileicons/icons/openspec.svg deleted file mode 100644 index 8cc93ff9..00000000 --- a/vscode/guardex-active-agents/fileicons/icons/openspec.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/vscode/guardex-active-agents/fileicons/icons/plan.svg b/vscode/guardex-active-agents/fileicons/icons/plan.svg deleted file mode 100644 index 15255686..00000000 --- a/vscode/guardex-active-agents/fileicons/icons/plan.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/vscode/guardex-active-agents/fileicons/icons/spec.svg b/vscode/guardex-active-agents/fileicons/icons/spec.svg deleted file mode 100644 index 7b3da2be..00000000 --- a/vscode/guardex-active-agents/fileicons/icons/spec.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/vscode/guardex-active-agents/icon.png b/vscode/guardex-active-agents/icon.png deleted file mode 100644 index e0b975050f2e20a4488e6b862032105788c759e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 230734 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%0D1ka6XOgP*K{uskB#3pHcU5n?dV;SM5wfsnQuP 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∋;Q+{ik~%-*iqKk1jc) zk(!l}CAR}WdGLSLUt6)T|Ha9hiN?OSN`_40!A38ChC-hr^Uq{j(gF?^RGi-TZrz69 z5N|BB`pW!xaQN$&v)QCf6PWNzTd~6%^TlpsrYrZ~p)-KRdtzQT`=o_iqgi%4e6Fnj z6wUGPW$HXfotK$)rFG$GgsDbqgbj%k4p$9ek>(lThMjmN^ybSh3{-<*N8{oJLZ!cd zG?7VQhY@BFb12l8iPLQ`Q5_WGxj%I`|A0>}z*inZ)pq;j*4@25$wxp<~)R!^k6BAcQ>v3G)4bmH2hH)-kpjHBE^ zu9Ql01bK@iB)u6_1E@&2A!+(8+8D$!5=p7(Y2@?~N{RCi;|kzVEAoT+l zld<)x@{d6uY#Oy?5Q!3oeDXSoD75+U9rZ%N4^TIHRaL=W^Y32>RTie6F($H!crXXF z(CqsiNt}l-E(CTN3+&!o2=oe10>u^i!q^FnGQpmq-K%7he#g|{f_;F=qPByo zPv-N#WCTlsmG#?>G?!PNbPFzNb10jX>QG;lxg(Pj~qln~*hd@1q5z4t4P)S_wjw3xjxxIcnmR!=EoLsvZ zc+q&7w@g`|%$9lQ95 z;3t;Wpc*TDb1)LhQc!kuLd9Wdysr`xz?%hfw!j+%9vGGZQoJ|VniHE@*+;5LfK!fm zM+yUak#SP9+i~!l8m|peP+x`ZbO+CfJ;B2xApQ=s;yndh_93SLaNktcAyt5Y$O@Nq zz&Um1w*4fUFp0BoZwu8} zzu)fXy^ZtZ?Yph^L#ONVf{=p63mtSM6<<)G<&Ef9R+*`h^?!S!k4!By_{cDXd}3p{ z${e-h&s0!LVZ<}8UQHhu28Z1naVgY!PoeZqS%>`NXS|S3fgW}Z zEaDD#LwM65f?cri`M;ygg8!L95ZqnVrz(d(GODwj83qo^bfOhYpb=YcXz`E{6gOwk zPP(I7j%>F?+&<2*Mq8C`V-Kuxv#DKQZSrfaGFs(>>>1mFW(Bm8oTj8ZxK`&mbk$uRJaS_7?4Z3Zv? zvc$TZ{;m$K4KJ@1%N+XXz0dmVOIyD@-fJUF9%+OdKxU} zPa{lOZ-1Iy*`E&tUhuCDXG`RszC9r`G`qW;c5!nHb@E%K%RO!U%R2pUHSVH*)5#OR zihKwx?w#g?X&Bm=L`{FoH$OTSD`jxZ%+2+=i9|TuS3`!SBt6%|9s4w(Y47Y2fNkAp zE)b+ks{3vDa|LK7r0yu?1Ur*3K}z+H0-voT6#EiU<^^mxX3Z*Yd-Biua|C>}S8gPbX$rX@iMiaoj^m5lJP4GzS83Z6Z!1VSnBOTDMB5@) zVHJcIIBe{8?B=b+hyCiZy|jk(%Ue@YS(@wDUl*@MdW{Nl*Aeo}Lr*952$8QK7jRja z+kLlt#RSK#aKM!+;VBZ7hY$+Hvra+KXvCE&Yc*Pr74r!8qSW zvc#3X)#lb_NEYi0iAXp+yOX&?xKE%Wt4u0T4c$$N|1CkWYF@v7<5<2t0EPoPg0fsV7=CBqaccoF>cAr> zsLWd99MjW7I;R#nrq(hhpl%pZy@?6@^F)WSx3#A1E?Rf%NT;xhS6zn} zFDbu!u_mHAHOrN)FfaE8(+r$IlyuzWTM{?5s8Z#&<)QS{AbdAe!_47kDiDMImUIqH z3^Cs(8?6o(u3)`oH~$m=Xn&MeK_2g!CZSEp&snjA=2Gw%1Cdl|g?0+Nk=57VOeU{! zP~3D7o%Bu2<#w~B^tdAGs9J);FE(8#Ef%0hO!hP(joVdKhxHhfJN!a1Ju%1(QZ z2X|i{gz4?sbYelCQ^jF^2|cuJj%|JP=WnLc-EhRvlhJ#k>p(=ITCV+^B9wrw zqu5T|6r)p|sg8Vbj#o~jmA8&H<9f`q$OwTWdD zXB98r<8}OBP5!JK1X&@T(vYYur8$vo3ZWUu)eT15Np_=}T~zD*{8CpU)FlbBgXB`A zBF6EL4OjTpdWQ(y-@n5{PfvFe&}U}>`5^H1nBw@DO{1N*uw{l{SwEW50eX5)7xHh>=rH3J zH0?jb@Aikq0j}|kpXj_&dvHNW(IXS0At3L?i^Gj&SeN%PPkm)er(l@z!NIt0UM@nB z8i`9v56zLc%mWIP-#3ilBMDHh>NaT%S85$q;E)EfN}_d+IWH#aJ;-`{-?yrpYDH0D3(0A9A78c0vc4Pnd2 zK#`NafyyqQN}~yDy9~F~8oiBRPf?r2!;S6YPK`!KW)0{pq4|a?>-dAn9vD)W1cp0V zjNM^AlAS2<3x03a0#W!MrIJpUhp03u%<8^dp)9#LkqbEx^q3Rxn=(($9$XaH()@TG zcJbiX^>ZNc1;3ad53kJe-G;koN3Tj8mx&63@t2v*ilHceQtXfeZJsc;XoQjuH5-7l zLXL>Qp|AA?@hd}@9Gl6DV=zXV5qz!`_99gQ9)-gs15n3kUQO_r>o`i%vWzcxxO#|V zJw)n~Zlqu7kb|1IQv!C_7yEaj?i1`gxa6P(#6#Wv+%%%Q8|VcckxwiK*N>LoBzF3w z724Tq_!94@w;UnJU+CLocGwDSgs7VMfWYHO1+Tpqhi#Feda!E09FuP9x7w5mF|fII zRjo@@fAL3%+CK3QZUAU77Xi;VAW%cP$r9m+?7Wv}%*edD9>40IDW16xA%eybf@5EQ zMk4HN$b$o1BQnd<_vk9Lc7`JLvG2^UpB+Fn7r6>wc=XZ`#jELZcgrg2bDUDkOL&3D zEhNq_pNq^qF*qSoS9|e=fYB1nt79nMUzSOW`3eOIWl@x8IsR(pHB;<{Zh%NJ%hl1;b13xaz&^lIV^9wEOD4>u z4RM!i zf;wTje3D59yXjR!*f{MD#$0$@m%FGpG_T0AIB5SBHW(0>mO|kX8xLw*E9eZ?&5h=; z;Ipzht`!P54-Ug_D)EmjMyPOQ&wJpbVYn_cze2DQTT)QtVsaTh7B0JQ_oRFcwiDR| z-3&T5e@scGs&^SaS$G8aQcG9Ye;uCxf@{rp1Tau0yBrPm+Z}r|23t%*@O2{rB5@3W+Klj=n6>&HdW_i2dVyKtGhP!aLg!$4=+1|c0zqlUA z5h4qyWxvINks*`68`IsAegK1aDxh2YZ6&^GJEQ!Pw!tL?_^Ge&-G#}h+hte^Y#{ev zRiTs6vm&H2_l6H7Rl-PK@ zQF$)uBWVD`(dkf6PEl0H>PTT`@mL-eWh7o|?SJVoXl$IxIgnJCG%2p}Bu9F?Exuq| zzfv}oT#cSFcj^3G=-RO!yIk;hXQHAIBcYunjf)<-YIywi)+I3TZq|2Va|fKZnve1W zYPP+{AT7=q*ANO77fa*D@AQeay6O#R(&n9`VqxkyWe|DBoS;Q~)X1Np??-0YRGXMK z6uc*QW^^tND-nCsLZ9!`dW6W(ro+SuHxE>!-Vw)EOiC;<{O09kWKH}4?S{{5rI!N4 zQGGL29i9tU_+zwi3yZx!w*)~mf#WOtq`x61uQ4TE(++6)o*AjeTPo>BE%B>V!ST2( zviob`bo(zI0L@i>#LM=f)L7Alpok5uJb?8;j$G0A>sJX=6Fy)K(iUbP7Yg=dEOwB% z$_Zv(+PS7j!2wmDUvlZ}A0A zu&z-rE2W@}0rGC7+EXv0N7#PvQdE)Y@W&w5m81yDpskN+lvf2?Y=AOxV~uO8k9FXI z0o~}ByVbiwuC|+2&oL1~pnyXb%BHT{{`~WHFX7-sFn zT2mQ8AXMI4!wEDM@d-dzcY9&ZE`)wZE|+3Z@k65+)Fh^G-~v+G z*+V33wn6NX6zhaVNO`?^DuT@rE@#08uHkl`+oEDJnd9%XcLiQU$ItijX_2uOiS zQ=Mo`2wN>FG&UC9(ZpBy&4>Fr1t4et+CIn!OM&jrphgkVzhDD-VRiQ-_pbJCGw`_a zmT6t)_uE6zmrnEX|5XrGKNo(s_&8k%V36Q)L4UWCwbjpe)r-)`sbZ|smw84iSo0}? z3NDk2W=Bp<-O}w+vSC~ybY}_=1W7BZ#v*xhS97*rony1<&2-vgbH&%bTT$=ne$sY! zJea7U`^%_MRmY?*IhEjk6@hWpUE&f}tf5T<=R73_GG!KyzDfzPxxD-R-=|L#6BF}Ojr%ta?!&$dXWr5y z*;ncO`oGSkieQpwzmlE90EY6TgE8D1iwW8skl3Bi{crsioiA%hZZL``sKllvV1%JY zbdrVNF}`AxWE^-IKA7wD>nohaJuP(JH6{o?3Y_=0)<2%OS)Ih-BE4*js_KQiUp}yw zAt1LL?2<(u#3>7d_0ZU#m(8G(w$nG^Viu}o$m}KlgnZ`;^E{mGlG>l39VYzprX1qzPKk zt}rYgYjbrO#GI&0Qz_y_1&^fQn27+Tk-u}&_c4YOho_gJJYnuce(+{QGSy?%M z!IeNTXuj3I@b`xlWj#GO7wSJ93M|ywm&4T0En1gWks~u@gGEKtkQx#M>qfxpk^dYUS44{}~ zWuX;^I-yiEB3e$-h>H`8n$XsP?P6S=s9%F$nwSZC+1CF?q;q(f65K~HSh8|(D?1~C z+O|nhD$_C6n2(6Njgni=TGII^--xVjzIs5K2IdfL^gv;bZpb#|tOv-_vjKc9|DD&*#PbQ#uqsT4hh2Q}lJpAD1MwB;ha*tzJNT#Xi$B87~cJ9$Dna_P(5!Y}2j9onMT~Z}ID3DcUDnQk<=)&~L zpOF&@VjvY2{~Xa(afGeC?p3!_2Z&-p1>V-xix*5qU#6tc(V^IS=Z~7c)0S3VLF7t%EoEpWpjA=tWCu z0XqrmtV&Pm^Aw(ueOV@@AZ)NW`7theW~M7;MU^%o!ZJt%uTUx5@$K?x1W{IZx_q>= zajRRe%l#xZ&4;`osQ-mE9~&I_It{& z1rE>jg3HT4kz@FNr4zu+IKlgAfPmPLXFYTt{=PgET2c1#zJ!n_jxqm( zGj_t_mw5MyJO|@v?ikZwcsX{ejVeh{F9{-tLr5$Y4aIl%9v@y3--v*fz~x{4#g5lf z=8_eoblG3&9|UZMGb(MWa`g(rthK-ovk*7Iv?(+sMB(@bVnr?{ScJf69rJ}-&Js&y zkzwQ#GjeoiQpbMx3axQ7C}$gbr#geX?H;nkioM3(ra+P&)Ua(gRaR|M{)rcFVQzle zUY~!Z^-N|4qlD_vm;Fcd|`Lk5-zN;$K*K$SNU%b8849Fcw9mzz!`Dz zTVWE%Z65L`G=_>l$dCxrTG{ZuK^Z#IaAvyhKDnG?i~~-&kRP7Odv+2#dd8f*Z8mM& zW?`joUu;-VJTFuC6~w5XsCz4U-Gvw%G~Q3q75XoJ)LrV7f5$?c#;O%GQoR;1D|0^E z!&va&SKwr~9N&C{D#`Jpr#kfz%+V5HNN}+Tr1JIs`mq#G8b)oUWbve&5$uO@}Lu_4J41Zl6wyy?x_7s6JpchiH?^XX|EcuB^T4E8~-{x+_`ayA&8A9jWCd} z$g2O?FWhowP>FUnq{Cp(z`^dO1(Cii*HI%YAab~@AF#v$NpZT0fh2$*1g*V}veyp_ zw=L^TYd@v@*${$m(kLP)!QH+& za{(z%VaX`QtR`g=6O0}QPZvnCFs_CWJK~d4=6hLdC23R=I?lI+BuF>MR5*Niu%aJo zaGx@Uy=?5@h1Y@L=anht?%ey%1W=7Ln%EwB&=z^_U2wd(0UIyA4_fO6VH_OJ9IaFC z|Jk(BtUzbYKZLU6)dSHlP{?!J71>rB{Mg0+2RzF^PInk`2EQzuqetMFW9MXpvpU9k z?}`MZ$vfzE#PG|7V#n4W%WJLBok>R@5C@Gy?*p2IU}w)^T?cN}WWv%HI{PNtMG7hi zu4o^eu};rU1de6DPXoPObtE9xj2 zK7Yet(o&W1S)bp^#+R<36!kosqEJ{%kvgGoW@b(K1kT*$(+VvKb)+68&QXXZ{Z)wT zzleNk{0fdyyGc%U>WT2n>jI?@7Lts?oU!fPTOP& zefd|vc^_w2rb-fE#_)Wgs>CC?TyV7uyJODC;B0t0HY*^QXvwCG8qa9E-HR|Nr^QZh zur_wv-FhX`XL9eOe*3KNNWn?@S4MY8n*#doSmx&Lfax|OaJk?KC zJ9CJ7OqzeaDYPG7Qe zy8}e3JHBOB9^*yAAII*_Gw{WrP*j~(ktChIL0AW@iw_pRykoQW5XuK#KJ*MstiABy zud$;iQXI9omu)IKVw0;z5M`rmCaBVKfMy!%KP)~)U4poNt3hk4{~}6YOe|%|g`Kl+ zpr|hJk0B>fwz~MwbTRlpZE{Ne$`{HLLI+55Ng6q>uIo4P#k-@PQt%F+dGjXnTYpo% zk6S2Q#Ro_6&V6v&FLog1k6;Du6q?5YL3tn6u*=c~vg51MD_z|DP+&qA_bvA3hGCH7 zsTgqc^1HnC3IG1`p9e7IRaAuo#1bSHc5>#kp!zIa~|U3P|Wsl4J* zQnx3W;LcJ=Rwa?TIZigB(1aze#u@c5Xt;|&| z@bvPJfpP5NPypv*4G+6g`*BSBdC5TOwW8_NFJer0hnT6?am^JQ@6&SAex0{eb7);u zu{>vfVAO4(6hd|QUW9hy5I*r8#`LQt%ca#4=njc2;@QUE%K?)a{IQs@5emu-a!X4SaO1W)dKiiTnt4G2#sKQ2vcWG2 zT3TQ?7k<;~An=fHXMbhDEP(8ci19aFJ?wi=`oHR>8qTVEDFs9F!nnxI<~EgSk04#R z;Ha%+{=e5Uf8d%XoS`z5m_!f;^kctT{BgjMVB0;W0Ej4d9W|(sC3t=0Ku8`3&d$}M zMAD1-YbW<(NNG|rx9Q77s*Ie3XjeW_@vs%8e4|x)WoqHl&|%pWj;1Ygg+C%ZyjahX zGmIj1ZzUc4`^Nr9maL#MwUt)DpL_ujpm|(b)zPbR7U*UUsi}^mInDW_BOT1_A$GtHz zOG;dSHTg0Dt)r41L#*Hz?`@)thF6x_>hkK7Mi1?j&;}{5aS& z%IIzeC!XBw-u-RAw(oEM&a(6YGB0U#w{xN$31KQX3%nt|yIK6QRnXoh(-egCBIxe$ z-~Uv~Xk zBCU>X80SEUi6YW5IH4+iZ^XlJngm!k%%CjWXw2%;^;7VHibmCmay3pnii|1eEj6Rg| z^dKO*Ge0xr>Efo|#<@>e+KW&~7^sV7@(WhjbLxVW&PD{FB3^VZVWtjDd9%6B)s6qB z6AJ!o;ZvQBo-m|pgp`+a{j3NX!{g#+#&tMYxWPG};`gIS{!T41_b^sj-iQ7YW9lU^ zAw8-?AKXC7De9&;-GkTOV(H52O#!_TRIX}FE~@eF_&D}+4V!g16Er$sQ62Y2Adkr) z)X4~!8h4!JBOXqoGBq?MwL&#A>fUOGN8g;+ZIew^D{G5i5#i~nr`NpDWEtICv|98_ z^};&T1Ir0kmQZ9B8dgMJH~y<$Drp##mVM1>+Woxe-iuD~qGkkO1$t;sR?F;otiTzI zx9}q_a;58*5%??S&7PjK{E#uUH^;}f=jGek6?pjZ48wF=|KH9BMKb!-jyCtmemt${(Gr};|20lvidj8W3_Z0{x)dNAxH&wI16bZ1i; z(T8=QZ3_vwT@TC&1b^Jl-430n)d&1;ypwgmJJ>M0_sWb5KP}vmSw>Nbx#e_g;9pOd z&s4`%5H;&Vl_+uc6+$`3))!VAK0jw?JNFidz?_ofRt7w%eG!d&7SE=tFWf1MN9?xHLGOdA8j$5)l!3 zQ)5$G={%gxBl_oq7iq*WF@g$TNvoZX-43G{VQ54|$1iL|7dn|nz93L{>vtOzeMF+St_ zyP>4KkKjZQUeUjV!^}>w;HpC&tLX@(R#&DZ_4TU=h*ISu!5me7*zrh0)&vW3O!h=l z3)h6Robm3-?Mk3I#*$6IADaNy2#LV%+yt~j3pjEy?kPdeK|t-Jk;@ZZ_p$t%MnXzjPGMlnJARs|yjB8Y>3 z)Q}s*kALxUVPZB1cfbcUR@DYc?$1AlDRlU>_WVnVkdy81>@E$@Uwy=j$ggvDjBT(R zJH-Swu7;TUR3D>t@l=_r+%2evspVE*$JKB0=*#Oxz!IDeq5)XASJ#$qsorbo)u0bx}Qdl{E}^|XVK z{WQFX#IkHD0dnArcDKjG@ zvyN5vd<#d&jPJA058vCZKj7%K9?$D>zpm>uxO1zd@V22+#GFhqF;dxjS~7C7Tnj$$ z{ddSOXdCc`MrLwgeB<&xLC~gQbTfzez89nKu18uPPLb@e;sYLp-_I3VDhmDo86=jNdxS+k%vPT<>k`8EMqRq9$@W|icy+dDt= zT-@!v+IS}wu6|9*wvBeuEU#tq${Bo~GA1jQ#Nfa;rU)y1R{HR)|6<+ZVZWm!Q`7q| zu#qK2vyVr0G{yRjMP8HC8-k8tT?RaC>vq58#ok+sZ+Q;-DG{liuD z@XRxU^y~kqbr2ddFg4*Tu1QG%2F7?vkO!D6W%ea|#B3kMAEz?g`9|DAu@9((w6eAV z0TiO2@FHbtbR&01@m(<-%Lg52kJ+y0;h)KA91`Hr821GlMD$W%J544gOpFq*Qk9#x zXTV6mb-U6T_{sil%KG<;5h77d9CP)>Fnaw(QBh( zv65Y;5ukbZW+hYyN|b8YV=gR|`*m~<0t-IdVs)I`y*SR|Cd}9gMa54)zi(2j5$1pY zGYw(Q1LqRnfvk^n7RWQsV$nTMeZ!HKFQ&`sh+OVoT4W?|uQKHc9_J#d2RxeSB&(0z zsJ?#0mJ`Hx{gnfI&gbp%vyF#IauAvgxqJh9Z3Y@(dnN1x!KygR{i`3oDL^C0e*?dcsY;eiX3drKFdhO>fnf+6`y5jLdCR!pW+ z&QEisQQ5hu```H*%h()waq@=bEEu@i#N}YP$+ndD18NmPK2-1N(s%DD7{2~mhtfu) z$~L8#FXCOHPQUFw=P$nDGNG}iwjW@vUm}%*)wn~qQq(8ZXj(J%Lz*fO0sM~? zM-|;qn>24x>ML>*%Pzi4F@>q&IoC>}Xf)sQ4VUln@hr~&B|Q8airua?zgg!Bs+Psr z6HTAx-s#b?mKFTu;)`!Mv zS_;C3uX&2k#=f@OG-!qDL?*-S2bV|GAt7ulL-CE$NQ+;k$^Vf!^8G9m+;1;A5TLf{ z{Xli^cbAp{ljGdWH-2Yi7ZDH70N)=AQ-i0kd)+3hV7?%?{ZCGROmM+)P3g~chCmWE%~f9Y%rm>{we@FrgOqH1 zd|0PAJ`TR~Ce%|$BMoBDA=Rbcb2RK$b9PtV?0sp#!f>t-{4Wecl0CsculE)Wf5i`? z&h?RzJLJ6g6#}cq?!~Fx;aI>k$D&=O;WBl3P#Ev{<;| zO?3n(5kb!yig-xR+L=&;TzH_|#M}6{Hq;@xkr*DP?Rqtl{lJEN%zPZ@)xFcYW{XeH zCMtcWy=)WI`Cc$N`i)_H(U>T?qswBDvJxdkxR$o8Fv)gk=r!Rh!(KtR5noA<_;|NWQDgYM$XiI9|s}K}9v!!i|ytjEyU~IKMTGFlgU8+{nH)24c5;PhgW>C;hou16I1)15Jnh)<@xg#d>~Rcd9w6o^H2#z0Cb zLzXkHof_4&g@dg?k@K8oznERJ_D@>*oFj8h5sfbNyWAgwF@f=9edh5f0voy(@#&Z^ zC5&$PR=L;5M)W9~^ifKbM1WGk9tm)xE5V!?Nbk}Pho)`lDZ(g!hC1Z?rB=UJF3`?% zKxwnp5<958trKULk(cY5*@9}t2$N57=`ju=uOwQNT?UlA*f2^;#Qq{k#1NtWO8$5{p+0X`OdLHCbc=T z=|f_O{K1B&|9lxq@;z{r*bTopPYRPaYY&+x`ttDRt!_OTt4l)-p>0a>LN^KOFLF^; z!tzR2W|tZ_Wz?7ddXR8;$wgF(=x1ie7j-_bE2fAxajO9GP&vOqA-S2Rs4g_gzQN_%eX}(f`}+!O}r5> z&P*#?+0kA0;w<(bObeYY!0d=%aB=}^QiL~k@FuEJA@K0S+RcLiXR4g$-iU}|wxBB7 zq96W=IQ;m^CLR`y$0NBkk~S4$5Kg27Bg~+9#j#j_iV;p{c(;@X`sYQn7;{oxTT_%e z8CFp24aVce^LU_1QIwKq)mW;=r;P6S#?zXGyQn_7l;Ezc44nuczHZF-#_vC3g`>lj zM~PlW1tk&7HNPdUwV`XZt~eQmyn~2ho@7HWr;vM^chI{|d+k1HcIcvfgWqRsf^InT zw2?DLXUTi~41c)5Tk=*)k=Mbx7Km~B@9OMoMd{q_GNI$o!z&55qt|~A$KRaYv!dx; zqLJq%>MTBTp3)WVkhH?$u2O^(?u|Y%lGJ#Tuy4SYa?5v&_ds60%Qbpl@u1jdv$4_6e=Qj)tb~#BA!$;M4z^W=n??M z7YE=Bd^p^2PnFDGKl16R&9_rAYDLaI^;~+p_(sd>PC7B%;zSe$B>C-gsaCAL63kG@ z-mc43>Ux#SZsOh!&`P&)y1vecT0S^)Ku;xcq!hVuLOn z&K9{}H#Qa;*ppT}J*k5c*|OlWfJQ(9ZiyE@K4NdRre_wqeI>&U=pkPIa{bTw9(DZt zI-NYatCp+|MELm1PSIh*YUHe;xZSoF@Nht{n*UzgXw--1WBXiO*+*Q?>WPT zV@^SCs2v7gH2qr@&$`T$q{S2Hdmvr?z*)v2&PaXo1^B<4{jO_s7c5`9?Em)@t_=^z z#Ar!UhE+*t5BF6Jk-uJE2OnUhc91${ialB#t+xl9_KhJ2w}im9>s&@(6~cUz&jRmfv) zkdMxmics*~X4|oi82Pp%f;f5l?`Gny`R=c;eZ*efNj4R6Ge;IZ%MeFLh;SYKJsr(p zjawzmojiktif1IXkLQmr=Duj&ntX@B^JUIRnRZ9hqXgrt-0XiYAH1F^d(3$~s&Rl& z=9qW+hi&arXAGxJ?#wf^XvZPjgKeq6vONva8#vg9w?m1#VX?Ad1Lh!U!48I*FuiHw z`)5XAf3b^;>EJ`-!V1J4hD_c3No$*@v2d(>XG~qcWZF?Jn3nN2UExkZk6>{&0QJGH zq$co452GmsDq?w7(TU1+RWA!G+1Vvo)W1*+lQw1A=p;yl)} zH8gLB`Q^R42F6mthH#^9-BG24W*3i*ksvRtq{&`-t4W}c3Y*+4^pgH4OW8Oc8%Vq! z*)8`iW18~3UF)$>53NQ+fz-V+``MX!JJ1bfJzZW|S!-V7Cyyng6ftoQWmZ#Koajx+ zzM}=&x!=E76Yoo7flSj7a3)LV*rndra{l(=mpW1-^)H0wj9tUP9g_ki%0do*#|bRJ0xzTT-*4oNp6@@el+^(h+A?i$5jd zBwFuTOwTqdf-{80k*p0IDv*!sar!2G^q~Q1`tCckjKN@|KH7FLAJ9S3Y4ZPf7$byc z=IqcpU!?u=@;t;&vhqo>$MvLp_ZU5w1@c?N#w&Q0C~cfZd3s3EWZq0MWcTlkN04a! z%m4P0BlMJSDG7tSL47mmY4sFSONpq@P8O9$KNe!vf5KrK)z6>AiiE7J&o!RNUL)}9 zp&?TJM!uMCarnDIJ7}0$@z$*d46C$fm( zcIsAS%0r-PWsKLf|Jkga3s(0~gv(U1yR^eB3Fbd~t2u5y8@W90vM(dd=vOtg_BQsC zrJbASTrFG9OQ${xUJuqH5PdrEn(sWAH^I=!naDVJ<`ARb9$k1ph<(i%%ir==;2<8$R4R)8pqE#^|$v4W7VMge5Ziy6CGdRLn>Tc4AuUuZTuqD5|K$tY& zY2cAnKTtCeF9+j1C)2c;IkfTT-etSz1{Qw$T zzWl4k>B2evyYQ>Ir8@@SRlL^*Nx~42g4bN%<)ySX@}qf^=taQQe16WwsnS(e*21s0 z|K@xZxCw$2s$(?M_eVe8-EdHH#+)tz0z?er{o;9a`I}E&S)x0|qsyYouG1;Ko|Z{Z zO`VhNiq`78mX?sfUzCWkt%LAy$ij|9Z%k#PF?u7D}W{*U&91Ld&hkNlSNIfYol& zEBlVXKP~>^7p4?JeyXg`nGy05&Ufe42WryF?=r%fB6cST5BwrQu9V$zaqjJGgxVgR znITmhN2kuWb+YG+P}<$A#WiLmk;5vnnQieQ2mR@h8b= z*0f#fkp@=wa?B3l>?*7jf<*OlF{cPI-Fi-x{`+wLV!FXr$iJZ0H=cW8-=7jaDXXti zrbJZNHnl~yVhvi-85+^bP4Ki5%T^KmkJ%hs9|wxLISag^J|=GUxIeTzu08}o zjE;*#@V<9g+~1!1Csm43(j%nULCH`g!o$~By(=I$_v`*r4960_3uTww-B8P9{L-W#P>XcpCmd&Ng2t_6~AsD8r&*t*oqsmkxSNykawyqk@%Ut@VNNmWcK z#J3Uh>Gxp5J(0{IrKQ}NH+lm5hli9a1Bg+@b4gG1v&NntIi4F1&XmCN<|VCh2lMfm z#@*drkUc~!ohkaG;3+BePnMrjKE=Ve)2d*J-14NgAbR6y&lpHD(H--lois^XQiAO= zFwXTo1^;GjKA748#jf4d24H1*U1hGEWCV}azkX8kBuK|YWPEtxFrD1FHpf4@yikeW_**TVafB?dC5*nN8s3y&-s6OQlNFJ6|AI{f^ z+aylveTp0j?fh|k`9p;6Q|JqjXwKDj4%sfbu@j_B8Hd5K1;VeBz|V4xIqyQc*M_pd zJU-^#h9@=*l_2&u({J(nZRPt?bE4u?whZ+xhe)S z@4E|+Qw7}jmlfmdBb%skEIo*VYWq^D3);{g{jpjcxnYin;?>m5v0JsqElLD&Qjy@7 zned3kw6|L|Bd}?bWDvix} z;5Zd73N9xkw6(IJkv5cpLhf{{JYO9{W-&=NSf36l{mxaq2=RmnL*{hXy$VhzX(vISx52*Unk$OG9aXeC`U_EfpsfUl7Me_rWJL-DdUCqhL zxeDJoRijuE#h3?vwXi1@WDxTdImvzcdvrV&kYr$-m$qtXXGhAV|3bLQ={TR{GVeAu zXUk`*)K0U6N};jF8>)ANdN}9Zm>#I0Y2)7v5>dHDVgOcLugN15ksjLYg#R0lu> z)Q*3%m)5|T4-9G7Mhp}xX;mQtHV9&%{%8sV=A?L-L2zg}SJ0tR#Q@%IGZtwGjVHCK zn~){skQatlgX80XX;qjxKoP?zPf3fJ|1P2`(Qpl}W>sgf67m%(C} z43hI8JWbPE!)JHlbB1u1`;TU^zWSuF*w9LmiameOk7+OWI+-Cc218#zsgVLLg%nFQ z#g#g~!PHr|$d|lP#iz>4)0@Nn`aTu0EnKz}A zEBZjTBZak_~c< z068enbU0|3&3L@O{fsZ^&*Wb6(H9~-ZS{=TVUg4sP0BcN?)|AF7_DBv{p`=5W%WWp zBZ(S!y#bE$V>G@8J3dRf5gF-{cEss}WuJOD=}jrfxQ$)?3!Z5hzUmt)+APa0%Q{Z( zO2`In5Md3cY@FRV9OXR)Kf;b3FqANi<4 z0O6?O0WT14s2XPA2@Q;`ST!>_@QQajb?B9GBUNRk72~leeNJoe$BDIg3A8@pK+s3A zGf7E$Jx9Q#^$5|BpQ(W=27`cdp#+|wb$%sr5hJSW0H+}**X1bLhr_`?T1dz^M7~-$ z(q|5QRF%^Iimu@%&oi}H&T87gP~q#KisR+c)5Iq~3j}|BP*Y9`{0gePax<8vCBDc} z*xzWRe?Pjl#WrjD%^OrBa$M&yLSyoNUT?X;HC(OFJ>$b|H| zaoDna5p#+Mn9@M;Vbjw0=W)7Of1KplfpIp70^BMcD_X&{u&A}?NG|1Nflfff=d{tdhKMh`20@%a}TV{%9{c6B{^lJ$J0nsXDSe;Tj68@ zUd$jE?Q%Ng2w>xE!41dn+zyWPG1}~z)Sbmd$|=%xDaeIKe`zazcnIdMCi{pxKY==a z)_-_>y%8sfj%nX1NX(p*YOwK3`>|!@$|@LY6rKE5-t*4PNS4uvOo@6Be)A)>BFxli zvGiJgFU6<=a4+4o^$F2pzjuqXCltg{h`0tt_68D^GM25N5SZrcV=rz1)Nemc>6GWa z-+}byrP7Ifj(5cPBaxufY+9rXG4*d%eVmZ%50Xz^ZRBmE%p?l|I?Rjjmp%o_SA6*e zDGjAX{H7Q5_A*#oz71GDS6^})&3oeDWyN$mf90U+1 zON;A5DVD>1YnkYM_jp;rN>xVo&&pwGT_TM|Ma}pK5`t&dJor#W2U6e>Wj_mT#il%P zhAMkoDI*hU3Hx3|j#p?JWNyUy0N%6nk@$L1ZKG+Gsjx;H+q`_Hc*uC3r3p*Ygd7$9^`Yh$l&AY(0J(r8`%_QyikcV`pO z=5qG<;_%+zyFI;70fF)gEgmncd!SMhY*r1-GCrBt$ZFDTvk4AV2$l4poqsFVdV&O> z6|4N!kIDBT?q43B&srRIk+h5*`ANcv?#308 z4t=L@_rObvhou}>DSKIZA2})nuctGi7yCHg7;@V` zW4a_50R_PBn)dR{woR7BdkTFkbkSYK+X0k+!7z-d-u4IWQQni?Gz5>gXai{HK>=i0R> zZ&Wpn3E8%})^>bMOfHfUPT%CHl7c~KUcTH{$UlW&SCr{C={V!ZT73+h4@2Sf=X_*j z^BlT49B##70>5lhy2hJgaoKw1_os-~6BwO~<2(`9Ovx0+C;fgje##=yn~bOE{qV%i zZ7QVUxH9HZ&|DIlJ(I3eZT#iwhSlU2E-`yzlxc*;UeXS8pD9x%k|d^Il2SoA5_pQM zUS|%~mo)Du+P7+@U0ZX_}V_R7RolXL>n-Vb^_ zxQey|emxt-$C*6r0dI4#&(fNkGjg97%jo{F$3ZKYBDT@HoNcQ~iYjC`-pFU>=B78c zB_?^iWY>J%b(Lb6@})>CDe2&MFQT1R=KWyRGrdkHHpLLFjY>v65Nl@ExFOlDZqD38JND;# zY^)^m`ABFwcR1YgYb$U`Y{ge;pg4*CaD^(bwr$jd*S9ci&toNS`=r0)LgUBB0FErt zhn@TTDAicRcPY&GPnOjM8D;f?q=w(fHwE}gl4D=Vmc=H1F5Q;%yNLo8s@CVH+yKZv z=}H^dgc#~@-eJW>JYdn%DzEsDemIw?kntsTlE?NR1;6~`7am)wkh02pm;i8O1H`wP z72wO`$AsYO+`~B{yLe_Wi?tC$k7$sB;aY3HJ~KOB4v9S-#59JH9Orc$>m(#GCyO|j%1ZLw5*scJ$|u~_2W1Yj9^6mL z;AXzWoWp%QTkOJ66df~N_TvYE%_e?5Jw3VVPm_sKoEU-{<2Xk+BNF${F55@|AnI=G zX5#1r$@K&fC$|!%n5I~ZxMB)-HzPIb-9ZyhP}ktRpLd1Ap31_($svG5Z_ehv$p&|S zI#EPSzW{jMf%MA9#!FvsErjt$?N>nQ@^}Ju7@VqYFiW(=zZE%qNli?j&vuz6a9Mn* zLjA%F58b;aO!n;2`X(WN8W}T>C80f4C+6K0)+WF?y4h*5m<@~Kiwoyh{qo`yoG6m$4iG_G?_Q|hjD#Zpw z+yxxOruO^bk?kHIKvC~1gi22sAm5QI`oL6^Kas|9zJwvCNwxLSM&at#x=;=-%{Z!; zs3NR0wvyha3~-gjg_t7j&cFGAGAOCC5+vBQ)Vi|VHy-Gd@pt!%6b)Q*{`Z$BKmmJx zO>{R7Ya`$1D6csut*Uy+?r^||wQ9q(%m?3dJu+{YShqZ{Yz&v8_s=l2ptR7&v&#NP$ zhKZ{d?x`<AlYRZF6Q`&5-$L08kck^Mg08V7jWG)MmjvrQW9EuH+9_uMVt?~Z>YUc`gK5z=NnE$xdNPGjUcc#IvBkj*-B_R9 zrMKnSl^vh?&;CEh+9K3|_G>H0YVRBXeNwR3<|v)}c`U$#Aeo-lfLQlyGry$?^|mo# zflVs>Mxkw4BrQz7M4K)})lQQVa5X!@z6ZF~sTq@Yp5w{Ao}*egWMO=J^m0aN2m)db z>HCU3ER3_gf<`r#z~=X5(rAH{fq~pVk0eI@EIQkw60^t`6Zlgy7G)a}l6CW8d+heq z&4aas^XZwH!hX;T9Ojr$1=cfziQgPB5Nnn1^U|nPINOuY-W{V2B zbrX4GEP9i_E1k4|6T;0N3%aKdpBB-=#ZuHesoDF?8+VYgDhuaU`7nV+AkxHDR@q>D zyBsEw@%5Y>(4r(Z`-0+02`{UdnMdI>UbVCN($VOOBY?a9gRy?R+oP3-xr`PAUoda7?V3&|T`o>^GJljmb z0Dj`+p}s7(b&tff81=~OX0|M4Sk73NS-&PS=dJ+0%gBlmp{k5#@!9P zsiYD9UR@nUFya_FT?F1|iM?R09+hwn-zyqK6)MJXQ}IBOfsShaU)Xs=HFGBh3~T_} z-g(ZyQs^Dwx@n6MBhNOoyxFJf1RTrU6+e0hTQNoQArD`d^$yHztv$Sm>9`M0yj5D; z7WN9@dz?7+&)#_1_iMKm+pH{qG~Y0uqvfp^yh*^zO;2Gy)wDUszFih^On7Gd#j`&M zAduxi$zs4DWZS%uJfOB|p4Vf~#k z&-5HtYmS=J?v8-nH470Y(l8*KX|AvUf9STHpyB;kbLQG^-0Z{KQ8g|_+yci;NP)k4g*?jyK+-u0Nl6pEmlryFbYl#?1oj5y zz@Mc$nU=YyM3O5S_L6Ks?pY^?!vRqsY2?zJr`_4V$&lS! z;IbF1Mbz1HdiKjfY3OQa;o|k-WXHMB!N~62pa@Dog{aVMK2006tyhrYJHa<< z$wQn3hnT=6eUq}e8*$I7OIs^#L=nTy&H5QcnF)MndDkOpOTIdbl*0M0i}K~>=Aq~( zE3iFUHId;D9`Ml-yb&*n?ak`2t^vms>&}hfeU2rs_Rc@J`voXSDnDF9WxFhFf+dGk zZK0S$t{%rvjNIBzP!SsXTj0r=j9@406a1ZGhU6ws*Mgu2@SZ2wPbm8-)f(YaQQw``N6-Z9nbg__~E*cOHKDai8Ofe8eCl?`gDA{ zXoL^ybhP9V1L1z>g3S&U+w?%791`J$>E0zwQKhuC&sE1yh*L*sDIIbW{hs%a z>ow5bT|@@!xtX`&r_xomPjRFuzE`x9zD67H~*M!GJ1AuP8~`XkP6b;$~9&3CC5| zIece(&78dSfYhq+Z{7{2{{~AF9CMhq%c7cyU$JYTG%;E{*4PKNeSi?TTm?l!Nj)hS zi?(@dW8Sf---{$UdgWew^EJPpwiN-x)*)ddJyClT{TtAriZKvFF9ANccj0eu$d`ZR zN$=U|Vn^w+;^Fe#*oPg_M`&vc;x6-u;$j9k&fpG#V(b?>HafGqFz&ENI{o|xm$^dB z1iQJsVHAi@6sc{`jZJz_v{~@ET$gR&D+RrrZC`Jc{oj z`@HDXW-fso1GFPR6e!-i+j5+r-+m9YQr1#37-icAcrYGe3ilmh+_5LF_*kTeai)AF z;|re_?ewqQslpTq2!`y?ZgbMNCTh;T*jmZ!_q3g;#3(A8x9QZ>t+J-8eU#DJr&hvD z72p4(oSex%_VL-=+*D6JDG-Yk9DHieOEmH!(J=vZZEQgY5x;hxEhXOSc)8^Z%UcNw ztD=1z&ArN6q`7eZ^)LeIFoAy0$rV9m>Z}L-Pv@6?Q8w>V5-BgIbX*o+rjl7hWWBP8 z1yVy50+96_jPrZU?)TYbNDRHCv>CY_@&_qjVHlzpcTKeiILT$&{lojQR$+C-2Mv4x z{wQoxwaPHP`M~||Apyrv+0Nu@FErh3v&Q7$#mAdMUa$TZU&DdYnA_q(moq4Q zG)U6Cf1hvO@7(GK2q+b>_Ct+k0PR=$!IK|oBxP`?|A#&==jckTn3qCMGv6N1lSx!G zR$zl1#`Uvs%Un)gv3p3gBi5sj!Y3%^_VMmYTz?Hi_W0xU8wG+<5M|O()@?2c&)rCU zjE=IR?ewZaXbP?UGL}Oyew(YKiyX}?7C?(YY6=#6=I|}%W}Kdj6}XsF0{HqX*&U=t z)dOd&R))BFLlg0U0FWu<|9+zyzQ#RvTTU(-m6^f)Tjh{(_|Ax2A%#%)$RDOAJ%Ll& zID_|wV^eA|m8lhvG$N~SAQ!8oKpfC#FX`T;rIf}q8LrwZJ_hDTjA#`i{D45U$HU0MEL7%mm?dO)c^9S-9|SYuFALr zTRca4h807A>)@n|<3jOl)md}}L>+@2JG9gEW+ve5CIj1*r;cTf6l@TWM3QCf?wh2- zNd>E=oJ-$x$oHl{=PnowQy|BRh%v|ICB}YXrf{ZYwg2*@Ln3x`28x_pHiUNoM-@nj zkkV6wbeYBM`%*%SIfxCWCAt`y&`yGS!zhmhnpHJ*(8_q1Uq<2e%CmN`d?hlaHsFAz zex{^6WvY^3)OXk`h^1cpPY-Q#^9xQZditB;mrtS32o~ASJlUDGlRv?9a=z~T)fQMhWH;@rG??O<3%U1n%n?xi_7z7{09MyL%K8{3HI){>K>Rv{m=-ahd`%^KcmcL;j=^$QA;Ew1e6HpmI_REe zzNQP@^~^ti@SHbrs7c0i^DqP6@uxeCh|B-gk^7pKh z8HYFBb{~2M_Q{eNekwMjiM|!1${NTji&oqBh`iGjHaTrOZm;QQme%8JB-z8XyUhRA zUFDI+@qEhqaJu`4`oC7yh2zx*=p1v{DBdx0vCVz=6&+8p`@}q-1~|8@x3fC_lpNE~ zcGYXRViQD@XppNRdXf~`i+AMsiJeO80v;zl^xyJ3ivquL?1e&Q{VGD`3I~?VqXTAB0=$8vR?YmTKgT7iBRcb<$mTz!-Fst!m-x z33`YX2McdEOG%G}x3lV}Gr3H5nUbA3P2B*Sr`mCgDjWzBRy8)!*R&VUBjwfY(+!DvaD+3PdCd?FFH9m;9f(XB(D%q`uQ>`6VsV!)Z!_$|XwY!sd)p7PB?)A|V4k72>4}VB}1{`X(q`*!PuR2@br+tg3G`#^r zuNqH^vcZShy)0t<8v3m9>59o!bc;G&T$BC;Azk>%ygfTlMb)Q`i#Evt+uTVK&*p$s zn9LKnM%as=cG9nxzMFt^5~;}B9XUFW^*hi~#%R~htu1E7)&QD+ZaCFD@4eQ5uiIvV ze8k*v=18nk7{PZLG!ow1>@t3=pc?CY@YP4`Ay!PuVtGKI#@<;v!_O~TC9&Uo#1Qh} z9cU>j#0n!EP?Usru>jW#ywcu`3DZGVP2ISWscUM_HyWpm;upLjaj^D05zt}(~yrrqHL0EwwVZmu-8UvjL5k^ zx#eZ0^>=L2zbR9Dy_?IIMk!LhfFLcEJ$RkUhLp}~vPe*ue7@MpoHsP*N#edQDAFB9 zsiWg&Sae6sPZkR^G|_Ps8~TY)|C^)UE~9(x3LWeod|xC|XD#JtS>S#=67d>G9srwvLM@ZZ=7{mOe1N z1+vyGoCgG#Rc)Wqv>oq~gf#DcGy$Zk?x_4hy8jMC&6rQne^M_-7I%Ms{MK~cZiM6> zvc4(jnQKVR6xHs0KtV^CUzmc zAjXJZjN&mxmM~jvNsw>@+Q6v1M{p2|Ge2|@6(~qOnK!U1#-efN6nuA6O1_#=h^-uY zy9NvVEgchYw$WEzr$A;2E zD=RhUntU&3%9qGv8YVyd_%TNuq*Q<HsKw)nIC#h0WD&Ibu#K5ZWNAxt;y zk^Q2rEYC!_o=vZljw*C1dA!hcz2&pAmKK1C0L{Z09osGYBC6OHU#t24giF2ADyf+f zt72?CGebN60_6Mrv){(Xa)C^&E+j9|BYIM@qdtt$OfUi;g}F0`Ls4@IFRH57m$goM z-omdbSp8mz0*qduUH)ay>H0Hh2}oQM)6|5b$*zm`bRq||iIL+2`^=GfDenbKhQ4_# zXd{0sy7TmYj%K}4W05{gp#dwEs(u}fkv6?C^$gQDzboCO_tEJTKvGyiuuYDKm!@m0 zW8N(IZ3J1j+{k9|K!cOU1i6EQgYWNwen3pq0R&2xs18Vciy-c9QPAl-ijV8s^CCo4 zK#G#ZBTQLWfTF_fwE?f+ElQ^YSy%5wLrxPHd}$WP{VS%5p^J0wI}cl*TKldKOi{D5 zU4r?aBxKg{@9I#E{OG9CF=ZS6Lc)?MN=`wx48VkU3!1k9`hky#sf%s|!?WQua3}+8 zsQC$l|L*}-P^l>EGw%Y!1KGO|@qm{kc068NSnTon!@eDQcWNQ7YTHDt3aIw`D)nt&4* zjE4?=CwBoa3ABgsqer6815v?-fWPA~e_O3UV;I>lDDcWJCibXR1x@E|90u1n zw%d4#v{QTt-m2$dQ0y88PQ^ON#5sf-vkK|!yw2rYqAsnH1Vz|DuV0I_RZO=WD{JJd_ArZo1v$q<;J zvF^O44{ZQh)A2FEXW5-^^cFcuMp9d4 zrA@}VyajLRDjpa*nTGb3# z5RGtYyc#KkXy4MXR;!kbmC;)46QND#Mmt5#)9Ay%&``83_h_|2ozG-~>y;>Lp!n`PEMQ`fth7qtmS(ELsV58doDg8t0yj{?njy^fv5H&!k@s(|#ecp!Af2%! z+T{I&Gvpd_S4%d2cYMv1yi03`{@~c?^+GO2LX$kFhZolwYm0~57gbh`^xq6x=rSjP zjvW)?-bt+|$P#lniZdJ8MNhEsheX?9Ho{C+_7O4t{qMcEc`VzWGlSuBKESd1gOi1j zWr{aDc?q0>bUxte+{-vpy*aveg28kb&u@)X=#6AMF1>#VcWI=| zdsFPcn8}HR(Urr)J95OhR8)LU*~)34O2TP8CS!WJ%;oJKiJYuLntHb?x$p+t8~@+L zyYot&J4w4sKy`uU#>a86U7ze)+F!RP&a9XLt9eYHFmu z26@ob8tDVkC`dy*vJOYCVPKP%b@zjJo?*J0;;1lloD5wQttsy$%)Zfb+R%>PMyVju z?2Sy2^&kAha$Bz+{ zX-2Q=oSn$Zd2J=p!i954a4T133Qyn)D&liB3PyVqg#s$p#NTK~hAy)v4}Da&c+QSW zGgLqInEb$@JQ%4b5vF|^%^=gK{rYprq@O;TN1UgG?HYv#e-GlcJ4(TSt9PTeL1J!r z=7WeXh?(hrCLQI$1h%O@yYr)pUNps2AV$euJYUTzv2YtUsvFKgY{bsth;T_UI>qZ= zZ61u3;NRYB$I$Ybyn)0vF_dWQJk1||HU8p;<FHbkHIq=xnRPQ=#XTKV9o(-W4y>M7gDwO6k0o& z_EOgp-J9V5JjUsINUIZL8RnSg0{}P?L9?vLNq@egb!?Eq_oRWWuQ+8f*ug09D6vJ1 zB?+{HN44*bH{bZ#b_D@sfp%QO?Ve6Y!s@RdEFMW#`pPjISv}BUExz~i9;hqyci8X0 zoaPAGjuUVl>p0)Lbh|WzHpc`j{oODI4Hon=&%?HYxL4vC?MP*Pe1K3clFTe=aDmXwa6Q%Xuw=|<`9 zPU#j9k?uN==Ra!!GjH6_zW2U%u}DJp$6L8V^xvHu3GbSTL#uv8q|^03R~#ezP(fvW z?$zLnjtAO9=U^F1^e~lv)>j&Ntb$vk&V_aK=G@6s1wBVuD-h z#X#pYRNb3&0`K3-U^8k702|c_H%@~gj;RMk~TixvHV`N#0yKmr!# zT2MK6+D2;LF#-!i0BW&>h^TFsy+9|p>veML`)bDp5g_(u)X<%g5Cg2B@wKC(1kC|h zH~*in%nA|WY|nJ9bKxL}rO#qUte!t+rsmK?V^h({Q*l&rtI9tl^bVkfH;szW%yX9tYJUnrsC+cj0XBP76sI9+f61MMWr5#mKy z_z~-FNda;uP`q#`)I6_(5)e!J4InIqx#;I}z@fc3JN)&4%wg%hfcAD-VRwE$E9WOR zIYTX{vCUT8_u{tTNJEEg!4gp9{uQ((S z-GWTWE3iW)7(M8Mc(((H*wfMn_!(H+l4Z%OKQblWeAgyxOQnF66A}-jfEo+nGyU?Z z57d%LgE2k26W3|X%-L%qntb!1J5S+6F;RP>n zT!^HeBVAu=n#(=AxnTM~28ibohz=niwVeqAW6|BoV(Dv8YZtY{+{`PB9)^V_0kV1+ zl!AO5(Nl~E?~*`TeEILgIzV>l+s(L3CCji19Q6D5 zHc90k`1h=4O268UK+>Rdgpbct6eF4j>7zLMZayJ2=_d_=gZ}S>B5EsoGj=?@2>Rfd z6lJaGgiq>pE7XI_c;(XT;N0XVNw-Upl|6PcTiF^)9w!nu`E|36#CrqyyoWF3{a$a+ zt4eFVy!_Az0zE-j=ZLpY{qRig(8p)eioLah}8=)4%59?C*H91S(`Np2D9Vp5C9#`#(A! zU-^z-K_M--zf0X7Zr#223se7vRj;iaW1dUURX69e| zlxiw57ICk9n3N={6+6QzHTtHe<)=3mmk#B_I!mL0luSLcbZT!V7DZXqmP*SwDZGC? zklRg`%~fgVGh$)|f2R=bXtJ#aG?>=f;2oFHrACo13r_wVQUbi+HJdezHUvq-Os2;# zs3ohfm`ukV7fVg7P^YbYtMJY^{FkZM?e|ju~L>)H2h>16W2vq6G% z{n!=q$KZR)O~b$K%y$r=$jO#l6%RX2mO>~5k<4#@p2yWSH`A8;6z0|m%!hm}8#l4y zvvvl2$8qLW(H)j7TEnCD!7%>a zb@xRW)Kl1aVjUdiQpK5z=>_bl&#oa3ge5f=g#~Y@{>C65l+_J|<7nx{rz|7ATO^j+ z;46Z|XkNy;opcF5&&o7|p#n>{EZ!HQ5^>)LVdNJD^(qe2fX|C3%J{5b7`gNK*T5fB z^qH$x8@^Mj;v;X64%tjEml*hgY~)ADi8l$d1E3yX4+3jopo`Z}aS#E)nWcD;(!VRado#|rj3C!M2jTOx8kjCJTKOb><> z*K%kTN~CkY$b^x+{sZJL;~ucPXKuUGsN8IBzpFpLuT>$*n3-;LTzmLdlU%y?bm!;q z<>`BG4r-qN8nZsT>Nk4YIBLH~d+Hc3D9-jZctz|A{^m1wti9Xpj3Ocw-z89Gg}H(i zBDsFzsc2nX37sv=-T5?YCc7LDDdlVwm{95HMsfrczEZ%7uh+)E;B$%I zj6+Mi{Sr>|X{j+}-SXEP62<-D>ceq8cuthy*BGE9d~$`w z*|~&Dz!B!)znc#VXcxH-!xGVw+8gIGT5utBQ)#l)Qg}QrK@_uCH0tW#;1Gx?B0-L^ z#F!ze3fn)*lMI&1}@l3 zXTl_oRpm{&O4!c<70m4Tg~!nCQY&bM{_Qogs4`^ z5BK+slFnWAS#&UcJEkI8=BQ^`99&pPi(iUM%HqXCsD?E1g}zu$4={J*XuDdvb8os<-^t&Di_4 z7Q@Kru<-i)Kb6G>J4*lEgQJH5lJ@)AqouVa{$@|FqsNoU{bN^=D_5qqN6M94%FX|q zzUC;WD19H$8dHZat^i5qK!v5tTI9?y$NMDiX);G^?JixVsO|6^upqO>NYEv6VS=K$ zU3%BhNnE7GA_5L=H9+AP5Ekh4@{^M{Xa^wyN3F|F zbOj8wZlAj7Yvy2qSkwe)ydz%uAu_m1PGbq$nE53=U{ti{DFn8yuCEm!Xpk1&zvmMN z>YgE6lQ8gsl(`1bYf1$|X^unablP@{vOzg0Jbam-@_`%5S@RGK>!+m{pZ>RKbVFB_c6WRa+|{EgCXma*|%b);Fo;0^IE z?vY5wA+Y1%#8ny9pMw|w7s`-gN{J4Th62DQP-=l1tD>SNun(#zMs3j%VFE&Lx`Exx z`Kgl{K@t$Nuq8Y87bv93og(?o^l1XJYJX;os(!*L)e62zA1UqMBAXtB4ewwEUriyH zG1U#(+b&0=s>@f6MNG^vs>`b#BN*}mKDAu6XprvTyBCWBPSLP19FL_yi0|U>DQifTuCyX}+q97vah=`{BVWm9-Du0o@Pw{QwoBIK~ec3zULQ z6hUsjE|J0A<_4{95rFxE*)sy5ynlcA(DuRG4s11b7+FmDD4yq-)Ej`Lg5ZY>W^hbf z`rtGtpE~U0yFcu>er}IbjxFsj2doHRhw%Mi5Q||4GsbeKnVFy-Cjv>j_N?+*H*X%D zRWxLL+IR8*J0WCs63ac|-VZ?Pr8Ue60W7_`cBRuP*{o`&YrTJa$Pvx8)1la-m_I+;5 zYKN`_(|G&az^u&iZmBfJauel-aeoSVKcup>1wRU8>ZmgOzI`xzV zPbZi~!y82s33tK~UmlbBMNVg5_s9|KAOD5J1s04#mpO@f?hJwMifFb?o=H=938SvFHY{5} z?-%2IhD^o!U|%!uV(Ib`#{`fIiTbuM{x1Ir-^wS5?x+3p$DX<;WC^%U|F$BPxyr~T zKcM`UUmS25PPuA75*kzf1x#nJTdr9bt+1`R4b(aWqPheWtu;={$e+=KPEYd_mlCz$ z+LuYpUu}Pc7E54$CT(!DNgmoWa8CW288GjT>MbNn)dem10D9H z9p9g+n8E~uaAEi0<>Iuvim=w|6Hs&*0r>;k>VZI|3dM3UoCiR(syQ=YQP0Y9h%UjG zQtQyKo>OL^waJIJGDvai`fNt@ADfHZc5gI(Jn|q%75Z>6cev)`35!ug@G@gK73i^Cgm)e$N?b*}mh#iE&P*-aq-@~nX5 zy7r3HzyU5H54l>889u@OY{(@A>fkB1ODr3NjKYDB?`VW%VVBEV_%fWDQx%pvjk({V z`ZF3kdM+(inom%;7aZ=kohae}OPy6Jj!q=|*)QMYucf~Q>wuX78j#2AeDsI_@GD!}&}>4AjA#*hu=ps*HGBblCtWAF1i%9Zt$9*_)*$**v)oC?dDRa48^M5vzz` z;X9NO;YtH#<5;xz9*GfVbp#U*{Z{?tx<}pbG%7&=D6&`) zH9e^NaE$xVa1PYeK2J?YkY)V{m_%qIzP@g!%M2nHr#fI|KDW>N$7^ly;EOCG@(Uu! z6V06CHE*}MiXg|Mg8%;OM=&r98ZRznJboMZy-lio-0?=b6Dp z+PS1D|BO1?^yQ)?f&UJgE8A@&5!D;%d+Z7BteZjd`EO)e=o~H^_NarhCFxfQQf8<< zRP=s@v-~5LXB0p@^rdn|Pe47g-9svbRU6fVX)LlUGKk#eZe;y!vQ%ImE8TWs$p8_K zAG%lH`?1|(WGc9`KI1Lj2Ns$jPR>Nx2ssyR&*|SN{m6C}!lm$^`U)KM@rde*b!v*P zi3P1ucXy1#h(~Sy1feX~z=AJX;B>qWExiDO&(^K^Jo{%Qr;9~Ptl*KiS63d@jm^RT zT47RO|D_EHc;oTcTp(S1a5Nd6s+^nYRkqs!7Un^S3*(_LH5KRBD`To3pAPIh)!AttH?AX3u>zS5&*b}bLp{)%2km=MH=RaOg1ZTe-3 zL8BI8gY|!9^M~|CdIH$8Rbw5D!A1M=!B*60@)0GhC!w!D5&`^VoMC%p+Yxe>58HY4 z&kSX;0szc_Bsn-ph;;BXG#=1p5VEW4yd28wVINRRh0~#W!$hUc_s~E!vMrKSWNA%7 z8V$)D9>G^i?~z>vF2nGPCcukBbMqCD8N%xXA4L8MJhIC{HmpoG=@nL{E0*#$rc6{X zz5^RpdOD^~uI*bqW-GRkE2Oe|iydb%atVCuKeBeAvn7EXsE%z9Y*w{QFMR&IlQtPX zYOgq~uICP|4eFoM7x#h7hWI9Ny!UrYEh!19MeU}lh&6>>uOFv@S{m9~-zm@E-^lNU z)*hJ7KcZ0zC97+GA}ZtNBAfl0F*N>7K9FkgE`NZi5FMp_be#!6G;h!UC~`YU0E+hm zIPFcxJ^lRG+QeY|_?tXOkdM#%Vs_r>_V?QD(c|sL>-!bsP6m-1jFzj_N^+)qn~zbI zz!Lm~c(8U)>tDd9H|l=`gytDyh(6pKw0h*o=b}YU(S@{QO25oK#IE zpf03X?Rx!eZZlP}9@_tv^{$EA?E>8mX|HcWDHkCYijjd8QD>?q7A9VxIE=3pCV}%d zmR%IByY5kz=9PYD{8viM&o!(pU2d|NXrhQcvRt}0x#1U|o)3Jx;qILnF(s^rb{9`4-a zu$~4~<<`y}MgvxFWN2MVv^zeCgt2s$@0h)0c=$Z;?BE!;=DFpNX*^q5OkyfxUNBYHTUPpU@<&^tR)6XA;6vbRpdY|=NB-#9fmy+(bgUTYL0fl zlH67^UQbO5AI86oXS_k)j+>b!=0rJT{3IE?Pp8uT-gp@)x#e0&BT%DuWBRC;ggNqD zR1s@LvC{{}qCtx2jGx%CQOUVlDwNDiLuURE3> z%z(NG(nyB$YV?_YLs5tv$YpbO0Q11*j%4nzNh`Z5DVrx|t4hfC!?cVW!i2{op65TD z*HK#8Dh6U^rd)}#4)kE^_dLsLPIOfx3ZT)JCX)?m8 z_&80^yf%j2(b$5%#qcYjiV}`t)$|?!i&We|LZUaoGQsA(HYw@F?nF6cbG) z)~0m<>)EQeMie&N%J+H>Lek~BSDfYpx=EP0=62T4?(Uig>{0Eo!IK1#x0psN&Mabx z?VcA?_#_e(Ph#esTf<%zyD*|4%CeY!e*0UkS;|P1>9@=S7L?mvg5wLMmNj@Xk-S3< z|EO+mpoWzLQ6I`gBR*s_FY!ejb7V5nL4}&LPVGT3-ST5D)AMLC{b45V8oKqHfSLhO z0D}md&7hId{fq>4_uUovW4u&YQ?~c)={zcJ)aqw@LwJ82@Cm-!@rnf{EKO&B{)5FS zfTpdDxtN2WD@&bUOR84wot)b`zLxB5nkmoeHq2qE(kW{Y7t)c4u^^#7>yXh6XL_cs zytaxK_{%2CrUR4iP+j1G#X$MURu^f2psPDa>Th{6vN}v1MeA004C5TGDPovV#N@IK zONT{|Sb3gqLqP@KuX1wT^7*|*oY5i#^nob+ILa5tMc?EwBWzwbjcweUAMBZfKhX;j{;vIf{`V^_Z03I~CWmVkxk#ASsE1k8I`!>_&anE( zoZwD|XuHoH9mdyNXeIF^c$7dDl6zKh@4JW{7rBaORdc7kzOnO0{!yS5s;a*Jlk)dh zEv|wuVpIwFXC0me(D6c|x4H=|^ukUg)9(6gsy_wAzYL-xh__BT73>H7C@ilspz|M2 z$s@5SDPp8)y?3IFyW9l2z>ZOngk!tcEFvn3zbHv9r=FTl7S-}cp$uC1&kTpjn+25E z*jQUTANoZ&o$CFRj&VQ8s;q4oXZ3rlp=3Ik!oWl&*Yt zfdbQDfJyA&CA`khl*>VaTKaO#b$Z5g|7Lgd%Kw)S_^4V=de?4$TT_Y%!`hGk)K&@# z^V$28GvpSkArU@F>YRIw_q6H~4`di4 zgL*TkfhcmxP2g`JT_Oit+AQ^HgU;MrJJV<@EBc5+OKtt`H!`bUjOB9VNj>ip`}*Jk zh_;0h8mw$-)%x=-dX9{>#9Jq6-U(V*2{DuM*r7^AFufFP;oA-K#Q(D=%Em4ag~ncu7t)p&inMbQsQqiCDQ~;-@{2Cn$oNbO%ytoF7=*!I1kFZcyqb zxtJ9mk7bPVDjX7aNQLiy9QE4>cD-xuZ1usd(k)kYB+E252uN2WMEOxsxQFCR7!bE? z!L94m;bb_aa8c_+<7DEdZ13uA@<{swVgtQpqKR*kL_y4B-fO3rS`3)}#_vu#SV%=9 z{L5k(%ZLY6jST(>8K9W13wJu|q-9k9?tX^)yGeSw4xQpl3|w3#b{xSfxlq(P`5zJ2 zeT6+-)^KT~Fg07okXKc{=ac*}h8HL4R(a!Jn(Kglx9B`T+vbUd6sc)p>+aXEf)~$M z5jpEV(FAF2i>^ecSODhAl)*wc7)AXY({h>7^aV%r%f;{2x#aO zfE+jD5Z+W_T4!oi^xFTz{Y9LNCNT==Kmdmn32<{9mSX^eE`xKoMIBe-)*!OdN<5&x zXn*&(LI8V1Kx;`0lu^MsS+sM^>whhBH!=;2lLu&`-irsrjOc#!P7QSDv|L4L+H@+8 zuq&69hCfZQnXWVS7(RcHpa_#3lK6NI8dwuB#!gueF3WQHN_k&N9t6*cx^0YX4a~gNT18kOmWiM*Dt9UHxbo2aW^W^dBj8Q?-S{UZ6(Q{pRjnZAg zbe5XFXJcZv<#p;pji__FB?n}yG8T)ns{v~Jy9xIQ=88{g5npARIg=4t%56pe3~BUp zgi+I>V_{MU)QOWl;?i!cUK0OwwPKaP^CHo;L&!@T!J#QO+S8f`v~n*phT`4(TXILW zH8?0qc&F*CF%OJYg##U_?e}DUhtDe4FXmUfHa0NWekpuk#}i*Ka*$)`W!^!jGL5sW zp;n2zN+>Y7G8ONZM5;JLmKxxsoyIY#!kRk9Z2M#R4UH=BH3MyHa6E@35pF`y{@|Yr zl+|MPFRS7mU6s4rYy+H=uLM1h(Jj>gCf^7BVt*^iFi4QpJhxm53s2i_BvrwM&7@6t z{a4IK&|Ex?&VjDX&us^Cs4chkysGou_Eundi2Q3?KyF51Uo3D?c8-5OkQlBXh`+qqrgJERj8z?zRdy30N)8c=(c z%0kqlbKo7kmGoa>J-wCcw7`_Ma+Bdwwa1HmH92{v)iPe2j9B{=38wT*`BYUE25s5%wtNT+Oqh1|j9|CGg#XcgSS)6kAKh_Hwv87tng>vAF7!dN8lu*oJQKPS! z`BiuH_6C6qiN{^SaQbwAkkpWs7&6TKOwlQe)|_x`dYAHfmb&3QSGw}Lp=g5jKn)g~ zSgcue8P``^eVg*zWt0~&G8T!mNy%NkzJXxSP$|{`h8_04iFft%NqM1$yPyGj*Kn#< zvIk}hdR=2GTZ0$u72no=)8&$*9`9BDRv*Se3^C{Wrb`CB(uIC2t1S1H)^PG1J5p9P~kCy4_|F?gG6I{SNc)Ip4uAi2HaktK%VjwLt0p zaM-w}_c;5s^A!0wD01`ZYMK0re;Q5XdmF|)8g|FdMYAC-6moy?^sAktg!e$i)6MtX zqs3L^e)HM5wg0Kgz1saA<+a$ML8*(ss*GaRcS+oW^r&SPidGM3_dy`SLW&S(chdxkqM(n8+7sz5eaPhcDv656xdu!x>duwI&d(fQ* zx_y4e3=*J>g;ln1SyRaa2ww;ZbhO^ZsYBI}b}8l#{*@U?m(tbPF`b)7NhU;d(9{wu zt19##g%tTF*Azlm7TZuUgvN(73cdX<98-<=kv6%ukv)R(!MKvlRoZ?I;ZbD0r5RB zq~wCQJY;L5Q{Maxf8TQV>+RFujg3j;@o29{hQ}VtEs<;U99lc>Se4jm+s@0AJUBJg zf5cwdu0fIUb7WW;Qv3af6rRu&CK`QK9X>umu^h{_25j#o7!@%cDfYXP15o7EKl9-^ z{T2sg!mO>U$Vh?fph#-E0Utsj7$NgrJQ@!?mB4WZc!tFlCQWe^7h&v)@=Ih9-BDOs zEsLvfY+fM@v&g)-1JCyW`vT07Ko3p4Zs;~>4H`ZRhiLH|- z{?WstTEH~Sn$eUZg@V8lW|6Hj8hh2Po1V_`LdWd`z0ZBaVO8eqn{JR<1=X|OXEBZ8 zv*OckKyEnjj#;+yyxAc+o*DfNsM?+`ZgB9d)6!z1VbeZVzCH;^JF|GXK|o;K6Z@`8 z1Zc1#{wZ$@?CBU8BZ#z-UmDAHeSp((e|ggSPtrQRgGje3vX6-+aUQFt9f@SX!KNzzTLr7xA}&B66Q^O>wOiWXQ{&JG+~P|UgY>=G8D zGhO!tY;!YRow6kKwAl4s;R*7lcqX;q26dw<+itlH3`PL`pjtnQM?#y1KT>e3;ShA_ zfpz_azM59bw`$YhO>9d3$mGw>lFg2?kX6L|iW=;`dv$koy;vZ#=TSF0Zf8MGPwE{5JrPEbDQ>e@CS+)z~jEThKcQ z^m?)biI#$tgoFgNHha|u<_)}5!V#UG0}X)S@v*z>`i|^`l-D5`usyuCkO0cSBu4Cj zB0BNGXvK~5QsvHqCbVVXhaE_R=1i)6Ur8 zDuSESnFCaSSu9r4!Y~sbUPciD9QEKiL3e-0B09Qkhk%^@0Oz}L*NCPVjt=+dJ5sTC zSCr1_B;Va#b8v8CLGO4kZ(k7bn_D}K2@^s_Lh(_b_zKVaM zk?)C@{qL}`@!f&{BGOPWAPh>b=orIxWi9nBB)$$NlRhC(lNhN4OD0?xB(c7dxnk`OW_WTHzP!1fe^>Jaef+q8__ugB#mtOUvTHMKgnf~=1pVqk$4u7oW| z`%xwZf5NEJ*7uO$f$pt=JY;jaW%G7!%5$h=5$xDhby>7()z)>othQ~splNNUToKdv z*rthi_`=)iE4$2~8G8ox2vVSySDfHDA3a{EI_Br5jb1Y;hGOfQydl;hK~VKoH3We> z2|H!)Sh#JzEa-2seTRq_BTW>|Q&2B{Ougg4rCXsRgo*HZ6xDinmrqcz^WIEsP8ZxT z>mb=g@gip;Uj{9?&^B|5>|itu;U(c%a?k(;|G0~;-JnP8DJO$ES<(q$N}uq&5aK87 zc}-o&!a2k#qeqC0xG+LLSy@c1YQfRq0 zPy}xGZff}|IV?r#BB*_G5ilIujE+P%IqV&;!=h#*c@N0Ej5@5s2=R!#5LT87WR+iX zAiUcqtmLaDP3BmKCF-t0L?a!ss}`uTfw>>Cx>gF4nkGcFCwfF45?}wz9r#zzaSTakd|tMG*!`n@dg@P7w0=N z;l*XA;P96^bP);o*CigI+JG~*1ayLXjDV|bb9fKd^~}%A%*;-*XOx0-r7`!djAF_h zKL~w~@!fW5 zWt*P=+LiC=GY^0PfE@QfT~ih>*oPTE?ifE75yAhdjNT&Lkf*Ln%9}s9t3VNLztCB% zc=+~aTYFuDgP^h3ub| zH!L$_X1zy29B59z?QFT0%Xow-4Bh-ysultYO*zP>36a}s^dkEKCq$tDX)By3kQ%pXty6#>p1#TPE4t7u@p zSCve!2eUN?vYRrETUP2r!xXR(ZlX?P+-}NXS)}g>);f(P5!aCOzgDZFKq%+N3rtRd znLtg-M3uzJzTB-Rm?7#i$uE0}+DagIST%@rIL!-edr1R>XKbAE&E9n~aGW77jeuoF z>o6Ez^z58Icq?;A57k6h93Gn0>jNJ1<_>NvdThtZAQM1N0autSWRUsNaBoAP+UiZ zHkY4v#hzs_yK_ zp7R67}wNIEH@(##-jyAo z%NF{SJ8i)f`CXaZ#^Lz*gaFtDD4tVp?QK&iO{*tZ*SN@PJTtcpBG$Nf$tG#7*KDJg z5f*6{op*Qh{t(;if>KE23*QsyALD1FA&Zk?j7oG!Njl4xK==H8`3eM{JALebgI##y zRAjO?g+xKoL66@vD`&VT4ZY@81it4g?XR0V1~b8`Cf+X`w*)#k*Hx zytXv#&*KoFr>yxUWYpvTQ3&!EJUrNGYQL;w9Qo#sZ$xvm$C)zU$Bw|RpWuoggx?a* zof4fe-Jssn{N@c63X$)6!8${}`#WK}cRF^VdqQ56c(h5Ak>b(uM+e|M4o|N(a-3** z4vL|gzNdk+WlMY&o|A^;7%YYpdwm0< zF!-gDZa7LBw9;X-VP)K~IF{9g)gmaZU7_s{gUJ~3d5F^WX{u4UTydjOaR*I9@Re}P z5E7jjM=0-)*t|I4A?V8xMwh$1-#dK5Qm3oHkXLIz^@UzB1xZK;8RTIX5DJYv6c@CJ ziLVJeJvDmQm}DS&9+`S8Vg6S)dE5z_Z*^6P8tZbqTMe!Yih~et%u!#?c?0B1u z3{L|IdA`QK&=>9B)j32#dx_KtrD%lkIcbvz2JkpI;hh|)zom)`EaW$uV$CgrXux<4 zGQjTBHLkHmN!5h&2AXG_UWphADR1o^l{f%^s<9Cbaz3lG)&a)v7&P1$lR$mhMXZ|| zaG@<2Dfnca3{v<)?j!5>$r`F&$CvPYF17#gtdK7W69^XGlvE->@OXq{aw+BtfEJI_baf4HDB0P<`{UIjiH-r7zZUOOp7R)$09nf*Cm3)*cWHM@-T0;uFeSb8`0(Wif>z7VU!cT`t@w+ z6if6pR7TnFAQ2{-{dUAk!_tlH(^YXbEW!E)dK=V}nbgG`wX-f1m=?{GSjT`uRuFMY zBLQ#K_|mz$iUSzZ4!^@R^X_cR5fUqz`W3&-Ts-ROyII#e-1Y}G3=!M66BoSj6U4JL<4t*y-u{cd+QwLMl@Qk(L!wj_j| z?xs}uVbFvd?x0WngBRf` z-C&c{<%O5gKba=^-=<1{=QFBab`*%=PNpz+SqPxeI32Ch`rgb|PU;wae17#pu*=&J zcRD^>{aa$G)XU{s8mQO>lwM95eQMiiX{BfkW!e8&w#Qp*Fc8VsA(zOMCrJ>)%ZP59 zktT@NRl@^9fP)*mcf1D-#AF9e(o1gZP>s$wt_t~{0X_l2KHq98J5~or2Xh~vNDw~w zJR8zgQdP|MeRZuG0Iy5r zxCG0}-rklRZK~Rok8nbbPIh?!F>W|iC4L$G3HoVZWKnkrSx6DXoaN=AJcT_vN6c_x zI&_3Rky{mNExj-KY-Yga*{%SXYd8Ou@NI0^BL0#r(;-J#_=!b9v)E9`yXH;8>uA)! z)sJSgSP|alG~IB%so+t-q+4+fOaZ!ODHHP3_I4?efzr_k2}6b4zi<0K>` zjtWIy8psgux$luXWq4zXii=|Wp@FLarHx@B?)DP|7h9~7Bv}DF#|MLutWEL>CGjh? z8RZzXoq}Y-DJced+t|By_}|RKdo6pIugX>BZz-(#=UN=gbS$(boy8w?6f-#^5oizFP~PQ691JKQAL3jo_F|D3 zH#4wFrC0Q@ucy#PVZTP<{o=@j%53Vb$vFsRSLWoUmk|%?A(ydboeD8&OQvh9VU~Oo zPlj@2;KF@5T^hfxnMXt=wVg@AdSMk@Tgy+H@9ASF9d(1LZ+9{@kDbC&JuU2g9m4Y& z8$RPxB%gZ!a)aGK{}mI$xA>sSTUBWcBW-yyZ6!>PBVmYbXda^|CVGM#lP&G3Ac;c} z4c>`* zLn$98qU!w3bBdGjNF5~0y5sj#<8)bddf@DRfb61@{l4`HWUFpBdDk26l`Ma?8&_T_ zARS}7Biu7Qr9NG&@EtuRrHY()xvpKc=9~jC=-BkeBnSBNN?7sLD#NaDvaZVDKt7xD zt`Eb_7B|5Q2#rT~328VmQK}^41#}8aEmI9%n?+Q;loOZwB0#2GYOIi85jW)BNB%P5 z9(&tC5z9%&GgmVgwrz#ZrKIH8?wZ<)3#>Ruy8`j_7TW9F@=m!BnTUPvFhY!~@cd}c zPqX%Q^zoYmtlF=oWPQey>13-GJ7CX_vW_SIy=^l(b0@!=?FvzgnMY4@u4;uj&`0_y zpZpLymib#G8A%AW{a)?88^2`lq#i(kD{99hO`yvNALG}xQ;Pv>76tp?s1Eaka$2^0 z3}TE_7_3846iFv(I;Ag}P%_6;Piv*Z&a!J+^NYSsy1xL-fVx`lZjhJlgE5~W>aiJb z5l}v%*G+wYTBjC2{{HbGjPTvdt@$E{q>dP9-p>vTvu-L=A&4qu*ppapPeoqA#aw{; zYK9UK>eIF9csJE9cyra$ent3;3qSYEcR7ODC$;Ghd_wJ_(>$Q6`OgG_B|GLJ4np;7 z6N!B4HL({(WOG~)=+xC=Yxus}CAP%v;wlRIU4lsr0?cWwyFWe*YijU%Ebz8kaC*W% zD>K`?Rj=DdF4un1s!CMY+c$s#tzWl-2VxFa)Yh+-zaG@QlL}(`Sn-{);?Zq{LJC1e z{vu&i)Mz{HHHoK-Q%D|bV!QDNO|Y)D)XN=k(RF}87mE~3^+JG{F6iZp^()}QJ9xG! z%mYUsuZ)MQM84&G^SPk^uAYGbtGf4BP;J|{#X(PNdl^$NO6dF|xcZwN9|<%w7#9KT zMbYWLz&nBYaG4AH@bK>@P_uv0q%Ow~dPb{hnF+o~QpcRX^f)mWIpz00DSX2il}Sh% z0{zL4wkxz~i}ZzxI5lRs!Ve)9VdsqNdyy?@`8a@u?7cD2{%-D#4^QDf9k~OdpKC z@SDzMgHlw(0sD(j;+7gAXHh@?tfpxQKcGs#)ip;)u5$_z7kNv0(kmxTD$(=e~(@DUd3I_I}uoxD;BY;{Nit^2!y<+&go>g`0 z1FKA|ZrS@tqmJJi(q1qd?0NfAW&QjQ#pjE#hDPhWM6^zy@Dk>vDg}Jp*B(ol>GiCm zl2lglJg6K(K(4@m56Cdm`rfnFj!?Tul66a;Y&bePo-Ki1dt6G9&N=%iiRhUR<=Yee z0XHr$ZEYZLc4f(fN4+J$8b4_e8TWD;#E$n z^qC9{4Azr!?)kF)e)0HS%o}ws+)x11T!%}&_|oMu5eC<91~O{TkdD3HDEW&CEwcNk zk%YUNwVbLzVFnPyAdo-UJ7*?UuH7dnV7&U!IgBFqbR$Qeoq>qg97y=PsnfSja;5HA ztnY7U|1gJVvKFH8gEwnjc%qLe)6GK=)9k~e`>YJTaU|(S^q*zCKWE(4?+=ifXmB{< z|BPwi7mk|jBo8C2PJaNdfepg|^+?0ruV*h-`7^V|tMw}jP30?_hw0H%XLfp&ep6=o zl41LzI@_>cv}0muZ& zbAwEUbjZD(Zs_u(PbXe(sg6_m_?w`t9ze4|u-g#?b$L;O^+1do86uT};s523Z~VXL z50*$(PfInw9f9Z7AC%p3G z4C|9+?5xWxH|6p)D!#u^L?HRNmffsAptaQ3hhBKcRJL9AUH85j{~t+b9TkPzb#Y?o z9J(Zi4uwIaySrPZ8y? zjYCLFua=`)Jb&=HHE_j*b`p?;+S-%>-wNf+mz;ji-wR<8A3}w=EHFPzmeCVN5>7Wi zxu!Ti(`ju>Nnsou0cTA4Vj^Wnk4dE&#a1>c%xpflgdg0@US6%ASNU{Pg_M{o3LLx0 z9ckzVgm-B|Pmi$lmRZ|Jv7W)?2^9*7k~Gsi~MgjD2it z3BtpjuK34--1^d9mNMr~@sS}!JiPtw%>2{q^}4sfw^48SZ1ftb#>#80PZd&&gz{2=_b$NRO&{Oj-e{Q$;T)gZgvtf^dhAUv-oLWqHE zzpa~(8#R91a95EW#YTG-)y>2lOG^xYEf%I}%y5SNNQ)iCc@S!aIZ(j5K5nup)+EIw zT}Wggvn323m<+V&blh9zI#Ow#IORhM z!i>_UKZF7vm}56M>?)mW5$W?|BT3RnAlvxujd6>7ijuT($sJDgK45FGBfds4m^FR} zQC%(rPH@&KvZqQ+W?urX>OgsFwx{ah)VGT+w?FZ|{g}1lRZl620_6oGPg|Wpi_W%B zh-jmrm{>X3vlYlsW6#`U)GptX|E-hkW0GIS@KNu&{Qa7qQJ5A`?FG5lFo5525KXsr zdZ^b7g75*3$^aOUbe0`&zEq|By zPYmHCM_xO+ z%+t1dnsfT5xv-ar^kq78FZ3D2BR7&X7wIV(P7Hh#yvCwbdPqsU#>5J=sr^K=(2$aj zlRg=HPj+EO%CE#w0K>KyrYu$&wPj#$z1;-s#?@rMh=#GF!!r63rn4!VX?!R7C(ol0 z7?TI9ThW@FjjYszvE}TuRT!9osQR9ReR*M=6wphXh}41+rle?x><|K!Z^Md1RO)KD z(C2S#Aj`_w*bVYXr@eZF8-*KOjk+XY&1L_)(uIL;Y3qkm>kcR~dU`WutG%D``Ag9` zoGKiJuPBr@9Z`Yo*L|t878iRe1a6qymyrKFc>C;hwX{;3UMvH5EwJAHI}HQwL0=5e zAj?wNC2r52b{^fipZ%I4rlEaOY~NmDlE9AAW@=i&bc0jrLM%rR%^b8jMm;#+|pCUb~;IXxgg$wUgcFRkJR8&BdpSYAWEoKz=Ip zluQN0TB`+MO>s2Q#UyU)Cy-Cg{wwnkp3z#R?Nqq-)Jwu9rq|M~MCJ`a{iR08a`E$? z@gQ0}=TVW34uLsA{(OO51Vdio{SHli2UHC4L7;e(pQ2@zshJy3I4vu;mo4xZo9m_l zd2{)t&YLebO|PD%a5B0*b~g^uyU?VP%V~|YGfcUh{`6!*M{a2?0~UQ1xujUd3}V6< zMsRUNyUeaG85p7(t1c~KD~uB4bQ|LxhiO_%hk9De#T%Fg5Ff5~XrvrN$jMdo$VxfJ zLRY;{pFE_QUJvF6+@A|f9`~I;^$Zy1M>5UVJpViu7=#@EsBT0SWG0+Rq`(y4mNOsQ z0Llf+1})wb`olih5Bvhk@wXW13=4!|je(igADyEjOaVZY&6v0gJbTLc5JfviY$3b& z4wN2f8N)kMA=_eU58X0%kNdr3?3*ab-#R6MZz07+q+>)s)%UzwPlULu4&-^0^bqg}~V6 zf28!~iz1^ePnh%(Nf^mC1!M1k@_25WvvKMIyQE^M3IeW6g2sP9<=lOZ53uX-TxCeL z2JIgj^2!)PI}^-HnG zY3UVmDL-q-0rus$EA3JVv=T+2+;7__f;DnjP)vw4+1#cuE?u*L0(3Ym(J8nLNPd(- zSm4PJp+_6Vdy^9s1gP37mOMt(UUgGy+TIwbGXm{Dz^%@w0x#PAzaCZKEf$B1J|KCdg{#9@2<5k`8} z^o&9_b(RB5IBP4djKx#7lAXbv0ZB%Mqz~QzAwv!eZZZw?cpaeMHeLoo8J)v^lt&ALdRA#xP~}XALx~ zJ}`hHbTyk*mTo}6r-#fO9V14)b4>iOXZ&(r=M+~fBW8u;wRK8TvK6WOv204GMYNEx z1KSj!&wVeDzp6#fHR1rH$?*`iv$?K5xB-JZpS&Z;p~L~4;N)uG%uL6wExGBH*lKML z^!DQN%eMz=pMRfEfK}SX>P-agE`xt|e9wb&;)j>FjxibJv($%aWSFvz>}vKl?`YIk z3b@Ae#v%z}BvIAPO_c(Ok&;yT^j*yYh!%rG7CwD9(~|3NOF^wvGzyf|(dV&W9m2S# z>KYs6JXc(7s%2AA?kU9LHu!`W8RWl9IUw)UHK%tH*eyjQ#B1z<*|89`-KGCr(EbwlW@1`IA;v+_rn-H_>xKS{KziPy z$B0H=BFj!>k;vJAs%=~wwwg?mXwx4)y!;)ynHciBPZBp{Eu#dz{exTAx+9CXENiP` znzTk5X?*g=UpTIJU!0@YXc2 zW~0Qh6mHAd;AzoviVQkw6v=H^gZ+Nv+K`LM=9psY=r#D)WSGBo-ug znX{VV=0!zKi7)|AHRrBt{KtI>pZ5*0VT*d%1gtzz?0iulO$e`mngUQwoNNWV>%@4beK_lHgWH}I3Rw{wbXYS`s$(iLBe<=hA_ zy3Tp%#w!!DbLn%=dw!Wwm{o>DwW30NNw_4BCj0E_H-asR0bE;(R`}WiZduR{F+S2rszR_Clq*ya z^lS}7k}vy>Ig@qxaVoiaBMC`{6@_=QiHT2O)^nNe&KK%i-0!xJk^f3~%Rci)$TN#K?6l^7evU^XVZk7j0y?%$1nM%w`Qgxi&W+PkoL4=8PZH=Pq!!=tj0%Wlt9}F9 zpkoXiBm$D_(Qj`-$3!Zh3_$BGUc3W-^W&L>08DXjlbVI5uI9L}HYp3t*lLRm1LA6l z>!EZ}PKeYw^^ByR6J-t7o$+AuYis=Q-f*I!v`{q%!40CljjgWaI6r1{U)Iq(g)!f; zFDmb`Gj2lXQE=ba8H^K*KXwTlWNmvbc1l!Ei!nL% zmTCxL7=m=O%JTXK(%Qc(V~TJ#tvb>=k#IS~Z1X?I)qE%MoWrd~FEZhVO|=tI;sT;z zQYl3_mVPEVMS^@7g)3iGogwBIXWjI=A!+tOWk-3{sH3LjY30fZ8%<@UMDmch1*_B{ zJH8_BhXsRgR3P<$Ngn%^kUd8>)NUES81xORPrZ9Aemo62!zpLAja)0a;pr5XXnXeQ zQU~>@qdo9pHz<(pA?S$UAtXEYdNb&BgP`ZEsXl!-wlw*Tw~Dd71xf=KbzRdc&Qwop z<$1Yqw$ZF{@c|8`qj{=4CM_5M<(VnWo?b5_(hBJ_h-6+O>O+FMo#f_3Y zP{S+rP|k2nvgW_{Qxu?4-r5;JRV)s$SqBwsK6#dpH{xJa>_v#St`ph=F)k}!1_9ym zKl<$-?3LFr!GpwB$_AM-rnJE|d?{2)Snr-!kaSeT?wF@rw4&ru=an4c<>4KEZ2_1{ z5n6iD-$p6eP{6Tm7vc|_#8B_{LYs}rluAN9;ZjI{xmxY;H8!4aU=K+O>F`R8dk-(``wzS`Q5|W2dj0+?f?4gMy ztX$L|U>g|uI0afv<|rob{P2_AGB{`eO2(|J{|DZI`jw6~^QGYq8Tr~KeF z|50?M%a430E6AEZ^9~3Qm#WK6%Iu{!b68PlNK!_%2B4xGpuTC8Vxr-OpB$TNEP19q zX=P;>C|WX^g-T;*vkL}H%JnR&V%b#XGu*Tp*aqmwUpnD^GH?WAh`pa}+Z3{T|KzUh$f~Ik?lk6umM94R)8WIIglKvJ)A3j>qRe22`lb+rBg6k-&BxQ9iEtKRY8btn^FJV>gt%)w74 zVBBt?V_?-URj$w2evCJE!sg!+GJ4GZy61C3nbXYt3*KxzDIsHfm(*OJZSfBndyt&O z#$I|884h^HQZQcxM%YzVGtuRE8=zW-QNnwT|{ZS5i?KYVSBbj|Zd&aUc$Z zEp+Q|S0u>D10u&BFB*TL6BW;gHQ(Enmyd%%$EFV%PRsX!nMiB%2M6WDYm00nL`PZ3ZFe6}b8Eg6SQ+&>?KT|VPPCNui}=4@Y&eZal-bw6 z`V`KSVO_UiCY(#i&g@`i(+2V1dt|hdVBKUXEr=u2`f#8aGGYh{-#Kg8#<{?wZZBHp z;uM4d{_QTQO!Zt-$)1RHVeUajd5=lgyxo#G3!4ZyAZRxkf2ucj21b;y;|kRWUmQ!m z7+%)G)IFvLQm6Fr)3fd8p$l0}`l;kX@>|&ajhL&Jd{l*19MDytslDyYS#;~xPtCL> zUx$o?8Pms^zggA_#g&fK-G`pHa^3P25_G-qXil+91x$b5U&Ny5#2b4#It(PJiiP3-*276NX#4UU3`5bC@h>W8B6)}$6tWi?~Sm`3l5%I zW&iCRg3`sb?3%P>r?(%Mm@;NVT=)OjUYTFy0 zYd4hO4G5=10txT&gQ>n3lZFJ|v-c*HyqM=VTX)mUxIs!}nC#4i2CK-jsSbyFqYO9V zg$fn~1Y2(R@alHKw3R&W4-0seZ&WUh=`T_k?|I}Yy7fOPFfsp0veG5QVF@-Ijny;J zP@-QH5g@8E{?9cvluFobFX~1vnC6$yx*(=kKJ-Q`C`XqKi&&>!AugwQd4BN8p1Ws?zp6t;(R~q9j^8|#S4GI0bD%R12iKe z6~6xc`$lf}s0PkI}HHhyB&n zx0xr?q(~>y^dOrfYqVVw7urloNNliXhZHVKZv7cHxWr~*f!(Kdc>J^X*OI!lU=!7V zicvc90DF;+KJK&Q-rJRO0?=XMdvpZ;#!z`{G#2%(K%rB+&#nyN!J^|lv~g@2*Jnm7 zq4X9}mpvm%->X}vN<+D{KD|CDVPjx1i&K5tc`91bS&Tv79zwBmmL+W@nV4J2um1{S z+*ek_jpgoE91AxS&h9miC8V7DjvGS)zAtP;ie_bH_Otdf@DrIe;A;E#(;Wilp*{~TRX)`qL9Dp^;47XvLol|AI%5rQkH8bGPn zmP5tx8MC%l<|{K3TfmU!TuRZOgH^GN_0G#T277L5@L=?31}GVn^4`w4Zl-ta_o6vD zs0<^t5@Q*g0|y->befN!>}wO>;bka423nqYro^gDY8aPHr{#A%de~c#Rx*?cZgAet z3CBKJVD#4d8=Dgx`8%547!uFG?8ED>*zMy3^VZx*AySJM752G<$DlPi(L@Eer@PcU zc8Gico|+THQf={f59P-1kOhaaYPmd|U#wHlgRKW`s{gWLlgNbgNA!u7c&eA{yqnzE zKEnm~%HgdUp7p3Cp#l1k*Yb@T38tKKtFiPaHxDmV%PmEH&pv>O@I;g`_gA^?u6w)B z7S&737~p#Q^H?cyzxDVhBk7&=m2)=)WHks}>oZ z-OwSbV9Z+gx`kdV%FxSodV#ml6S>3%-E`i+#UpL+oXAgg2Yf|cRP}|^P8KZ@@Nh*2 zL74UtLrIDUybXV73;HIVF^VvOS&m!Meh`-mTl7**mC8JMHi`i@#am=`zCZjmcp6Dg zw?f+mTQMp03z4rNakkm-9`Wc3__$lcf}-f#D0;b9B$Pe^2thfL-LRauIyy<#l=>g> ztTT^Rfdk(t9mIJUrraqk+niqR|1r(UN^f!E$wE}nq#du1!q2y~9G~X6x3zs>gharp z1e||cHXClN_+3`A#cU^=fkZ#hGQl}elrxbOO%Pe@<}Q+}k!PXTF9BS3)Vide8sjGq z8%9OQj$<{lsgU@@KMVc(gootZs43+?FCe0A&wpJ&F5Td-{rD0tPeB3aNf;(|a{jX3 z#W=Ga0&+~%BZi!DMI-D?xXsNL7{7z^!|Z@u2bZrJ<5kOpfnWTNg_-Zy?~Q8~MRUKf zHKMD{VNOZ*Dl&a%P()Te$@-ns4+4K}-9dPlbfQf02vKe%q?gOje-i#0iVNs;zkG-T zk8ED|3FqUl#m9x9JFg&tqRzF1;&|EXc}Mu%|3(v*_?b)hcmn%AzlGRVYlCSyM~abbE4cSCTA4J6~0 z*?<3iTg-7#^3IJ@?4G5!PN(X0*;Ow zDyBYa;X0nNVAOUXhJJecQZ-^*AfbJA{pNwTZ0^_vTc4@E2PC=`k%fV;VdMnY+6G+4 z6gfR|JKwTcLKU86d`v}_y#FTj=ixn*qB0Q`#*b%$e`fsLV2O788JywW6f9{zK^Qvo zepql4t;ztI?>*Y$JH)pg1;6XK*9-zSHdPXO&CXG0xl`;F`DoPtR+_mxaO{lScoCUr*7K4lFH0I)Pf#e$X?XRQmmc8tbgxOBv&6;&L{( z{r31KIBy^7H#?OsdB%v|hV&FtDMZo@rd=uKDw>NGZ5#*33Ch8FL_$JBl67XaSd;Q3 zI_}{x1#FZ-D*U~;?5pLDAwZWEIb5|K6`hHZ``8Zz+eslmU;h)a$1C!n4Hb~S3q0_0 z?RdB+S#h@N3tY>22svCwU!MZR=#bo99`x(nzUH_}IoC0O{KkR}QCnI~cK0k-0bmq}G;PSI~GDFDTMo>VD!d@6D z+v!BXr)^%xnkiBP^}d0l*9(_z5ex~vu?BE;zE%y zFx11sk$~?TF5T=XY~DZ}wgc>Q4sqG7wK%Kq8m06|*xWKF03vJKDE``l{iqDmw)dwv zpeSDzecmN2RpvypSW5f*h8t6$P-_q0&1ExTMo`2@ygnCsxhbMVl(mG}IlE5o`<^fF zng|x5=QIShVH@FJ3?@(Qzlq;*%2pAepR-PeD<}Wt<#ko2v>M$j6)&+X;27dm4?7#Q zTtkKu8{)08Dkm{29eXG>kz>U7uIs8=zF$RF>MBi+*op1rzujH0fl1 zFqH$a%nO&Tp%?!wquXaWwsTm2e!mUNoMBDY*9!q<9irpxQ_qd`U>1C|&<{q`YD0$h zRo5pzJWC~{CgBS)F%7}mSxsNTH4z!aE%OjVRS66R$^_|Zmbe9q`c&+eg-+COzLfp& zj0ce7d2c@X?UA9F_3Piib3iA|)tyECBM3ZR20c!S5Ol9VLe6X-Ci468Z{36NACKrB z`kgNhXFT-UVqxA7&#>I(2k?)Yax@%idy2V=~HN1Gi&VI~r6o)rdpXt=(bA z1K-A`hL64UvwP_Ei{wCsRCOEUSc?UlbXh@iS#uQ@H7JrL*K_ywjgc$y=jjcisJ-*^ zo5+ECr6Qn%^rD6P`__BR8sZ26SadiErK2GF=s7+^QI&EqW$Yk0>OH6YJdBB=rI!Xo z3Sp^hCmTv*S3MFpV!V**FC#YTD3RtRlhAMT)OO69B1{|RdAq5$=`p;AD`?uisbv24 z=HCrvhCZy;NCv|WpNK3XYk~mkiKvQe4H8BHmvTrurl7SZRrxJwA?oy0Qf}m8TF|}0 zG_2JZ%%9TIdoNr6P6`@=qG10k$_xSM${<2H_wIvgx%XCFYu$G~w(-ypgK_8%z*o8b1lZzK%FMp9+9#sPzgH|dY?M4Q zFyBvP;LRL`zrMkVg3hst+y}%f9Zzn{sA30Ft(I3>5xh(|<1+I`Huc)s~g|mp}p4hw2(`sTA$M*o-bF8_(Z78vz&>; zu2`;dCNk6q0|`JLmz#`hk%Md3zA5Axg%iSQ6ZR(Erv^?>ub-=J325ZeJZuVdklnrg zBynr|$n~C3+y&Xc{7Ng+iUo!r6hfeGt!7?5Z&b(uW%HJ z(yIyQrxFo)38M8MCS}=%kz5H0m5j*vsN&cn6y>R@5{65fuHLg+*7uy3&%ma@<|a2l z#yHj>tG8p}uKOQq-2!a9lmcBy9#5-pRU{NfVxLeO#}K?h9P*Tz5PUgEuznWT$QkK_ z(;P%5G==Hgh{y~Jh>oK?AxcFUvXK5f6@rZ)p8vkfp{UQ79+54t+&NqI1UpC6#MP{U zNih(gm$S0kL!t(rUH$-<`f9lM{sJDw2DVvB>cb>XI#|p4y?Lc|1U^i#{Ez(A@-%Qu zpc~@ECw_LKVpt=91b@5Qh7UR-I280qfP>}_jSsmj7nTopvvx91Iq8R;uho@ zw(FP2o%c!|yQsL6!6q*RN+2^k!@e7N^ z_9@8f8nZQ{Vy?0DiZ>f645h!k1sZ)IrFT_R7+1Ps56=dyPR$&TIt-GR)3e zOOLrp9or5~XgaZIQfRUP;h)(1%<&D;^W^F|!-0MunCB3M83L`bdKGA_un*PsBMquh z$&|ehAUl2dz4Ky;dNJq>v>@*Y0CRLUGRBRVsR1SL+TTSlUDP8+i4(@NVu_pLyYEwh zkI!E(KD_K$UiL!3X`~Bk-8mrKXkxAN3Yj(wlGJg?b+ZYW8eiL)VRe@T=Z zw1Hjy_KpH3jXQ#ni|QuuGMBtsTF4@qqGqCDUKxkQa(7Hgk~3Hg0DPOgHT$5#LEa{s zrS%h#R|K>I;)9&aZ`Jzq7@w_-(3y};&7bC^l=Xkc7nk3bLoT+?dDFBZIzD2I$};qQ zk8-~(L|eAuEhdkApUt~(wp5l#bveAesM$tijjHr=481Joud*7;w0e~JqzU3xWwC2b z;|$|q3zXy#BVp|(Y6&H(P9OCq%H@tt#GEaS|Vk`!+V3(aP zg9$?P)AQ7Z*@YaOSSqtrYI8@E7l?OW?_3@qEW-(1PjAGVU=%g*>@S32B#k4zU;ye4 zx8$DcIJ^6piaMRJIfxMAf%Za5k<{Ah%)!HaF^5#}Nv(?Rtemk%`M!@i&4ps|lA?2- za5OeM^3&}(_N}>d-n;vZpoN~NH`NG_w?D`0rA@sehnI_kxt$0y7Qc@6`=!PxF30ly zXoKL5pcae=m2>(IKfo^~h?$rBtkaPVjgel6=1GY~kM|J#w?neiKy6t{d>Rtfed*VY zBLP4K=AyL#sRy!iADa_zfhayy98=Srx+?^BHOIW z-B?>$OYJcUkm?tc#O?#%zYjhXvtqpbXCMfCIA2`5diN5ole_k?0)5@|W-BNs@WM6_ zeR5cX_}H||^VIiQ^*&V`J-@w|ExGadm5|DnadC>nniu$@?flD6Gsz9vZ5zoO$D&J? z`g`sE#{*Y8lcY=ByE<1EIz3a4uq>x6B$e)BV_L}D7 zml(+bW%edd6|snBwv)5lWcqACR0C-sJ|`8=WriC`vl3l7W!Fk+{ZsMj>p>c2=#E?j zj2jcuSEvsJc`F{aDc*2GN%|#VfL+Z4&%FQ6_$Nk40ztqpG=M>Cw@=CtI^p`a|75F$ z(W>rrW)N2uk*rmlYo~a6KCT@+KD(UYkW+|F&da=g?m3I6y~d48d}Y$Rhy&Ll{VNwB zzOI*JTlWWP! zy+p$UzV>V31xczyQ&-dzYn*WLu!6^aUA~=b$`i~)@v?%m9n{Y;GY7~Kih{)?i?TJB z0BV~JQjDhZbIrWS2$U#et_;IZ{NM-Dc8=X?9}h^-2(6|dCxbFch_0TJGNo=BeITWz z-WX(*;@aEhxhxO0rK01Dv(Ok4kzICl7E3%Rlr#C~D|R`jx%$ia4-`Xo4GX+UMdG=Q zQ;=hJF0~|iOer6!S2~JFCF`gSqQw~|-g&0io6cg3O)GnAchrx!1`no>>pi!*m?1rJ z4clP{mwsK`RKY?fa_qq9-PUnJ8@tCRcY5>`*lI?~4)0Zr5)c9g;VPd+EV6S~n)~ zl+i*c{Np2@_>(?S0<>Y9d1x15Kq>-WlToXayp;43WU9mkdUS-69P)ougy_NVzoL(q zVop#@=;L&~Z8Ra6Wh&+ZK5fQ0%mfxjZ6={j@V)Ut&nCCW$SEVlvP*u*Pff~Sy!fKJ zyDenaO9hDe#q~zc>;A#+__NMzfHG9bhJDu`bcF+QcEH2@)WJ%-ZYY#wCM+_6Eg}jW z&;>3&6|TPF(co9wrGbZw|H>WquTQo4=q@PIgJe-c0aFZ7Rn~ozg>Ri%Y)|!eIbN}H zp57>vk1cG}Lsc32Gq!Y_F4%~@h^x_VIUR|l3?2T0c1`YTZHp z_4Tgn-MSd)P=Mr}#fMoD_NGOn1+BDowaRoWAc8Wrc}XT70vPJaS7hQLR|I&a}zZSQUhq zNVH+2n3DkGM%oC_%(EViTJt!DU9rd#F}9DP?UP!VA&jViFo>T=Q)n-8 z#-`)=RF%d5L^lx}wsNd)~6Fw7a4<)=*Ye{Ri?? z4||-Zvbe~=-l8dAyxZRTdR$Kn#ydB2%nm$>iPmI(w|;EnM6Vakd1+7oUf@4nbQJYJ z`!J`J`3e(UEv?h09pUIp^VHi$_{DlB0i63e)o4pCpf+5TDgg=BuyEwTTh`^+VYh^z z{Uu_cl%9dxqB1$#C27|#iwr2~NI54^2y4!c5BIn5W-9K^BO2B84V<0_xm4@uJrDFn zp{Cc7;Qw&BQnHY-h(IU;ZukQ!A8P@egHcqie*=nBa&^=3$t?g$w6*oO6 zrMnj?qtwj6X?s!)qRXu@hKVR)H33wdq zv)s4<9oJEzHqaN#zx1h?b~;T%vp0>or%kgdlx0wCLM51i3Nk2-rs|ZuUxR}m zY}$7F=>eF)&C82I+i=R<&BN*KLC4K9AXRz9JTttjEBovS!InGg`fp-a(UL(afK~^ zr<-#PG5bbhQIZ({<#NQ!Maj4KFD`I_fx0@~aX#BvI!f|aJH9a|gbs?VYC}9Q@;gUn zuBk%c1r*xS+7d2JA*u)k|0Q5f%)EO7q{gNNK;9Yn1NDOHjy60V8j5@1huB6IgAy(Bxs$Zp$V(Ix<;N4g@SKI^u1OmFg4*P24A7(M#b*9{%9u zl1#k(IAcTjo2U{QQJgw0_&!_04kZ!`nYB@>=cOk5iBM8Rq(9DVI9RoF5!Zm5i(p}_ zIuKsr6a~hT=1GQeTquBw&`-G=7JKtmwYkRwfBmncBh1t5yOi`t9ceSyv7?#56@ymi@^z`DshrF zc?i7>dDK|nS{Pck*^4-Cg9QUM0!az4nr$NpwTm@T*3pJ%sinAK!W@xQ{}D!E$$mwD z%l9;?ZWV*ArhviZWiHVJOL2Yp)wa<4Ur@m!C*e0s@?5cU&)}U%LWdP2?)voYzNh9k zv&K4l=@c4F*=IZ8X?sWZ2Cjh3IAyS2U^1wy!Bfo_7^ ziC{QYM-`@Xj;j9>hR|(vS{~9o{mJ>}@Xf~0xPxE97pM>6 zi2F@DQt1z*jTVLcP|xW(4sVLXOOgEzJmgnTh;RpUQw}zh3Amw{7JrmdH+r(rNh?*lcjCDcPFH|NXaXWuiM55>suB^lQeR={0B2dn5p&OIQ^cynMNFqwavjllOMH@qz(W>`wqGW-DD>@zg>L+GS=;x$1rpEKTbZ_R z8K>NVe-ZH2w?iW@^G+s;+~Y|`A0!|N6fnwSEY-SEI(2$JC)BjI8)f@h2f+`Lp$~jj z^Zwp+1uqz;xPGrEv3l>C>s-apNW;%ywA+7b*!38ZkIyE<8n~wU@%#zTUDB1z6w{4< zbbH3$Y!=LoD!?LkXzib?Nav`WNjM_&yh*nN029D^89eyZ6#^z}d{! z_T_)EpZ;WdI7bs@xOyerZ!E3d%iAfx(=q5s}$9NQDWW%r9uyya0{?)$GUMLY0h-v`z79 zj@iAN7z%Scs`;JWONrL@o32QE*;yPWhIL6AUH0zFv66m6RL92oyR+g)6NI8@6KfSY zc9r=4FE?H|(xYJ9!`B^%WK>2qvCFs*#-b1nO%A!p8Cm){O6 zZ(vh=(>IZ=rWUGZLt#mr%4y+e>xcOVH_z3@djOpIiU8$g{pg4=yKHdsFFzQG{JRxz z2-4KAXqD}4>V9M!QmL0VBN501lXg5sG-Iwd*3087=N30`%{1ZJ*Zhn3P(;(RlzvFH z*nN`Mey@uO-TC+_@Q;kd>2pwwybC@CVuy#zva()(fAqP19tgweNl{%7JMi)gP~b!Q zekcG=#L(b3S*@cZrqrN@(Q9=)JC08d zIW-aXWfG(6ATCEbc^s?;(Su*O&_9}|D~+gQl2{aI>6y2@P~kLwU&Us|8%xiL0???c zEIO2#k-=45nW;tV0iQIb=_r5aY|+|(R7~QxV4af(0>2>L(e^0AYQQCJ`o|Yhn4ZfZ zyEYBH&&Fr^r7&W>2FP+rZs2k#w^MlksvZ&}M0>qx#2y4;+U<$Z4GA7ProTdX;3d1pjB@$fm?4>&hXrPRkabjSKC!Bi>eTaE<@k+@$$hp-9Mr zx9F*vJSC3MlEJOWrV<}8ALepI&&}ghSIbc)(*D8KNo+f^o?r=~hCDn4;r< z*w@Z*VVCXXP`z^skGWsW2Aol(?=wtc@w5*uwB}C2W{R3?H)VN}3<|^F zP1|yx9o~yWrOx+4Wk;Gw2E=vu2U7`tFvRhz?# z9CVYskUyRSDc4*$n_fj=>)GG~b`DEP3fsobd4LPT-_1?Acm^}G;s^^0#)4uz#K0*L zrpqK)$pkA;`y`FhWo1>dYV1=%nbeGrf{zW-Ec^4Llq4hdt9$#js3v_u z5iy0At!;t#REkO82)nFg0RPLb1`xZYF%6%kAFZO6tfReLSu2RNFy0Q$kB9$*U+TaWv%j&XGJix)V`4J>V@uA}6Nak8Hn%zQQAfB*b7FhG)VRmTbs zsDHw8Y+x56vtdB-#+zt3*5gUocKdQlrxtvOq;yf7q^s;$!%kk zo=9l(zb_IOGpi%Mc!s(!7vo@OPVek&KYNqF-u@~>aVShmgEru2<}wHh)c*Mks5ALP zcDm+pQ!|}Ku${L$ulAl!N#F!S!@lXC%1%sFGox5#@o~VZ(%M`D=)fNi^goWZ zCzZTBTpMn7Ts&%hyjXkCCq(XcuKkxBnd*FaSbO}0jO3GdUkR9m`D@?5$0SLCu+H>5 z6F5c6OqB4hPuF*|`d7*BMAwdKQY#=Zz;BmDwN=0ECUJkJ^Q*HTKpAYFiX0Zkm4V&> z03uV{tytxomRDC}fGFjR^SZ5$t4U(XmajRn6vW{Y`oYx*zewi0IwPd=jDR3y4H zng&E9!&~)+m|Y481n|*PWS*uHeamNilc%-JFDTMQ@n4|$Nm2%gDRm=k^8>I`MlqK~ zmaj$>#KQqn#?s4++9#>;P}l+>L}heZ_Z*eA%C~C-mN5NGC*(d*T4}EnNsh2O{ye{T zTnEM8STBp%+z9RUjpw{|L;F5(lNoYtTa}T3uZg9tZNy5h(~LmVpXtsYZJwxpejWe5 z5KXh*f6fJFnx3+3DP?Jo%q&wpuZ~l!B#9^?49k$Lx?Hy_wIjG8L1EICdUq{m#Swks z+t~;aQ%avHO(e{Ci&7#@a8~~3VL-*6eM%;)*f#-x{p3o$e|W}VVMDM6y`rz+$vQ-k z>i2EGmWi(SVJ1=uhG44oD|2+hy+B4DK4PM`)@DRAytxmhLXti273p+th&=s;T>2!h zY>D8-?QaajKjzb`lpOtbZ-rCF8FI?~qjCVGFW5`u^8Qs`?l^#&3@1ZJLVw9TTkvdb zf`fbNc?JWN_x@=2L0zo&f2JG;(3b^y5;lDrjPlm@&KYKxa-|jNvuHvu2W+TB+MNP zIo6pD`w`0v42==nOvM|o1Q4-dmn4jg+}PSkC!Ooy{Au`Q$*wj#LK>4VQx#~INek$n zbJL)NFT{_4_CG&@ninUp2#4Wd-u}r}q-c3o@WUp)jz4UHtlOPuA(S!EmJ$-$8>jhO;1lvO^nH5y16=r>8Y9SO%223m^MDsZKkHC zerMm`KVWm+bIv=S=k=&*|2jCwbbXM5#PSQMmY45AnI5CNTiW>gVvV6lHcrV4L}ZJ1B)-b!{IC8^b3a`HkSXQB4sk^R!5qj;4kYaTao|NE(j3 z<`Yk~srs9&ce?esWAYN3F+-r}HCcCCw~sOfLk%LaHO)HQnVMFK$Dqm)#ZN{u#jNs|$E&&AOweEMW! z(<&@)y2&;|@>X~#9>3&-EgG63Ozv+=|Ck^`y>3`)Vl6L*=uSzKafGEtsK*3oTc{|) zBV&AAv1)|Q{c+K87N#`Ts}kqx=k#|vm2zC31r7=J(X;BCH3(-y$U7K%L`G9~D1Gt| ztc>Hr*`9o2`rDUUsy!di&p*F}#KvY*ZVdnV zFfee-3l8C#(b6lSN{*?ox(TVO8@M1)aZ_T@-c)~6jo-8uDK%&(5Uro$_g_!8_9Flb zWNXAXEtm$w>K~en5(niB`nb#f;Tq6H=CK3rF-G09`Z*GK#S-Z7D=SNfy(4Q~)`H~p z$W3bJyggs506HF89avZ=6E5sWkz-4M~Pi+;W$laQ8;`6d9|(qNUO%yZJ=x&u*m zT+D*&=>*(@$}| zo2RaTw+_B+f!jASQ92>pPrxk}K1ucb`3?Zt?V-E8YtBO&=VIn+kxoqTR=7(3_bHf$ zx9g7-ka6hE6O8k2`yKD{YbWVUO1x7Nag8{Ct*Etx)a7fc7$HuWwT6Jql&es{Bw=2~ z>7;O`gV)cc5Ydjw?MSfMPeq2h1MYGBHHnV&9n5XHJ!#Jy6(oZ`Q00{OR#3fT!;|v4 zA+NPs>04W4*glKuXWyYzdr~ZkJT&NBZ0rLgg=}qB`Y@*6j#xO;`PKF_0~vH258PL2 zX$~#&k+6h^MWp2*NgStPsxff75`sButc!g!%_}qF`h8NAu$%vWb z78daM?(;J>a`ZH|)Y`oJRHeiZn@?WCnaNcMqNBsqyrE{C#8rEb?YlQ$o?9uJ5!8@{ z#r5FG_pkG(1SwJdy$tUT4u9v3Tqo08`*!D&MF=4Zw1V>VpmDah>`w@0s3<@$0Q$yx@(-+=$xogB()hT>z{SrqG+md@kLmZHklQBIn?{${ zPPWis&}H~qwpgfwD@Z5kl+^Dg|HfD3;C$Tbw(zd4@TSb_R=A-v&^<`8?lK51^iT0W z1A?FFn-HI)#R5juL#w3{AZT?oNcAFo{+vRCy}lt+A$BuAf}dPIZAX_3<5X&++zA+? z1hGrWdSgPt)+}8g@s=#CA$#J3+tVZlfVNTJL1jlmFsbr0X(*AKN1{1J8LWlt4x<99 zKZTqP66KUYQs5jFKxCI3EH-C4lEBrLi>kIrfY7Yh`5R~_l=XB|w@6en=;buiHYvm* zxF+UYN_1JpZsIEFM}TW2+0juU3O-nbO_$}Fw+KiOf+_ZUvL{6L@H=s60IvZDE{A?- zq5!Vz(laUArcKz;28JpZxx%kefgfRi;YBU1^0Or6>2V2?ib|RaOj816zzNf0^6pTA zV^rZHr|^ocva#O*FkgO9ZjZ7gh&ZOD+tRNR%ctA82$|XfLf_ie6?*PDm7(cs8Btg0 z(cjwSe~)Pin^*NVXI{RQyEum)dANex5@DBN&7m1o?UYK)rfCrXIsTPaqIFgqUHLSP zMirs1)2yN~F#@>hdQtiL+){TluAwjx5yma$?-{CR`sF4RB(MO@DtO_l2!I8r*6W|3 zU+brJoB0>BK10K_)eM~cqt)qJqp}1@jqpfij~}yH(y}4{(4~Ji{!k(!<&8e0)PC>R z`Lo->Nu~~^XJV7KEC|WqQ4pN>|Ne3k1{#SqvFWAHrLDkg)!|j z$PG7quX{ALS^Qx6dX+O&bpu)1SakS<-0kOoS*umQ9!8f7?-w%5cJdd_YC(!}!pYZs zukn$wN}F%hVTzN*rEbEn8N~qF zKnDqPe!#=kJozc_8;EXw_3@*hF0A?EN6tUPt$Nm=6lgSO48ut?sch-B*2NZORJUPb zA{BlYKf+_h8ite5f-n|8vDYuXJ(qxEIE1N<`pKrA6F+;R=~xpRp?XYfXrZW&MS~+7 zW?dscJ>y;*Kci7p$-#%^SzFRrxwpC~1Mai;R(FXjueTFF{_Ya(&g)JJF^1ee4GCPw z3zfZo^icYDHG&ABF;BPF{I0wLBqf^x-$Uv29UUQ-x3~8ft!Wz}1QIe~cG@e5GSg0| zo(86ejn!(uJe2_WXTTj&0~={99rlwOZ~H80PnOzH>!Yi&8{+!&o!94XCnhZxiB>l% zxb15G&bYWf{Rk4j>KvJ%4z^>8TdxV`B`%0NC*R*~wVD4%K_M9S!;ZaCNdD)H0Zd$s zFHw~#6gJ{X!20%ui7g7GH{SLcA6IOKLMV1TNv^dJ74X4bE_WyUn4Wn{kz0ni+f^&` z5Ah@L(PQwjD-Ad`rtp0%6gj^X!9$zB1d5a}tEZHINI9r7iU#&md>YFCz)}LKYtzdUPp(oqf;q$$M52Yq~G~$7C#ly-rgE9`RJCz{C;Pw zLSn1q61jSgb%4uble=Zx6pA4%O0BQ9311aU=R()VR|egH!IMh00vGf|^bn9r`EM`_ zkP)#U158R<2CVO5_^Ci(qt&%x{@cMe?+Qz|`vEp!%FPFCg zY-UH9<17i=jr2aOk&%c{<8;TI(;)u`~KQu{nsyrh2JHpPB}I$9H3@p(Lx%H zl9f;6Fdu5W3Jgz6^*M?u8P4W5n5CI)Dg%Z(PgPDDbC`aPtIe!Ut6UDN^)s@bfJe5H zJM|{DlOlu%o{6pL2HlOd*R#f!@8na{^{D`&RdEB$(YJY0EH3tUmYLlm$hXMpND@6{zdl|L&mSv zDY}^LyAKHrhyoT&6vq-8t2ZGfj{Ho$n4C4t-tW(V>6P&FpR$R6=$KwRslE5~8O%80 zms8JFJ#&q0vheU{v@mLf8l{*Ri^~sCW8|Dzm1(h53P1v5WN!Jpfxx&{7e67ydelzg zyJ5RAC8eB;E=Nw(wEdx0!wen}w(nm)f4%}hNl~%@osxYH0{XdIu1yoshiz|JA zxN=1|JbmP_ZkbBgtxDM<@8VMCZoftJ{Z4~4JqInl$Qx<^CV_ESUX9Q!=*&qKEsF$C3DQtmO+r%kfVx8U5*LMC3rbWp)0_?SLxGg-U4lr2?FCYt=$V1bROIA$NiN_%j=!f{1zX8u_} zf_`E+1$&Fi&NO_>SqtK4k-6Db@_D*3j-}y&e@ZBdF_F1Rtx2iAnWw0hBUCd4PnVuF{AO1$HFCU(RKk|EX)*6( z#hE)ui<7fA?DL^HmFoK``H_z*smObi37wd7TaSVdPI!DFdDPc5=qIFPF>5R@w_4G* z>NtxFPr_VS{;@8lj>9a0;XoxVj1RL;z1=4J zqndHAA8nb+PUc#U2AjR-Kwb$A&~vwq1q{j&$+S~_8yrSOlg=nAyj4wkKh`ukjq(sG z5?UnGtgv#@By#%N$Gqgv@EdeS)|7CY8knp52n02IMzueli$Sjl{xDz19Qo`SKyo~_ zt#G3PDL$dkfB&a+Obf8KBDh!0`XItpb#O5ITUIBiAilyi2W{>AdDzoUqtDwnd+Uo@ z-yE}-2y<$|32`})sPv5D-jV>@#Wl)M1sO&dn<>0roJ&>5$JkNtOtnGr$!7vdZ{@?@ zUnXmZ4K4n~>HzD`Kj?>NgIyVn=egJZV$y#5J7A!wLllC0(w^Qfme?rfJR>lL#NvMmc-f8^gFCFSJcF*VjFxS8i*L zzE7S|Vk(D4+)1bHEo(hMYiw;uG! z^f%Om@dNB|cAAp=AnMJkU$z7>$+md=_%jJwP)LoOA;2Dobs1(nN(L)UKM!j#=kXVV zfB*ekWU-YD;h9-XROd79N?6?!h9)S)E-+WmSUm2--!R$dlUT}>X7ttZG>plVH!0ti zn#=Z-e9m#|Yv|iy6D|K#TRyh>?hdAnA3kww))2NAzjFF)LWR_I=USMrDI>OQ;utBS zE{6Bs!f?2-CSDt&;$~~B3LywR{M+P6y~qpN0;qv$M=cEtbRsqvUs<0n^v9Tx z@d6<kPk*HsekY*;hv$?idY_O2?f9#Jw2%Bem#0Ore=+)IFwr@pTkhRXDY?HEdTMqQgs7 zN)Vn^GS6_Y_QD&tUm#BIh;234Laysx4i^qmWCsG2X4uS8;>2E^+iX6YJlV}Q=Gs); z_aWn0WLj@V3nDIR$(&7%O?g(+TUFN?8Q~I;dIrfbBq$LE;4|Px4Q%)lLnrgaPN4Y2 zvEF0Zz~>u6VPWy@6rlLGV%V*)5s&1N zE$l8(YguM=LsreOq#<67lv~qOyfBG&=$DsTa$8 z*;Wi*_t^=R?s=08Jc-=1Qvi7{@mmU?(9H1mbP?F4 z%5ANCJj^$}p}+%EgHtW1k$Op3K&iq`%of}~stKX48N!BRBP+s4YRtVv>7oVyKM?QI?5z1T(To^?h@j2@mR9^=B61mB^MXu7nc`B&FcmT$)Typ z&dUrIoahaP){m^rpN=rceVE_b$(TuTD;yt_;NjPhg+HHLq#q^SHycz^vf#AY6jH}l z0zM^Btc1PIKT{)ltl?zmgen*O+gfb3vJxsvOG3?qlU)h}($wKZ1o_4 z`?Fb}ck>_c{0qu_e=kodq3=j&G;cvsc^m{mYLyTbnG_smU?}WQ=jh_1!4(eEp3VS| z96E0boOsR0aVbf`y0$9E%70547ol7Gm;)K!#8pNl8<@b{08fq<1FjrDsqo9N(Z+J2 zuf3TEX)R=b3)J!Lox(ww&zAFm>vTUp%E>q1e7Q&oIz_i4AeR!1Xq-`?mUlCAW+hs&m|z-RC?bjbDLepuVFc0^l`c0NDU2)SQ>JC3@vnSNRbH zYwejeEuc}~e~E1Rf$z_=e+spmL4vSU!#@OYuIB5D z_p~Y{3}A-M-;u3I9nUz?&?1c@|0_h@Cbf)Q2^V}aX6$BJEc=x|u+ zYdlbP6Wdf)ax->(B+99j@-a#;*7g0p(zM3hI`ML(Wh53oAH$0tv0M*n;;@^$)9t+9 z7`GvztPAgdIk6*@GE_MnEUdkLKc<*C=|Maz2&^$S&c7LY9s%0dGO*wH!^+Qg0LX z{%itp4?LNcMt`XVP1T!PRhKWHi!CUGlL&^hPK^01>E_SVRsR zfkK7Msv^U&F@7>FgpF9uJLnsg>#MzT(ej zFp6X#ETnzbQk%kxLr8eBi&yx-1^CQKT#tvblG`gM4S?b8ow+e+;ov>iVseJF|J8|Y zH;G?dyz&>IOh5RKb(lZN4jMAoosiczB+JANVyLWX|B~3w)+Y#3w&-uD-6 z5d7S(Y8e&?#R)LpwIuAqvRV#m`16d^^+iUf_6w3||0ZAKQW1m#Yjm7OP^hR!6txlW z%RGPThx=_=;_cFwu*?)H0kd>>;!VMsc{YjY`#=BPYqiPeCMN|ZMRLn|ei|YDZl;)8 z%%%Qah}&)DbFbC?`i>JY-N+djCc?x@`z@)Ndh*Z2BqVwVwh;a~Q|A#bLR18d-$%a} z4GqId%P=*4SeVsoUYIisQ?u&U;LD(M89nx19^Ryn9J-+3mCKvad~k$R{FTlxlGEVtta$&_v9=&T@|S1WiCtu?QxC%* zr}*j&#%Nf7obXIZ*TlHP_VDEJTz-e|z3k5Ooo~s1UtE8Zl_AFHS^4`kB>waDx0h2N zKcCk;jL+YnTHROP-yO;Pr4H+R4uYw#ZRE$~mRg(h>KsL^6kOu+Eb1Otj^N^#kBg7( zJoa3UFzswAIl&%1N9MLh_|kUK{pYW0^y+iQdv;%o$1yrOdYLX%gqWx=CUKXRp3Jx^ znx@G)b*+t4*Fab+_oIzRg$)g7eHZe!Dcfq9R(vhdL86P#EVjN_(yRDl2ppamEi{Dp zY4uq%>voR@d*wbAqV#Egf~s0|`J6bg0nLjC9-WIeSk7lBJo@hyP4gt|WdR8L*zz2R zU}UO?_O^rXydglzvSCurRgvVMRg6$E%lJ-t+!~7#jJ!!`&MjBY!A;n`IDG22{hOxq zb_(yRVInB!rMWEq5zVYmVteI>57gCjhTZ8D?s5F}sT!hi!VmF6Us_$SPtutHt&55< zUM$D(c-d=y22-LIkIx2B zYrX!PA!u<0+T+O2n{ zQC!LKw88$qK4{6CX11`m6)bXyGiujj`X|*aAd_rhY-0XcQ$XB}AMWdyf+Ipm>39fF3H9j?~5k0D_0M3Ftye zwKLc}x9-KpT30hikPy!ukN}IE-_deVD0YN=*ugx}YS*B$wKPlfDm0v4DTN^HxzEBj~N(Yo6lN62@2&0 zZ>xw3daQ<%r={*H$CpW^tM|~(YFYA8G7^`HLy2O?GeacXr~qbm%T#G1LkWN@_St-1 z1<{{6zn9z2Cun8G1Fx9G++>JL9_~-#*dYIo3<>0R6gRZ>?fRcTo4iNAUvyVDuT|Ej zmd90Afnf5qME;p(g1t}AVF9VE;fO$WXYu|3%@&>pp2x*QvzO!XA(_glTB+-XE!-pR z#f%g6T`f_S9|H8~1qH#n7ylGdHT#-rYCrEeYKn>c^KJk8RJmM|QrN6kVyjCW%X{j( zKTQ)aldEuekobsf4R#0XX4c%@#Hew&qDX^#euNjHla0sCg{vix@r;o06qo#y`Tj6* z?5WmyhI%d1ktPI6!lYwp>!G`cc#M7|7EwV@OlZWxjrZAClr-kG4&Rs4g1#@;%Canb z&UpVD80D2t2EgX{T+zCX#a@|LuUh%xn}CP(sWW@hvmwEfCn-uRx+kIi!(^+r1dLA# z%Pj|USsZ|p;hP`}Vw{)9@8!#6o3Md#@u}1-x(rVC zYw)-GnUImlU){orln4{q#_G>Q+ixe_*%9QaLY{I(`O?~3*jgAJFEhoW7G^NoIU)DF z@pq(eppI>pB{eR7bUo-Mxkb+Ce}oYDdb4CewJaCe$OVF;6^~H@ZaRvG-D$yqy_}3{ z-MLL6hxL=!4*9E(z8(+viXb@_OhXi#QBKY_6y%n~i9Im^v(p+g&EVC8%7^BYcuZ)kLDRj0my*|3(q5hNlCL$q;`0@@BbRQZRTQX17rKS6sA$Fa|jI3^bt&Uz^ zub52{%Y96EneS3GxB&IIM`;`67=D*h45GM|K*`uEw$@43L zSS4%F={>lw+lo?-N!%w9a;G$R!+ATB$=w;P2xcu4r3MJQvWS-52#iM1b*Ud!JQNmZ zi7|$*EI?LiX!=GiUCHkuOdVi~u(RYD8wfH)n$7U$8P!Q~j!y|oXTA!THLpWaaSYT* zc+v1im5^j08r*p?8uWa6_$bqhDM}VD&h6x2xo=`z7V_FVt#-80g2%Y~_TP!&kC8pb zVTXX%<>lRDq7aCfq~tpx^@nvop+G(M0cKQQ&t5O=gc&mdBQE^{#TW9~N-eZQ;yTOz zbT1824?L4v;b#x_beno0x;kfuO;geVgog{Yp1aio;8x40^5P4_q^mXz3L z*e4F#p@*K_;!Uhg*L6ki`6U3u2R^R8o;Nd1L}mmZx`9u`mSs?xJdU5$bKx?9ANlo&c=S@say zk+d=+jTg3_5-VruxOj%)06HVn5$WQb-mF1IB^B7Nf>Ii|+CpKe!sNZ90i0zK`)6!y zT4hWw`{cmY>`tExhee=*bRtV*l8h;%?`FL|BnL}AG;{VCU~x_$AL!wsuE7atIJgUg z<)1q9n6$VXxzLE;)jH#=qW#-O64A-@rw_(})RmZ4r%8YLUXZnR9S9PMNn9E%jrkTq z+9b$&>yQgWIm(A&m<>WoAt!lwv`gYdndX>|+|I9+(Wz;$^c=%vp{np)hfD|Y;?SF) zA-9iua=c$|^|OBGeYH8J$^RUCfiIkmb|L5+ zo8+;m+kbNZQyIVPx{;9yJVrNj`s{xu(RDl36!c3J70U>@fTe>QX~PHU`EJ%H9wWtD z@xcY!Bg2QmOWG@=Emgm3gR0tS#W+xON$WZdCK3E{BTBvUwG@d^)KqW{MZ@VM<@Mb4 zHh-ac85*fg36*1F3TUKjMb>H{SIt8YBt+&`Z*5fiBVBH8sN;*~B%ra)dfr!m;6!xN z+aL%*_H|%|j?RR`!)W*Y{QdIFyXB7$Z0;yBt;sq{V9@NZD>4+)Yj(pZhkQFJRKqc< z0d^-{lME?YB8kOx!c>uL!Q3flZloAv{PbUMjp^ofpf&;y+}vPz z2Wlawr}#*Oz3D%L9=McG)?$*~13f_+0VNF$Fw0z_H(jId>|`}_JRaM|^YIriX|Lsn z!=_Gr8ZT!j)ghP^5h#X+QL6~dlJLAUM`vO|@;(K^ver~;LP2m;>Ny@)8?zwglYC6q zhtuEdj(i6_qO`P3l0|o~qpo$kblH}t@;Hv2gUfT|o?Uo{uiL-3#BI4VY}jw8v4fQ| z{U&s-EzzAna#>n0&Zhf_xop~wFqD)YC$Nnk+&OzDVV0yD<~3)+0>P z>Fv<~iJ|APrHgdK{4?&;7Um@X$k!trg4tfn1g&`gcYs;WBQu>*D+ipxMy~6-06Kj` zL$=52cex3Ja01n|FRI3yQb$1sg=p$FqD53?EF9uGq-#Gpqqx4gw`G@j(axE`UA_w? z(ud5u2-W;|;BbZ^>>A(h1Uk~NUia9wdO2(X^4~$rW>3d<@Dfxoe*f1NS3Kkq=!rbB zBO#s-7UbYTBFtQvRTQ7@_4FvI=g_N@xPG5b@B7f>a_Z4AQICIm@F%AePe85$<`$EK}FPArR$ zOl3@QMa^M`Z7pNT(Oa1{dC}6BuSz!yd{<%R%=g`0B=ni)APj>lY>M?O7cc_ks)-!9 zVY|9$Bmk7Q3ydls-F;}xX{mL%S9{Y7m1ZjJKf}aw0e^M$hpn@n^oX- zRLB*u-QNFF1@yr@x_@haE8)y{!`^@6RdOk8urV+QLB(9Ge`E(EtMb8&MZ@#pUHk;W z9_;dIpk9^PBn@h*YjI!%(OisWuvwod>Ukn}@u#b|w`qx!=nm#%+a_G(cWjosujY!K zBs1&@9Lg)J`K`EgXWztPykSVf{syjpV=$Qj1JgJy-C1ED*N7Qb#|}b^d4?98WEH0U zz?j;@1#wjk>I4-hGuR#2 z?8f#(&;&?k*9D+avJstL)Ogm`{xs@2t0x7?GZXo7%bLG^YjGyyCk?W&_%$g!~IJ^aP{(g$_cx*{fXP#AX*(Gg*O zEmdYn1g1GBPOy`+Xu$HYU4pZPyi3GdQK&D<=$5`rr?cRGv{zcING>lQaDplHY^dsd zpAiOl<7T5zAb*!u>UB5~oz&OE+5HJ)4|N1wruS~S~SlG*9 z8Nk624$6+Mh@k(vA0R7tcXXjWS04&*B~GQH7;(D}drcIp<@)4_nX1VMq7^;tp~H8@ zZX(%s7X@Hm*sNcjp<-b?Bf_25kNZsME8A#Kq&g>K8wM_l9w*}t`gi*T1{R!m5Qr_e zLg#(2W^cfh3KQRxvG5|XmW5s8Q>pir&Q~=bS4zM{SE=M#7|<5bk3Ko=eb!~kG^+pl z;LsOTAV~RMfIELzYnD&;;|TmGSHU`S?aU)MkOUX1Kms~cG4!vZB`%7>u?(`mR)1g}J1is3!rgH8n8BJ4VJ$!lI0%EK zHJLqyeuA^C(UmDA<6VtuyC2S!l;lritgo9ToP=j)>9b&zWEOiQxKf~LMKrAgSiOF^p zm81)HUSD+h-EOiUfy_=VlGnFa?4U2HTUmulp`U@YH2Zu_uOz z7UTk(a2q@3VEPv*s#fzflFm$)-v@ zc#lv>Sbx*7``Y5Gw&^=p$x>5XecZuEkHloVBfaoq%Lad-)~7=)^}W+M0aG+RW0pfIbP!^ zq26Ee%<=27zW!Usv%PRPy7pNent_XnKfJ%^g(a4Ad&9x7jQ-lg`lYPF z$@5Ek1`^H!BWFQF{nFTG#$B7P{7j0ROkv7q=ffi>Ekj=EdMaEfMLoZ46PnvK zRPYmI?;eFvSzUd3ezJKC-J!@TZ7I(lS3COO5&2C7Bu@Y`5A7VJe)+S{QK%<5WD5`M zDtvug?VQVSG;oRrfj_kp=>8mipJSQot@cJ>eP2{#+K`U!d&m5u{B5jwP1u}3;^fzq zIn3UGAy6wIiZVjO67*rHqe3dUuEp@TV_g1_t#%|QWmNGH3Toyk8Fv^u3#~k&6^Cb9q&} z0X2z()TTEq_(lE*44ZwOD5T}rW%;Bq*%FL4SaT8vtDDQ;#|(bQ{qI^^PbtjE5G@xD zraU$+mP`}+z!O(QMuDEA(mq^S3r;`JbHU(?A4^xio?8w)tUeD$6|Fsbl39NB-;QiV zdI>qX*J-$c&AZNIm2(@IM)+rp4s5zkcs81@vuWDT1~Oh}7V(g`D%`8#v)%Q#zY zk$MV;w@PA)&DIQYhh)j3?%z{VuOt(KNwsG91WXM+h%N? zu}#pFw(MeL=x>@ol2>G|13YRwrzz4c5vjoYAu=kch=Fy3^H;vYg2j2dcU`mHL<@Tl zo%L65o5K|7nCai5X+pQV0_g@>r7f=;u)kMQyaahwNaoskIPu8E02VYJ%6))ZMphI{ zh6AbC!B$$+E%bu+My%7Iylx2$6~QdL8*>3NxTCHIl%67ay%%{D{+Ub@f>J@R6K30i zcO6}DLk zu`fb}ys?U_qi2-)1^H)1LvPWZOiRG4l-+tY=S-aUIUsU5_VmiI;vO9&8o`cKPOD;? z3HWHM=MReBlnbesIbw(M6SE|$QP>q#I!gtg!p^P{416{7EWBARuTM8q=wyP&j=F9> z=U9xNte;J+4Nu!53V}PtQ%1U*uWI1b_@Mo|g|651ERGm8cpVSI!rWabYSv9}R=o?$ z#DY4NO!`vM2`~L0M>twZZh?Lo6y65IU((I$~66qFkmXG3ky z(-@Mk3qCAG#S$+Z!pvJs#hRcnp^ki0yQTcf}$aoA3XUc|-8hez?w7h&RJ8^B@L zD_>wM#s&gAoFwv3t;Oi05nN~2s2|;a_T)bm-L0~^Mos?8WHy$WVbWJMixlJAuT-MG z#?s*ImBG^8L4cj49=^2}$s;UJUDgkWuFqm`Y#qW*-Lb<;`E;kIFO4Tvi!|?s>4;{( zoljXEQb!Vd2$xT*8tQC~#BrnIV5lO{6fBpIwvK z8`_FIT?Ki@>wkM?Ub<>U)xXCdWmT3Rmc#JZ`+onMrwH3($y3kn&O0SRL=BX%()$$O z7mJeBxaM6vmzyrb<_=o^`%-MJgCbA{dHh=id)F*x))@oyiB7C0y4-+~(}@mW)ubJ| z!|%O3&q5Z{c*h#hDr@K@C+W=#6JTO=dNq@LB;y%>4!$h^?v(ZqCrc~$w5=cC|6`Z! z?Y-S!{QYsi{rbJ^ucqyt;NLsP6c6iGrB>G^_Tl1x4GK|^xurS1db*Vw=}mFo zL7-1VcwKa|N5;??jM}4Z)1L`_le3>~VA9eqxhZcG9~BkvHX>}?pONLW6`c>}*lv^) zb;#*9d%t++SpnR-L?~O{VyCEuk@RwH*&oVo#$^nG5Od0bX0Ex0t({_&3&>( zrry~!C--W$Ap0$nQMJH zjsxEGEW0-B8Ie)6J_HChGWz$ii*t^V_4SOE8#T^@0~I|@M>Um94#s!e z7+ISE(x>lUT&~0a;*wVMf1p#2VYX2!8IC>7GI$7NaZ1TBxe~dMe|u%o07o}o=pcAa z849~#xN^LwUkCGT^LLe0AT8eFc19yOsrd~PW{a(hfmN^rtmSBkInAaZ`6Ed8@IRNQ$mU1lB0Tp@+!aT z`Q`vBE|QS2*29J?EHwX+y~fz3*%d3-bwsavUr&=0vKy(uffm(qFp7AaaJ3DIb6C!TUUr4`s?et~|PbpM6K z4*T^F(C_r1xbCb4`TdRWzLNd>zWb(QxGfR2<>{`4y@NsSu@fUOO5@qe@>B}^8uB=@4`z0MOUGGFAar9lNv6~1|KufzeKv6ij zBXj8lX2I5cLr!hxdOw9s>NsD#)OuFk>)eHjY72hc+#GsDFU04`vyc$13E2lkw!F{J z{-3qp3B7_q+%#)ol0^ndYD0N-)|PT*Px;}|oZKme-HQTNpn;XoEX$o|Aw>BzzwU4d zJ;SW^PKotL!!5pjUJi|mly*qz_28bFpEKrTprItx*drKOgnu*R+;1XSZ)bY7=cahS zhX=jzz=##=N`FH?jLtbOur9sv>leu!2RIkEV)uUv|FV7t0d%Vuhq-`|_#7BGfUBPS z^;*<({(Q}2Kuh9_unIiEaLys`zC1iy|8q++F6pTC$>&Uh#AS>2OC)GFa=+1Q)5KTS z?*1#c_A&qkQ%{tDn}X9hmkPQQFP0geT-8Qs&mwu#dMHAmky*xuH0P>O|DsgH^$nYz z`77=yy{AA12n&dYC@2L`K(f`@0kYl*84x6wR_-i8OUe#1cz{m403lQMUMHEISLqBj zRabY~2OIy@_!A3Jm;%sZp{l*Bxd zzF|$Ls;cVPA%Yo<4pSZ@Nv0=;s8uUl%;wp%lPM>)y0`~>#QCoH)A4rSk^uFbS^yq3 zKea7;swQO9AGziZ_3dU~z~VRs!lw8GqW%RvN_maoa70>T{Bxk;;CnJHLSHV%k#VUc zaG+(*OJ6>v%%7=WKbO|RuCQaT#M#deWf!~Xiq+Krq4utZ`o}v`6D1dCMUJM{;m+(|P^Pfupu5v0d!8eZxEB zX_UxB2;U+Ug&tEk$Sy}NNV;zGlI}3J0nW$Qz?8^8`rp~@rWZRYjSq|FLO;=;10>Yf zk1;S~#bK!>`x}Md;cV8){QU^;YL)Cgi^s4S&bORz4)oz-{^l`0e)6Lp06=o(Y&5(m?J@)7)t-f+i!lTwTM z-j*bI!m2c7l{`|!{9)J|y$lw7CK`-Y*L_b8{1{$Ixs_3w?f9-cz#I_%ybY}p{beeN zZ=!F~l#ufLD=^f!#v0w)$CmxC@5(mh_%c~15Cw|LSD^hF*7@L^!+adZ0-=64PsP9* z|7W4@UZOik%T3#V*(B(SDPZK&%QDM1E2{7IzYPfx@{qaSf3nNEEFxYu$;?KJjYE|y zKwCdsgy986izuq37h`6<2roYXISxJe|K@mS1BibNpN>{K-vN~3Ap$ro?f!XlZ;7f7 zUw)F4i^Rb701J>TOh^S#O#IgtMI>_t@wc=Vqq2GFOQF!GSiR2;Qg`QlsJkM)XmVPk zO4o+Z4mKQFppmIt{>}0JJg0e$QlMM5CLKk>FxIhMhyx1yw=6x-petlF9Uq~$<+d{; zO0;d&jxTId%W+;)S;JD^xjt2xdFh+TWGf_vWnfR4PYEn`BJwt*-Ze~RWIC+2BaI1a zIK7%qiM3uc>a6IE%<|Z@5sWb+RH!ee#Ka|IL@=MG;{er?GAJUf9bi#KZ>PC_{HH0- zhve#;`wer-DL#;B2K>NjFZTp)@~7p@3+K}n%;`ntnVcxPl(xWEN4HLQI~6iII!iUCboXpMFJ|uazTA|mi4FnSGm}{j zu1f=Yb@H+E+5DKwmi-YXa*~FIij*vE7p5E?0XA=G`Bd@w7^9Au*5p1({6TqP1hR3$ zLwX-{?$+v$s-bzmL9ymW&v`z z;}2O@p+vMB!`b>SfIZo$t^tbUb!N1UVd$79nCDSJkv}kDu8sOImbsC36lOvMeYyPz z{#mj6+~*IdXG;BevTy`3xoC-P$|ew3$Y5YjOJ!|!?XypgqB zh^+gbbIv|{U)SdnchKd3f9XYLdiY3CFq3YD=59)Sq$r0ta$wO53tp~q{?@4pS*Cv6 z%g9Qi5SEr~L1oR~$cOgK{brs3$ZIzuZiv88Bb}KzJs+N_7dH~4XuzEfM4^9x8Kr8E zLQl`qR1fGtAfXA1sB9yZm-j@gHjp+ifPHTB2n7aECoah*lNayvoV=7eY9sdLKK3%l zgs12a#2zMDd#yYPnE9L>7dxxgE5zEbl_(I~@j6y7U65&h-p8iYQ*>N?C1dG43DmX_jwe+dh1@}t!%mshDl}q6mR~P2&Q&o{gn?#FoM&7)?|64 z#&`h;MidRd!|*ZPgDch2!l0=%~)?AY?N6X>8U<>34S_@n* z=?`TJc%1L6{zHwP^t1)gPTsMdYlR;GI8oN>BTz>(h=P z@U|%od1m;pim(O>!`ygivuh3&NEw#$e1vD5sf^yv+rWV?Ma&ZSdmZhCkNUhpduvb1 z8gh02Z0+_$5(VYQ%1SgpW?8H$TYieecNW6Xr@7Nl?A}*JU;9)dm?5S7dleg3K`8Y7 z$d9#qfg#_f@>9mQGM+|zsia{e60I@)e7Q~$83XF+INZ^&^gcr_YY{7J-_DWbpQKEBJs*}|2uW;&RlzK zgHhdB0#DkYUa|7i6|Vg3>S&+?JNCo2=9+q9CnP4;@6rl39>0q&BFh@-Ec=o(!thJe znt6a_*E-2%3YC{B7`DFR?|?XSf{{OB@9~p&>`&ELOCR4j+}bQL_pPoHzBH5U1z73S zN)E`dlVP|uqc#~;YXvDl7m5W1!APm-=YV8KOjvCA)BTm(#y`O|=|ak~ObV6X#~fAd z!$IA>SHDr47EyiX1|1XYK|IQXu|&@l;J{t@7-=i@ja0LhnQ5b;aCYija#M;2dcm zz;SPxBBCx++PbY*tC@Z(#%H#_)FPwmzJBv$9sE*2%I63=X4HhBWl?Wdb9{eCG<7$?trDlFetq{*~FVJITU%WhhvSeutm(l#SOBkY}1%QY&fc(-kby9 zu%B4J15fY#JXsk#rdqjD6FTtP`0KlvsNDs}_R;pm*vw^;agexE1A57h@AE+qR2+!< zoQ$))iZcU5yJygJJJmpS3JW#%MG^qo_U2SsN2=C3bOzWASd1m@8HBfb1|wk~;q zCpW0F*wXUqugE6?R8rMs_UkP{)Zw^dp)4$mu;iDX8rK;@f3JQ#D~|k^{IOBmE_)OI zhuDYp2=+z(G$M#hCrU#icalEhxRpF<20M)Ez<`6xlD3ezMGxI-mlpQ3m4J%L)Mq)q zaV2B>G(ojJf70NwI)lQ3Ki@wbkO|-?wv;HTSV|1sD)|KkOq0(jPX3c-5H1Gee<%l7 zA`ms#ZNxnm5g^?%f&Ry{%l>VN1k;a9x3QMhn5bKTni8=AiLeSSVTZP@RXTas5+8N6 zY7I+CM?Rklgt2QpRqbk-`m8Z%;Ne+9FeF>(s70ovi|0xZerl8WyM&!x86P$Dd%#e_ zP&|nDUht7sU)emkrEMBY&mPA}RBkiTtMcJW8J2BK`zJ}A1gKxY%FPQS7+Zq);rvFC_!!N-@=qu8|vhiQbD41Z#WiKrswn@4f3^*muPcmuMKxrw*=`EX89O z1}tx(@z{=qN|5J+v#5z|F`9sp(5L!DBBj;!q0HJWLJqra-QOu<_W=?Ccv_l-MOGqI zn^T9zjjxUysc~uD_y71*f5CmY!dePZhr_Lr8(Xrk?~;6XcIT`CQhOjUg-GHSx+)CD zK(VZhR_`@6ra<#D{wgR=Oj9}@c={}=tL@KyUx9{zEi&I|;(w6~A_W#*wV;s!KJq1b zLt!wWSuZAkHz`u)h`JzLy+B@7fzs=esIbuT%T#I_H#UOdy~FgrYZ)yUWH7OQ1|o-N z(W4Sds~;@L*-jn5fZKPa@#;EOXi!N0WJ*>#Or9wbw0*YLJ2Nsz_#$+01?%Y7lLXg> zwgzPXJE1jQ-rSwxRfu2&vBh8w(us-5EeBNX;}%^_JZ=l#W(7G?okTy@mSyK!4S;{9 zBKkt3q--2lzK}_I$kn=88gcTa(pa~p1tW8xn!Yo{d|sw+>v)6Mj6CcNgIfpeXQ)%6quTmv>?kB7##Jdg^+kFgJ^zgJ(h)V@0%JjxBq$# z1`M-+7%WT)#OeOt?z#V0clV;_n5M@NsV(9w!42HQET_b>_aPS#J;+~8izgwKnOFP% zS@m9KW3Pkfrk|l|@5uW?p3|d}T!iEqXb1gT{`EDbAinxYQU$KtU`+t2o>7`bH(eQp z850L>3*tmhVVtGQ){rah!_*&Vpzl@$yh{_K%wq-Ng$u8$TM(xmwOh5Pe6T<3y-JCF z-p=t;!eBFYPgOB`ijE3)EX)Dgk3&YWKEoD#beo7B*tl)nU6(8LD;5JF&X#Ep`Wyi* z&{ffm=5hIx*VHOBLPree?Ot{A7Q}um|ggf3b_;Y3)|Ez$aPZ%-fLJ3% zN(mT6{qX39?mWCXamCV#wDWS)3v9Ob0Pz+5-6@U}B%~7*mn3iAM<@cv49{OAK_hd- zY?_; ze-uLR*5gIwpSEccLJ6jib7R<|9mX&+jLwT7*N_w-&3LGiG^gAH0-`xd6$b(D;|^fc z5W8t)e89qFq&6ZObXwUH*0ti=dHzKe!12K(AjazvXv1EtA|VeyJG+T$+h4&jYwPR` zbsgdi|G>)wwh&)XIi-Qe3)=T4C6UfHww^lzz8Me@Af@Y=hl+THogxy-X@hE2F3-&1D@iwgWJO4hqKl!8AOdv>IYU z`2`;#p|%HKy4#j$4>I@!c&Vt1Ii2}j%QT##xGC66%C*w?a43kYWmj6C0+4|bF;E)! z9@MS1B_AQz57z>Zx&o1}ms_(#GXGZqlJ=lp{t@g^Ub~v|?cf~m^19zvgFjt|=3Eb3 z%2G&+nYq&ATdhvM^xw!@_RT2=5T4gE&nF;0J6wDc32{e$#UuuS3L&&rjGvabl zxE^E1elfApR&h>d62jGHd^Fgkam=c+Dhwb(wQrjHBtCqssv-zk3mkLPY(($QxGN?~ zY^+LUp>SP($o;6S+=dWW^XGsOJ(*=!twW1R1U6s~^kw&4`d5G9U#bXW9rBq|x>Qj! zY2szT5?3))W|jI}TOuPYZTdn{RyLL+G>#wJqTXKUNK21^B7qzXXBS#Q=LUyP zJRn?=hzKYu08PD15b-C^4Yc!7UAMo!;$j=N5TZ?eN^OO~hh4bn6&ep`TUPuq&o%o{ zmvL8<_rE=0;HRg1H0RlpXzhyf1^9dy*8H$E3+di*JHxDn)aU3_&`Hy36votlI3!6k zPGFL#Jp)S5WkR0`FBl0H$7cjBPRCijd2#X!Ad%2GJ$p)-uhXeYrA7DT$*b*rW{zFL2WcTwi7%2dNP3wLMqCQQAYkDTZ zT#j?+2emAtU)#E1pD1MTkc23LuVKcucrduhaLJl<*o$SFI7#Y=aAz!}#!8f&CR{hRmw@K zuA#SZvX~Hc>(nv~XeL*S!e9$myv1Z;`qbZZ*@zDY+ISCwn;uIe;z>4Xg1waQvewyHX-}LRdGrhgPcpciv z`L>T8Qy9!*v3j)}i%NlCsFdreG5`Cv9|{h{)s734?u!V)v7uB3 z_$Jg~FjBhK<8CH%p8cgdiE6>=w+n;$+?;a`jm*}4#Kel|En=he_sq-~lDwBF0|TM# zm@mnT(Mx}Qc}$P=2-r#zbiZ4_b~1fa2f57D79kminBd%_>x|v$)6rmsUSlg{2Cn;w z%?gGwVS6n3l@n;IXp8;a;_;M)_B7(Y1Rg3267*h$q@R>*xN`H<0)S)6No8%ok*Y%t zG7A(2l)X8hH8CWC=-n0sBQTb-HzuMsMKbz4QgXCBl(hu7m{BmrUkAL3?^SYHmEe2> z;hh4*TDfI_4z0dGA_XJ#1Ros?JjD_*%6*xC~waya-bR=Dv^o zh$5R`fOj}h$ZuTkkf}56hM8__>+#PjG4b)Xy?>#K#g-{y7o8BI4nJ@a2xPued0oJ1dZNFH?kJ-a@{HCvd6B~CuK0ysf&=p@9-cIP8& z!HzjM_YCF=-oNqXNp?>tjUb@~2~)<0L5A_CKrM7KG{jP)&oVXrWKmdA0{cZjwJdtQ zGAHBTt1e%L8qm^f*4fGbMZz2x#+&~3-5afEbA!D`kB(<~cPi=_W0^!zX~NdY381Ot z+F{E2?!^DeeXB$^#9j-bqtin>M_+n(x?QdVDRlI2CxRo|LxBC>2kOc0`<5pg<)Hai z7koIrM`I2?egAZyyz`=?4?-~dKlz(byN`|_lU6d!+Boq{-pbJJJo!M`V89Keb@!{> z?X5|KG+>Ik;1LuQy@tG}`D%3PA^@0pLdw9_1YWc&39r06$8FRqH>cI4g4NhoJL?l% z>J-J%6EaZU10Szj_do7n^-(&X zbgr!xqna)?KGx-S&-rq*NnM+(s7eMpw^gp_f;@Fe3{hcKMcj|0ot<}W)C}^4LmO5# zN&7va(40?|xX)^qUPQaXa^t*D?}+Q$kjrL_!er=Tqhhw-eW_OwHUtRB?#81srN{CS-1HjJu|U5FdIw~p$!@HjmBBnC69ShO=uNxm20ZBk~ID+>?EWy683vDR9(;-nB zfnslOKO;HKGrtbzXu#y`PosMQq_jqOc z-Fo zj^3`oi{q7}p5)Rk($8bY34O((@TBifET!HQqtLY8(A-uGAt@TeBW%jh2D5CqoG$LZ z9?rIy$dUA*`&85Z&*7m`K_QpbpS0|_PUI+Gc9M$gdfIOgPmWA9!^mC%s89kP`q5QW zQSh7l0p$AHfW28D&Rcb!T(!1pmFld=a8!zqChrWR3eg6b7v=UOe+F!M6 zK&K6J*8yp3?Q$vmcngStqUAw&J#YikGmSq5p?GYX$%rocPLPrmX_blwHI36pcV(t} zJ~U09Q;I>3D7>+#OfnT3pKD_=;t7O6LLtg>($8`EJi0Ke$EktdQ~GfYiSy?wmOs>t zU!Ci^Ccx9{2hyDQzR>)PpQ(#3(ag%lnf*?to>{iPx~z}HSK5OkB-<`Pf4k@|*GY6* zPmfXa8OR7>u=s4ycLlNL%l3%^HZTz?FN{&-vz2NtugPy$o@)Nj&{n*Nqk?DVn5zxf;Cgr7KLLy86; z8f0)LC{GipY?i?evpS*eJZ(_0+++5Rso}MtR5F2(1d9|VI$AR&3I?M(JbhF;iszzP z2}-C5dpzKc^jX%S5u6)B{CM(ZT7))<(s#S0zJk| zTO4On4{fqV@{68bt$Fgjb~{)6nykmiGm1_d^WLbe{dZVIg8sk_fGg36qwhQ;V`4Vp z;T;s7kf7RJOKKOiU@1nsxrUsRjG$SlLqTD0B=gaG$1X>DtlD#%qWy`WVvZv@dD`8> zKd#`AOGiZ+C3D=n;-wSwd^iLg$m?fk_lXCm-*NQ}nbc<; zZggSzF=_qVrN33p5*qqFcdTfAHKWcvoUFWi=7`qo;{vnB1^*%ix7w2`1aW;gV~eaB zrtgZZR|g-F+L(j@=)|;mV9fm=GkLR*Q2jR(u_J6GT49)Zqa}Bxq7(i;J>)JmPaNCnqhGFe0i^SK;^(g zJG%-1TeU;S$2g$=E&*f}Ma`l@ixv|P-qD@mn*IC=4*zY7B%prJBylHpi~x$WkXn=c zHvesQWlr{`tK3#%)6|*wRk*JEv9UH*ig_+%9^OuITtxUn+c`Ly2IE`D*duI}E%1N7 z^R}NaOTFiVOz0lWG(FQ1_rpj;&-)PM%9;QOHLupFM&Z~YN{=ViilBb%D=ZjrA+u;kBY z)0<62{saKm`IPG2%RP0J-=Zn?8F=x^hK5Rf_cDB~Ew zMJ*+azZ8mg`^tfkjZR{zKZeom6eyHr?@kG0V;$(lROcfUDBt3-w)i2|arWA!bD3#2 zOHSz$ep3T_S6f@#pl3_?5fu)zb2J!5V9KYVOaOy{>#|C=6Q5bgmaCXV5U@;t@mbMX zX%?d+bbLw$Xx5f3X4WBYl>TX!UEJC)PHyJ3+jhGYt(aYTC!g7XtZ#={=bJ$Av&mF# zupRO!)R)ithuTWgorhX9^dGG<#j8qf8Y@~P*ReUF+PEqtBGP^mcF*FUwQU+mUGUYv z?yoG}?-%9}sPc&dOCH!bif%q5Q}6el+=~?kpA&+cQaLD};|-owJe{(~1AL+v`~Y_d zx>By{l{wSdT5^8(SN4dVF-8pyjfSpg@*t(P;GpRZ{JBq3WA)|O24ji>+FlC~6C4ZV zdjWIPpWbkc><+C7{;$;jS?L0}^Q@a{R0M$hRNQRv_}`zV59=~ghWdFS*-5dR5`*Kc zSxXfPhWbC2asf#{Rp4m@uv_#-fU6r23$=IV&$#o*<=O)ln9>|N0Q4Fb9*5ldBG=qk zc_9GgQ*9Gc{GW4JsQ*~4g&iL}wG7JU{WNJy-R-ce5*o^>z7YW~?MkHM1JYqz8BK3m z$B?!Hd6OYI_R6>0Bkea>_c`Q{p6kPqewCRFX#cdMYiqNP#Y_>8Y0Vu+7TS-|8-Z_< zp@NLkd2Uesw=RdJTS`?6NyV{cx`f!;HPses^e|>9emS45qsq;VX#Q`G@Cz`SWvOX# zS{C>(n^}LJ8qb+@!-s~{_e<@OgHqb_$=D88YpyFLk8w|JhG`1bs(BX70n~&cS^yv0 z_f%JK_N=weGXQgHndaJ|L^Qu6CFgi3CGj(=kHnoLPNpTNgb{E>Ly(=lqG0;zgw6>Z z-6e;#dlh{_-GiVhLnKF%Hw#cW+zTv!vhhSf%j54~!g#TWn1h)>`cK4!klgvO2*quq zW>>Zwvl?(wwpH4?9+K~p)fM#Aavnp$#&7L^QCaJ?ZotgT@J<8;_3y5TQH_cxckxRj zqZ(CwOf-;zQdKRuh{9VJ7AKWX4`Fts?o&?$pQ{H4gEb3>W*SL6b=j6v9YfiBcz>piu3+`5X9SG1n@@H-yQB za{-{C60lsP<^&Ahuq;(bZ;Q~Sj5M2YqTqrUg#O(tVN7|H^521Z?b3R2qWv%e*?uy2 zjrtM)Lf869^!T8V@Cy|6&dO!+_8!DsMGiTcXMG5J(wmXCs6foFA!3Ei%=1tDkxNT2Hk|BIWT#*9D_4-U}z zu6OF2HU9h74SCpeS1MsG@hU}90|Np94#H(q)9i)LQ%TFDJ8TER{ty9_FfzTKI}>`3 z?>%ozxBe?bFU}ESt%>HN(AKC&dBbyE&QlDPRoZ;HbkmC)S>dd=%x6%e|LrqjU!ZV4 z{1Qv;pq(%@(xc1~7qwxZ7AI8xrGt^928ry_4ts9{w62C$Bs|mStI@mvR5N`5fV1Qw zI&ahn?;Q2;1C8*nME%iIAZ~ikJ*1Ld(K_N2E_HcADS@>ucE22QtLfqHsu|QPzWR!% z_wOnpK=r!jU2ST69V>18l4^1qpQvETJoov=$`G||P ziHfKQFh?7i!z0rP?$px`RBIZrCF(E2+FNC@!xFpps$yeBx5o@7oR))eOznO#IU}GeJ^ryUPSWj?ioz+qJ{(4WoyG z!aQ4|i6(}PRMOF1w}mdH)J;p-abM;}YfN+7oZ%}hyh?v5l-koY-Ri#Dq@O$cJT?{h zCy24hntAifalpVBN48itYkR%-XDe4t_G2J}Ux!+xGsk12)DWa01eljzJoY2Rmf#_t zap@ieV9VE=`{oDUqY3Q1?V502Hd5yl0YZ|Vj)0zwKZP@n%s()No%){ zg7%Gjk{`IRLO}rlKmy7^Ft>7l^_s)sPfi`^*ItB)Pb30=`$Mkp-(Un30G}z%{a%$8 zU`rsRo{F(=ZWRaxlp2jR~!F{rdTptRd$Dzl{@v)pySE;w0k# zD&ez6VM=MF%M2IEaeU))#${yW{>gx!1um2iDm8%JI74iZPEg`iRZ&;kZ)1nW#c2-( zDil&`Q+~rL9Y&%6nTmu$^7VPss3B8S#)^P1@YEuMG; z-8MXi{Ap4xDpSUIj~q34_B@Z4($T~EK+_x5HC8G(>g!18yEP7y4}K13(U5fBGIzPi zYsxSRaaa7~qp!rj)Y~VF^!*|) zq;L1-P$rB37z$VIQ^osS6`lP?eAmYvTYRbwW^aUqg(JMeT6n*GTwt4v!#=6D)qa@} zZptCwB|qbz0%Vu=9NkxQ<|CSSgD=2LRWMw9LDo`uK-?qAA;U5#6UU?XVuMpa!Z+DtQt$Js0jJ#eLurf4^|(Ii z$1BcH0n~7e13{QN!)(ZOl&w!j`ba!pS~|N)INwXBcr-&{`<9h#_=ZBod8!fLXGLrI z)dL#?&>h-3+cHZA3~U@Rb)-z?jmgH!uqH-NIQt7s^8K12IpdA+N2c`S|EPWRw$2Xl z=i^_M{MAKcLXTl=7^2NMCBqp7s{Q+=``X^X{H^vXKwjPQ z-Gf%2nVkuMSTO1C4rD{kIJNZeE@A+=oTEJO@$vSOFpw+Gd3$)oU3tb8T@JrMWFu$I zduQgXMI<|<4C9BTn~4Y{13Z<~=PbY`aO@ZPKb!<8U5W4W%#%syH^QtAZwqfvk`2)=19@1~~bHj&AA(3=%f#p)zK<{SNAhQd9mO zrZDvaUxPY*zPxFzA^y~#p|H7K1MFnih(VRKZ~JUZtr`7n=!6r%vMa@~;4>jz>tl_d z@Aa*P3lJvPdyRf7eKoQUbjB;XI(sZ6Av`p5jMFHVfvW8YhXY;Gw2vv;U~^MPR24Ba zM|onn{~*5_5i36cV!mAQmBWT@5^bnJ5?t&UOg$`Y$jYOS@wSLE;rCvQUt$oPr?;0a z2K~~lIm7?=?}gV#lb_aZL;Q}Al*2nvN2K;z4`6@Hbwnml2$>hQ-Kn}k|mDRmr(bkdo<20 zRbqmGn4v0kVgRcGIc!g1O0My#nDhFi7g}!^Xz#g`RZdNw?tE;QF0_FWVCGEPgIJND zaqC}<@M^FNsU!gKK2v}u#gBT(Nea~9YP7LdT8U+C!rrE!-3MaH?M5}kT5#+QaVf>Z z-Uc_Hu^7U46Cgli*(3+`?M`TDi~r&?rVJ+@y#Ub}8xEl>E2ey|c$BocRjA>i`fAO> zyJj)C3b{+LOfnZnR^WgHlouGK*%D zd$*obI=d#S^H;v?endLB2}8L`#CCX=LP3`sO!fy8latT!_N46mfq;f3ok^4U+VLs`{6I^o&93WYZ$8 z&kAE&a+Z)C=EQb_ftG6frb}zthI{CA&^3Qq+`H3LJRa;G1YQW?lb*{UG8}5B4vLSz zX&){&-N=Yf$Dk26=GJq^P4!6p1pSv|yx8>d(32pFqMM)gAQaG7>&15kuO_3K2UrAv zB;J;W+;YjwU7k7DS@4=~7Wn?{gC!-5V}NTQm)}uCOG{F_E8-Ht0rLJpt~-nfORLO% zf0zIiA1~we$C6U%WH_RLIF=F^-gt|+JoblC^goin{MnYgE>1bXs7$DY#?y!Ohc26o z3Xej&8HCFLO$C5QJX8BrX~6vgpy#C`5e_vV?Qau{doYw^Htug}^5l!%sq(izq!hLZYBaO(A^_USivI=dr;E^J(U_*n~|Y3!TP5^i$R~(Rv5; z&I^1Uxxr4r;;=zZ7;!Wpgk!j|!#MrGgocCt>CB#-hfTGOxrP;^s%)0MLsf1N)Xhsz z&m#1X*G7BF2$R~dHC(;Ryy%S|^j(tO5t{J3j~?5P_&9JqA~~h$a0~W%inxyNvFJf! zb$qs2K;d$0`H*K+s}C<#HNX;+F;Hy<(A$WqHa`9rv6yhxX8;7x=*Lk{ee9n> zAs*%14ZH>JPrha3i4ec5StbY<8gz`Fr!GhsY`+v9Z&Vu|uW`W5jPDa5P8s37QjzD) z_o7qZ0+VPpF(rzR{8cI z4{JT51#{v20udp&IxOnemB2afu{DfRNcRZL-bviLK={S$3%XdyHtLtE!JvlT$$c!q zlQeA)-omVY^T>CFF9_Ku%kGLzyFfjInafOJ(bIr%byeqO_e@k|!i<>L!;p`x&G57Q z?93Zvfh%FrWoF%C2E;doY6{=@(%yft^-M3P%A7wCQH*QBmaecW%PjsN8UxK?-ffJy z2y5hwXaPZn*;;d{tcH1D*KYp+pmu1mQpZ5&FY`t|uU=_PCHM)VYl3I-z<V_|>OiY~=L6_FGE|l%m+b z^q0TLAjshq<0qYG-mhPuHx1|5x6B4xbE}?zTm$c>Tz^vmizN?@;pl6n11%E04)N1!Q^=EGj~J({)N>t zGo$ll1p^!cf|$gp0W-eyW(w=a@-kx{_O`?%NunYelx*IX3L_y@o%55c>gg3~;oI23 zHhsUdODM=1Pnrjr5Fn&&uaS{Pntw(Goj#|kF+72{+=A6rZZSV6vb}W! z%pim&-Ze*G6v}lotY{(V;;o2QQ|^Qnc(77=qkEqwFQ}*zZwD2PD)!wZ4FCj+!N;)} zsgFML3^H^hr9OI!NujY6L*ejm0(pa(Vn>THQiXN1bjptxbrlI>1%NO>hDzD0x->lG zDK$_*T&9qbU=!Oij>XRm{g>GfuI5ML01>2}ReoUHL80IbJoik>x8j5)uc}CXk#Mys{Mw%A+h~xsgEwI*KcdmH+5S$ zkr%wuN9t^) zN)63`&)OaRB;?qu&F2Oaun4NEBGvH$M@S|{GiCEva+J%i=viD=M~5hQ^=~+)8Aj^a z2L-ChYgCC<(piPJBh*~^KL<6K*Pxk1bVqc2 zdFhMMJ96jY;c2ohe{XFOBs{waCuE>TTG25e!;oA||6Pw(yj!@sutmLgl8UzFCCrI5 zr+bX&`YV)JnnLbt37P-)sJ|jD>b`^-Y4tBVP50HI+6;?o#?mP3qx=|tibAwRUfRwV zGvy3&Qso?%5V%5?ol=d~-mlQV@12cvCn>SHpptt2;+CJ5j(0Ned9+XN-0kpyo5Wq3;Lq51;Eh2?^7^PAo>Z6 zi0GFL@S|lR3^mV#xa3w7f;pA(O|W@So;z60!`N$-oe7+i5GV%c>4tVFvkpuZSyuVn zUQ+I63fsJ1eMST7bI`G!jKJfZmJeEE2HvEM(V{E@md*qiv2SW>jrmMtKws5L>@Dak=CT4C3ngVXabwdT&X%(i)hD2GvhcOX3puR2 zPuX4U_;ifeU<7jNvyQ(NX)TjU`wJVvkT3^ReL!>z4hZs-^G}-tK_S)r?gb;1J(t#g ze-}kzAQ2G!0yqawMrswLWpP8m;x^;cxCsEwZ>b{Sm0$n>BZzTdjq4IrF#F`HDA#Mj z6RHz0y3PLP(&=Sx?ow@et*hbVk`0I0LDoElKE=u}egB+yti!R;;#eel+N9>Z%Q7ok z$xUN)fZ$Umau_GhKpVO$psd7Pjxr#P5-!AA%@5j1AcjOCLRuO$+G7+)$U2#?iHTjD z8VsrhMz;p3pQ`B2HN&%#L}IF(1~>P_TWXk $~sbD?k1VxZjHc;rEs9Y{p>{r=kR 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

1u&F@vsxZHg+f?J-+at`!9#1iIKcyj3 z!+BE7aoPLT)1QI+C|`*YiC61?Mh5T4W$8qYstPjIR%**uy4XlC0?-p1hdk!a3+aDY zdl|3yjlP_z*`;GxW6dyn6a)0rpbz#Pygqgvq2PtO4i*Wd% zG+bG5hzrk$hvg7bu@)zau{S#NSb>;5lbB;;tJKh2L7A-lPic_60L|L(-m2jJ3;vhY ztjjM{Uns&dv75dr3pdD(lKj1Q2%*nWq7|idB-vEBr7{rFirpcf6bH0+=5(QHQYkRO z({L+m+sEU*+J+omrzBCd^m@I5s$Po67$HS<4mZEw@uA%WvyzBeL({OBtFJhG1iN9H z3a_qJ8j1_ zpIpl}P#2|vRM|q$QiXNNwBjf!fVjl9s-NZg2r$Q`I1=M-GJNc0+fx$GVPp(W+NH0! z$zKi~y`hc35Qoc+SrlJvq2C(F*DU!pE*T!t|t0I}YmskG+ zE+EhW7|M=$i+S)Ni#g%`$FaGR)vD_WC4Pyo+~F&kJ*Y{zu1PdRM#f7NW$H95Cvf8 zB5?rXqkn3)W0uuYp;3#G=qv5YlWWA>D}j=!`}qbwUoeF=1UF2W( zxYr(Fe2}3O?x+b2GvUJ^ee$i%0iWmt#X4A8s5^`BZKi39G9=h;_OMp2C;Ef9_GZJc zsP(0;FD^x3W{HEhAIWiY1zte8g&EvFuWo$)a`X0<$owt}0QXSnZo%HC7;pmluG)8N zO|fZrdZmsBrnVpDgv$E{gDkjIEK9S%$+%fPDXMqXp--s+mXdxb<@YD0S)qc*fz+ju zXJQPJ^z8sK0VwHMO_xP5W3Tm=?MM{xG zk{mW!!xUbnJkxM6UBwQZ=sWB%Wg~Ra*1SEw zCo7;>WQSIM4Hf-zAp%JjHw4C6mrhM{TVU@DWZuZ{?&IkvqL*ZejtR=uw;X-1+f?#KBoDkbE|}>Z7`aw}fls-(-x=F$xaAg2F(3R~dAF zjz+-E*Dd(4DlQCn02q}UbemorTStZ#jyq*$>xM9griu`)A>jcqFtFiH%-pc8PgqT* z$2I9bf!Lbi@3$_qx87y2(jV;XS-q2LUcriq6ATs!lqIao5dy$;vh0Gc=Oi=sF~B~7 zL8Yp~@WJ!Mp5vJJ^zC^!mIY?TVIz<-uIYP9R$bg2Pj%~&HuUo`wIJG+-z0-3jn1(- z!jCWT=t=O@VT5WwOiFa4E>pji!xufsaECpsT~d47_?SFT(!(?`Rx3dMKs(s;i6cS4 z1!y8Ku@os`*&82M4C3#6x^ugy>X78sy4FlZww}WGsox+mnURmznB9ZCLv%YwQHR=1 zlXg&LWA?G6Dr}(+e+XKcth8g(c-A0!=JK<@q%!zblPfcXhWS_szbyN%pK<^%Bo-}d zto62uq+*MwP&7X$z~(l1K!+Ts0=MGX)LABS0<=>_WH^E84?6)8Z0@7@k~R555;TBD zAv!Q4bY`_Np3HoHjR7EP-CQF=C zb2KBZFFW-Y1#92iA!ckLkOrKaS)D=63Yr>gLUqWYZ*8^c``9OM*#L#|r$NkBM!vlB zTW8)Zf70bdi8Fna>n$xl;QX7AO+w`_rea%qD~}v|)ydxdmx>5u`yfJ=g}=<%h4+5Y ziN3I+8m(F#!C>7&RLeB1O^lVMz=9)=0PPT7EUUo6I4ACgBcn0EC!!LimcEI`*q6px zOJhyZ7tV8i`!*7Y*iH-(#x_&EctCgtQiKTZ9S#Ewb~vpY zIpdXt=DMy$&VYMsPy@+JnS!IY-(OScqQW0`SbUFwcqUL(5ptHJTi8p^iwznpMVyy>?Bgv1O9#w%EhXfgFdW9?N1aeqGuGNtz_Ue)sN z%Sef3Oo}+TWlou2H|t)7F0weTnxqX0bMwR`DZ-?E31w~}Dpzp!r{#@KkhsTGD|e{| z8X|}?QIw0ylW5p;D0!Bds>wU(V>jA9>@qqkZan9#=-QHJP`4dxq*bO`VQPWbNH2-t zLSoW|p+;Py2e~^t=m|QT=D2`QEba!Ix{6z#oKes6Dz%Lm5zCOB|8eQGjUX@bi%fH@ zB2Bs25x?i#JAlW#u81(r01vIv?A9O;3Flv!PF?lpmw%a7_bbNZgY9Y>mWA8g zXD8E|654XRE+jeGWWRpRm)Vd!FkZbcx-eWw61{#d-HnW8LB0DBndH5Blh5>6eqo}l zt#zV9;kT1kMp~wZ!5{8`A;@p6{4a`rpu+pfC3BA#W*fIly^Z8_4$Fmxn4BcKmVTG# znOUJYK?Xr?c|MDaJ%o23uvNN3GBtHMf?iQtWL7{L#4AhPjVcIu+L7$H5jB5bKIvM*XP}dPcD#zyMiAHd<>#cTq zqkg97ViH7?mI*F<^32r8<6%XXIRjpL5CF|vz7;DdLFY6GZ4B1rM|kCCEi zhktC+R0xnKubuMZMUV}5zZi3wPEd1d+9Q_y>N>g?FArLC!`}y)WLBkpW^W=+w+v<{ zcL_V+6suru$Qj7AZ*rhCuf5H`rr%HP_?Z(13B02{%ofp5Lw4TmBxb=q=DRJZ zdqAYB8x(z=0g<4jS!R5UTmnJBc#r-$jkLggWK}`l3wDTMs`Leadl9=%|4N=dMgLx$ zvjoh+kKaR~*!w4d6Kvs_XUyYku>=y?pAJgq|qxITp$mVQpyD~g@l7+{se1ZuurBf%AS=C z{k{O2!V2xD zBXDCCgI=>NLm(kTC}#9+s&GYxbO7Cke~>rBouCCY!447RNdo7fC(T7ul@WaQP97A~ zNLJXZ#)9d)NSk6+O% z5}m|IawXdk>tMhqu*ZEJST7J#jq}2NbZ#D|O^6U)an`hubBD?`;?d_YFha-I&agD3 zLVh;~?Z9(iU>10ZHpt_}p!0U_&QaXU7mY1}tZ!EeDfHt!8yrqylCd71R+)O}o+-a4 zAr_j2F)W&<=++6QL8fYyT^kQ7b-Dr=#KCu$r1w05_g%M@*Y{H?pU^=foP*(sp9KVF#3_z2sCLxq+} z$aO&|xc|#y6eWRrYKmR&yJO0ZH-X}uNGmQMQvU9ZZ#tuj#WA$ut#rS0#XQ-DzCUX$ z=3c1M=w!GgwRR=F&(pLRVq8BSAAWxKK1HaW;WlEgOzFP)ezLW5#(p^mJ{YHqew ziBW=7SdKve7kxs1@{lw-1UsrHF8l&@@q(~{G%r2Y5V<6A!7xNN$8~ikG)v}YKi)Re zP|{4GXQuukXAsPq(sn`+JS7o3Pz2N$z4PuWT@4_09AJik|0}%44z-!-mIxi2P@200 zqg0VKuhbJoCB(jUXA!xL{A~NuCNo?}2%tdkZX2|KS~U07&D}pFYd9sTNwfWTf}1|S zwzqT)B1`_kjh-Sfdwk(om>?@Yvae4bp+hj^F&`v(aOzl?L^O))zdtWYwK(^sHVFS- zcIuZ)zJ;vvwQ*&(*-e$>Qdjc8+if-)LRxgcjhqMSI0)=Q-T|f6aRD6AExG=jvM7Ba zUM!xkuSeRFakTH)()~-`G2jGav5gKy^BHyFe4k(WF07hZMC!YM zpcVNt<5W=5>34_cB30Ow)J);sDtfCG+40X5q@#40D~~U~q*8)MR9{|&X2D8Jn4b%E!mJgB z_!fw)(TbIgT+}t5iGnw=*>q_vISIPE1VR-cveSkkH{JIZFvk`WbB8@6f?`o}>dYUX zn=~c39oIjG*P(7+M7t{Dwb}eDO-@qU0Qb$pUgYL$Rd(z3Anxux44156Nd9z-$)Ax7 zENx8cuxJCpAWNv*FFX{)UGryT(m%ZO3?}igoC^z<+v6b_oJLBKNfu3lEwLow%}|Sv zkb8x6T(KA8UZ>(Q9=Yu0kt(XRh7F)uF5?U5M}qMw+oY7Zw7#-xFxScYrgXMdesZ-* zDRD=hVLZElkUF6Cu~I9vb~h^Z`-gsn@YXKMzQc60&sGZqGybO7hGE$8DX`KB;t$WB zW*Q&>n13@{MY*0x)(9Ya(qku7l4xQP1s}j>W`*=jCj3Tc*HO=J;+n|TdOF|<8Ggr7 zv{MxyyDXg4CX`9M+AJJ!$B{a0;0%k3^!%I|LY8=HFR0{suVr2Risw{@`bXolke^qf zzyG~Ee#lv91p(N^l@hpzRgu~Tsd$SqP68@Vei8hag0lNVWDT-=zaZOw8;!@?n^`ZI z=xK!&b}MQ_umXQlL~LfIDP@WejYw)wt)PqEPIm2DEs}{RY%*}94AG-{c?xPD%LHgdOuaa#UiKV!?te!R0R7+3e%A!cVSht2vZN;j}pS)xF4mwx$Oog zQu!AorR3&%eg#&iDK3AvKXM4krx1!vsydPq&iusBKl22m@8^c8Mg;G-BU1MoiXK`l zHELQ~aroa*JlgCDZl5oUySCo^f2~b_CSlmql|(>hD?BVlHsGE|6~%RJyNs1Cp|{-$ zv4oDqMf{8`ksMe|GV8MQWSQ6bZk8N%B{mdWe_zlX&Ih*qSo2@HxqI6+iPO#KB306d z1ro?`_7K~S6VLrw&iXRHe))V%j{i~9IGkrA-ln`gOXmflUGrFS*LV&YYq1#Kn1la^ zTn}C{u*J^-u?V!KBQF`eR6q5%xgZR!93e|proetmme~jmdJ`c^-{BH;mKpekx*U{~ zvNQwx_&S0HJ{GXYBf`p(ZS^ceeqI=(+T>klqhDNR|G@9Y=$tP`{8>8bDffW24}DqQQGN9>3z7?mP}xKN%28r(iBvAdW?5y9K()1s zLIuF}xC=&^;%v*h#FoR`hr%X_%&v zZ6;z|pG%Vk=kFx5{O+tin0^ey0GWB8gQJLH+zm>{hzNV=hY?2edvftQ1-y{7#V5yL zaL~u*u52c^(2E;p*9p*9j%xJbJC%t685By6c+O3Jqmw`PO)~DP{qvFR^}Qvua=XMv z>J+if#1XbWB0i)z?+etgtvtyriwc0&A{&QIGcT$ikx)<-8*Xe;{zRv_kyDUA*Qs0} zj-^#FY`?^Of?9n+P|E8y>Vz)MBri497&r)Z^(!hx8l3SG|4sf;c-|#i4$Ey65f*6v zLnZMM(ytJsF0^nwP3lWO*mbk92+&uzSGMN=zC5heT$F+?hkE|WBn*I#Wo{X7AMHN9 z&+j^MhP;JPu|V}PbHUl3MpBk*oHO8wUj?nt4MX4g+I$GmNI<+==AabhwAM5c8Q#?+ z9ZChcQf?)&dliMp#mm{)eqV`t zP*ETvt~r-171in|-LtavG*sVOxGD$^vj-a&e=mue@skhKBfg%A%dk0J*gr0pu>WX9 zRdEPE_@*FPACsD6!$vwfED@S~)bJ;)`>QKQZCJA_GgN52Ra@m*BxRauqHd8&dzT+X zc8KHr7+V+>)6%($0?M*G&9-P53dn{uNv!3&Q^_L&zLK;RBo>-xs;OEJPYx=Gb7GX| zDo)DwME`#Mv5NcimDk7bq0cl;lxes1AszCrd1{jiz$STvF|mQQ9IHdLC6!xcxCuA- zWH!wmvk%uHcs2@`@!Htp>7*h;)i~0L! zmDmN%jj5a5_?$%@h6&=M;k&p~F0F9rqiwomXW`M19YL!rD(RZ2Jo>#5*Tm%bRaE~U zfg|xZatb3@UZzsi)!s<^BIwzazL-KgW$u>QNYDpQtba#cc$-9FkBcP`<4eGOEtz`wyfC=!^oB*#+H3wvP;=1WGiAc z!!VO9F&b$eO2%$H)XXRhAz6|sBgt08%sbD=_rv=Kyw`QE>wLPupL3o2cWz_a-j3(L zGqG|PqfBmPevwhxKAne{_`HzuOM)509ANIvlxQ(a&P?hrnHKjCOvIm;C?#=3S5XKw z+1tlZylm%Y7}Xg86yr3P9m<0r7!R!p`~Hwp%sT_CKfMzbs_H%3tOLX1j9yp&`BDhR z0y7;i1YM>DJT`!7{VY&tEZ1^q3kWyYFO&*$&aP}ME<6gEVx?9VGLB4GSxQ)lbzK4=xO8%BUMZao)Ch1vo z!-yL0o$U#g6uGUwV(0au){}p$gf`>-xZerMzxE%_$m~z)0hA4hiLNNXdKNTUFuQ4S z$f)!=a|hekvAOm1WeuGqRSS0^NIGY(ux{u|;!5GvgGFI&1j~jGALONAXT(BmyxvRI z$cv0d*hZY74O)%5KH_*c_S|0bh-5aB&+MZ zFgl^B7Cj6o|vH{Ny+Sw!8tX7PT!%07g>%su(EX0UtY zuzWygebsF7$YB=k|bFHraQ7zNZ_#7O8eNeas zSif`Vr+pQzl92Lz4cTQ@RF#a&G41$>>}#)FNl<^R2uY7s1iASLFtbxv*_5-AX7tvU z{SyPsVc5lW2bj7oZ3=xm4dC)a*bvTTQ^rxSw)v{|j&cw&_KUz9x^-?5n;;5FJ9pSC z(;~UP@8T^^|KxKZ%~=o#8|?B_d>WEEy@t==cQItW?N@n-c_LFcH#_<59k=YGR=!x> zd1-N+2cI~vkt~ZTB6g9Lli>1Z!R=Ek1?wgG+KbkS@M{9}$6eGC#7 zKap1_ALQbWI~x>&?6zl^G$%g8$jI59kAhxOt_f|>Aua4USivJP@~0xj23m;rP$dw$ z!}VORCSKNZy6Pa|Vw3)dTY!0l9BL=Xx|dF6@OU!LUh~*87E1#JTum9YP?+XMwq!3i=yZHan9tzSK@+?~s3>-H2t0JJn1H;y`mAN?w*2fvUZfa+> zbRfiBN;lC7DoxJyEy;WH{6#6v^uy+g?U;?ud8STHMCHz*AucmoAge9&m>yE4l(Dx? zO)F2|@#=f^s%w@Zoy}0>X3`t#eS=5d2zS&^7%$g_>qDEk+dLa7KnajG-dBSPsis-G znq=?kgWt7)CAp_Oyr`VuyGfA}mW|OiiHC+w$YYw3{_M4{kByxC{g6W7G;?|+?kuA~ zscF$y$oc!-RWxVW(~Zxs;gqf=<=fdAU6hxR_Sv3VuA2kR32=^(Ue7EzI+SW@GIO>Y zxH*Cv5mlbfF?bR_=~lq4gSws$STlxK#}|_$W@xEgyGwI! z6X?;c-5#Wh;MAF{@c}s@gj_KXPvS92tIv$eK_a@=K*4IA!26z8fi3Ew{R!)sXp6*p z%*0~EE@yDZO)kyMk&gcJP*4~z1oa)=d$ zWK$alG=w_cHXm)gF`3mmK>Z?Jy(gXg>tBe`ac#?P3ya3fo+M`X>PyMgpOkz)8Fv4rDqO|hvUUw@etcgn0 zUz|SiyxIGo<^tpu#$4-iBFv5!1vEdL+jb6KBDX?JgJ?c(#*K4Ch`(>@x(03JC#SSc zkA=`*;%SV&NOv@5vZGo*v(#2ioqre5%+u66uzpj&%`4x0`?mQC8anZ2 zWTf{W?X0QBRy`R94e{y7ntO}$DGybqe5fUe0mIz9I{*LdD}_&y76|bR<|O3;h2;SUFB&uA){~87Oz+ykJF15}k!5mh%OywU?><*) zIo3^Qn898`YQgHq$?E(cJS?Z}`v5lT`O(5N`Mp9P` zK+!v8^w!@;T;VE=a+JBvSuMilVGkb&+9Uzbw5qE|0ao@*6FZa~%?6IZmoT%3jlcVTaNn#jR z=?@jq!c$fA^r~N~4p(KSuMK3c3!05P>^ae9Z(Xqi62TsSjdgvU5wKqIt+lWHhaGJ^ z9GjxL13H7~etvK?<46wkuL}vBW2-W}7Y1~l<|$?Sfn-x z+{&##wyczIht&F2XoiwJ#_*t?9vojKYCo+r+x9GE1u*J4DYcmKQ{A8L=Sc@y1_(q( z@Q@MR^M}C5xNG2pgXu~3x=-msP7mxHP>W7bk@d3QOI0p<*#wUE7;e7Z8fREnHKDd* z@+f89hPFej1D;Abl*Do&sH&Ipin%{ZS{5ov8U#AQMH04yF1Kmig*=4I>ZFlo#6j*@ z`NsH^(bYw`@-=MgGfY9(p~3SV|C_Y{g_$8e9~r##YEW5+X!0jAangWgBM{;>^Xu`< zvbRt}o|a)fj};T;U2-0dT)KONvJ= - - - diff --git a/vscode/guardex-active-agents/package.json b/vscode/guardex-active-agents/package.json deleted file mode 100644 index c725ca6a..00000000 --- a/vscode/guardex-active-agents/package.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "name": "gitguardex-active-agents", - "displayName": "GitGuardex Active Agents", - "description": "Shows live Guardex sandbox sessions and repo changes in a dedicated VS Code Active Agents sidebar.", - "publisher": "Recodee", - "version": "0.0.21", - "license": "MIT", - "icon": "icon.png", - "engines": { - "vscode": "^1.88.0" - }, - "categories": [ - "SCM Providers", - "Other" - ], - "activationEvents": [ - "onStartupFinished", - "workspaceContains:.omx/state/active-sessions", - "workspaceContains:.omx/agent-worktrees", - "workspaceContains:.omc/agent-worktrees", - "onView:gitguardex.activeAgents" - ], - "main": "./extension.js", - "contributes": { - "commands": [ - { - "command": "gitguardex.activeAgents.startAgent", - "title": "Start Guardex Agent" - }, - { - "command": "gitguardex.activeAgents.refresh", - "title": "Refresh Active Agents" - }, - { - "command": "gitguardex.activeAgents.restart", - "title": "Restart Active Agents", - "icon": "$(debug-restart)" - }, - { - "command": "gitguardex.activeAgents.commitSelectedSession", - "title": "Commit Selected Session", - "icon": "$(check)" - }, - { - "command": "gitguardex.activeAgents.inspect", - "title": "Inspect Session", - "icon": "$(info)" - }, - { - "command": "gitguardex.activeAgents.openWorktree", - "title": "Open Agent Worktree" - }, - { - "command": "gitguardex.activeAgents.finishSession", - "title": "Finish", - "icon": "$(check)" - }, - { - "command": "gitguardex.activeAgents.syncSession", - "title": "Sync", - "icon": "$(sync)" - }, - { - "command": "gitguardex.activeAgents.stopSession", - "title": "Stop", - "icon": "$(debug-stop)" - }, - { - "command": "gitguardex.activeAgents.dismissSession", - "title": "Dismiss", - "icon": "$(trash)" - }, - { - "command": "gitguardex.activeAgents.showSessionTerminal", - "title": "Show Terminal", - "icon": "$(terminal)" - } - ], - "viewsContainers": { - "activitybar": [ - { - "id": "gitguardex-active-agents-container", - "title": "Active Agents", - "icon": "media/active-agents-hivemind.svg" - } - ] - }, - "views": { - "gitguardex-active-agents-container": [ - { - "id": "gitguardex.activeAgents", - "name": "Active Agents", - "contextualTitle": "Active Agents", - "icon": "media/active-agents-hivemind.svg", - "visibility": "visible" - } - ] - }, - "viewsWelcome": [ - { - "view": "gitguardex.activeAgents", - "contents": "No live Guardex agents are visible in this workspace yet.\n\nThis sidebar tracks Guardex session files and managed worktree telemetry without taking over Source Control.\n\n[Start agent](command:gitguardex.activeAgents.startAgent)\n[Open guide](https://github.com/recodeee/gitguardex/blob/main/vscode/guardex-active-agents/README.md#quick-start)\n[Refresh](command:gitguardex.activeAgents.refresh)" - } - ], - "menus": { - "view/title": [ - { - "command": "gitguardex.activeAgents.commitSelectedSession", - "when": "view == gitguardex.activeAgents && guardex.hasAgents", - "group": "navigation@1" - }, - { - "command": "gitguardex.activeAgents.restart", - "when": "view == gitguardex.activeAgents", - "group": "navigation@8" - }, - { - "command": "gitguardex.activeAgents.refresh", - "when": "view == gitguardex.activeAgents", - "group": "navigation@9" - } - ], - "extension/context": [ - { - "command": "gitguardex.activeAgents.restart", - "when": "extension == Recodee.gitguardex-active-agents && extensionStatus == installed", - "group": "2_configure@2" - } - ], - "view/item/context": [ - { - "command": "gitguardex.activeAgents.openWorktree", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/", - "group": "inline" - }, - { - "command": "gitguardex.activeAgents.inspect", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/", - "group": "inline" - }, - { - "command": "gitguardex.activeAgents.showSessionTerminal", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/", - "group": "inline" - }, - { - "command": "gitguardex.activeAgents.finishSession", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/", - "group": "inline" - }, - { - "command": "gitguardex.activeAgents.syncSession", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/", - "group": "inline" - }, - { - "command": "gitguardex.activeAgents.stopSession", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/", - "group": "inline" - }, - { - "command": "gitguardex.activeAgents.dismissSession", - "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session\\.(stalled|dead)$/", - "group": "inline" - } - ] - } - } -} diff --git a/vscode/guardex-active-agents/session-schema.js b/vscode/guardex-active-agents/session-schema.js deleted file mode 100644 index 5d2b22c0..00000000 --- a/vscode/guardex-active-agents/session-schema.js +++ /dev/null @@ -1,1348 +0,0 @@ -const fs = require('node:fs'); -const path = require('node:path'); -const cp = require('node:child_process'); - -const ACTIVE_SESSIONS_RELATIVE_DIR = path.join('.omx', 'state', 'active-sessions'); -const SESSION_SCHEMA_VERSION = 1; -const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json'); -const LOGS_RELATIVE_DIR = path.join('.omx', 'logs'); -const AGENT_WORKTREE_LOCK_FILE = 'AGENT.lock'; -const MANAGED_WORKTREE_ROOTS = [ - path.join('.omx', 'agent-worktrees'), - path.join('.omc', 'agent-worktrees'), -]; -const MAX_CHANGED_PATH_PREVIEW = 3; -const ACTIVE_SESSIONS_FILTER_PREFIX = ACTIVE_SESSIONS_RELATIVE_DIR.split(path.sep).join('/'); -const LOCK_FILE_FILTER_PATH = LOCK_FILE_RELATIVE.split(path.sep).join('/'); -const MANAGED_WORKTREE_FILTER_PREFIXES = MANAGED_WORKTREE_ROOTS - .map((relativeRoot) => relativeRoot.split(path.sep).join('/').replace(/\/+$/, '')); -const IDLE_ACTIVITY_WINDOW_MS = 2 * 60 * 1000; -const STALLED_ACTIVITY_WINDOW_MS = 15 * 60 * 1000; -const HEARTBEAT_STALE_MS = 5 * 60 * 1000; -const DEFAULT_BASE_BRANCH = 'dev'; -const DEFAULT_LOG_TAIL_LINE_COUNT = 200; -const ADVISORY_SESSION_STATES = new Set(['working', 'thinking', 'idle']); -const WORKTREE_ACTIVITY_CACHE_TTL_MS = 3_000; -const MAX_WORKTREE_ACTIVITY_STAT_PATHS = 200; -const WORKTREE_ACTIVITY_SKIP_PREFIXES = [ - '.git/', - '.omx/', - '.omc/', - 'node_modules/', - 'dist/', - 'build/', - 'coverage/', - '.next/', - 'out/', - 'vendor/', -]; -const WORKTREE_ACTIVITY_PRIORITY_PREFIXES = [ - 'src/', - 'app/', - 'apps/', - 'lib/', - 'packages/', - 'scripts/', - 'test/', - 'tests/', - 'vscode/', - 'templates/', - 'openspec/', - 'docs/', -]; -const BLOCKING_GIT_STATES = [ - { - label: 'Rebase in progress.', - markers: ['REBASE_HEAD', 'rebase-apply', 'rebase-merge'], - }, - { - label: 'Merge in progress.', - markers: ['MERGE_HEAD'], - }, - { - label: 'Cherry-pick in progress.', - markers: ['CHERRY_PICK_HEAD'], - }, -]; -const worktreeActivityCache = new Map(); - -function toNonEmptyString(value, fallback = '') { - const normalized = typeof value === 'string' ? value.trim() : String(value || '').trim(); - return normalized || fallback; -} - -function toPositiveInteger(value) { - const normalized = Number.parseInt(String(value || ''), 10); - return Number.isInteger(normalized) && normalized > 0 ? normalized : null; -} - -function toBoundedInteger(value, min, max) { - const normalized = Number.parseInt(String(value ?? ''), 10); - if (!Number.isInteger(normalized) || normalized < min || normalized > max) { - return null; - } - return normalized; -} - -function normalizeStringList(values) { - if (!Array.isArray(values)) { - return []; - } - - return values - .map((value) => toNonEmptyString(value)) - .filter(Boolean); -} - -function normalizeSessionHealthPayload(input) { - if (!input || typeof input !== 'object' || Array.isArray(input)) { - return null; - } - - const rawScores = input.scores && typeof input.scores === 'object' && !Array.isArray(input.scores) - ? input.scores - : null; - const score = toBoundedInteger(input.score ?? input.total ?? rawScores?.total, 0, 100); - if (score === null) { - return null; - } - - return { - score, - label: toNonEmptyString(input.label), - primaryDriver: toNonEmptyString(input.primaryDriver), - secondaries: normalizeStringList(input.secondaries), - outputLine: toNonEmptyString(input.outputLine), - }; -} - -function normalizeTaskMode(value) { - const normalized = toNonEmptyString(value).toLowerCase(); - return normalized === 'caveman' || normalized === 'omx' ? normalized : ''; -} - -function normalizeOpenSpecTier(value) { - const normalized = toNonEmptyString(value).toUpperCase(); - return ['T0', 'T1', 'T2', 'T3'].includes(normalized) ? normalized : ''; -} - -function normalizeAdvisoryState(value, fallback = 'working') { - const normalized = toNonEmptyString(value).toLowerCase(); - return ADVISORY_SESSION_STATES.has(normalized) ? normalized : fallback; -} - -function sanitizeBranchForFile(branch) { - const normalized = toNonEmptyString(branch, 'session'); - return normalized.replace(/[^a-zA-Z0-9._-]+/g, '__').replace(/^_+|_+$/g, '') || 'session'; -} - -function sessionFileNameForBranch(branch) { - return `${sanitizeBranchForFile(branch)}.json`; -} - -function activeSessionsDirForRepo(repoRoot) { - return path.join(path.resolve(repoRoot), ACTIVE_SESSIONS_RELATIVE_DIR); -} - -function sessionFilePathForBranch(repoRoot, branch) { - return path.join(activeSessionsDirForRepo(repoRoot), sessionFileNameForBranch(branch)); -} - -function resolveManagedWorktreeRoots(repoRoot) { - return MANAGED_WORKTREE_ROOTS.map((relativeRoot) => path.join(path.resolve(repoRoot), relativeRoot)); -} - -function splitOutputLines(output) { - if (typeof output !== 'string') { - return null; - } - - return output - .split(/\r?\n/) - .filter((line) => line.trim().length > 0); -} - -function normalizeRelativePath(value) { - return toNonEmptyString(value).replace(/\\/g, '/').replace(/^\.\//, ''); -} - -function normalizeProjectPath(value) { - const normalized = toNonEmptyString(value); - if (!normalized) { - return ''; - } - - return path.isAbsolute(normalized) - ? path.resolve(normalized) - : normalizeRelativePath(normalized); -} - -function readJsonFile(filePath) { - try { - return JSON.parse(fs.readFileSync(filePath, 'utf8')); - } catch (_error) { - return null; - } -} - -function readConfiguredBaseBranch(repoRoot) { - const lines = runGitLines(path.resolve(repoRoot), ['config', '--get', 'multiagent.baseBranch']); - if (Array.isArray(lines) && typeof lines[0] === 'string' && lines[0].trim()) { - return lines[0].trim(); - } - return DEFAULT_BASE_BRANCH; -} - -function readAheadBehindCounts(worktreePath, branch, baseBranch) { - const normalizedWorktreePath = toNonEmptyString(worktreePath); - const normalizedBranch = toNonEmptyString(branch); - const normalizedBaseBranch = toNonEmptyString(baseBranch, DEFAULT_BASE_BRANCH); - const compareRef = `origin/${normalizedBaseBranch}`; - - if (!normalizedWorktreePath || !normalizedBranch) { - return { - compareRef, - aheadCount: null, - behindCount: null, - }; - } - - const lines = runGitLines(normalizedWorktreePath, [ - 'rev-list', - '--left-right', - '--count', - `${normalizedBranch}...${compareRef}`, - ]); - const match = Array.isArray(lines) && typeof lines[0] === 'string' - ? lines[0].trim().match(/^(\d+)\s+(\d+)$/) - : null; - if (!match) { - return { - compareRef, - aheadCount: null, - behindCount: null, - }; - } - - return { - compareRef, - aheadCount: Number.parseInt(match[1], 10), - behindCount: Number.parseInt(match[2], 10), - }; -} - -function sessionLogPath(repoRoot, branch) { - const normalizedRepoRoot = toNonEmptyString(repoRoot); - const normalizedBranch = toNonEmptyString(branch); - if (!normalizedRepoRoot || !normalizedBranch) { - return ''; - } - - return path.join( - path.resolve(normalizedRepoRoot), - LOGS_RELATIVE_DIR, - `agent-${sanitizeBranchForFile(normalizedBranch)}.log`, - ); -} - -function readLogTail(filePath, maxLines = DEFAULT_LOG_TAIL_LINE_COUNT) { - const normalizedFilePath = toNonEmptyString(filePath); - const normalizedMaxLines = toPositiveInteger(maxLines) || DEFAULT_LOG_TAIL_LINE_COUNT; - if (!normalizedFilePath || !fs.existsSync(normalizedFilePath)) { - return []; - } - - try { - const lines = fs.readFileSync(normalizedFilePath, 'utf8').split(/\r?\n/); - while (lines.length > 0 && lines[lines.length - 1] === '') { - lines.pop(); - } - return lines.slice(-normalizedMaxLines); - } catch (_error) { - return []; - } -} - -function readSessionHeldLocks(repoRoot, branch) { - const normalizedRepoRoot = toNonEmptyString(repoRoot); - const normalizedBranch = toNonEmptyString(branch); - if (!normalizedRepoRoot || !normalizedBranch) { - return []; - } - - const parsed = readJsonFile(path.join(path.resolve(normalizedRepoRoot), LOCK_FILE_RELATIVE)); - const locks = parsed?.locks; - if (!locks || typeof locks !== 'object' || Array.isArray(locks)) { - return []; - } - - return Object.entries(locks) - .map(([rawRelativePath, entry]) => { - if (!entry || typeof entry !== 'object') { - return null; - } - - const relativePath = normalizeRelativePath(rawRelativePath); - const ownerBranch = toNonEmptyString(entry.branch); - if (!relativePath || ownerBranch !== normalizedBranch) { - return null; - } - - return { - relativePath, - claimedAt: toNonEmptyString(entry.claimed_at), - allowDelete: Boolean(entry.allow_delete), - }; - }) - .filter(Boolean) - .sort((left, right) => left.relativePath.localeCompare(right.relativePath)); -} - -function readSessionInspectData(session, options = {}) { - const repoRoot = toNonEmptyString(session?.repoRoot); - const branch = toNonEmptyString(session?.branch); - const worktreePath = toNonEmptyString(session?.worktreePath); - const baseBranch = readConfiguredBaseBranch(repoRoot); - const logPath = sessionLogPath(repoRoot, branch); - const logTailLines = readLogTail(logPath, options.logLines); - - return { - baseBranch, - logPath, - logExists: Boolean(logPath) && fs.existsSync(logPath), - logTailLines, - logTailText: logTailLines.join('\n'), - heldLocks: readSessionHeldLocks(repoRoot, branch), - ...readAheadBehindCounts(worktreePath, branch, baseBranch), - }; -} - -function normalizeIsoString(value, fallback = '') { - const normalized = toNonEmptyString(value); - if (!normalized) { - return fallback; - } - - const timestamp = Date.parse(normalized); - return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback; -} - -function runGitLines(worktreePath, args) { - try { - const output = cp.execFileSync('git', ['-C', worktreePath, ...args], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }); - return splitOutputLines(output); - } catch (_error) { - return null; - } -} - -function unquoteGitPath(value) { - if (typeof value !== 'string') { - return ''; - } - - const trimmed = value.trim(); - if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) { - return trimmed; - } - - try { - return JSON.parse(trimmed); - } catch (_error) { - return trimmed.slice(1, -1); - } -} - -function formatFileCount(count) { - return `${count} file${count === 1 ? '' : 's'}`; -} - -function previewChangedPaths(paths) { - if (!Array.isArray(paths) || paths.length === 0) { - return ''; - } - - if (paths.length <= MAX_CHANGED_PATH_PREVIEW) { - return paths.join(', '); - } - - const preview = paths.slice(0, MAX_CHANGED_PATH_PREVIEW).join(', '); - return `${preview}, +${paths.length - MAX_CHANGED_PATH_PREVIEW} more`; -} - -function deriveRepoChangeStatus(statusPair) { - if (statusPair === '??') { - return { - statusCode: '??', - statusLabel: 'U', - statusText: 'Untracked', - }; - } - - const code = [statusPair[1], statusPair[0]].find((value) => value && value !== ' ') || 'M'; - const statusTextByCode = { - A: 'Added', - C: 'Copied', - D: 'Deleted', - M: 'Modified', - R: 'Renamed', - T: 'Type changed', - U: 'Conflicted', - }; - - return { - statusCode: code, - statusLabel: code, - statusText: statusTextByCode[code] || 'Changed', - }; -} - -function parseRepoChangeLine(repoRoot, line) { - if (typeof line !== 'string' || line.length < 4) { - return null; - } - - const statusPair = line.slice(0, 2); - if (statusPair === '!!') { - return null; - } - - const rawPath = line.slice(3).trim(); - if (!rawPath) { - return null; - } - - let relativePath = rawPath; - let originalPath = ''; - if (rawPath.includes(' -> ')) { - const parts = rawPath.split(' -> '); - if (parts.length === 2) { - originalPath = unquoteGitPath(parts[0]); - relativePath = parts[1]; - } - } - - relativePath = unquoteGitPath(relativePath); - if (!relativePath) { - return null; - } - - const normalizedRelativePath = relativePath.split(path.sep).join('/'); - if ( - normalizedRelativePath === LOCK_FILE_FILTER_PATH - || normalizedRelativePath.startsWith(`${LOCK_FILE_FILTER_PATH}/`) - || normalizedRelativePath === ACTIVE_SESSIONS_FILTER_PREFIX - || normalizedRelativePath.startsWith(`${ACTIVE_SESSIONS_FILTER_PREFIX}/`) - || MANAGED_WORKTREE_FILTER_PREFIXES.some((prefix) => ( - normalizedRelativePath === prefix || normalizedRelativePath.startsWith(`${prefix}/`) - )) - ) { - return null; - } - - const status = deriveRepoChangeStatus(statusPair); - return { - ...status, - originalPath, - relativePath, - absolutePath: path.join(path.resolve(repoRoot), relativePath), - }; -} - -function collectWorktreeChangedPaths(worktreePath) { - const changedGroups = [ - runGitLines(worktreePath, ['diff', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`, `:(exclude)${AGENT_WORKTREE_LOCK_FILE}`]), - runGitLines(worktreePath, ['diff', '--cached', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`, `:(exclude)${AGENT_WORKTREE_LOCK_FILE}`]), - runGitLines(worktreePath, ['ls-files', '--others', '--exclude-standard']), - ]; - - if (changedGroups.some((group) => group === null)) { - return null; - } - - return [...new Set(changedGroups.flat())] - .filter((relativePath) => ( - relativePath - && relativePath !== LOCK_FILE_RELATIVE - && relativePath !== AGENT_WORKTREE_LOCK_FILE - )) - .sort((left, right) => left.localeCompare(right)); -} - -function resolveWorktreeGitDir(worktreePath) { - const gitPath = path.join(path.resolve(worktreePath), '.git'); - try { - if (fs.statSync(gitPath).isDirectory()) { - return gitPath; - } - } catch (_error) { - return null; - } - - try { - const gitPointer = fs.readFileSync(gitPath, 'utf8'); - const match = gitPointer.match(/^gitdir:\s*(.+)$/m); - if (match?.[1]) { - return path.resolve(worktreePath, match[1].trim()); - } - } catch (_error) { - return null; - } - - return null; -} - -function deriveBlockingGitLabel(worktreePath) { - const gitDir = resolveWorktreeGitDir(worktreePath); - if (!gitDir) { - return ''; - } - - for (const blockingState of BLOCKING_GIT_STATES) { - if (blockingState.markers.some((marker) => fs.existsSync(path.join(gitDir, marker)))) { - return blockingState.label; - } - } - - return ''; -} - -function collectWorktreeTrackedPaths(worktreePath) { - const trackedPaths = runGitLines(worktreePath, ['ls-files', '--cached', '--others', '--exclude-standard']); - if (!trackedPaths) { - return null; - } - - return [...new Set(trackedPaths)] - .filter(Boolean) - .sort((left, right) => left.localeCompare(right)); -} - -function shouldSkipWorktreeActivityPath(relativePath) { - const normalized = normalizeRelativePath(relativePath); - if (!normalized || normalized === LOCK_FILE_RELATIVE || normalized === AGENT_WORKTREE_LOCK_FILE) { - return true; - } - - return WORKTREE_ACTIVITY_SKIP_PREFIXES.some((prefix) => ( - normalized === prefix.slice(0, -1) || normalized.startsWith(prefix) - )); -} - -function worktreeActivityPathPriority(relativePath, recentPathsSet) { - if (recentPathsSet.has(relativePath)) { - return 0; - } - if (!relativePath.includes('/')) { - return 1; - } - if (WORKTREE_ACTIVITY_PRIORITY_PREFIXES.some((prefix) => relativePath.startsWith(prefix))) { - return 2; - } - return 3; -} - -function collectWorktreeActivityCandidatePaths(worktreePath, trackedPaths) { - const recentPaths = runGitLines(worktreePath, ['log', '-1', '--name-only', '--pretty=format:', '--', '.']) || []; - const filteredRecentPaths = [...new Set(recentPaths.map(normalizeRelativePath).filter(Boolean))] - .filter((relativePath) => !shouldSkipWorktreeActivityPath(relativePath)); - const recentPathSet = new Set(filteredRecentPaths); - const prioritizedTrackedPaths = trackedPaths - .map(normalizeRelativePath) - .filter(Boolean) - .filter((relativePath) => !shouldSkipWorktreeActivityPath(relativePath)) - .sort((left, right) => { - const priorityDelta = worktreeActivityPathPriority(left, recentPathSet) - - worktreeActivityPathPriority(right, recentPathSet); - if (priorityDelta !== 0) { - return priorityDelta; - } - return left.localeCompare(right); - }); - - return [...new Set([...filteredRecentPaths, ...prioritizedTrackedPaths])] - .slice(0, MAX_WORKTREE_ACTIVITY_STAT_PATHS); -} - -function clearWorktreeActivityCache(worktreePath = '') { - const normalizedWorktreePath = toNonEmptyString(worktreePath); - if (!normalizedWorktreePath) { - worktreeActivityCache.clear(); - return; - } - worktreeActivityCache.delete(path.resolve(normalizedWorktreePath)); -} - -function deriveLatestWorktreeFileActivity(worktreePath, options = {}) { - const now = Number.isFinite(options.now) ? options.now : Date.now(); - const useCache = options.useCache !== false; - const cacheKey = path.resolve(worktreePath); - if (useCache) { - const cached = worktreeActivityCache.get(cacheKey); - if (cached && (now - cached.checkedAtMs) < WORKTREE_ACTIVITY_CACHE_TTL_MS) { - return cached.latestMtimeMs; - } - } - - const trackedPaths = collectWorktreeTrackedPaths(worktreePath); - if (!trackedPaths) { - return null; - } - - const candidatePaths = collectWorktreeActivityCandidatePaths(worktreePath, trackedPaths); - let latestMtimeMs = null; - for (const relativePath of candidatePaths) { - const absolutePath = path.join(worktreePath, relativePath); - try { - const stats = fs.statSync(absolutePath); - if (!stats.isFile() || !Number.isFinite(stats.mtimeMs)) { - continue; - } - latestMtimeMs = latestMtimeMs === null - ? stats.mtimeMs - : Math.max(latestMtimeMs, stats.mtimeMs); - } catch (_error) { - continue; - } - } - - if (useCache) { - worktreeActivityCache.set(cacheKey, { - checkedAtMs: now, - latestMtimeMs, - }); - } - - return latestMtimeMs; -} - -function deriveSessionActivity(session, options = {}) { - const now = Number.isFinite(options.now) ? options.now : Date.now(); - const pid = toPositiveInteger(session?.pid); - const pidAlive = pid ? isPidAlive(pid) : null; - const heartbeatAt = normalizeIsoString(session?.lastHeartbeatAt); - const heartbeatMs = Date.parse(heartbeatAt); - if (heartbeatAt && Number.isFinite(heartbeatMs) && now - heartbeatMs > HEARTBEAT_STALE_MS) { - return { - activityKind: 'dead', - activityLabel: 'dead', - activityCountLabel: '', - activitySummary: `Heartbeat stale for ${formatElapsedFrom(heartbeatAt, now)}.`, - changeCount: 0, - changedPaths: [], - worktreeChangedPaths: [], - pidAlive, - lastFileActivityAt: '', - lastFileActivityLabel: '', - }; - } - - const blockingLabel = deriveBlockingGitLabel(session.worktreePath); - if (blockingLabel) { - return { - activityKind: 'blocked', - activityLabel: 'blocked', - activityCountLabel: '', - activitySummary: blockingLabel, - changeCount: 0, - changedPaths: [], - worktreeChangedPaths: [], - pidAlive, - lastFileActivityAt: '', - lastFileActivityLabel: '', - }; - } - - if (pid && !pidAlive) { - return { - activityKind: 'dead', - activityLabel: 'dead', - activityCountLabel: '', - activitySummary: 'Recorded PID is not alive.', - changeCount: 0, - changedPaths: [], - worktreeChangedPaths: [], - pidAlive, - lastFileActivityAt: '', - lastFileActivityLabel: '', - }; - } - - const worktreeChangedPaths = collectWorktreeChangedPaths(session.worktreePath); - if (!worktreeChangedPaths) { - return { - activityKind: 'idle', - activityLabel: 'idle', - activityCountLabel: '', - activitySummary: 'Worktree activity unavailable.', - changeCount: 0, - changedPaths: [], - worktreeChangedPaths: [], - pidAlive, - lastFileActivityAt: '', - lastFileActivityLabel: '', - }; - } - - if (worktreeChangedPaths.length > 0) { - const worktreeRelativePaths = [...new Set(worktreeChangedPaths - .map((relativePath) => normalizeRelativePath(relativePath)) - .filter(Boolean))] - .sort((left, right) => left.localeCompare(right)); - clearWorktreeActivityCache(session.worktreePath); - const changedPaths = [...new Set(worktreeChangedPaths - .map((relativePath) => normalizeRelativePath( - path.relative(session.repoRoot, path.resolve(session.worktreePath, relativePath)), - )) - .filter(Boolean))] - .sort((left, right) => left.localeCompare(right)); - - const workingLatestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath, { - now, - useCache: options.useCache, - }); - const workingLastFileActivityAt = Number.isFinite(workingLatestFileActivityMs) - ? new Date(workingLatestFileActivityMs).toISOString() - : ''; - const workingLastFileActivityLabel = workingLastFileActivityAt - ? formatElapsedFrom(workingLastFileActivityAt, now) - : ''; - const workingFileActivityAgeMs = Number.isFinite(workingLatestFileActivityMs) - ? Math.max(0, now - workingLatestFileActivityMs) - : null; - const isFinishedUncommitted = workingFileActivityAgeMs !== null - && workingFileActivityAgeMs > IDLE_ACTIVITY_WINDOW_MS; - - return { - activityKind: isFinishedUncommitted ? 'finished' : 'working', - activityLabel: isFinishedUncommitted ? 'finished' : 'working', - activityCountLabel: formatFileCount(worktreeChangedPaths.length), - activitySummary: isFinishedUncommitted && workingLastFileActivityLabel - ? `${previewChangedPaths(worktreeChangedPaths)} · idle ${workingLastFileActivityLabel}` - : previewChangedPaths(worktreeChangedPaths), - changeCount: worktreeChangedPaths.length, - changedPaths, - worktreeChangedPaths: worktreeRelativePaths, - pidAlive, - lastFileActivityAt: workingLastFileActivityAt, - lastFileActivityLabel: workingLastFileActivityLabel, - }; - } - - const latestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath, { - now, - useCache: options.useCache, - }); - const lastFileActivityAt = Number.isFinite(latestFileActivityMs) - ? new Date(latestFileActivityMs).toISOString() - : ''; - const lastFileActivityLabel = lastFileActivityAt - ? formatElapsedFrom(lastFileActivityAt, now) - : ''; - const lastFileActivityAgeMs = Number.isFinite(latestFileActivityMs) - ? Math.max(0, now - latestFileActivityMs) - : null; - - if (lastFileActivityAgeMs !== null && lastFileActivityAgeMs > STALLED_ACTIVITY_WINDOW_MS) { - return { - activityKind: 'stalled', - activityLabel: 'stalled', - activityCountLabel: '', - activitySummary: `Worktree clean. No file activity for ${lastFileActivityLabel}.`, - changeCount: 0, - changedPaths: [], - worktreeChangedPaths: [], - pidAlive, - lastFileActivityAt, - lastFileActivityLabel, - }; - } - - return { - activityKind: 'idle', - activityLabel: 'idle', - activityCountLabel: '', - activitySummary: lastFileActivityAgeMs !== null && lastFileActivityAgeMs <= IDLE_ACTIVITY_WINDOW_MS - ? `Worktree clean. Recent file activity ${lastFileActivityLabel} ago.` - : lastFileActivityLabel - ? `Worktree clean. Last file activity ${lastFileActivityLabel} ago.` - : 'Worktree clean.', - changeCount: 0, - changedPaths: [], - worktreeChangedPaths: [], - pidAlive, - lastFileActivityAt, - lastFileActivityLabel, - }; -} - -function buildSessionRecord(input) { - const repoRoot = path.resolve(toNonEmptyString(input.repoRoot)); - const worktreePath = path.resolve(toNonEmptyString(input.worktreePath)); - const branch = toNonEmptyString(input.branch); - const pid = toPositiveInteger(input.pid); - const startedAt = input.startedAt ? new Date(input.startedAt) : new Date(); - const lastHeartbeatAt = input.lastHeartbeatAt ? new Date(input.lastHeartbeatAt) : new Date(); - - if (!branch) { - throw new Error('branch is required'); - } - if (!repoRoot) { - throw new Error('repoRoot is required'); - } - if (!worktreePath) { - throw new Error('worktreePath is required'); - } - if (!pid) { - throw new Error('pid must be a positive integer'); - } - if (Number.isNaN(startedAt.getTime())) { - throw new Error('startedAt must be a valid date'); - } - if (Number.isNaN(lastHeartbeatAt.getTime())) { - throw new Error('lastHeartbeatAt must be a valid date'); - } - - return { - schemaVersion: SESSION_SCHEMA_VERSION, - repoRoot, - branch, - taskName: toNonEmptyString(input.taskName, 'task'), - latestTaskPreview: toNonEmptyString(input.latestTaskPreview), - agentName: toNonEmptyString(input.agentName, 'agent'), - projectName: toNonEmptyString(input.projectName), - projectPath: normalizeProjectPath(input.projectPath), - snapshotName: toNonEmptyString(input.snapshotName), - snapshotEmail: toNonEmptyString(input.snapshotEmail || input.email), - worktreePath, - pid, - cliName: toNonEmptyString(input.cliName, 'codex'), - taskMode: normalizeTaskMode(input.taskMode), - openspecTier: normalizeOpenSpecTier(input.openspecTier), - taskRoutingReason: toNonEmptyString(input.taskRoutingReason), - startedAt: startedAt.toISOString(), - lastHeartbeatAt: lastHeartbeatAt.toISOString(), - state: normalizeAdvisoryState(input.state), - sessionHealth: normalizeSessionHealthPayload(input.sessionHealth || input.sessionSeverity), - }; -} - -function deriveSessionLabel(branch, worktreePath) { - const worktreeLeaf = toNonEmptyString(path.basename(worktreePath || '')); - if (worktreeLeaf) { - return worktreeLeaf; - } - return toNonEmptyString(branch).replace(/[\\/]+/g, '-') || 'unknown-agent'; -} - -function normalizeSessionRecord(input, options = {}) { - if (!input || typeof input !== 'object') { - return null; - } - - const repoRoot = toNonEmptyString(input.repoRoot); - const branch = toNonEmptyString(input.branch); - const worktreePath = toNonEmptyString(input.worktreePath); - const startedAt = new Date(input.startedAt); - const lastHeartbeatAt = new Date(input.lastHeartbeatAt || input.startedAt); - const pid = toPositiveInteger(input.pid); - - if ( - !repoRoot - || !branch - || !worktreePath - || !pid - || Number.isNaN(startedAt.getTime()) - || Number.isNaN(lastHeartbeatAt.getTime()) - ) { - return null; - } - - return { - schemaVersion: toPositiveInteger(input.schemaVersion) || SESSION_SCHEMA_VERSION, - repoRoot: path.resolve(repoRoot), - branch, - taskName: toNonEmptyString(input.taskName, 'task'), - latestTaskPreview: toNonEmptyString(input.latestTaskPreview), - agentName: toNonEmptyString(input.agentName, 'agent'), - projectName: toNonEmptyString(input.projectName), - projectPath: normalizeProjectPath(input.projectPath), - snapshotName: toNonEmptyString(input.snapshotName), - snapshotEmail: toNonEmptyString(input.snapshotEmail || input.email), - worktreePath: path.resolve(worktreePath), - pid, - cliName: toNonEmptyString(input.cliName, 'codex'), - taskMode: normalizeTaskMode(input.taskMode), - openspecTier: normalizeOpenSpecTier(input.openspecTier), - taskRoutingReason: toNonEmptyString(input.taskRoutingReason), - startedAt: startedAt.toISOString(), - lastHeartbeatAt: lastHeartbeatAt.toISOString(), - state: normalizeAdvisoryState(input.state, 'idle'), - filePath: toNonEmptyString(options.filePath), - label: deriveSessionLabel(branch, worktreePath), - changedPaths: [], - worktreeChangedPaths: [], - sourceKind: 'active-session', - telemetryUpdatedAt: '', - telemetrySource: '', - lockSnapshotCount: 0, - lockSessionCount: 0, - collaboration: false, - sessionHealth: normalizeSessionHealthPayload(input.sessionHealth || input.sessionSeverity), - }; -} - -function formatElapsedFrom(startedAt, now = Date.now()) { - const startedAtMs = startedAt instanceof Date ? startedAt.getTime() : Date.parse(startedAt); - if (!Number.isFinite(startedAtMs)) { - return '0s'; - } - - const totalSeconds = Math.max(0, Math.floor((now - startedAtMs) / 1000)); - const days = Math.floor(totalSeconds / 86_400); - const hours = Math.floor((totalSeconds % 86_400) / 3_600); - const minutes = Math.floor((totalSeconds % 3_600) / 60); - const seconds = totalSeconds % 60; - - if (days > 0) { - return `${days}d ${hours}h`; - } - if (hours > 0) { - return `${hours}h ${minutes}m`; - } - if (minutes > 0) { - return `${minutes}m ${seconds}s`; - } - return `${seconds}s`; -} - -function isPidAlive(pid) { - const normalizedPid = toPositiveInteger(pid); - if (!normalizedPid) { - return false; - } - - try { - process.kill(normalizedPid, 0); - return true; - } catch (_error) { - return false; - } -} - -function readWorktreeBranch(worktreePath) { - const lines = runGitLines(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']); - return Array.isArray(lines) && typeof lines[0] === 'string' ? lines[0].trim() : ''; -} - -function deriveAgentNameFromBranch(branch) { - const parts = toNonEmptyString(branch).split('/').filter(Boolean); - if (parts.length >= 2 && parts[0] === 'agent') { - return parts[1]; - } - return 'agent'; -} - -function isManagedAgentBranch(branch) { - return toNonEmptyString(branch).startsWith('agent/'); -} - -function deriveManagedWorktreeStartedAt(worktreePath, now = Date.now()) { - try { - const stats = fs.statSync(worktreePath); - if (Number.isFinite(stats.mtimeMs)) { - return new Date(stats.mtimeMs).toISOString(); - } - } catch (_error) { - // Directory mtime is best-effort context only; fall back to current scan time. - } - - return new Date(now).toISOString(); -} - -function flattenTelemetrySnapshotSessions(lockPayload) { - const flattened = []; - const snapshots = Array.isArray(lockPayload?.snapshots) ? lockPayload.snapshots : []; - for (const snapshot of snapshots) { - const snapshotSessions = Array.isArray(snapshot?.sessions) ? snapshot.sessions : []; - for (const session of snapshotSessions) { - flattened.push({ - taskPreview: toNonEmptyString(session?.taskPreview), - taskUpdatedAt: normalizeIsoString(session?.taskUpdatedAt), - projectName: toNonEmptyString(session?.projectName), - projectPath: toNonEmptyString(session?.projectPath), - snapshotName: toNonEmptyString(snapshot?.snapshotName), - email: toNonEmptyString(snapshot?.email), - sessionHealth: normalizeSessionHealthPayload( - session?.sessionHealth || session?.sessionSeverity || snapshot?.sessionHealth || snapshot?.sessionSeverity, - ), - }); - } - } - return flattened; -} - -function sortSessionsByTimestamp(sessions) { - sessions.sort((left, right) => { - const timeDelta = Date.parse(right.startedAt) - Date.parse(left.startedAt); - if (timeDelta !== 0) { - return timeDelta; - } - return left.label.localeCompare(right.label); - }); - return sessions; -} - -function deriveLockTaskAnchor(entries, fallbackTaskName, fallbackTimestamp) { - const sortedEntries = sortTelemetryEntriesForAnchor(entries); - - const latestEntry = sortedEntries[0] || null; - return { - taskName: latestEntry?.taskPreview || fallbackTaskName || 'task', - latestTaskPreview: latestEntry?.taskPreview || '', - timestamp: latestEntry?.taskUpdatedAt || fallbackTimestamp || '', - sessionHealth: latestEntry?.sessionHealth || null, - }; -} - -function sortTelemetryEntriesForAnchor(entries) { - return [...entries].sort((left, right) => { - const timeDelta = Date.parse(right.taskUpdatedAt || '') - Date.parse(left.taskUpdatedAt || ''); - if (timeDelta !== 0) { - return timeDelta; - } - if (Boolean(right.taskPreview) !== Boolean(left.taskPreview)) { - return Number(Boolean(right.taskPreview)) - Number(Boolean(left.taskPreview)); - } - return (right.projectPath || '').localeCompare(left.projectPath || ''); - }); -} - -function deriveLockSnapshotIdentity(entries) { - const latestEntry = sortTelemetryEntriesForAnchor(entries) - .find((entry) => entry?.snapshotName || entry?.email) || null; - return { - snapshotName: toNonEmptyString(latestEntry?.snapshotName), - snapshotEmail: toNonEmptyString(latestEntry?.email), - }; -} - -function deriveLockProjectMetadata(entries) { - const latestEntry = sortTelemetryEntriesForAnchor(entries) - .find((entry) => entry?.projectPath || entry?.projectName) || null; - return { - projectName: toNonEmptyString(latestEntry?.projectName), - projectPath: normalizeProjectPath(latestEntry?.projectPath), - }; -} - -function buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options = {}) { - const now = options.now || Date.now(); - const telemetryEntries = flattenTelemetrySnapshotSessions(lockPayload); - const telemetryUpdatedAt = normalizeIsoString(lockPayload?.updatedAt); - const branch = readWorktreeBranch(worktreePath); - const effectiveBranch = branch && branch !== 'HEAD' - ? branch - : `agent/telemetry/${path.basename(worktreePath)}`; - const label = deriveSessionLabel(effectiveBranch, worktreePath); - const taskAnchor = deriveLockTaskAnchor(telemetryEntries, label, telemetryUpdatedAt); - const snapshotIdentity = deriveLockSnapshotIdentity(telemetryEntries); - const projectMetadata = deriveLockProjectMetadata(telemetryEntries); - const startedAt = taskAnchor.timestamp || telemetryUpdatedAt || new Date(now).toISOString(); - - const session = { - schemaVersion: toPositiveInteger(lockPayload?.schemaVersion) || SESSION_SCHEMA_VERSION, - repoRoot: path.resolve(repoRoot), - branch: effectiveBranch, - taskName: taskAnchor.taskName, - latestTaskPreview: taskAnchor.latestTaskPreview, - agentName: deriveAgentNameFromBranch(effectiveBranch), - projectName: projectMetadata.projectName, - projectPath: projectMetadata.projectPath, - snapshotName: snapshotIdentity.snapshotName, - snapshotEmail: snapshotIdentity.snapshotEmail, - worktreePath: path.resolve(worktreePath), - pid: null, - cliName: 'codex', - taskMode: '', - openspecTier: '', - taskRoutingReason: '', - startedAt, - lastHeartbeatAt: '', - state: '', - filePath: path.join(worktreePath, AGENT_WORKTREE_LOCK_FILE), - label, - changedPaths: [], - worktreeChangedPaths: [], - sourceKind: 'worktree-lock', - telemetryUpdatedAt: telemetryUpdatedAt || startedAt, - telemetrySource: toNonEmptyString(lockPayload?.source, 'worktree-lock'), - lockSnapshotCount: toPositiveInteger(lockPayload?.snapshotCount) || 0, - lockSessionCount: toPositiveInteger(lockPayload?.sessionCount) || telemetryEntries.length, - collaboration: Boolean(lockPayload?.collaboration), - sessionHealth: taskAnchor.sessionHealth || normalizeSessionHealthPayload( - lockPayload?.sessionHealth || lockPayload?.sessionSeverity, - ), - }; - - session.elapsedLabel = formatElapsedFrom(session.startedAt, now); - Object.assign(session, deriveSessionActivity(session, { now })); - return session; -} - -function buildManagedWorktreeSession(repoRoot, worktreePath, options = {}) { - const now = options.now || Date.now(); - const branch = readWorktreeBranch(worktreePath); - if (!branch || branch === 'HEAD' || !isManagedAgentBranch(branch)) { - return null; - } - - const label = deriveSessionLabel(branch, worktreePath); - const startedAt = deriveManagedWorktreeStartedAt(worktreePath, now); - const session = { - schemaVersion: SESSION_SCHEMA_VERSION, - repoRoot: path.resolve(repoRoot), - branch, - taskName: label, - latestTaskPreview: '', - agentName: deriveAgentNameFromBranch(branch), - projectName: '', - projectPath: '', - snapshotName: '', - snapshotEmail: '', - worktreePath: path.resolve(worktreePath), - pid: null, - cliName: 'gx', - taskMode: '', - openspecTier: '', - taskRoutingReason: '', - startedAt, - lastHeartbeatAt: '', - state: '', - filePath: path.join(worktreePath, '.git'), - label, - changedPaths: [], - worktreeChangedPaths: [], - sourceKind: 'managed-worktree', - telemetryUpdatedAt: '', - telemetrySource: 'managed-worktree', - lockSnapshotCount: 0, - lockSessionCount: 0, - collaboration: false, - sessionHealth: null, - }; - - session.elapsedLabel = formatElapsedFrom(session.startedAt, now); - Object.assign(session, deriveSessionActivity(session, { now })); - return session; -} - -function readWorktreeLockSessions(repoRoot, options = {}) { - const sessions = []; - for (const managedRoot of resolveManagedWorktreeRoots(repoRoot)) { - if (!fs.existsSync(managedRoot)) { - continue; - } - - let entries; - try { - entries = fs.readdirSync(managedRoot, { withFileTypes: true }); - } catch (_error) { - continue; - } - - for (const entry of entries) { - if (!entry.isDirectory()) { - continue; - } - - const worktreePath = path.join(managedRoot, entry.name); - const lockPath = path.join(worktreePath, AGENT_WORKTREE_LOCK_FILE); - if (!fs.existsSync(lockPath)) { - continue; - } - - const lockPayload = readJsonFile(lockPath); - if (!lockPayload || typeof lockPayload !== 'object' || Array.isArray(lockPayload)) { - continue; - } - - const telemetryEntries = flattenTelemetrySnapshotSessions(lockPayload); - if (telemetryEntries.length === 0 && !toPositiveInteger(lockPayload.sessionCount)) { - continue; - } - - sessions.push(buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options)); - } - } - - return sortSessionsByTimestamp(sessions); -} - -function readManagedWorktreeSessions(repoRoot, options = {}) { - const lockSessions = readWorktreeLockSessions(repoRoot, options); - const lockSessionsByWorktree = new Map( - lockSessions.map((session) => [path.resolve(session.worktreePath), session]), - ); - const sessions = []; - - for (const managedRoot of resolveManagedWorktreeRoots(repoRoot)) { - if (!fs.existsSync(managedRoot)) { - continue; - } - - let entries; - try { - entries = fs.readdirSync(managedRoot, { withFileTypes: true }); - } catch (_error) { - continue; - } - - for (const entry of entries) { - if (!entry.isDirectory()) { - continue; - } - - const worktreePath = path.join(managedRoot, entry.name); - const worktreeKey = path.resolve(worktreePath); - const lockSession = lockSessionsByWorktree.get(worktreeKey); - if (lockSession) { - sessions.push(lockSession); - continue; - } - - const managedSession = buildManagedWorktreeSession(repoRoot, worktreePath, options); - if (managedSession) { - sessions.push(managedSession); - } - } - } - - return sortSessionsByTimestamp(sessions); -} - -function mergeSessionSources(primarySessions, lockSessions) { - const lockSessionsByWorktree = new Map( - lockSessions.map((session) => [path.resolve(session.worktreePath), session]), - ); - const consumedLockWorktrees = new Set(); - const merged = []; - - for (const session of primarySessions) { - const worktreeKey = path.resolve(session.worktreePath); - const lockSession = lockSessionsByWorktree.get(worktreeKey); - if (lockSession && session.activityKind === 'dead') { - continue; - } - if (lockSession) { - consumedLockWorktrees.add(worktreeKey); - merged.push({ - ...session, - latestTaskPreview: session.latestTaskPreview || lockSession.latestTaskPreview, - projectName: session.projectName || lockSession.projectName, - projectPath: session.projectPath || lockSession.projectPath, - snapshotName: session.snapshotName || lockSession.snapshotName, - snapshotEmail: session.snapshotEmail || lockSession.snapshotEmail, - telemetryUpdatedAt: session.telemetryUpdatedAt || lockSession.telemetryUpdatedAt, - telemetrySource: session.telemetrySource || lockSession.telemetrySource, - lockSnapshotCount: session.lockSnapshotCount || lockSession.lockSnapshotCount, - lockSessionCount: session.lockSessionCount || lockSession.lockSessionCount, - collaboration: session.collaboration || lockSession.collaboration, - sessionHealth: session.sessionHealth || lockSession.sessionHealth, - }); - continue; - } - merged.push(session); - } - - for (const lockSession of lockSessions) { - const worktreeKey = path.resolve(lockSession.worktreePath); - if (!consumedLockWorktrees.has(worktreeKey)) { - merged.push(lockSession); - } - } - - return sortSessionsByTimestamp(merged); -} - -function readActiveSessions(repoRoot, options = {}) { - const activeSessionsDir = activeSessionsDirForRepo(repoRoot); - const now = options.now || Date.now(); - const sessionFileSessions = []; - if (fs.existsSync(activeSessionsDir)) { - for (const entry of fs.readdirSync(activeSessionsDir, { withFileTypes: true })) { - if (!entry.isFile() || !entry.name.endsWith('.json')) { - continue; - } - - const filePath = path.join(activeSessionsDir, entry.name); - const parsed = readJsonFile(filePath); - const normalized = normalizeSessionRecord(parsed, { filePath }); - if (!normalized) { - continue; - } - if (!options.includeStale && !isPidAlive(normalized.pid)) { - continue; - } - - normalized.elapsedLabel = formatElapsedFrom(normalized.startedAt, now); - Object.assign(normalized, deriveSessionActivity(normalized, { now })); - sessionFileSessions.push(normalized); - } - } - - return mergeSessionSources( - sortSessionsByTimestamp(sessionFileSessions), - readManagedWorktreeSessions(repoRoot, { now }), - ); -} - -function readRepoChanges(repoRoot) { - const statusLines = runGitLines(repoRoot, ['status', '--porcelain=v1', '--untracked-files=all']); - if (!statusLines) { - return []; - } - - return statusLines - .map((line) => parseRepoChangeLine(repoRoot, line)) - .filter(Boolean) - .sort((left, right) => left.relativePath.localeCompare(right.relativePath)); -} - -module.exports = { - ACTIVE_SESSIONS_RELATIVE_DIR, - SESSION_SCHEMA_VERSION, - activeSessionsDirForRepo, - buildSessionRecord, - clearWorktreeActivityCache, - collectWorktreeChangedPaths, - collectWorktreeTrackedPaths, - deriveBlockingGitLabel, - deriveLatestWorktreeFileActivity, - deriveSessionLabel, - deriveSessionActivity, - formatElapsedFrom, - formatFileCount, - isPidAlive, - normalizeSessionRecord, - parseRepoChangeLine, - previewChangedPaths, - readActiveSessions, - readManagedWorktreeSessions, - readWorktreeLockSessions, - readRepoChanges, - deriveRepoChangeStatus, - readAheadBehindCounts, - readConfiguredBaseBranch, - readLogTail, - resolveWorktreeGitDir, - readSessionHeldLocks, - readSessionInspectData, - sessionLogPath, - sanitizeBranchForFile, - sessionFileNameForBranch, - sessionFilePathForBranch, -};