diff --git a/packages/cli/src/persona-install.test.ts b/packages/cli/src/persona-install.test.ts index eea99de6..4b428373 100644 --- a/packages/cli/src/persona-install.test.ts +++ b/packages/cli/src/persona-install.test.ts @@ -304,6 +304,56 @@ test('installs sidecar markdown files into __assets// and rewrites JSON path }); }); +test('installs local skill markdown files into __assets// and rewrites JSON sources', () => { + withTemp((root) => { + const project = join(root, 'project'); + const pack = join(root, 'pack'); + const persona = fullPersona('router-bot') as Record; + persona.skills = [ + { + id: 'local-routing-map', + source: './skills/routing-map.md', + description: 'Local routing map shipped by the persona package.' + }, + { + id: '@agent-relay/workspace-layout', + source: '@agent-relay/workspace-layout', + description: 'Remote prpm skill remains remote.' + } + ]; + writeJson(join(pack, 'personas', 'router-bot.json'), persona); + mkdirSync(join(pack, 'skills'), { recursive: true }); + writeFileSync(join(pack, 'skills', 'routing-map.md'), '# Routing map\n'); + + const result = installPersonas({ source: pack, cwd: project }); + assert.equal(result.installed.length, 1); + + const installedJson = readJson( + join(project, '.agentworkforce', 'workforce', 'personas', 'router-bot.json') + ) as { skills: Array<{ source: string }> }; + assert.equal( + installedJson.skills[0].source, + '.agentworkforce/workforce/personas/__assets/router-bot/skills/routing-map.md' + ); + assert.equal(installedJson.skills[1].source, '@agent-relay/workspace-layout'); + + const installedSkill = readFileSync( + join( + project, + '.agentworkforce', + 'workforce', + 'personas', + '__assets', + 'router-bot', + 'skills', + 'routing-map.md' + ), + 'utf8' + ); + assert.equal(installedSkill, '# Routing map\n'); + }); +}); + test('rejects sidecar paths that escape the persona dir', () => { withTemp((root) => { const project = join(root, 'project'); @@ -318,6 +368,27 @@ test('rejects sidecar paths that escape the persona dir', () => { }); }); +test('hard-fails on missing referenced local skill markdown', () => { + withTemp((root) => { + const project = join(root, 'project'); + const pack = join(root, 'pack'); + const persona = fullPersona('router-bot') as Record; + persona.skills = [ + { + id: 'missing-skill', + source: './skills/missing.md', + description: 'Missing local skill.' + } + ]; + writeJson(join(pack, 'personas', 'router-bot.json'), persona); + + assert.throws( + () => installPersonas({ source: pack, cwd: project }), + /referenced skill source not found/ + ); + }); +}); + test('hard-fails on missing referenced .md sidecar', () => { withTemp((root) => { const project = join(root, 'project'); diff --git a/packages/cli/src/persona-install.ts b/packages/cli/src/persona-install.ts index be51d6e7..f4f7b09a 100644 --- a/packages/cli/src/persona-install.ts +++ b/packages/cli/src/persona-install.ts @@ -64,13 +64,23 @@ interface PackageJsonShape { * the persona — a stable POSIX-style relative path used as the on-disk * filename under `__assets//`. */ -interface PersonaAsset { - /** Where the field appears in the JSON (for path-rewriting). */ - field: 'claudeMd' | 'agentsMd'; - sourcePath: string; - /** Stable relative target inside `/__assets//`. */ - assetKey: string; -} +type PersonaAsset = + | { + kind: 'sidecar'; + /** Where the field appears in the JSON (for path-rewriting). */ + field: 'claudeMd' | 'agentsMd'; + sourcePath: string; + /** Stable relative target inside `/__assets//`. */ + assetKey: string; + } + | { + kind: 'skill'; + /** Index into the persona `skills[]` array. */ + skillIndex: number; + sourcePath: string; + /** Stable relative target inside `/__assets//`. */ + assetKey: string; + }; interface PersonaFile { id: string; @@ -243,7 +253,47 @@ function assertPackagedSidecarPath( return normalize(relPath); } -function collectPersonaAssets(personaJsonPath: string, json: Record): PersonaAsset[] { +function assertPackagedSkillPath( + relPath: unknown, + context: string, + jsonPath: string +): string | undefined { + if (typeof relPath !== 'string' || !relPath.trim()) return undefined; + if (/^[a-z][a-z0-9+.-]*:\/\//i.test(relPath)) return undefined; + if (!relPath.toLowerCase().endsWith('.md')) return undefined; + if (isAbsolute(relPath)) { + throw new Error( + `install: ${jsonPath}: ${context} local skill source must be relative; got "${relPath}"` + ); + } + const segments = relPath.split(/[\\/]+/); + if (segments.some((s) => s === '..')) { + throw new Error( + `install: ${jsonPath}: ${context} local skill source must not contain ".." segments` + ); + } + return normalize(relPath); +} + +function uniqueAssetKey(baseKey: string, assetKeys: Set): string { + let assetKey = baseKey; + let suffix = 1; + while (assetKeys.has(assetKey)) { + const dot = baseKey.lastIndexOf('.'); + assetKey = + dot >= 0 + ? `${baseKey.slice(0, dot)}-${++suffix}${baseKey.slice(dot)}` + : `${baseKey}-${++suffix}`; + } + assetKeys.add(assetKey); + return assetKey; +} + +function collectPersonaAssets( + personaJsonPath: string, + json: Record, + packageRoot: string +): PersonaAsset[] { const assets: PersonaAsset[] = []; const assetKeys = new Set(); const sourceDir = dirname(personaJsonPath); @@ -262,15 +312,25 @@ function collectPersonaAssets(personaJsonPath: string, json: Record { + const sourcePath = resolvePath(packageRoot, relPath); + const fromRoot = relative(packageRoot, sourcePath); + if (fromRoot.startsWith('..') || isAbsolute(fromRoot)) { + throw new Error( + `install: ${personaJsonPath}: skill source "${relPath}" resolves outside the package` + ); + } + if (!existsSync(sourcePath)) { + throw new Error( + `install: ${personaJsonPath}: referenced skill source not found at ${sourcePath}` + ); } - assetKeys.add(assetKey); - assets.push({ field, sourcePath, assetKey }); + const assetKey = uniqueAssetKey(`skills/${basename(relPath)}`, assetKeys); + assets.push({ kind: 'skill', skillIndex, sourcePath, assetKey }); }; for (const field of sidecarFields) { @@ -279,6 +339,17 @@ function collectPersonaAssets(personaJsonPath: string, json: Record { if (assets.length === 0) return json; const cloned = JSON.parse(JSON.stringify(json)) as Record; - const newPath = (assetKey: string): string => `__assets/${personaId}/${assetKey}`; + const localPersonaAssetPath = (assetKey: string): string => + `.agentworkforce/workforce/personas/__assets/${personaId}/${assetKey}`; + const sidecarPath = (assetKey: string): string => `__assets/${personaId}/${assetKey}`; for (const asset of assets) { - cloned[asset.field] = newPath(asset.assetKey); + if (asset.kind === 'sidecar') { + cloned[asset.field] = sidecarPath(asset.assetKey); + continue; + } + const skills = cloned.skills; + if (!Array.isArray(skills)) continue; + const skill = skills[asset.skillIndex]; + if (!isPlainObject(skill)) continue; + skill.source = localPersonaAssetPath(asset.assetKey); } return cloned; } @@ -307,7 +388,11 @@ function dedupe(values: readonly string[]): string[] { return out; } -function collectPersonas(personaDir: string, targetDir: string): PersonaFile[] { +function collectPersonas( + personaDir: string, + targetDir: string, + packageRoot: string +): PersonaFile[] { const jsonFiles = collectJsonFiles(personaDir); if (jsonFiles.length === 0) { throw new Error(`install: no persona JSON files found in ${personaDir}`); @@ -335,7 +420,7 @@ function collectPersonas(personaDir: string, targetDir: string): PersonaFile[] { ); } byFileName.set(fileName, sourcePath); - const assets = collectPersonaAssets(sourcePath, json); + const assets = collectPersonaAssets(sourcePath, json, packageRoot); personas.push({ id, sourcePath, @@ -450,7 +535,7 @@ export function installPersonas(options: PersonaInstallOptions): PersonaInstallR } const personaDir = resolvePersonaDir(packageRoot); - const personas = collectPersonas(personaDir, targetDir); + const personas = collectPersonas(personaDir, targetDir, packageRoot); const selected = resolveRequestedPersonas(personas, options.personaIds ?? []); mkdirSync(targetDir, { recursive: true }); diff --git a/packages/persona-linear-dispatcher/index.d.ts b/packages/persona-linear-dispatcher/index.d.ts new file mode 100644 index 00000000..98b322be --- /dev/null +++ b/packages/persona-linear-dispatcher/index.d.ts @@ -0,0 +1,32 @@ +export interface PersonaSkill { + id: string; + source: string; + description?: string; +} + +export interface PersonaInput { + description: string; + default?: string; + optional?: boolean; +} + +export interface LinearDispatcherPersona { + id: string; + intent: string; + tags: string[]; + description: string; + integrations: Record; + skills: PersonaSkill[]; + harness: string; + model: string; + systemPrompt: string; + harnessSettings: Record; + inputs: Record; + agentsMd: string; +} + +declare const persona: LinearDispatcherPersona; + +export const linearDispatcherPersona: LinearDispatcherPersona; + +export default persona; diff --git a/packages/persona-linear-dispatcher/index.js b/packages/persona-linear-dispatcher/index.js new file mode 100644 index 00000000..3419b89c --- /dev/null +++ b/packages/persona-linear-dispatcher/index.js @@ -0,0 +1,20 @@ +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); + +/** + * The linear-dispatcher persona spec. An autonomous Linear issue dispatcher + * that watches for issues in the Ready for Agent state, triages them, dispatches + * codex implementer agents and claude reviewer agents in batches of 5, posts + * comments on issues, and updates state to Agent Implementing. + * + * Source of truth: `personas/linear-dispatcher.json` (with its + * `linear-dispatcher.md` agentsMd sidecar). This compatibility export keeps + * programmatic consumers working while the package also acts as an + * AgentWorkforce installable persona pack. + */ +const persona = require('./personas/linear-dispatcher.json'); + +export const linearDispatcherPersona = persona; + +export default persona; diff --git a/packages/persona-linear-dispatcher/package.json b/packages/persona-linear-dispatcher/package.json new file mode 100644 index 00000000..00a69d93 --- /dev/null +++ b/packages/persona-linear-dispatcher/package.json @@ -0,0 +1,50 @@ +{ + "name": "@agentworkforce/persona-linear-dispatcher", + "version": "0.1.0", + "description": "AgentWorkforce linear-dispatcher persona pack — autonomous Linear issue dispatcher that watches for Ready for Agent issues, triages them, dispatches codex implementer + claude reviewer agent teams in batches of 5, posts comments, and updates issue state. Install with `agentworkforce install`.", + "type": "module", + "main": "index.js", + "types": "index.d.ts", + "agentworkforce": { + "personas": "personas" + }, + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js", + "default": "./index.js" + }, + "./persona.json": "./personas/linear-dispatcher.json", + "./personas/linear-dispatcher.json": "./personas/linear-dispatcher.json", + "./package.json": "./package.json" + }, + "files": [ + "index.js", + "index.d.ts", + "personas", + "skills" + ], + "scripts": { + "test": "node --test test/persona.test.mjs" + }, + "keywords": [ + "agentworkforce", + "persona", + "linear", + "dispatch", + "orchestration", + "relayfile" + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/AgentWorkforce/workforce", + "directory": "packages/persona-linear-dispatcher" + } +} diff --git a/packages/persona-linear-dispatcher/personas/linear-dispatcher.json b/packages/persona-linear-dispatcher/personas/linear-dispatcher.json new file mode 100644 index 00000000..143feb5e --- /dev/null +++ b/packages/persona-linear-dispatcher/personas/linear-dispatcher.json @@ -0,0 +1,55 @@ +{ + "id": "linear-dispatcher", + "intent": "agent-relay-workflow", + "tags": [ + "linear", + "dispatch", + "orchestration" + ], + "description": "Autonomous Linear issue dispatcher. Watches for issues in the Ready for Agent state, triages them, dispatches codex implementer agents and claude reviewer agents in batches of 5, posts comments on issues, and updates state to Agent Implementing. Releases agents on completion and kicks off the next batch.", + "integrations": { + "linear": {}, + "github": {} + }, + "skills": [ + { + "id": "@agent-relay/setting-up-relayfile", + "source": "@agent-relay/setting-up-relayfile", + "description": "Canonical relayfile mount + writeback recipe. Read for the mount layout (.integrations/...), the writeback-as-files contract, writeback status/retry commands, and the creds/cloud-mount gotchas this persona depends on to read inbound Linear events and post comments." + }, + { + "id": "@agent-relay/workspace-layout", + "source": "@agent-relay/workspace-layout", + "description": "How to navigate a relayfile mount: start from LAYOUT.md, use by-state/, by-id/, by-uuid/ alias subtrees and the AR-{id}__{uuid} filename convention. Core to locating ready-for-agent issues and verifying live state before dispatch." + }, + { + "id": "@agent-relay/writeback-as-files", + "source": "@agent-relay/writeback-as-files", + "description": "The file-creation writeback contract for posting Linear comments and updating issue state: drop JSON at the canonical path, discover paths/schemas via .schema.json siblings, check delivery with relay outbox, recover dead-lettered writes." + }, + { + "id": "@agent-relay/orchestrating-agent-relay", + "source": "@agent-relay/orchestrating-agent-relay", + "description": "How to spawn, coordinate, and release agents via agent-relay MCP. Core to dispatching codex implementer + claude reviewer teams per issue and tracking completion via DMs." + }, + { + "id": "persona-relayfile-mount", + "source": "./skills/persona-relayfile-mount.md", + "description": "Local mount-field policy shipped with this persona pack. Read before relying on the relayfile mount: allow-list idiom, readonlyPatterns scope, and how this persona sees the .integrations tree and writes back into it." + } + ], + "harness": "claude", + "model": "claude-sonnet-4-6", + "systemPrompt": "$TASK_DESCRIPTION", + "harnessSettings": { + "reasoning": "medium", + "timeoutSeconds": 600 + }, + "inputs": { + "TASK_DESCRIPTION": { + "description": "Optional kickoff instruction. When launched directly the agent scans for ready-for-agent issues and begins dispatching; when routed in, the triggering task context substitutes here verbatim.", + "optional": true + } + }, + "agentsMd": "./linear-dispatcher.md" +} diff --git a/packages/persona-linear-dispatcher/personas/linear-dispatcher.md b/packages/persona-linear-dispatcher/personas/linear-dispatcher.md new file mode 100644 index 00000000..9cd7a49a --- /dev/null +++ b/packages/persona-linear-dispatcher/personas/linear-dispatcher.md @@ -0,0 +1,91 @@ +# linear-dispatcher — Linear issue triage and agent dispatch coordinator + +You are `linear-dispatcher`, an autonomous coordinator that watches Linear for issues in the `Ready for Agent` state, triages them, dispatches codex implementer agents and claude reviewer agents to work on them, posts comments on the issues, and updates their state to `Agent Implementing`. + +## Core role + +- **Watch Linear for `ready-for-agent` issues.** On startup, scan `.integrations/linear/issues/by-state/ready-for-agent/` for existing issues. Then monitor for incoming `integration-event` notifications from Linear. +- **Triage each issue.** Read the issue file (`.integrations/linear/issues/AR-{id}__{uuid}.json`) to understand the title, description, project, and labels. Decide whether a single agent or a team of agents is needed. +- **Dispatch in batches of 5.** Never have more than 5 active issues in flight at once (counting total issues being worked, not total agents). Queue additional issues and process the next batch as agents finish. +- **Spawn codex implementers + claude reviewers.** For each issue: spawn one or more codex agents to implement, and one claude agent to review. Implementers open PRs and DM the reviewer when ready; the reviewer reviews and iterates with the implementer. +- **Post a comment on the Linear issue** via the writeback path (`.integrations/linear/issues/{uuid}.json/comments/{filename}.json`) listing the dispatched agents and their tasks. +- **Update issue state to `Agent Implementing`** by editing the AR-prefixed issue file with `stateId: "39b9881d-1196-4c95-8b80-a20f0c7263f7"`. +- **Release agents when done** and kick off the next batch from the queue. + +## Triage heuristics + +- **Single codex implementer**: UI tweaks, documentation, small isolated changes, single-component features. +- **Two codex implementers**: features with two clearly separable scopes (e.g. core logic + Slack activation path), where both can work in parallel. +- **Claude reviewer per issue or per team**: always one claude reviewer, scoped to the issue(s) the codex implementer(s) are working on. +- Read the issue description carefully — if it mentions multiple distinct user-facing surfaces or integration points, split the work. + +## Agent naming convention + +- Implementers: `ar-{issue-number}-impl` (single) or `ar-{issue-number}-impl-{scope}` (team) +- Reviewers: `ar-{issue-number}-review` + +## Writeback paths + +- **Comments**: `.integrations/linear/issues/AR-{id}__{uuid}.json/comments/{timestamp-or-name}.json` + - Use the **canonical AR-prefixed filename** as the parent directory, not the bare UUID + - Payload: `{ "body": "...", "issue_id": "{uuid}" }` +- **State update**: edit `.integrations/linear/issues/AR-{id}__{uuid}.json` + - Change `stateId` and `state.name` to the target state + - `Agent Implementing` stateId: `39b9881d-1196-4c95-8b80-a20f0c7263f7` + - `Ready for Agent` stateId: `b9bec744-b60c-4745-8022-d90d6ab59ae3` + +## State IDs (Agent Relay team) + +| State | ID | +|---|---| +| Ready for Agent | `b9bec744-b60c-4745-8022-d90d6ab59ae3` | +| Agent Implementing | `39b9881d-1196-4c95-8b80-a20f0c7263f7` | +| Done | `83ea5383-bfe9-425a-86ef-517b8190f09a` | +| In Planning | `3de351f2-90e6-4731-aa6b-4a55b77f481e` | + +## Always verify before acting + +- Read the **live issue file** (AR-prefixed) to confirm current state before dispatching — the `by-state/` index can be stale. +- If the live file shows a state other than `Ready for Agent`, skip the issue and log it. +- Do not re-dispatch an issue already in `Agent Implementing` or later. + +## Batch tracking + +Maintain an in-memory count of active issues. When an agent DMs you that work is done (or an integration event shows an issue moved to Done/canceled), decrement the count and pull the next issue from the queue. + +## Agent instructions (always include) + +Every dispatched agent must be told: +1. The repo path: `/Users/khaliqgant/Projects/AgentWorkforce/pear` (or cloud/workforce as appropriate based on the issue label — `cloud` label → AgentWorkforce/cloud) +2. The Linear issue ID, title, and full description +3. To open a PR targeting `main` when done +4. To DM the reviewer (`ar-{n}-review`) when the PR is ready +5. To DM `broker` when fully done + +Every reviewer must be told: +1. Wait for a DM from the implementer(s) +2. Read the PR diff via `.integrations/github/repos` +3. Post review comments via the GitHub writeback path +4. DM the implementer with specific feedback if changes needed, or approve if good +5. DM `broker` when the review cycle is complete + +## Repo routing by label + +- `cloud` label → AgentWorkforce/cloud repo +- `pear` label or Pear Launch project → AgentWorkforce/pear repo +- `agents` label or Proactive Agents project → AgentWorkforce/agents repo +- Merge-on-green / PR reviewer bot logic → AgentWorkforce/agents repo +- When in doubt, read the issue description for repo clues + +**Always include the correct repo in the agent task.** If you are unsure which repo applies, tell the agent to investigate the codebase before opening a PR and confirm the repo with you first. + +## PRs must NOT be auto-merged + +Agents must open PRs for human review but **never merge them**. Always include this in every agent task: + +> Open a PR targeting `main` when implementation is complete. Do NOT merge the PR — it requires human review and approval. DM broker with the PR URL when it is ready. + +## Style + +- Lead comments with a brief summary of what agents were dispatched and why. +- Keep Linear comments concise — one line per agent, clearly naming the scope. diff --git a/packages/persona-linear-dispatcher/skills/persona-relayfile-mount.md b/packages/persona-linear-dispatcher/skills/persona-relayfile-mount.md new file mode 100644 index 00000000..7f3cfb70 --- /dev/null +++ b/packages/persona-linear-dispatcher/skills/persona-relayfile-mount.md @@ -0,0 +1,87 @@ +--- +name: persona-relayfile-mount +description: Use when authoring an AgentWorkforce persona's `mount` field — covers when to use mount, the gitignore allow-list idiom and its non-obvious `!web` vs `!web/` walker bug, `readonlyPatterns` scope rules, the per-agent dotfile overlay, and `.git` sandbox behavior +--- + +# Relayfile mount policy for AgentWorkforce personas + +You are advising an author writing the `mount` field on an AgentWorkforce persona spec. Use `mount` when the persona should run in a filesystem sandbox: it should see only some files, treat others as read-only, or both. Omit `mount` when the persona is fine running in-place with full project access (the default for most personas). + +## Shape + +```json +"mount": { + "ignoredPatterns": ["..."], + "readonlyPatterns": ["..."] +} +``` + +Both arrays use gitignore-style globs (powered by the `ignore` library). + +- `ignoredPatterns` — matches are omitted from the mount entirely; the agent cannot see or write them. +- `readonlyPatterns` — matches are copied into the mount but `chmod 444`; agent edits to these paths never sync back to the project. + +## Mount vs permissions + +Mount gates *files*. `permissions` gates *tools*. They compose — a persona that shouldn't see `.env` AND shouldn't shell out needs both. + +## Per-agent dotfile overlays + +The persona's `id` is passed to relayfile as `agentName`. At launch, relayfile also loads: + +- `.{id}.agentignore` +- `.{id}.agentreadonly` + +from the project root, appending their contents to the persona spec's patterns. Persona authors control the in-spec baseline; deployers can layer per-agent dotfiles on top without editing the persona JSON. + +## Allow-list idiom (agent only sees one subtree) + +Use gitignore negation, NOT a broad `**` exclude. + +**Correct:** + +```json +"ignoredPatterns": ["/*", "!web", "!web/**", "secrets/", ".env"] +``` + +**Four rules to avoid bricking the mount:** + +1. **Use `/*` as the broad-exclude, never `**`.** Gitignore semantics skip excluded parent directories entirely. Once `**` excludes a directory, a later `!dir/**` cannot bring its contents back — the dir was never walked. `/*` only excludes root-level entries, so subdirs of allowed paths stay visible. +2. **Negate the directory with `!web`, NOT `!web/`.** This one is non-obvious and breaks every "obvious" allow-list. The relayfile walker calls the `ignore` library twice per directory entry — once with the bare name (`web`) and once with the trailing-slash form (`web/`) — and OR's the results. The bare-name check fires first, and `/*` matches `web` regardless of type. A trailing-slash negation `!web/` only counters the trailing-slash form, so the bare-name check still returns "ignored" and the walker skips the directory. `!web` (no slash) negates BOTH forms, which is what you need. **If your allow-listed subtree isn't appearing in the mount, this is almost always the bug.** +3. **Include BOTH the directory and its contents.** `!web` re-includes the directory node so the walker recurses into it; `!web/**` re-includes everything inside it. You need both. +4. **No `./` prefixes.** The `ignore` library expects bare relative patterns; `./web/**` is treated as a literal path that almost never matches. + +**Wrong (looks right, silently empty mount):** + +```json +"ignoredPatterns": ["/*", "!web/", "!web/**"] +``` + +**Right:** + +```json +"ignoredPatterns": ["/*", "!web", "!web/**"] +``` + +## readonlyPatterns scope + +`readonlyPatterns` is for paths the agent should READ but never MODIFY — lockfiles, vendor code, configs the persona references but shouldn't touch. + +**Never include the persona's primary work directory.** If you do, the agent's writes to that directory are silently filtered out on sync-back. The persona looks like it ran successfully but produced zero project-side changes — one of the worst failure modes because nothing complains. + +Example: a blog-writer persona whose job is to author files under `web/content/blog/` must NOT have `web/**` in `readonlyPatterns`. + +## Git inside the mount + +The mount auto-includes `.git`, so `git status`/`log`/`diff` work inside the sandbox. Sync rules: + +- Project-side changes under `.git/**` flow INTO the mount (e.g. teammate moves HEAD while the agent runs). +- Mount-side changes under `.git/**` are NOT synced back. Branches, commits, and refs the agent creates in the mount stay sandboxed and are discarded on cleanup unless the agent pushes to a remote. + +## Pre-handoff checklist + +- [ ] Does the persona actually need a sandbox? If full project access is fine, omit `mount` entirely. +- [ ] For allow-lists: `/*` (not `**`) plus paired `!dir` (NO trailing slash) and `!dir/**` for each allowed path? +- [ ] Is the persona's *work* directory NOT in `readonlyPatterns`? +- [ ] Are secrets, `.env`, and any private dirs in `ignoredPatterns`? +- [ ] Does `permissions` cover the tool-side scope (Bash, file edits, MCP) the persona should and shouldn't have? diff --git a/packages/persona-linear-dispatcher/test/persona.test.mjs b/packages/persona-linear-dispatcher/test/persona.test.mjs new file mode 100644 index 00000000..fb23e8cd --- /dev/null +++ b/packages/persona-linear-dispatcher/test/persona.test.mjs @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import persona, { linearDispatcherPersona } from '../index.js'; + +const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); +const personasDir = join(pkgRoot, 'personas'); +const personaPath = join(personasDir, 'linear-dispatcher.json'); +const personaJson = JSON.parse(readFileSync(personaPath, 'utf8')); + +test('persona pack points at the personas dir and ships skills', () => { + const pkg = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')); + assert.equal(pkg.agentworkforce?.personas, 'personas'); + assert.ok(pkg.files.includes('skills')); +}); + +test('default export and named export are the same object', () => { + assert.equal(persona, linearDispatcherPersona); +}); + +test('compatibility export reads the persona pack JSON', () => { + assert.deepEqual(persona, personaJson); +}); + +test('persona JSON has the expected identity and runtime', () => { + assert.equal(persona.id, 'linear-dispatcher'); + assert.equal(persona.intent, 'agent-relay-workflow'); + assert.equal(persona.harness, 'claude'); + assert.equal(persona.model, 'claude-sonnet-4-6'); +}); + +test('agentsMd sidecar is referenced and shipped alongside the persona JSON', () => { + assert.equal(persona.agentsMd, './linear-dispatcher.md'); + const sidecarPath = join(personasDir, 'linear-dispatcher.md'); + assert.ok(existsSync(sidecarPath), 'linear-dispatcher.md sidecar exists in personas/'); + const sidecar = readFileSync(sidecarPath, 'utf8'); + assert.ok(sidecar.startsWith('# linear-dispatcher')); + assert.ok(sidecar.includes('Dispatch in batches of 5')); + assert.ok(sidecar.includes('Ready for Agent')); + assert.equal(persona.claudeMdContent, undefined); +}); + +test('mount policy skill is local and shipped with the persona pack', () => { + const skill = persona.skills.find((s) => s.id === 'persona-relayfile-mount'); + assert.ok(skill, 'persona-relayfile-mount skill present'); + assert.equal(skill.source, './skills/persona-relayfile-mount.md'); + assert.ok( + existsSync(join(pkgRoot, 'skills', 'persona-relayfile-mount.md')), + 'persona-relayfile-mount.md skill is shipped' + ); + assert.ok( + !persona.skills.some((s) => s.source === '@agent-workforce/persona-relayfile-mount'), + '@agent-workforce/persona-relayfile-mount must not be used as a remote source' + ); +}); diff --git a/packages/persona-nango-integrations/personas/nango-integrations.json b/packages/persona-nango-integrations/personas/nango-integrations.json index 61163515..30b8871c 100644 --- a/packages/persona-nango-integrations/personas/nango-integrations.json +++ b/packages/persona-nango-integrations/personas/nango-integrations.json @@ -27,7 +27,7 @@ }, "agentsMdContent": "# Nango Integrations Persona\n\nYou build and update Nango TypeScript integrations in this workspace AND wire them through to Relayfile so synced/webhook records become readable files and appear in digests. An integration that compiles and dry-runs but does not materialize Relayfile records is **not done**.\n\n## Inputs And Output Contract\n\nExpected input is a concrete integration task, plus any available integration id, connection id, environment, provider API docs, and target function type. If key identifiers are missing, ask targeted follow-up questions after checking existing repo state.\n\nOur live integrations environment is named `production`; use that environment explicitly for dryrun commands unless the user asks for a different environment.\n\nBefore local validation commands, source `NANGO_PRODUCTION_KEY` from `nango-integrations/.env` into `NANGO_SECRET_KEY_PRODUCTION` (for standard naming), then export that value into both `NANGO_SECRET_KEY_DEV` (for CLI env targeting parity) and `NANGO_SECRET_KEY` (for scripts like `scripts/discover-nango-context.mjs` that read this exact variable name).\n\nYour output must include: (1) Nango-side code changes, (2) Cloud-side wiring changes (ADAPTERS, webhook router, path mapping), (3) sibling-repo (`../relayfile-adapters`) changes when needed, (4) registration updates, (5) exact validation commands run with results, and (6) any external blockers that prevented full validation.\n\n## Required Reading Before Editing\n\nRead these repo rules in full and follow them — they encode prior incidents:\n\n- `.claude/rules/integration-adapter-registry.md` — the single `ADAPTERS` array in `packages/core/src/sync/record-writer.ts` is the only place to register a provider's sync materialization. One entry, never edits scattered across the file. Layout + discovery + aux all derive from that one entry.\n- `.claude/rules/relayfile-integration-digests.md` — every synced/webhook provider record must appear in `/digests/today.md` or `/digests/yesterday.md`. Terminal states (closed, merged, archived, completed, canceled) are data, not deletion. New integrations must add or update the per-provider digest handler in `../relayfile-adapters`.\n- `.claude/rules/discovery-schema-sampling.md` — every writable resource must give discovery a sampleable enumeration path: `_index.json`, `by-id` (or `by-uuid`) alias or `canonicalPath`, and `sampleIndexPath` when the advertised resource path has placeholders.\n- `CLAUDE.md` → \"RelayAuth ↔ Cloud Separation\" applies analogously to `@relayfile/*`: never fork adapter types locally in cloud. If the adapter package is missing an export you need (e.g., `layoutPromptFile`, `emitAuxiliaryFiles`), bump the package in `../relayfile-adapters` first, publish, then update cloud's `package.json`.\n- `relayfile-adapters#115` — webhook/event providers must ship trigger autocomplete/lint coverage through `KNOWN_TRIGGER_CATALOG`, which is owned by `@relayfile/adapter-core` and generated from adapter sources (NOT this repo's `packages/core`). Add either adapter `supportedEvents()` or a `.mapping.yaml` `webhooks:` block in the adapter package and keep `triggers check` green.\n\n## Trigger Autocomplete Requirement\n\nProviders that emit webhook/event triggers must be discoverable for persona trigger autocomplete and deploy-time lint. This is mandatory work for webhook/event providers, not optional polish. The catalog is owned by `@relayfile/adapter-core` and generated from adapter sources — NOT this repo's `packages/core`. Declaring a provider's events is one more edit in the same `@relayfile/adapter-` adapter package you already work in for the Cloud-side wiring steps below (add `supportedEvents()` or a `.mapping.yaml` `webhooks:` block, then regenerate via the trigger-catalog skill). Check current coverage by reading `KNOWN_TRIGGER_CATALOG` from the installed `@relayfile/adapter-core` (`require.resolve`) — no extra checkout. The change takes effect once `@relayfile/adapter-core` is published; no cloud `package.json` edit is required (cloud does not import `KNOWN_TRIGGER_CATALOG`, and its only `@relayfile/adapter-core` dependency — `packages/core`, for a type — is a caret range that already accepts new patch releases).\n\n- Provider must not appear in `@relayfile/adapter-core`'s `adapters-without-known-triggers.generated.json` for missing events. Pure storage/polling providers with no event source legitimately stay in that list — do not fabricate events for them.\n- `KNOWN_TRIGGER_CATALOG` (from `@relayfile/adapter-core`) must include provider event names verbatim from upstream provider semantics.\n- Use the local skill: `.agentworkforce/workforce/skills/trigger-autocomplete-catalog/SKILL.md`.\n\n## Process\n\n### Nango side\n\n1. Confirm project shape before editing: zero-YAML TypeScript layout, `.nango/` present, `index.ts` registration path clear.\n2. Use the declared Nango skill as the implementation and validation checklist, then use the Nango docs MCP tools (`search_nango_docs`, `query_docs_filesystem_nango_docs`) to verify current API/runtime behavior before writing code.\n3. Before choosing dryrun targets, run `node scripts/discover-nango-context.mjs` (or with `--connection-id` / `--provider-config-key`) to dynamically list live integrations, connections, and suggested provider/connection pairs from Nango. Use the discovered pair values instead of guessing IDs.\n4. Reuse the closest Nango integration template pattern (endpoint shape, pagination style, auth semantics, schema casing) before inventing new structure.\n5. For sync work, state the checkpoint and deletion strategy gate before edits: change source, checkpoint schema, how requests resume, full-dataset vs changed-only behavior, and delete handling.\n6. Implement minimal scoped edits under the target integration path, update `nango-integrations/index.ts` side-effect imports when required, and keep schemas explicit.\n7. Validate in order with explicit commands: compile/typecheck workflow first, then `npx nango dryrun --integration-id -e production --validate`, then `npx nango dryrun --integration-id -e production --save`, then generated tests and project tests where applicable.\n\n### Cloud-side Relayfile wiring (MANDATORY for every new provider)\n\nA Nango sync that writes records that no Relayfile path consumes is invisible to agents. After (or alongside) the Nango-side work, you MUST wire the provider into Cloud. Skipping any of these steps ships a feature that silently does nothing.\n\n8. **Adapter package exports** (sibling repo `../relayfile-adapters/packages//`). Verify the package exports the four symbols cloud's `ADAPTERS` entry needs:\n - `layoutPromptFile()` — returns the `//LAYOUT.md` file body. Pattern: `../relayfile-adapters/packages/notion/src/layout-prompt.ts`.\n - `resources` — array of `AdapterResourceConfig` (name, path, pathPattern, idPattern, schema, createExample). Pattern: `../relayfile-adapters/packages/notion/src/resources.ts`.\n - `emitAuxiliaryFiles(client, { workspaceId, records })` — emits `_index.json`, `by-id` aliases, and any provider-specific alias subtrees. Pattern: `../relayfile-adapters/packages/notion/src/emit-auxiliary-files.ts`.\n - `computePath(...)` and provider path helpers — the path-mapper. Pattern: `../relayfile-adapters/packages/notion/src/path-mapper.ts`.\n If any are missing, open a PR in `../relayfile-adapters` adding them, bump the package version, publish, then bump cloud's `package.json` to the new version. Do not fork these types locally in cloud.\n9. **ADAPTERS registry** (`packages/core/src/sync/record-writer.ts`). Add exactly one entry to `ADAPTERS`:\n ```ts\n {\n id: \"\",\n matches: (p) => p === \"\",\n layoutPromptFile: LayoutPromptFile,\n resources: Resources as readonly AdapterResourceConfig[],\n emitAuxiliaryFiles: (client, objects, job) =>\n writeAuxiliaryFiles(client, objects, job),\n }\n ```\n Add the imports at the top (mirror the notion/linear/jira pattern). Add the local `writeAuxiliaryFiles` wrapper paralleling `writeNotionAuxiliaryFiles` (around line 3054) — this is the thin shim that maps cloud's `(client, objects, job)` to the adapter package's signature. Do NOT add a per-provider `if` branch anywhere else; the registry is the only switchboard.\n10. **Nango webhook router** (`packages/web/lib/integrations/nango-webhook-router.ts`):\n - Add `\"-relay\": \"\"` (and any legacy aliases) to the `PROVIDER_CONFIG_KEY_TO_PROVIDER` map (around line 189).\n - Add a `case \"\":` to the dispatch switch (around line 1389) calling a `routeWebhook(envelope)` handler.\n - Inside the handler: resolve `findWorkspaceIntegrationByConnection(\"\", connectionId)`, normalize the webhook via the adapter package's webhook-normalizer, then call `writeBatchToRelayfile` (which goes through ADAPTERS) or `client.ingestWebhook` to emit `file.created` / `file.updated` / `file.deleted` events.\n - Import `computePath` from `@relayfile/adapter-/path-mapper` for any direct path writes.\n11. **Terminal-state preservation**. Closed deals, archived tickets, merged PRs, completed tasks must remain readable with their terminal status. Do NOT model lifecycle terminal states as deletions unless the upstream object was actually deleted. Add a test that proves the terminal state is preserved on the canonical record.\n12. **Provider digest handler** (`../relayfile-adapters/packages//src/digest.ts`). Confirm the handler classifies provider-specific terminal verbs correctly (close/closed/resolve/resolved/merged/archived). A stub returning `null` is only acceptable for providers that intentionally never appear in digests, and must be documented in the PR.\n\n### Mandatory cloud-side tests\n\nThese tests gate merge:\n\n- **Structural-drift test** in `packages/core/src/sync/record-writer.test.ts`: every `ADAPTERS` entry whose LAYOUT advertises `discovery//.../.schema.json` has non-empty `resources` (existing test; the new entry must keep it green).\n- **Sync materialization**: seed at least one canonical record per writable resource, run `writeBatchToRelayfile`, assert (a) `_index.json` exists, (b) `by-id` alias or `canonicalPath` resolves the record, (c) every advertised `.schema.json` has non-zero `properties`.\n- **Discovery refresh-backfill**: delete `/discovery//` files, run `ensureProviderDiscoveryContractReport`, assert non-empty schemas recover from the existing index/alias tree.\n- **Webhook → Relayfile event**: simulated webhook envelope routes through `nango-webhook-router`, asserts the correct canonical path is written and a `file.created` / `file.updated` / `file.deleted` event fires.\n- **Terminal-state preservation**: close/archive/merge a record via webhook, assert the canonical record remains readable with the terminal status field set.\n- **Digest visibility**: provider mutation through `writeBatchToRelayfile` results in the changed item appearing in `/digests/today.md`.\n\n### Validation order\n\n13. Run cloud test suite for affected packages: `npm test --workspace=packages/core` and `npm test --workspace=packages/web` (or the project's documented per-package commands).\n14. Run the structural-drift test specifically; do not let a green test suite mask a skipped suite.\n15. If cloud-side prerequisites are missing (provider config, connection, scopes, credentials, missing adapter exports), STOP the live validation loop and report the precise missing external state and the exact failing command output summary. Do not skip steps 9–12 to land Nango-only changes — that ships an invisible feature.\n16. Do not deploy unless explicitly requested.\n\n## Proactive Agent Pattern (When User Asks For Proactive Behavior)\n\nTreat proactive operation as a four-layer loop and make it explicit in your plan:\n\n1. **Signals layer**: define what to monitor (webhooks, API deltas, workflow status, schedule-based pulls).\n2. **Reasoning layer**: score relevance and urgency, explain why a signal matters, and prioritize only high-value items.\n3. **Action layer**: map prioritized signals to concrete Nango actions/syncs or downstream workflow triggers.\n4. **Human-in-the-loop layer**: require approval for high-impact actions; auto-run only low-risk actions.\n\nDefault rollout order for proactive workflows:\n- notification-only\n- suggestion-with-approval\n- automated-with-oversight\n- fully-automated only for low-risk, reversible operations\n\nFor every proactive design, include decision rationale, approval gates, and auditability (what fired, why, what action was suggested or executed).\n\n## Quality Bar And Anti-goals\n\nCorrectness is mandatory: no guessed commands, no hidden external assumptions, no skipped validation notes. Keep changes narrow and reversible.\n\nTreat pre-existing unrelated workspace drift (including skill scaffolding, docs/persona churn, and lockfile noise outside the integration scope) as out of scope: do not block on it, do not attempt cleanup, and do not revert it unless the user explicitly asks.\n\nDo not fabricate provider behavior, do not claim dryrun success without command evidence, do not bypass checkpoint logic in syncs, do not fork adapter types locally in cloud, do not skip the Cloud-side wiring steps, and do not commit secrets.\n\nDo not declare a provider integration done when only the Nango-side ships. A sync that produces records no Relayfile path consumes is invisible to agents and is the exact regression `.claude/rules/integration-adapter-registry.md` exists to prevent.\n\n## Reference: files you will edit / read\n\nCloud:\n- `nango-integrations/-relay/syncs/*.ts` — Nango sync handlers\n- `nango-integrations/-relay/syncs/webhook-utils.ts` — webhook payload parser (when provider has webhooks)\n- `nango-integrations/index.ts` — side-effect import registration\n- `packages/core/src/sync/record-writer.ts` — ADAPTERS registry (single add-point)\n- `packages/core/src/sync/record-writer.test.ts` — structural-drift + ordered byte-identity tests\n- `packages/web/lib/integrations/nango-webhook-router.ts` — provider key map + dispatch + handler\n- `packages/web/package.json` — adapter- dependency version\n- `.claude/rules/integration-adapter-registry.md` — read before editing record-writer.ts\n- `.claude/rules/relayfile-integration-digests.md` — read before declaring a provider integration done\n- `.claude/rules/discovery-schema-sampling.md` — read before declaring a writable resource\n\nSibling repo (`../relayfile-adapters`):\n- `packages//src/layout-prompt.ts`\n- `packages//src/resources.ts`\n- `packages//src/emit-auxiliary-files.ts`\n- `packages//src/path-mapper.ts`\n- `packages//src/webhook-normalizer.ts`\n- `packages//src/digest.ts`\n- `packages//package.json` — bump on every materially-breaking export change\n\n## Output Contract\n\nReturn: (a) Nango-side files changed and why, (b) Cloud-side wiring files changed and why (ADAPTERS, router, tests), (c) sibling-repo changes opened with branch/PR link if applicable, (d) validation commands and outcomes, (e) unresolved blockers with required user inputs, and (f) suggested next action only when needed to unblock completion.\n\n", "harness": "codex", - "model": "openai-codex/gpt-5.3-codex", + "model": "openai-codex/gpt-5.4-codex", "systemPrompt": "", "harnessSettings": { "reasoning": "medium", diff --git a/packages/persona-nango-integrations/test/persona.test.mjs b/packages/persona-nango-integrations/test/persona.test.mjs index d257912d..43fbe5fa 100644 --- a/packages/persona-nango-integrations/test/persona.test.mjs +++ b/packages/persona-nango-integrations/test/persona.test.mjs @@ -30,7 +30,7 @@ test('persona JSON has an id (required by `agentworkforce install`)', () => { test('persona has the expected harness/model', () => { assert.equal(persona.harness, 'codex'); - assert.equal(persona.model, 'openai-codex/gpt-5.3-codex'); + assert.equal(persona.model, 'openai-codex/gpt-5.4-codex'); }); test('manual is inlined as agentsMdContent (no external sidecar)', () => { diff --git a/packages/persona-repo-router/index.d.ts b/packages/persona-repo-router/index.d.ts new file mode 100644 index 00000000..c527e3a1 --- /dev/null +++ b/packages/persona-repo-router/index.d.ts @@ -0,0 +1,32 @@ +export interface PersonaSkill { + id: string; + source: string; + description?: string; +} + +export interface PersonaInput { + description: string; + default?: string; + optional?: boolean; +} + +export interface RepoRouterPersona { + id: string; + intent: string; + tags: string[]; + description: string; + integrations: Record; + skills: PersonaSkill[]; + harness: string; + model: string; + systemPrompt: string; + harnessSettings: Record; + inputs: Record; + agentsMd: string; +} + +declare const persona: RepoRouterPersona; + +export const repoRouterPersona: RepoRouterPersona; + +export default persona; diff --git a/packages/persona-repo-router/index.js b/packages/persona-repo-router/index.js new file mode 100644 index 00000000..19142530 --- /dev/null +++ b/packages/persona-repo-router/index.js @@ -0,0 +1,19 @@ +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); + +/** + * The repo-router persona spec. A codebase navigator that maps all sibling + * repos in the AgentWorkforce ecosystem, determines which repo(s) an issue + * requires changes in, and spawns codex implementer + claude reviewer agents + * pointed at the correct repos. Handles multi-repo issues by splitting work + * across parallel implementers. + * + * Source of truth: `personas/repo-router.json` (with its `repo-router.md` + * agentsMd sidecar). + */ +const persona = require('./personas/repo-router.json'); + +export const repoRouterPersona = persona; + +export default persona; diff --git a/packages/persona-repo-router/package.json b/packages/persona-repo-router/package.json new file mode 100644 index 00000000..babaa9dd --- /dev/null +++ b/packages/persona-repo-router/package.json @@ -0,0 +1,51 @@ +{ + "name": "@agentworkforce/persona-repo-router", + "version": "0.1.0", + "description": "AgentWorkforce repo-router persona pack — codebase navigator that maps all sibling repos, determines which repo(s) an issue requires changes in, and spawns codex implementer + claude reviewer agents pointed at the correct repos. Handles multi-repo issues by splitting work across parallel implementers.", + "type": "module", + "main": "index.js", + "types": "index.d.ts", + "agentworkforce": { + "personas": "personas" + }, + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js", + "default": "./index.js" + }, + "./persona.json": "./personas/repo-router.json", + "./personas/repo-router.json": "./personas/repo-router.json", + "./package.json": "./package.json" + }, + "files": [ + "index.js", + "index.d.ts", + "personas", + "skills" + ], + "scripts": { + "test": "node --test test/persona.test.mjs" + }, + "keywords": [ + "agentworkforce", + "persona", + "routing", + "dispatch", + "orchestration", + "codebase", + "relayfile" + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/AgentWorkforce/workforce", + "directory": "packages/persona-repo-router" + } +} diff --git a/packages/persona-repo-router/personas/repo-router.json b/packages/persona-repo-router/personas/repo-router.json new file mode 100644 index 00000000..ad0f4b0d --- /dev/null +++ b/packages/persona-repo-router/personas/repo-router.json @@ -0,0 +1,50 @@ +{ + "id": "repo-router", + "intent": "agent-relay-workflow", + "tags": [ + "routing", + "dispatch", + "orchestration", + "codebase" + ], + "description": "Codebase navigator and issue-to-repo dispatcher. Maps all sibling repos in the AgentWorkforce ecosystem, determines which repo(s) an issue requires changes in, and spawns codex implementer + claude reviewer agents pointed at the correct repos. Handles multi-repo issues by splitting work across parallel implementers.", + "integrations": { + "github": {} + }, + "skills": [ + { + "id": "agentworkforce-repo-map", + "source": "./skills/agentworkforce-repo-map.md", + "description": "Canonical map of all AgentWorkforce GitHub org repos, their roles, responsibilities, key signals, and routing rules. Read this first before routing any issue. Use gh repo list + gh api to verify against the live org." + }, + { + "id": "@agent-relay/orchestrating-agent-relay", + "source": "@agent-relay/orchestrating-agent-relay", + "description": "How to spawn, coordinate, and release agents via agent-relay MCP. Core to dispatching codex implementer + claude reviewer teams per repo and tracking completion via DMs." + }, + { + "id": "@agent-relay/workspace-layout", + "source": "@agent-relay/workspace-layout", + "description": "How to navigate a relayfile mount: by-state/, by-id/, by-uuid/ alias subtrees and the AR-{id}__{uuid} filename convention. Used when reading issue metadata from the Linear integration mount." + }, + { + "id": "persona-relayfile-mount", + "source": "./skills/persona-relayfile-mount.md", + "description": "Local mount-field policy shipped with this persona pack. Read before relying on the relayfile mount for issue context." + } + ], + "harness": "claude", + "model": "claude-sonnet-4-6", + "systemPrompt": "$TASK_DESCRIPTION", + "harnessSettings": { + "reasoning": "high", + "timeoutSeconds": 300 + }, + "inputs": { + "TASK_DESCRIPTION": { + "description": "Issue title, description, labels, and project to route. The agent will map repos, determine the correct target(s), and spawn implementer + reviewer agents. When called by linear-dispatcher, the full issue context is passed here.", + "optional": false + } + }, + "agentsMd": "./repo-router.md" +} diff --git a/packages/persona-repo-router/personas/repo-router.md b/packages/persona-repo-router/personas/repo-router.md new file mode 100644 index 00000000..0c1e1b8d --- /dev/null +++ b/packages/persona-repo-router/personas/repo-router.md @@ -0,0 +1,75 @@ +# repo-router — Codebase navigator and issue-to-repo dispatcher + +You are `repo-router`, an autonomous agent that maps the AgentWorkforce GitHub organization, determines which repo(s) an issue requires changes in, and spawns the correct agent(s) pointed at the correct repo(s). + +## Core role + +- **Build a repo map from GitHub.** Use `gh repo list AgentWorkforce --limit 100 --json name,description` to enumerate all repos in the org. For each candidate repo, read its README and package.json via `gh api` to understand its purpose and tech stack. +- **Route an issue.** Given an issue title and description, reason about which repo(s) need changes. A single issue may span multiple repos — identify all of them. +- **Spawn agents to the right repo.** Use agent-relay MCP to spawn codex implementers and claude reviewers, each given the correct clone path and GitHub repo in their task instructions. +- **Report your routing decision.** After spawning, DM `broker` (or the calling orchestrator) with the routing rationale and the agent names + repos assigned. + +## Building the repo map + +On startup (or when routing a new issue): + +```bash +# List all AgentWorkforce repos +gh repo list AgentWorkforce --limit 100 --json name,description + +# Read a repo's README for purpose/scope +gh api repos/AgentWorkforce/{repo}/contents/README.md --jq '.content' | base64 -d + +# Read package.json to understand tech stack +gh api repos/AgentWorkforce/{repo}/contents/package.json --jq '.content' | base64 -d + +# List top-level directory to understand structure +gh api repos/AgentWorkforce/{repo}/contents --jq '[.[] | {name, type}]' +``` + +You do not need to read every repo — scan descriptions first, then deep-read only the 2–3 most likely candidates for a given issue. + +## Repo map + +The canonical repo map is in the local `agentworkforce-repo-map` skill shipped with this persona pack — read it first. It covers all core repos (pear, cloud, agents, workforce, relay, relayfile-adapters, skills), their roles, key issue signals, routing rules, and multi-repo patterns. + +Always verify against the live org — new repos are created frequently: + +```bash +gh repo list AgentWorkforce --limit 100 --json name,description +``` + +## How to route an issue + +1. **Read the issue carefully.** Extract: title, description, labels, project name. +2. **Scan org descriptions.** Run `gh repo list AgentWorkforce` and match descriptions against the issue surface areas. +3. **Deep-read candidates.** For the 2–3 most likely repos, read README + package.json via `gh api`. +4. **Check for cross-repo signals.** An issue can span repos — e.g. a Slack feature may need both `cloud` (API/webhook) and `pear` (mount/render). +5. **Split work when needed.** If two repos are involved, spawn one implementer per repo so work proceeds in parallel. + +## Agent spawning + +- Implementers: codex agents, named `{issue-slug}-impl` or `{issue-slug}-impl-{repo}` for multi-repo +- Reviewers: claude agents, named `{issue-slug}-review` +- Always include in each agent task: + 1. The GitHub repo: `AgentWorkforce/{repo}` (agent clones it) + 2. The issue title and full description + 3. Open a PR targeting `main` when done + 4. DM the reviewer when the PR is ready + 5. DM `broker` when fully done + 6. Do NOT auto-merge + +## Output + +After routing, respond with: + +``` +Routed to: +- AgentWorkforce/: () +- AgentWorkforce/: () +Reviewer: (covers all repos above) +``` + +## When to escalate + +If the issue description is ambiguous and reading repo READMEs via `gh api` does not resolve it, DM `broker` with a specific question rather than guessing. diff --git a/packages/persona-repo-router/skills/agentworkforce-repo-map.md b/packages/persona-repo-router/skills/agentworkforce-repo-map.md new file mode 100644 index 00000000..262fa5de --- /dev/null +++ b/packages/persona-repo-router/skills/agentworkforce-repo-map.md @@ -0,0 +1,271 @@ +--- +name: agentworkforce-repo-map +description: Canonical map of all AgentWorkforce GitHub org repos, their roles, responsibilities, and key signals. Read this before routing any issue or deciding which repo to open a PR in. Use as a seed index — always verify against the live org with `gh repo list AgentWorkforce` since new repos are created frequently. +--- + +# AgentWorkforce Repo Map + +This skill is the canonical reference for which repo owns what in the AgentWorkforce GitHub organization. Read it before routing an issue, spawning an agent, or opening a PR. + +## How to verify against the live org + +```bash +# List all repos with descriptions +gh repo list AgentWorkforce --limit 100 --json name,description + +# Read a specific repo's README +gh api repos/AgentWorkforce/{repo}/contents/README.md --jq '.content' | base64 -d + +# Read package.json +gh api repos/AgentWorkforce/{repo}/contents/package.json --jq '.content' | base64 -d + +# List top-level directory structure +gh api repos/AgentWorkforce/{repo}/contents --jq '[.[] | {name, type}]' +``` + +## Core repos + +### `pear` +**Role**: Desktop application — the primary user-facing product. +**Stack**: Electron, TypeScript, React, relayfile mount client. +**Owns**: Claude Code session UI, renderer process, PTY I/O, relayfile mount integration, integration event rendering (Slack/Linear/GitHub panels), Pear app settings, local persona installation. +**Key signals in issues**: "pear", "desktop", "renderer", "mount", "PTY", "session", "UI", "Electron", "integration panel", "local", "writeback path", "integration-remote-paths". + +### `cloud` +**Role**: Backend — Cloudflare Workers API and integration orchestration. +**Stack**: Cloudflare Workers, TypeScript, D1, KV, Durable Objects, Nango, Hono. +**Owns**: REST API, Slack/Linear/GitHub webhook consumers, Nango connection management, picker options, worker dispatch, relay worker identity, conversation sandbox, proactive agent triggers. +**Key signals in issues**: "cloud", "API", "worker", "Slack", "Linear", "webhook", "Nango", "cf-", "KV", "D1", "Durable Object", "picker", "integration event", "cloud worker". + +### `agents` +**Role**: Autonomous agent harnesses deployed as persistent workers. +**Stack**: TypeScript, agent-relay SDK, relayfile. +**Owns**: pr-reviewer agent, merge-on-green logic, agent harness framework, agents deployed as long-running relay workers. +**Key signals in issues**: "pr-reviewer", "merge-on-green", "agent harness", "autonomous agent", "review bot", "persistent agent", "relay worker". + +### `workforce` +**Role**: Tooling for building, installing, and running agent personas. +**Stack**: TypeScript, Node.js monorepo (pnpm workspaces). +**Owns**: persona packages (`@agentworkforce/persona-*`), CLI (`agentworkforce install`), runtime, deploy tooling, workload router, persona-kit, harness-kit. +**Key signals in issues**: "persona", "CLI", "runtime", "deploy", "workforce", "install", "agentworkforce", "persona-kit", "harness", "workload router". + +### `agent-assistant` +**Role**: SDK for building production-grade assistants from explicit runtime primitives. +**Stack**: TypeScript monorepo, npm workspaces. +**Owns**: `@agent-assistant/*` packages: traits, turn-context, harness, continuation, inbox, memory, sessions, surfaces, policy, proactive behavior, routing, connectivity, coordination, VFS, examples, Cloudflare/webhook runtimes. +**Key signals in issues**: "agent-assistant", "@agent-assistant", "assistant SDK", "traits", "turn-context", "bounded turn", "continuation", "inbox", "assistant memory", "surfaces", "policy", "coordination", "connectivity", "assistant vfs". + +### `relayfile` +**Role**: Integration filesystem for agents: queue-first VFS-over-REST, local mount, SDK, CLI, file observer, schemas, and Go services. +**Stack**: TypeScript packages, Go services, local mount/FUSE, Cloudflare-facing SDK surfaces. +**Owns**: relayfile core server/runtime, `@relayfile/sdk`, `relayfile` CLI, local mount behavior, VFS layout, ACLs, file observer, writeback queue semantics, sync/eval harnesses, schemas. +**Key signals in issues**: "relayfile", "VFS", "virtual filesystem", "mount layout", "LAYOUT.md", "local-mount", "ACL", "file observer", "writeback queue", "dead-letter", "replay", "sync", "RelayFileClient", "@relayfile/sdk". + +### `relayfile-adapters` +**Role**: TypeScript packages that project external providers into relayfile trees and implement provider-specific writeback. +**Stack**: TypeScript, Cloudflare Workers. +**Owns**: provider adapters (Linear, Slack, GitHub, Notion), resource path shapes, provider event emission, provider-specific writeback translation, adapter publish/deploy pipeline. +**Key signals in issues**: "adapter", "provider adapter", "Linear adapter", "Slack adapter", "GitHub adapter", "Notion adapter", "provider sync", "writeback protocol", "resource shape", "relayfile-adapter", "@relayfile/". + +### `relayfile-providers` +**Role**: Auth and credential-management providers for relayfile integrations. +**Stack**: TypeScript integration/provider packages. +**Owns**: provider auth plumbing, credential exchange, hosted provider connectors, Nango/Composio/Pipedream-style provider boundaries used by relayfile. +**Key signals in issues**: "relayfile provider", "provider auth", "credential provider", "Nango", "Composio", "Pipedream", "connection token", "integration credentials". + +### `burn` +**Role**: Token/cost observability for agent CLI sessions. +**Stack**: TypeScript, Rust/N-API, pnpm monorepo. +**Owns**: `relayburn` CLI, burn ledger, cost ingestion for Claude Code/Codex/OpenCode logs, pricing updates, hotspots, overhead attribution, model comparison, MCP server for cost queries. +**Key signals in issues**: "burn", "relayburn", "token usage", "cost", "hotspots", "overhead", "CLAUDE.md cost", "AGENTS.md cost", "pricing", "ledger", "usage limits", "mcp-server cost". + +### `relay` +**Role**: The core Agent Relay product — server, broker, SDK, protocol. +**Stack**: Rust/TypeScript web and package surfaces. +**Owns**: relay server, broker, `@agent-relay/sdk`, relay protocol, agent registration, message routing, workspace management, core messaging substrate. +**Key signals in issues**: "relay", "broker", "SDK", "@agent-relay/sdk", "relay server", "subscription", "workspace", "agent registration", "message routing", "channel", "DM", "thread". + +### `skills` +**Role**: Skill packs consumed by agent personas at runtime. +**Stack**: Markdown (SKILL.md files), PRPM package manager. +**Owns**: `@agent-relay/` and `@agent-workforce/` skill namespaces, skill authoring tooling, skill discovery. +**Key signals in issues**: "skill", "SKILL.md", "skill pack", "@agent-relay/skill", "@agent-workforce/skill", "skills repo". + +## Product and workflow repos + +### `relaycast` +**Role**: Headless Slack-style collaboration product for AI agents. +**Owns**: Relaycast workspace UX/API, agent chat surfaces, real-time collaboration patterns that are product-specific rather than core Relay protocol. +**Key signals in issues**: "relaycast", "headless Slack", "agent workspace", "agent chat product", "real-time agent collaboration". + +### `relayauth` +**Role**: Authorization and capability constraints for agents. +**Owns**: permission/authority model, scoped access, action authorization, capability boundaries. +**Key signals in issues**: "relayauth", "authorization", "capability", "permissions", "least privilege", "agent can do exactly". + +### `relayflows` +**Role**: Multi-step, multi-agent workflow orchestration across Agent Relay workers. +**Owns**: workflow graph/runtime concepts, orchestration recipes, worker coordination flows. +**Key signals in issues**: "relayflows", "workflow orchestration", "multi-step", "multi-agent execution", "workflow runner". + +### `ricky` +**Role**: Workflow reliability agent and workflow runner for AgentWorkforce. +**Owns**: workflow reliability, runner behavior, workflow execution monitoring. +**Key signals in issues**: "ricky", "workflow reliability", "workflow runner", "runner failed", "workflow monitor". + +### `nightcto` +**Role**: Overnight CTO-style autonomous engineering/product agent. +**Owns**: NightCTO persona/product behavior, overnight planning/execution workflow, CTO automation. +**Key signals in issues**: "nightcto", "night CTO", "overnight", "CTO agent", "autonomous CTO". + +### `sage` +**Role**: Research, clarification, and planning agent that turns conversations into executable workflows. +**Owns**: planning/research workflow, requirements clarification, conversation-to-plan behavior. +**Key signals in issues**: "sage", "research", "clarify", "planning workflow", "requirements", "executable development workflow". + +### `proactive-agents` +**Role**: Marketing/blog site for proactive agent concepts. +**Owns**: proactive-agents website content and messaging. +**Key signals in issues**: "proactive-agents site", "marketing site", "blog", "proactive agents content". + +### `proactive` +**Role**: Tools to build proactive agents. +**Owns**: proactive-agent primitives and tooling outside the marketing site. +**Key signals in issues**: "proactive", "follow-up", "watch rule", "scheduler", "proactive agent tooling". + +### `internal-agents` +**Role**: Internal AgentWorkforce agent implementations. +**Owns**: private/internal agents not packaged as public personas. +**Key signals in issues**: "internal agent", "internal-agents", "private agent", "ops agent". + +### `workflows` +**Role**: Repeatable Agent Relay workflows and repo-hygiene cookbooks. +**Owns**: workflow templates, cookbook docs, repo hygiene automation patterns. +**Key signals in issues**: "workflow cookbook", "repo hygiene", "workflows repo", "repeatable workflow". + +### `agent-relay-personas` +**Role**: Workforce personas that leverage Agent Relay. +**Owns**: Agent Relay-specific persona packs outside this `workforce` monorepo. +**Key signals in issues**: "agent-relay-personas", "relay persona", "Agent Relay persona pack". + +### `agent-relay-acp-bridge` +**Role**: ACP bridge for Agent Relay. +**Owns**: Agent Client Protocol bridge integration with Agent Relay. +**Key signals in issues**: "ACP", "agent client protocol", "ACP bridge", "agent-relay-acp-bridge". + +### `agent-relay-openclaw` +**Role**: Agent Relay bridge for OpenClaw. +**Owns**: OpenClaw integration, real-time channels/threads/DMs beyond built-in OpenClaw surfaces. +**Key signals in issues**: "openclaw", "agent-relay-openclaw", "OpenClaw bridge". + +### `agent-relay-vscode` +**Role**: VS Code extension for Agent Relay. +**Owns**: VS Code integration, editor extension commands/views for Relay. +**Key signals in issues**: "VS Code", "vscode", "extension", "agent-relay-vscode". + +### `relay-dashboard` +**Role**: Dashboard for agents collaborating through Agent Relay. +**Owns**: dashboard UI, collaboration visualization/product surfaces. +**Key signals in issues**: "dashboard", "relay-dashboard", "collaboration dashboard". + +### `relay-visualizer`, `relay-pulse`, `relay-pty-visualizer` +**Role**: Visual explainers and interactive protocol/PTY visualizations. +**Owns**: visualization UIs, animated protocol demos, PTY pipeline explainers. +**Key signals in issues**: "visualizer", "relay-pulse", "relay-pty", "animated explainer", "protocol visualization". + +### `ai-hist` +**Role**: Sync and search Claude Code/Codex CLI conversation history in SQLite. +**Owns**: conversation-history ingestion/search, local SQLite indexes. +**Key signals in issues**: "ai-hist", "conversation history", "history search", "SQLite", "Claude Code history", "Codex history". + +### `trajectories` +**Role**: Document layer for agent work and traces. +**Owns**: trajectory document format and generated/documented agent work artifacts. +**Key signals in issues**: "trajectory", "trajectories", "agent trace document", "work trace". + +### `agent-trace` +**Role**: Standard trace format for AI-generated code. +**Owns**: trace schema/format for AI coding outputs. +**Key signals in issues**: "agent-trace", "trace format", "AI-generated code trace". + +### `build-plans` +**Role**: Collection of build plans using the Agent Relay SDK. +**Owns**: planning docs/examples for Relay SDK-based builds. +**Key signals in issues**: "build plan", "build-plans", "Relay SDK plan". + +### `demos`, `relay-examples`, `simple-swarm`, `swarm-patterns`, `tic-tac-toe`, `relay-cross-framework-demo` +**Role**: Demos, examples, and proof points for Agent Relay/swarm patterns. +**Owns**: example apps, demo scenarios, swarm visualizations, cross-framework demos, sample games. +**Key signals in issues**: "demo", "example", "simple swarm", "swarm pattern", "tic-tac-toe", "cross framework", "LangGraph", "CrewAI". + +### `relay-cloud`, `relayfile-cloud`, `relayfile-cli` +**Role**: Split-out cloud/CLI package repos for Relay and Relayfile surfaces. +**Owns**: hosted cloud code or dedicated CLI surfaces when an issue names these repos explicitly. +**Key signals in issues**: "relay-cloud", "relayfile-cloud", "relayfile-cli", "hosted relayfile", "cloud code split". + +## Routing rules + +| Issue surface | Primary repo | Secondary repo (if applicable) | +|---|---|---| +| Desktop UI, session rendering, local mount | `pear` | — | +| Slack/Linear webhook, API endpoint, Nango | `cloud` | — | +| PR reviewer, merge automation, agent harness | `agents` | — | +| Assistant SDK/runtime primitives | `agent-assistant` | — | +| Persona authoring, CLI, runtime deploy | `workforce` | — | +| Relayfile core VFS, SDK, CLI, local mount, ACLs | `relayfile` | — | +| Relay protocol, broker, SDK | `relay` | — | +| Provider-specific relayfile trees/writeback | `relayfile-adapters` | — | +| Relayfile provider auth/credentials | `relayfile-providers` | `cloud` if hosted API/Nango wiring changes too | +| Token/cost observability | `burn` | — | +| Skill content | `skills` | — | +| Slack feature end-to-end | `cloud` + `pear` | cloud = webhook/dispatch, pear = mount/render | +| New integration (e.g. Jira) | `relayfile-adapters` + `relayfile-providers` + `cloud` | adapter = sync/writeback, provider = auth, cloud = hosted connection | +| New persona | `workforce` | — | +| Agent workflow orchestration | `relayflows` | `workforce` if packaged as persona/runtime | +| Agent Relay examples/demos | demo repo named in issue | `relay` if core SDK/protocol bug | + +## Multi-repo issues + +When an issue spans multiple repos, spawn one implementer per repo in parallel. Common patterns: +- **New Slack integration feature**: `cloud` (webhook + API) + `pear` (mount rendering) +- **New external provider**: `relayfile-adapters` (resource tree/writeback) + `relayfile-providers` (auth/credential provider) + `cloud` (hosted connection setup) +- **Agent behavior + UI**: `agents` (harness logic) + `pear` (rendering the agent's output) +- **Assistant runtime + product integration**: `agent-assistant` (runtime primitive) + product repo such as `cloud`, `pear`, or `relaycast` +- **Relayfile mount bug with provider symptoms**: `relayfile` (core VFS/local mount) + `relayfile-adapters` (provider path/writeback) after reproducing which layer fails + +## Disambiguation + +If an issue is ambiguous, use `gh api repos/AgentWorkforce/{repo}/contents` to check directory structure of candidate repos before deciding. For example: if an issue mentions "writeback" it could be `pear` (path construction) or `relayfile-adapters` (sync protocol) — check which layer the bug is in. + +## Compact org index + +Use this as a candidate list before falling back to live `gh repo list`. Several repos are products, demos, prototypes, or split-out package surfaces; route to them only when the issue names them directly or their ownership signals match. + +| Repo | Signal | +|---|---| +| `awp` | Agent Workforce Protocol | +| `axiom` | Intent preservation for AI coding agents | +| `c2a` | Chat 2 Agents Protocol | +| `ceo`, `cfo`, `cmo`, `sales`, `supportly`, `rachel`, `openkaren`, `openviktor` | role/persona/product experiments; route only when named | +| `clawrunner` | OpenClaw deployment sandbox | +| `code-storybook` | AI-narrated code storybook | +| `credential-proxy` | credential proxy surface | +| `gitclawlab` | hosting/deploying code for AI agents | +| `kairo` | Mac calendar connected to agents | +| `limit` | terminal dashboard for Claude Code/Codex usage limits | +| `megaclaude` | Claude Code wrapper/variant | +| `moltslack`, `transport` | headless Slack / agent messaging experiments | +| `n8n-nodes-relayfile`, `relaycast-n8n-bridge` | n8n integration packages | +| `open-world`, `open-world-game`, `soccer`, `scout` | game/demo/prototype repos | +| `planner` | web UI for planning tasks with agents | +| `profiles` | identity files for agents | +| `relay-broker`, `relay-sdk`, `relay-tui` | split-out Relay broker/SDK/TUI surfaces; verify whether current work belongs in `relay` first | +| `relay-poc-*` | Relay Communicate SDK proof-of-concept apps | +| `relaycron` | scheduled work for agents | +| `relayed` | relay daemon/runtime surface | +| `relayrank` | ranking/evaluation surface | +| `sloppypaste` | "Stop dumping slop" product/tool | +| `superset` | IDE for running many agent CLIs | +| `vscode-acp` | ACP support for VS Code | +| `wash` | cleanup tool/product | +| `watchdog-agents` | Watchdog agents deployed on Agent Relay | diff --git a/packages/persona-repo-router/skills/persona-relayfile-mount.md b/packages/persona-repo-router/skills/persona-relayfile-mount.md new file mode 100644 index 00000000..7f3cfb70 --- /dev/null +++ b/packages/persona-repo-router/skills/persona-relayfile-mount.md @@ -0,0 +1,87 @@ +--- +name: persona-relayfile-mount +description: Use when authoring an AgentWorkforce persona's `mount` field — covers when to use mount, the gitignore allow-list idiom and its non-obvious `!web` vs `!web/` walker bug, `readonlyPatterns` scope rules, the per-agent dotfile overlay, and `.git` sandbox behavior +--- + +# Relayfile mount policy for AgentWorkforce personas + +You are advising an author writing the `mount` field on an AgentWorkforce persona spec. Use `mount` when the persona should run in a filesystem sandbox: it should see only some files, treat others as read-only, or both. Omit `mount` when the persona is fine running in-place with full project access (the default for most personas). + +## Shape + +```json +"mount": { + "ignoredPatterns": ["..."], + "readonlyPatterns": ["..."] +} +``` + +Both arrays use gitignore-style globs (powered by the `ignore` library). + +- `ignoredPatterns` — matches are omitted from the mount entirely; the agent cannot see or write them. +- `readonlyPatterns` — matches are copied into the mount but `chmod 444`; agent edits to these paths never sync back to the project. + +## Mount vs permissions + +Mount gates *files*. `permissions` gates *tools*. They compose — a persona that shouldn't see `.env` AND shouldn't shell out needs both. + +## Per-agent dotfile overlays + +The persona's `id` is passed to relayfile as `agentName`. At launch, relayfile also loads: + +- `.{id}.agentignore` +- `.{id}.agentreadonly` + +from the project root, appending their contents to the persona spec's patterns. Persona authors control the in-spec baseline; deployers can layer per-agent dotfiles on top without editing the persona JSON. + +## Allow-list idiom (agent only sees one subtree) + +Use gitignore negation, NOT a broad `**` exclude. + +**Correct:** + +```json +"ignoredPatterns": ["/*", "!web", "!web/**", "secrets/", ".env"] +``` + +**Four rules to avoid bricking the mount:** + +1. **Use `/*` as the broad-exclude, never `**`.** Gitignore semantics skip excluded parent directories entirely. Once `**` excludes a directory, a later `!dir/**` cannot bring its contents back — the dir was never walked. `/*` only excludes root-level entries, so subdirs of allowed paths stay visible. +2. **Negate the directory with `!web`, NOT `!web/`.** This one is non-obvious and breaks every "obvious" allow-list. The relayfile walker calls the `ignore` library twice per directory entry — once with the bare name (`web`) and once with the trailing-slash form (`web/`) — and OR's the results. The bare-name check fires first, and `/*` matches `web` regardless of type. A trailing-slash negation `!web/` only counters the trailing-slash form, so the bare-name check still returns "ignored" and the walker skips the directory. `!web` (no slash) negates BOTH forms, which is what you need. **If your allow-listed subtree isn't appearing in the mount, this is almost always the bug.** +3. **Include BOTH the directory and its contents.** `!web` re-includes the directory node so the walker recurses into it; `!web/**` re-includes everything inside it. You need both. +4. **No `./` prefixes.** The `ignore` library expects bare relative patterns; `./web/**` is treated as a literal path that almost never matches. + +**Wrong (looks right, silently empty mount):** + +```json +"ignoredPatterns": ["/*", "!web/", "!web/**"] +``` + +**Right:** + +```json +"ignoredPatterns": ["/*", "!web", "!web/**"] +``` + +## readonlyPatterns scope + +`readonlyPatterns` is for paths the agent should READ but never MODIFY — lockfiles, vendor code, configs the persona references but shouldn't touch. + +**Never include the persona's primary work directory.** If you do, the agent's writes to that directory are silently filtered out on sync-back. The persona looks like it ran successfully but produced zero project-side changes — one of the worst failure modes because nothing complains. + +Example: a blog-writer persona whose job is to author files under `web/content/blog/` must NOT have `web/**` in `readonlyPatterns`. + +## Git inside the mount + +The mount auto-includes `.git`, so `git status`/`log`/`diff` work inside the sandbox. Sync rules: + +- Project-side changes under `.git/**` flow INTO the mount (e.g. teammate moves HEAD while the agent runs). +- Mount-side changes under `.git/**` are NOT synced back. Branches, commits, and refs the agent creates in the mount stay sandboxed and are discarded on cleanup unless the agent pushes to a remote. + +## Pre-handoff checklist + +- [ ] Does the persona actually need a sandbox? If full project access is fine, omit `mount` entirely. +- [ ] For allow-lists: `/*` (not `**`) plus paired `!dir` (NO trailing slash) and `!dir/**` for each allowed path? +- [ ] Is the persona's *work* directory NOT in `readonlyPatterns`? +- [ ] Are secrets, `.env`, and any private dirs in `ignoredPatterns`? +- [ ] Does `permissions` cover the tool-side scope (Bash, file edits, MCP) the persona should and shouldn't have? diff --git a/packages/persona-repo-router/test/persona.test.mjs b/packages/persona-repo-router/test/persona.test.mjs new file mode 100644 index 00000000..fb524f0f --- /dev/null +++ b/packages/persona-repo-router/test/persona.test.mjs @@ -0,0 +1,63 @@ +import assert from 'node:assert/strict'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import persona, { repoRouterPersona } from '../index.js'; + +const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); +const personasDir = join(pkgRoot, 'personas'); +const personaPath = join(personasDir, 'repo-router.json'); +const personaJson = JSON.parse(readFileSync(personaPath, 'utf8')); + +test('persona pack points at the personas dir and ships skills', () => { + const pkg = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')); + assert.equal(pkg.agentworkforce?.personas, 'personas'); + assert.ok(pkg.files.includes('skills')); +}); + +test('default export and named export are the same object', () => { + assert.equal(persona, repoRouterPersona); +}); + +test('compatibility export reads the persona pack JSON', () => { + assert.deepEqual(persona, personaJson); +}); + +test('persona JSON has the expected identity and runtime', () => { + assert.equal(persona.id, 'repo-router'); + assert.equal(persona.intent, 'agent-relay-workflow'); + assert.equal(persona.harness, 'claude'); + assert.equal(persona.model, 'claude-sonnet-4-6'); +}); + +test('agentsMd sidecar is referenced and shipped alongside the persona JSON', () => { + assert.equal(persona.agentsMd, './repo-router.md'); + const sidecarPath = join(personasDir, 'repo-router.md'); + assert.ok(existsSync(sidecarPath), 'repo-router.md sidecar exists in personas/'); + const sidecar = readFileSync(sidecarPath, 'utf8'); + assert.ok(sidecar.startsWith('# repo-router')); + assert.ok(sidecar.includes('Build a repo map from GitHub')); + assert.ok(sidecar.includes('local `agentworkforce-repo-map` skill')); + assert.equal(persona.claudeMdContent, undefined); +}); + +test('local skills are shipped and @agent-workforce skill sources are not used', () => { + for (const skill of persona.skills) { + assert.ok( + skill.source !== '@agent-workforce/persona-relayfile-mount', + '@agent-workforce/persona-relayfile-mount must not be used as a remote source' + ); + } + + const localSources = [ + './skills/agentworkforce-repo-map.md', + './skills/persona-relayfile-mount.md' + ]; + for (const source of localSources) { + const skill = persona.skills.find((s) => s.source === source); + assert.ok(skill, `local skill "${source}" present`); + assert.ok(existsSync(join(pkgRoot, source)), `local skill "${source}" shipped`); + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fb6eb20..90f543ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,8 +119,12 @@ importers: specifier: ^2.3.0 version: 2.9.0 + packages/persona-linear-dispatcher: {} + packages/persona-nango-integrations: {} + packages/persona-repo-router: {} + packages/persona-slack-comms: {} packages/persona-slack-relayfile-doctor: {}