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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,41 +1,48 @@
# executor tasks

Handoff: change=`agent-codex-vscode-active-agents-canonical-source-pl-2026-04-22-17-59`; branch=`agent/codex/vscode-active-agents-canonical-source-im-2026-04-22-18-25`; scope=`src/context.js`, `src/scaffold/index.js`, `src/cli/main.js`, `test/metadata.test.js`, `test/setup.test.js`; action=`make the package repo root the canonical Active Agents source, materialize the template mirror from it, and verify with focused node tests plus OpenSpec validation`.
Focused verification: `node --test test/vscode-active-agents-session-state.test.js test/metadata.test.js test/setup.test.js`; `openspec validate agent-codex-vscode-active-agents-canonical-source-pl-2026-04-22-17-59 --type change --strict`; `openspec validate --specs`.
Finish command: `gx branch finish --branch agent/codex/vscode-active-agents-canonical-source-im-2026-04-22-18-25 --base main --via-pr --wait-for-merge --cleanup`.

## 1. Spec

- [ ] 1.1 Map the approved canonical-source requirements to concrete implementation work items.
- [ ] 1.2 Freeze the touched components/files before coding starts: managed-file resolution, scaffold/doctor copy path, extension source tree, docs, and focused tests.
- [x] 1.1 Map the approved canonical-source requirements to concrete implementation work items.
- [x] 1.2 Freeze the touched components/files before coding starts: managed-file resolution, scaffold/doctor copy path, extension source tree, docs, and focused tests.

## 2. Tests

- [ ] 2.1 Define test additions/updates required to lock canonical-source behavior, setup/doctor asset copying, and install payload truthfulness.
- [ ] 2.2 Validate the focused regression and smoke verification commands before coding.
- [x] 2.1 Define test additions/updates required to lock canonical-source behavior, setup/doctor asset copying, and install payload truthfulness.
- [x] 2.2 Validate the focused regression and smoke verification commands before coding.

## 3. Implementation

- [ ] 3.1 Move the authored extension source to one canonical tree and retire manual duplicate editing.
- [ ] 3.2 Update setup/doctor/materialization so downstream repos still receive a working companion, including `icon.png`.
- [ ] 3.3 Replace duplicate-tree parity plumbing with focused docs/tests and keep runtime behavior unchanged.
- [x] 3.1 Move the authored extension source to one canonical tree and retire manual duplicate editing.
- [x] 3.2 Update setup/doctor/materialization so downstream repos still receive a working companion, including `icon.png`.
- [x] 3.3 Replace duplicate-tree parity plumbing with focused docs/tests and keep runtime behavior unchanged.

Verification note: `node --test test/vscode-active-agents-session-state.test.js test/metadata.test.js test/setup.test.js` passed (`97/97`); `openspec validate agent-codex-vscode-active-agents-canonical-source-pl-2026-04-22-17-59 --type change --strict` passed; `openspec validate --specs` exited `0` with `No items found to validate.`

## 4. Checkpoints

- [ ] [E1] READY - Execution start checkpoint
- [x] [E1] READY - Execution start checkpoint

### E1 Acceptance Criteria

- [ ] The execution lane starts on a fresh implementation branch from `main`, not on the planning branch.
- [ ] The touched-file list is frozen before code edits begin.
- [ ] Runtime/UI behavior remains out of scope unless the canonical-source migration proves a blocker.
- [x] The execution lane starts on a fresh implementation branch from `main`, not on the planning branch.
- [x] The touched-file list is frozen before code edits begin.
- [x] Runtime/UI behavior remains out of scope unless the canonical-source migration proves a blocker.

### E1 Verification Evidence

- [ ] Executor notes record the frozen file list and branch choice.
- [ ] `phases.md` is advanced to the execution phase when the fresh implementation lane begins.
- [ ] The root handoff identifies the exact focused tests and finish command.
- [x] Executor notes record the frozen file list and branch choice.
- [x] `phases.md` is advanced to the execution phase when the fresh implementation lane begins.
- [x] The root handoff identifies the exact focused tests and finish command.

## 5. Collaboration

- [ ] 5.1 Owner recorded the fresh implementation lane before edits.
- [ ] 5.2 Record joined agents / handoffs, or mark `N/A` when solo.
- [x] 5.1 Owner recorded the fresh implementation lane before edits.
- [x] 5.2 Record joined agents / handoffs, or mark `N/A` when solo.
Joined agents: `N/A` (solo lane).

## 6. Cleanup

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ One phase is intended to fit into a single Codex or Claude session task.
- checkpoints: A1, C1
- summary: Keep `vscode/guardex-active-agents/` as the authored source of truth and route setup/doctor/materialization through that source instead of manual twin-tree edits.

- [ ] [PH03] Implement canonical-source migration
- [x] [PH03] Implement canonical-source migration
- session: codex
- checkpoints: E1
- summary: Update managed-file resolution, asset copying, and duplicate-tree handling without changing user-visible Active Agents behavior.

