diff --git a/openspec/plan/agent-codex-vscode-active-agents-canonical-source-pl-2026-04-22-17-59/executor/tasks.md b/openspec/plan/agent-codex-vscode-active-agents-canonical-source-pl-2026-04-22-17-59/executor/tasks.md index 7e6a2c0..8f81d91 100644 --- a/openspec/plan/agent-codex-vscode-active-agents-canonical-source-pl-2026-04-22-17-59/executor/tasks.md +++ b/openspec/plan/agent-codex-vscode-active-agents-canonical-source-pl-2026-04-22-17-59/executor/tasks.md @@ -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 diff --git a/openspec/plan/agent-codex-vscode-active-agents-canonical-source-pl-2026-04-22-17-59/phases.md b/openspec/plan/agent-codex-vscode-active-agents-canonical-source-pl-2026-04-22-17-59/phases.md index 902f722..67ccdff 100644 --- a/openspec/plan/agent-codex-vscode-active-agents-canonical-source-pl-2026-04-22-17-59/phases.md +++ b/openspec/plan/agent-codex-vscode-active-agents-canonical-source-pl-2026-04-22-17-59/phases.md @@ -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. diff --git a/src/cli/main.js b/src/cli/main.js index 70aae7b..e9fbe72 100755 --- a/src/cli/main.js +++ b/src/cli/main.js @@ -159,6 +159,7 @@ const { ensureHookShim, copyTemplateFile, ensureTemplateFilePresent, + materializePackageRepoTemplateFiles, ensureOmxScaffold, ensureLockRegistry, lockStateOrError, @@ -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); @@ -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); diff --git a/src/context.js b/src/context.js index f1c728d..2ff298e 100644 --- a/src/context.js +++ b/src/context.js @@ -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'] }, @@ -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, diff --git a/src/scaffold/index.js b/src/scaffold/index.js index 86ec941..3ac4c55 100644 --- a/src/scaffold/index.js +++ b/src/scaffold/index.js @@ -1,6 +1,7 @@ const { fs, path, + PACKAGE_ROOT, TOOL_NAME, SHORT_TOOL_NAME, GUARDEX_HOME_DIR, @@ -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, @@ -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 }; } @@ -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); } @@ -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 }; @@ -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); } @@ -806,6 +861,7 @@ module.exports = { ensureHookShim, copyTemplateFile, ensureTemplateFilePresent, + materializePackageRepoTemplateFiles, ensureOmxScaffold, ensureLockRegistry, lockStateOrError, diff --git a/test/metadata.test.js b/test/metadata.test.js index cfea9b6..0ddc5e3 100644 --- a/test/metadata.test.js +++ b/test/metadata.test.js @@ -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`, ); } diff --git a/test/setup.test.js b/test/setup.test.js index 675c4ae..c81c780 100644 --- a/test/setup.test.js +++ b/test/setup.test.js @@ -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', () => { @@ -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`, + ); + } });