From 76628a2abc63186c13550fc1922b38f5d25707b3 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Wed, 22 Apr 2026 18:48:27 +0200 Subject: [PATCH] Keep the package repo as the canonical Active Agents source Setup and doctor now read the Active Agents bundle and the companion helper scripts from the package repo root, materialize the templates mirror from that canonical source, and copy managed files as raw bytes so icon.png survives setup into downstream repos. Constraint: setup and fix still provision downstream repos from templates/\nRejected: Continue dual edits in vscode/ and templates/ | keeps canonical-source drift alive\nConfidence: high\nScope-risk: moderate\nDirective: Edit vscode/guardex-active-agents/* first and let setup/doctor refresh templates/ from that source\nTested: node --test test/vscode-active-agents-session-state.test.js test/metadata.test.js test/setup.test.js\nTested: openspec validate agent-codex-vscode-active-agents-canonical-source-pl-2026-04-22-17-59 --type change --strict\nTested: openspec validate --specs\nNot-tested: gx branch finish / PR merge cleanup evidence --- .../executor/tasks.md | 39 +++++---- .../phases.md | 6 +- src/cli/main.js | 3 + src/context.js | 12 +++ src/scaffold/index.js | 86 +++++++++++++++---- test/metadata.test.js | 13 ++- test/setup.test.js | 19 ++++ 7 files changed, 140 insertions(+), 38 deletions(-) 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`, + ); + } });