- [ ] [PH04] Refresh docs and focused regression coverage
- [x] [PH04] Refresh docs and focused regression coverage
- session: codex
- checkpoints: W1, V1
- summary: Replace duplicate-tree parity proofs with canonical-source/install/setup checks and update operator guidance to match the new source path.

- [ ] [PH05] Validate and finish the execution lane
- [>] [PH05] Validate and finish the execution lane
- session: codex
- checkpoints: E1, V1
- summary: Run targeted tests plus OpenSpec validation, then finish the implementation branch via PR merge and cleanup.
3 changes: 3 additions & 0 deletions src/cli/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ const {
ensureHookShim,
copyTemplateFile,
ensureTemplateFilePresent,
materializePackageRepoTemplateFiles,
ensureOmxScaffold,
ensureLockRegistry,
lockStateOrError,
Expand Down Expand Up @@ -1445,6 +1446,7 @@ function runInstallInternal(options) {
),
);
}
operations.push(...materializePackageRepoTemplateFiles(repoRoot, TEMPLATE_FILES, Boolean(options.dryRun)));
operations.push(...ensureTargetedLegacyWorkflowShims(repoRoot, options));
for (const hookName of HOOK_NAMES) {
const hookRelativePath = path.posix.join('.githooks', hookName);
Expand Down Expand Up @@ -1501,6 +1503,7 @@ function runFixInternal(options) {
}
operations.push(ensureTemplateFilePresent(repoRoot, templateFile, Boolean(options.dryRun)));
}
operations.push(...materializePackageRepoTemplateFiles(repoRoot, TEMPLATE_FILES, Boolean(options.dryRun)));
operations.push(...ensureTargetedLegacyWorkflowShims(repoRoot, options));
for (const hookName of HOOK_NAMES) {
const hookRelativePath = path.posix.join('.githooks', hookName);
Expand Down
12 changes: 12 additions & 0 deletions src/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,19 @@ const TEMPLATE_FILES = [
'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',
];

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',
]);

