From ba29236bf19adf8f2f69ec83875745441142dfcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Fri, 15 May 2026 22:42:51 +0200 Subject: [PATCH 1/3] feat: add auto onboarding support Add load-time repository scaffolding and tuple plugin option handling so global OpenCode config can initialize NomadWorks without PAI sync coupling. --- .gitignore | 1 + docs/setup/CONFIGURATION.md | 20 ++++- src/index.js | 152 +++++++++++++++++++++--------------- tests/plugin.test.js | 32 ++++++++ 4 files changed, 139 insertions(+), 66 deletions(-) create mode 100644 tests/plugin.test.js diff --git a/.gitignore b/.gitignore index 12c0bc3..d6eb172 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ dist/ node_modules/ .DS_Store *.tgz +.nomadworks/ diff --git a/docs/setup/CONFIGURATION.md b/docs/setup/CONFIGURATION.md index 88da84f..3fe2ff7 100644 --- a/docs/setup/CONFIGURATION.md +++ b/docs/setup/CONFIGURATION.md @@ -2,7 +2,25 @@ NomadWorks reads repository-local configuration from `.nomadworks/nomadworks.yaml`. -This file is typically created during the PMA-led repository setup flow. +This file is typically created during PMA-led setup or by global auto-onboarding when the plugin is configured with `onboarding: "auto"`. + +## Global plugin options + +NomadWorks can be configured globally in OpenCode with plugin options: + +```json +{ + "plugin": [["@neuralnomads/nomadworks", { + "onboarding": "auto", + "default_team_mode": "full", + "auto_init_git_repos_only": true + }]] +} +``` + +- `onboarding`: `off`, `suggest`, or `auto`. `auto` creates missing repo scaffolding without asking PMA. +- `default_team_mode`: `mini` or `full` for auto-created repo config. +- `auto_init_git_repos_only`: defaults to true; set false to allow auto-init outside Git repositories. ## Minimal config diff --git a/src/index.js b/src/index.js index 6b51122..a9f3ca7 100644 --- a/src/index.js +++ b/src/index.js @@ -59,6 +59,72 @@ function legacyRepoAgentsDir(worktree) { return path.join(legacyNomadworksDir(worktree), "nomadworks", "agents"); } +function scaffoldRepository(worktree, teamMode) { + const requestedTeamMode = normalizeTeamMode(teamMode); + const cfgDir = nomadworksDir(worktree); + if (!fs.existsSync(cfgDir)) fs.mkdirSync(cfgDir, { recursive: true }); + + const agentIds = fs.existsSync(BUNDLE_AGENTS_DIR) + ? fs.readdirSync(BUNDLE_AGENTS_DIR).filter(f => f.endsWith(".md")).map(f => f.replace(".md", "")) + : []; + + const nomadworksTmplPath = path.join(TEMPLATES_DIR, "nomadworks.yaml.template"); + const codemapTmplPath = path.join(TEMPLATES_DIR, "codemap.yml.template"); + if (!fs.existsSync(nomadworksTmplPath) || !fs.existsSync(codemapTmplPath)) { + throw new Error("Initialization templates not found in plugin."); + } + + let nomadworksConfig = fs.readFileSync(nomadworksTmplPath, "utf8"); + nomadworksConfig = nomadworksConfig.replace("{{teamMode}}", requestedTeamMode); + + let agentsSection = ""; + for (const id of agentIds) { + const enabled = isAgentEnabledForTeamMode(id, requestedTeamMode) ? "true" : "false"; + agentsSection += ` ${id}:\n enabled: ${enabled}\n`; + } + nomadworksConfig = nomadworksConfig.replace(/^agents:\s*$/m, "agents:\n" + agentsSection.trimEnd()); + + const codemapConfig = fs.readFileSync(codemapTmplPath, "utf8").replace("{{projectName}}", path.basename(worktree)); + const created = []; + + const writeIfMissing = (filePath, content) => { + if (!fs.existsSync(filePath)) { + const dirPath = path.dirname(filePath); + if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true }); + fs.writeFileSync(filePath, content, "utf8"); + created.push(path.relative(worktree, filePath).replace(/\\/g, "/")); + } + }; + + writeIfMissing(path.join(cfgDir, "nomadworks.yaml"), nomadworksConfig); + writeIfMissing(path.join(worktree, "codemap.yml"), codemapConfig); + scaffoldNomadworksReadmes(worktree); + + writeIfMissing(path.join(worktree, "tasks", "current.md"), "# Current Tasks (Backlog)\n\n## Active Discussions\n- (None)\n\n## Active\n- (None)\n\n## Todo\n- (None)\n\n## Blocked\n- (None)\n"); + writeIfMissing(path.join(worktree, "tasks", "done.md"), "# Completed Tasks (Registry)\n\n| Date | Task ID | SCR ID | Commit | Summary |\n| :--- | :--- | :--- | :--- | :--- |\n"); + writeIfMissing(path.join(worktree, "docs", "scrs", "current.md"), "# Current Spec Change Requests (Backlog)\n\n## Active/Review\n- (None)\n\n## Approved (Ready for Implementation)\n- (None)\n\n## Proposed\n- (None)\n"); + writeIfMissing(path.join(worktree, "docs", "scrs", "done.md"), "# Implemented Spec Change Requests\n\n| Date | SCR ID | Title | Related Feature | Task ID |\n| :--- | :--- | :--- | :--- | :--- |\n"); + + return { teamMode: requestedTeamMode, created }; +} + +function hasGitRepository(worktree) { + let current = worktree; + while (current && current !== path.dirname(current)) { + if (fs.existsSync(path.join(current, ".git"))) return true; + current = path.dirname(current); + } + return false; +} + +function normalizeOnboarding(value) { + if (value === true) return "auto"; + if (typeof value !== "string") return "suggest"; + const normalized = value.trim().toLowerCase(); + if (["auto", "suggest", "off"].includes(normalized)) return normalized; + return "suggest"; +} + function runtimeDiscussionRegistryPath(worktree) { return path.join(nomadworksDir(worktree), "runtime", "discussions.json"); } @@ -775,11 +841,25 @@ function getModePromptFragment(agentId, operatingTeamMode, worktree) { return readResolvedFile(fragmentPath, worktree); } -export default async function NomadWorksPlugin(input) { +export default async function NomadWorksPlugin(input, options = {}) { + const pluginOptions = input.options || input.config || options || {}; const worktree = path.resolve(input.worktree || process.cwd()); const debugDir = generatedAgentsDir(worktree); const configPath = resolveConfigPath(worktree); const discussionRegistry = loadDiscussionRegistry(worktree); + const onboardingMode = normalizeOnboarding(pluginOptions.onboarding ?? pluginOptions.auto_init); + const defaultTeamMode = normalizeTeamMode(pluginOptions.default_team_mode || pluginOptions.team_mode || "full"); + + if (!fs.existsSync(configPath) && onboardingMode === "auto") { + const gitOnly = pluginOptions.auto_init_git_repos_only !== false; + if (!gitOnly || hasGitRepository(worktree)) { + try { + scaffoldRepository(worktree, defaultTeamMode); + } catch (e) { + console.error(`[NomadWorks] Auto-onboarding failed for ${worktree}:`, e); + } + } + } // Load project-specific configuration let repoCfg = { agents: {}, defaults: {}, features: {} }; @@ -863,72 +943,14 @@ export default async function NomadWorksPlugin(input) { return "Error: team_mode must be either 'mini' or 'full'."; } - const cfgDir = nomadworksDir(context.worktree); - if (!fs.existsSync(cfgDir)) fs.mkdirSync(cfgDir, { recursive: true }); - - // Discover all agent IDs to enable them explicitly - const agentIds = fs.existsSync(BUNDLE_AGENTS_DIR) - ? fs.readdirSync(BUNDLE_AGENTS_DIR).filter(f => f.endsWith(".md")).map(f => f.replace(".md", "")) - : []; - - const nomadworksTmplPath = path.join(TEMPLATES_DIR, "nomadworks.yaml.template"); - const codemapTmplPath = path.join(TEMPLATES_DIR, "codemap.yml.template"); - if (!fs.existsSync(nomadworksTmplPath) || !fs.existsSync(codemapTmplPath)) { - return "Error: Initialization templates not found in plugin."; - } - - let nomadworksConfig = fs.readFileSync(nomadworksTmplPath, "utf8"); - nomadworksConfig = nomadworksConfig.replace("{{teamMode}}", requestedTeamMode); - - // Append dynamically discovered agents to the template - let agentsSection = ""; - for (const id of agentIds) { - const enabled = isAgentEnabledForTeamMode(id, requestedTeamMode) ? "true" : "false"; - agentsSection += ` ${id}:\n enabled: ${enabled}\n`; - } - nomadworksConfig = nomadworksConfig.replace("agents:", "agents:\n" + agentsSection); - - let codemapConfig = fs.readFileSync(codemapTmplPath, "utf8"); - codemapConfig = codemapConfig.replace("{{projectName}}", path.basename(context.worktree)); - - const cfgFilePath = path.join(cfgDir, "nomadworks.yaml"); - const rootCodemapPath = path.join(context.worktree, "codemap.yml"); - - if (!fs.existsSync(cfgFilePath)) { - fs.writeFileSync(cfgFilePath, nomadworksConfig, "utf8"); - } - - if (!fs.existsSync(rootCodemapPath)) { - fs.writeFileSync(rootCodemapPath, codemapConfig, "utf8"); - } - - scaffoldNomadworksReadmes(context.worktree); - - // Scaffold Task Registries - const tasksDir = path.join(context.worktree, "tasks"); - const scrsDir = path.join(context.worktree, "docs", "scrs"); - if (!fs.existsSync(tasksDir)) fs.mkdirSync(tasksDir, { recursive: true }); - if (!fs.existsSync(scrsDir)) fs.mkdirSync(scrsDir, { recursive: true }); - - const currentPath = path.join(tasksDir, "current.md"); - const donePath = path.join(tasksDir, "done.md"); - const scrsCurrentPath = path.join(scrsDir, "current.md"); - const scrsDonePath = path.join(scrsDir, "done.md"); - - if (!fs.existsSync(currentPath)) { - fs.writeFileSync(currentPath, "# Current Tasks (Backlog)\n\n## 💬 Active Discussions\n- (None)\n\n## 🚀 Active\n- (None)\n\n## 📋 Todo\n- (None)\n\n## 🛑 Blocked\n- (None)\n", "utf8"); - } - if (!fs.existsSync(donePath)) { - fs.writeFileSync(donePath, "# Completed Tasks (Registry)\n\n| Date | Task ID | SCR ID | Commit | Summary |\n| :--- | :--- | :--- | :--- | :--- |\n", "utf8"); - } - if (!fs.existsSync(scrsCurrentPath)) { - fs.writeFileSync(scrsCurrentPath, "# Current Spec Change Requests (Backlog)\n\n## 🚀 Active/Review\n- (None)\n\n## 📋 Approved (Ready for Implementation)\n- (None)\n\n## 💡 Proposed\n- (None)\n", "utf8"); - } - if (!fs.existsSync(scrsDonePath)) { - fs.writeFileSync(scrsDonePath, "# Implemented Spec Change Requests\n\n| Date | SCR ID | Title | Related Feature | Task ID |\n| :--- | :--- | :--- | :--- | :--- |\n", "utf8"); + let scaffolded; + try { + scaffolded = scaffoldRepository(context.worktree, requestedTeamMode); + } catch (e) { + return `Error: ${e.message}`; } - const initSummary = `NomadWorks initialized in '${requestedTeamMode}' team mode: .nomadworks/nomadworks.yaml, repo policy/agent folders, registries, and codemap.yml created.`; + const initSummary = `NomadWorks initialized in '${requestedTeamMode}' team mode. Created files: ${scaffolded.created.length ? scaffolded.created.join(", ") : "none (already present)"}.`; // Ensure OpenCode reloads config/agents after scaffolding changes. // Not all environments expose this API, so treat it as best-effort. diff --git a/tests/plugin.test.js b/tests/plugin.test.js new file mode 100644 index 0000000..e56f7ff --- /dev/null +++ b/tests/plugin.test.js @@ -0,0 +1,32 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import YAML from "yaml"; + +import NomadWorksPlugin from "../src/index.js"; + +function createEmptyGitWorktree() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nomadworks-auto-test-")); + const result = spawnSync("git", ["init"], { cwd: root, encoding: "utf8", shell: false }); + if (result.status !== 0) throw new Error(result.stderr || result.stdout || "git init failed"); + return root; +} + +describe("NomadWorks plugin auto-onboarding", () => { + test("auto onboarding reads tuple plugin options from second argument", async () => { + const worktree = createEmptyGitWorktree(); + + await NomadWorksPlugin({ worktree }, { + onboarding: "auto", + default_team_mode: "mini", + auto_init_git_repos_only: true + }); + + expect(fs.existsSync(path.join(worktree, ".nomadworks", "nomadworks.yaml"))).toBe(true); + expect(fs.existsSync(path.join(worktree, "codemap.yml"))).toBe(true); + const generatedConfig = YAML.parse(fs.readFileSync(path.join(worktree, ".nomadworks", "nomadworks.yaml"), "utf8")); + expect(generatedConfig.team_mode).toBe("mini"); + expect(generatedConfig.agents.product_manager.enabled).toBe(true); + }); +}); From 12cea40b94863033fe9e8f2d60169cca381f6d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Fri, 15 May 2026 23:40:51 +0200 Subject: [PATCH 2/3] fix: honor tuple onboarding options Merges plugin option sources safely so second-argument options still apply when input options are empty, while preserving explicit input override precedence. --- docs/setup/CONFIGURATION.md | 2 +- src/index.js | 6 +++++- tests/plugin.test.js | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/setup/CONFIGURATION.md b/docs/setup/CONFIGURATION.md index 3fe2ff7..99a706a 100644 --- a/docs/setup/CONFIGURATION.md +++ b/docs/setup/CONFIGURATION.md @@ -18,7 +18,7 @@ NomadWorks can be configured globally in OpenCode with plugin options: } ``` -- `onboarding`: `off`, `suggest`, or `auto`. `auto` creates missing repo scaffolding without asking PMA. +- `onboarding`: `off`, `suggest`, or `auto`. Only `auto` creates missing repo scaffolding without asking PMA; `suggest` and `off` do not scaffold files automatically. - `default_team_mode`: `mini` or `full` for auto-created repo config. - `auto_init_git_repos_only`: defaults to true; set false to allow auto-init outside Git repositories. diff --git a/src/index.js b/src/index.js index a9f3ca7..5111d52 100644 --- a/src/index.js +++ b/src/index.js @@ -842,7 +842,11 @@ function getModePromptFragment(agentId, operatingTeamMode, worktree) { } export default async function NomadWorksPlugin(input, options = {}) { - const pluginOptions = input.options || input.config || options || {}; + const pluginOptions = { + ...(options || {}), + ...(input.config || {}), + ...(input.options || {}) + }; const worktree = path.resolve(input.worktree || process.cwd()); const debugDir = generatedAgentsDir(worktree); const configPath = resolveConfigPath(worktree); diff --git a/tests/plugin.test.js b/tests/plugin.test.js index e56f7ff..26e16a4 100644 --- a/tests/plugin.test.js +++ b/tests/plugin.test.js @@ -29,4 +29,19 @@ describe("NomadWorks plugin auto-onboarding", () => { expect(generatedConfig.team_mode).toBe("mini"); expect(generatedConfig.agents.product_manager.enabled).toBe(true); }); + + test("auto onboarding honors second-argument options when input options are empty", async () => { + const worktree = createEmptyGitWorktree(); + + await NomadWorksPlugin({ worktree, options: {} }, { + onboarding: "auto", + default_team_mode: "mini", + auto_init_git_repos_only: true + }); + + expect(fs.existsSync(path.join(worktree, ".nomadworks", "nomadworks.yaml"))).toBe(true); + expect(fs.existsSync(path.join(worktree, "codemap.yml"))).toBe(true); + const generatedConfig = YAML.parse(fs.readFileSync(path.join(worktree, ".nomadworks", "nomadworks.yaml"), "utf8")); + expect(generatedConfig.team_mode).toBe("mini"); + }); }); From 126aaa087378055295e0d407a5b54627a76cd62d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sat, 16 May 2026 00:05:26 +0200 Subject: [PATCH 3/3] fix: harden auto-onboarding safeguards Preserve repository-local NomadWorks customization paths while ignoring generated state, and prevent auto-onboarding side effects when disabled or outside git worktrees. Adds focused regression coverage and CodeMaps needed for validation. --- .gitignore | 3 +- agents/codemap.yml | 21 ++++++++++++++ codemap.yml | 33 ++++++++++++++++++++++ docs/core/technical_guidelines.md | 12 ++++---- policies/codemap.yml | 23 ++++++++++++++++ scripts/codemap.yml | 12 ++++++++ src/codemap.yml | 16 +++++++++++ src/index.js | 6 ++-- tests/codemap.yml | 12 ++++++++ tests/plugin.test.js | 46 +++++++++++++++++++++++++++++++ 10 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 agents/codemap.yml create mode 100644 codemap.yml create mode 100644 policies/codemap.yml create mode 100644 scripts/codemap.yml create mode 100644 src/codemap.yml create mode 100644 tests/codemap.yml diff --git a/.gitignore b/.gitignore index d6eb172..d85aa6d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ dist/ node_modules/ .DS_Store *.tgz -.nomadworks/ +.nomadworks/runtime/ +.nomadworks/generated/ diff --git a/agents/codemap.yml b/agents/codemap.yml new file mode 100644 index 0000000..89cb76e --- /dev/null +++ b/agents/codemap.yml @@ -0,0 +1,21 @@ +scope: module +parent: ../codemap.yml +sources_of_truth: + - path: business_analyst.md + description: "Bundled Business Analyst agent prompt." + - path: developer.md + description: "Bundled Developer agent prompt." + - path: product_manager.md + description: "Bundled Product Manager agent prompt." + - path: qa_engineer.md + description: "Bundled QA Engineer agent prompt." + - path: tech_lead.md + description: "Bundled Tech Lead agent prompt." + - path: technical_architect.md + description: "Bundled Technical Architect agent prompt." + - path: ui_ux_designer.md + description: "Bundled UI/UX Designer agent prompt." + - path: workflow_runner.md + description: "Bundled Workflow Runner agent prompt." +invariants: + - "Agent prompt files are bundled package inputs and must remain listed in package contents." diff --git a/codemap.yml b/codemap.yml new file mode 100644 index 0000000..0c981e6 --- /dev/null +++ b/codemap.yml @@ -0,0 +1,33 @@ +scope: repo +modules: + - path: src + description: "Runtime plugin implementation and validation logic." + - path: scripts + description: "Build and release helper scripts." + - path: tests + description: "Jest regression tests for plugin behavior and validation logic." + - path: agents + description: "Bundled NomadWorks agent prompt definitions." + - path: policies + description: "Bundled default policy documents." +sources_of_truth: + - path: package.json + description: "Package metadata, scripts, dependencies, and publish file allowlist." + - path: package-lock.json + description: "Locked npm dependency graph." + - path: README.md + description: "Repository overview and usage entry documentation." + - path: Agents_Common.md + description: "Shared bundled agent guidance included by agent prompts." + - path: .gitignore + description: "Repository ignore policy for generated and dependency files." +commands: + - command: npm test + description: "Run the Jest test suite." + - command: npm run build + description: "Build distributable files into dist/." + - command: npm pack --dry-run + description: "Verify package contents before publishing." +invariants: + - "Do not ignore repository-local .nomadworks customization surfaces; only generated/runtime subpaths should be ignored." + - "Generated dist output must be produced from src via npm run build." diff --git a/docs/core/technical_guidelines.md b/docs/core/technical_guidelines.md index 895115e..32c7b03 100644 --- a/docs/core/technical_guidelines.md +++ b/docs/core/technical_guidelines.md @@ -3,12 +3,12 @@ This document defines the project's tech stack and architectural patterns. ## Tech Stack -- **Language:** [To be defined] -- **Runtime/Framework:** [To be defined] -- **Frontend (if applicable):** [To be defined] -- **State Management:** [To be defined] -- **Testing Framework:** [To be defined] -- **Database/Storage:** [To be defined] +- **Language:** JavaScript (ES modules). +- **Runtime/Framework:** Node.js package exposing an OpenCode plugin via `@opencode-ai/plugin`. +- **Frontend (if applicable):** Not applicable; this package provides CLI/plugin workflow tooling. +- **State Management:** Repository-local YAML/Markdown files plus `.nomadworks/runtime/` for generated session state. +- **Testing Framework:** Jest, run through `npm test`. +- **Database/Storage:** Filesystem-based configuration, generated artifacts, task records, and documentation. ## Architectural Patterns - **Feature-First:** Organize code into distinct features or modules. diff --git a/policies/codemap.yml b/policies/codemap.yml new file mode 100644 index 0000000..c55006f --- /dev/null +++ b/policies/codemap.yml @@ -0,0 +1,23 @@ +scope: module +parent: ../codemap.yml +sources_of_truth: + - path: README.md + description: "Policy directory overview." + - path: definition-of-done.md + description: "Bundled Definition of Done policy." + - path: definition-of-ready.md + description: "Bundled Definition of Ready policy." + - path: development-guidelines.md + description: "Bundled development policy." + - path: documentation-guidelines.md + description: "Bundled documentation policy." + - path: git-commit-messaging.md + description: "Bundled git commit messaging policy." + - path: product-guidelines.md + description: "Bundled product policy." + - path: testing-guidelines.md + description: "Bundled testing policy." + - path: ui-ux-guidelines.md + description: "Bundled UI/UX policy." +invariants: + - "Policies in this directory are bundled defaults; repository overrides belong under .nomadworks/policies/." diff --git a/scripts/codemap.yml b/scripts/codemap.yml new file mode 100644 index 0000000..7ccd53a --- /dev/null +++ b/scripts/codemap.yml @@ -0,0 +1,12 @@ +scope: module +parent: ../codemap.yml +entrypoints: + - path: build.js + description: "Copies source files into dist for package distribution." + - path: resolve-release-version.js + description: "Determines release version metadata for release automation." +commands: + - command: npm run build + description: "Execute the package build script." +invariants: + - "Build output belongs in dist/ and should remain reproducible from source files." diff --git a/src/codemap.yml b/src/codemap.yml new file mode 100644 index 0000000..31c8fb3 --- /dev/null +++ b/src/codemap.yml @@ -0,0 +1,16 @@ +scope: module +parent: ../codemap.yml +entrypoints: + - path: index.js + description: "OpenCode plugin entrypoint, tool registration, onboarding, agent loading, and workflow helpers." +internals: + - path: validate_logic.js + description: "CodeMap and workflow artifact validation implementation used by the plugin and tests." +commands: + - command: npm test + description: "Run regression coverage for plugin and validation behavior." + - command: npm run build + description: "Build src files into dist/." +invariants: + - "Auto-onboarding must not overwrite existing repository NomadWorks configuration." + - "Filesystem scaffolding must respect onboarding mode and git-only auto-init settings." diff --git a/src/index.js b/src/index.js index 5111d52..d15604f 100644 --- a/src/index.js +++ b/src/index.js @@ -875,8 +875,10 @@ export default async function NomadWorksPlugin(input, options = {}) { } } repoCfg = applyTeamConfigRules(repoCfg); - scaffoldNomadworksReadmes(worktree); - syncGeneratedPolicies(worktree, repoCfg); + if (fs.existsSync(configPath)) { + scaffoldNomadworksReadmes(worktree); + syncGeneratedPolicies(worktree, repoCfg); + } const operatingTeamMode = getOperatingTeamMode(repoCfg); const startAndMonitorWorkflow = async (sessionId, pmaSessionId, initialText, taskPath = null) => { diff --git a/tests/codemap.yml b/tests/codemap.yml new file mode 100644 index 0000000..380cd8c --- /dev/null +++ b/tests/codemap.yml @@ -0,0 +1,12 @@ +scope: module +parent: ../codemap.yml +entrypoints: + - path: plugin.test.js + description: "Regression coverage for plugin onboarding and option resolution behavior." + - path: validate.test.js + description: "Regression coverage for NomadWorks validation logic." +commands: + - command: npm test + description: "Run all Jest tests." +invariants: + - "Safety regressions must cover onboarding opt-out, git-only defaults, and existing config preservation." diff --git a/tests/plugin.test.js b/tests/plugin.test.js index 26e16a4..e1dc215 100644 --- a/tests/plugin.test.js +++ b/tests/plugin.test.js @@ -13,6 +13,10 @@ function createEmptyGitWorktree() { return root; } +function createEmptyWorktree() { + return fs.mkdtempSync(path.join(os.tmpdir(), "nomadworks-auto-test-")); +} + describe("NomadWorks plugin auto-onboarding", () => { test("auto onboarding reads tuple plugin options from second argument", async () => { const worktree = createEmptyGitWorktree(); @@ -44,4 +48,46 @@ describe("NomadWorks plugin auto-onboarding", () => { const generatedConfig = YAML.parse(fs.readFileSync(path.join(worktree, ".nomadworks", "nomadworks.yaml"), "utf8")); expect(generatedConfig.team_mode).toBe("mini"); }); + + test("onboarding off does not scaffold repository files", async () => { + const worktree = createEmptyGitWorktree(); + + await NomadWorksPlugin({ worktree }, { + onboarding: "off", + default_team_mode: "mini" + }); + + expect(fs.existsSync(path.join(worktree, ".nomadworks"))).toBe(false); + expect(fs.existsSync(path.join(worktree, "codemap.yml"))).toBe(false); + }); + + test("default git-only auto onboarding does not scaffold outside a git worktree", async () => { + const worktree = createEmptyWorktree(); + + await NomadWorksPlugin({ worktree }, { + onboarding: "auto", + default_team_mode: "mini" + }); + + expect(fs.existsSync(path.join(worktree, ".nomadworks"))).toBe(false); + expect(fs.existsSync(path.join(worktree, "codemap.yml"))).toBe(false); + }); + + test("auto onboarding preserves an existing repository config", async () => { + const worktree = createEmptyGitWorktree(); + const configDir = path.join(worktree, ".nomadworks"); + const configPath = path.join(configDir, "nomadworks.yaml"); + const existingConfig = "team_mode: mini\nagents:\n product_manager:\n enabled: true\nfeatures:\n debug_logs: true\n"; + fs.mkdirSync(configDir, { recursive: true }); + fs.writeFileSync(configPath, existingConfig, "utf8"); + + await NomadWorksPlugin({ worktree }, { + onboarding: "auto", + default_team_mode: "full", + auto_init_git_repos_only: true + }); + + expect(fs.readFileSync(configPath, "utf8")).toBe(existingConfig); + expect(fs.existsSync(path.join(worktree, "codemap.yml"))).toBe(false); + }); });