Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ dist/
node_modules/
.DS_Store
*.tgz
.nomadworks/runtime/
.nomadworks/generated/
21 changes: 21 additions & 0 deletions agents/codemap.yml
Original file line number Diff line number Diff line change
@@ -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."
33 changes: 33 additions & 0 deletions codemap.yml
Original file line number Diff line number Diff line change
@@ -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."
12 changes: 6 additions & 6 deletions docs/core/technical_guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 19 additions & 1 deletion docs/setup/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`. 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.

## Minimal config

Expand Down
23 changes: 23 additions & 0 deletions policies/codemap.yml
Original file line number Diff line number Diff line change
@@ -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/."
12 changes: 12 additions & 0 deletions scripts/codemap.yml
Original file line number Diff line number Diff line change
@@ -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."
16 changes: 16 additions & 0 deletions src/codemap.yml
Original file line number Diff line number Diff line change
@@ -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."
162 changes: 95 additions & 67 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down Expand Up @@ -775,11 +841,29 @@ function getModePromptFragment(agentId, operatingTeamMode, worktree) {
return readResolvedFile(fragmentPath, worktree);
}

export default async function NomadWorksPlugin(input) {
export default async function NomadWorksPlugin(input, options = {}) {
const pluginOptions = {
...(options || {}),
...(input.config || {}),
...(input.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: {} };
Expand All @@ -791,8 +875,10 @@ export default async function NomadWorksPlugin(input) {
}
}
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) => {
Expand Down Expand Up @@ -863,72 +949,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.
Expand Down
12 changes: 12 additions & 0 deletions tests/codemap.yml
Original file line number Diff line number Diff line change
@@ -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."
Loading