const LEGACY_WORKFLOW_SHIM_SPECS = [
{ relativePath: 'scripts/agent-branch-start.sh', kind: 'shell', command: ['branch', 'start'] },
{ relativePath: 'scripts/agent-branch-finish.sh', kind: 'shell', command: ['branch', 'finish'] },
Expand Down Expand Up @@ -620,6 +631,7 @@ module.exports = {
HOOK_NAMES,
toDestinationPath,
TEMPLATE_FILES,
PACKAGE_ROOT_SOURCE_OVERRIDES,
LEGACY_WORKFLOW_SHIM_SPECS,
LEGACY_WORKFLOW_SHIMS,
MANAGED_TEMPLATE_DESTINATIONS,
Expand Down
86 changes: 71 additions & 15 deletions src/scaffold/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const {
fs,
path,
PACKAGE_ROOT,
TOOL_NAME,
SHORT_TOOL_NAME,
GUARDEX_HOME_DIR,
Expand All @@ -9,6 +10,7 @@ const {
HOOK_NAMES,
LOCK_FILE_RELATIVE,
LEGACY_MANAGED_PACKAGE_SCRIPTS,
PACKAGE_ROOT_SOURCE_OVERRIDES,
USER_LEVEL_SKILL_ASSETS,
AGENTS_MARKER_START,
AGENTS_MARKER_END,
Expand Down Expand Up @@ -172,17 +174,13 @@ function ensureHookShim(repoRoot, hookName, options = {}) {
);
}

function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
const sourcePath = path.join(TEMPLATE_ROOT, relativeTemplatePath);
const destinationRelativePath = toDestinationPath(relativeTemplatePath);
const destinationPath = path.join(repoRoot, destinationRelativePath);

const sourceContent = fs.readFileSync(sourcePath, 'utf8');
function copyManagedSourceFile(repoRoot, sourcePath, destinationPath, destinationRelativePath, force, dryRun) {
const sourceContent = fs.readFileSync(sourcePath);
const destinationExists = fs.existsSync(destinationPath);

if (destinationExists) {
const existingContent = fs.readFileSync(destinationPath, 'utf8');
if (existingContent === sourceContent) {
const existingContent = fs.readFileSync(destinationPath);
if (existingContent.equals(sourceContent)) {
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
return { status: 'unchanged', file: destinationRelativePath };
}
Expand All @@ -193,7 +191,7 @@ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {

ensureParentDir(repoRoot, destinationPath, dryRun);
if (!dryRun) {
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
fs.writeFileSync(destinationPath, sourceContent);
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
}

Expand All @@ -204,22 +202,54 @@ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
return { status: destinationExists ? 'overwritten' : 'created', file: destinationRelativePath };
}

function normalizeTemplatePath(relativeTemplatePath) {
return String(relativeTemplatePath).replace(/\\/g, '/');
}

function usesPackageRootSource(repoRoot, relativeTemplatePath) {
return (
path.resolve(repoRoot) === PACKAGE_ROOT &&
PACKAGE_ROOT_SOURCE_OVERRIDES.has(normalizeTemplatePath(relativeTemplatePath))
);
}

function resolveTemplateSourcePath(repoRoot, relativeTemplatePath) {
if (usesPackageRootSource(repoRoot, relativeTemplatePath)) {
return path.join(PACKAGE_ROOT, relativeTemplatePath);
}
return path.join(TEMPLATE_ROOT, relativeTemplatePath);
}

function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
const sourcePath = resolveTemplateSourcePath(repoRoot, relativeTemplatePath);
const destinationRelativePath = toDestinationPath(relativeTemplatePath);
const destinationPath = path.join(repoRoot, destinationRelativePath);
return copyManagedSourceFile(
repoRoot,
sourcePath,
destinationPath,
destinationRelativePath,
force,
dryRun,
);
}

function ensureTemplateFilePresent(repoRoot, relativeTemplatePath, dryRun) {
const sourcePath = path.join(TEMPLATE_ROOT, relativeTemplatePath);
const sourcePath = resolveTemplateSourcePath(repoRoot, relativeTemplatePath);
const destinationRelativePath = toDestinationPath(relativeTemplatePath);
const destinationPath = path.join(repoRoot, destinationRelativePath);
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
const sourceContent = fs.readFileSync(sourcePath);

if (fs.existsSync(destinationPath)) {
const existingContent = fs.readFileSync(destinationPath, 'utf8');
if (existingContent === sourceContent) {
const existingContent = fs.readFileSync(destinationPath);
if (existingContent.equals(sourceContent)) {
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
return { status: 'unchanged', file: destinationRelativePath };
}

if (isCriticalGuardrailPath(destinationRelativePath)) {
if (!dryRun) {
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
fs.writeFileSync(destinationPath, sourceContent);
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
}
return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: destinationRelativePath };
Expand All @@ -230,13 +260,38 @@ function ensureTemplateFilePresent(repoRoot, relativeTemplatePath, dryRun) {

ensureParentDir(repoRoot, destinationPath, dryRun);
if (!dryRun) {
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
fs.writeFileSync(destinationPath, sourceContent);
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
}

return { status: 'created', file: destinationRelativePath };
}

function materializePackageRepoTemplateFiles(repoRoot, relativeTemplatePaths, dryRun) {
if (path.resolve(repoRoot) !== PACKAGE_ROOT) {
return [];
}

const operations = [];
for (const relativeTemplatePath of relativeTemplatePaths) {
if (!PACKAGE_ROOT_SOURCE_OVERRIDES.has(normalizeTemplatePath(relativeTemplatePath))) {
continue;
}
const templateRelativePath = path.posix.join('templates', normalizeTemplatePath(relativeTemplatePath));
operations.push(
copyManagedSourceFile(
PACKAGE_ROOT,
path.join(PACKAGE_ROOT, relativeTemplatePath),
path.join(PACKAGE_ROOT, templateRelativePath),
templateRelativePath,
true,
dryRun,
),
);
}
return operations;
}

function lockFilePath(repoRoot) {
return path.join(repoRoot, LOCK_FILE_RELATIVE);
}
Expand Down Expand Up @@ -806,6 +861,7 @@ module.exports = {
ensureHookShim,
copyTemplateFile,
ensureTemplateFilePresent,
materializePackageRepoTemplateFiles,
ensureOmxScaffold,
ensureLockRegistry,
lockStateOrError,
Expand Down
13 changes: 9 additions & 4 deletions test/metadata.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,21 @@ test('critical runtime helper scripts and active-agents sources stay in sync wit
['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'],
];

for (const [templatePath, runtimePath] of pairs) {
const template = fs.readFileSync(path.join(repoRoot, templatePath), 'utf8');
const runtime = fs.readFileSync(path.join(repoRoot, runtimePath), 'utf8');
const template = fs.readFileSync(path.join(repoRoot, templatePath));
const runtime = fs.readFileSync(path.join(repoRoot, runtimePath));
assert.equal(
runtime,
template,
Buffer.compare(runtime, template),
0,
`${runtimePath} diverged from ${templatePath}; run gx setup/doctor parity repair`,
);
}
Expand Down
19 changes: 19 additions & 0 deletions test/setup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ const {
defineSpawnSuite,
} = require('./helpers/install-test-helpers');

const packageRepoRoot = path.resolve(__dirname, '..');

defineSpawnSuite('setup integration suite', () => {

test('setup provisions workflow files and repo config', () => {
Expand Down Expand Up @@ -167,6 +169,23 @@ 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',
];
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`,
);
}
});


Expand Down