@guidobuilds/forge-ai 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,60 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ Versions prior to 0.3.0 are not reconstructed here; see git history for earlier changes.
9
+
10
+ ## [Unreleased]
11
+
12
+ ## [0.3.0] - 2026-05-15
13
+
14
+ ### Added
15
+
16
+ - Unified canonical **artifact** model: all sources live under `artifacts/<name>/<name>.md` and declare `kind: agent | skill` in frontmatter. Each per-platform block can override `kind`, so one artifact renders as an agent on one platform and a skill on another.
17
+ - `forge` orchestrator installs as a Claude Code **skill** (`/forge`) on Claude Code, while remaining a primary agent on OpenCode and Codex. This fixes the previous setup where `forge` was installed as a Claude subagent and could not delegate to `forge-worker` (Claude subagents cannot call `Task`).
18
+ - Installer **classifies each destination** as one of `new`, `managed-unmodified`, `managed-modified`, or `foreign`, and prints the status next to each file (`[refresh]`, `[overwrite, backup -> …]`, `[foreign overwrite]`, `[new]`).
19
+ - **Automatic backups** of user-edited Forge files before overwrite or prune, stored under `~/.forge-ai/backups/<scope>/<ISO-timestamp>/<relative-path>`. A single timestamp directory groups all backups from one run.
20
+ - **Combined confirmation prompt** in interactive mode listing every file that needs the user's decision (edited overwrites, edited deletions, foreign overwrites) with the destination backup directory.
21
+ - Non-interactive installs refuse with exit code 1 when there are edited or foreign files and neither `--yes` nor `--force` is set, instead of silently overwriting.
22
+ - New diagnostic codes: `MANAGED_FILE_OVERWRITE`, `FOREIGN_FILE_OVERWRITE`, `MISSING_KIND`, `INVALID_KIND`, `INVALID_PLATFORM_KIND`, `OPENCODE_MODE_ON_SKILL`, `SUPPORT_FILES_NOT_COPIED`.
23
+ - Claude tool and model validation (`src/adapters/claude-known.ts`): unknown tools/models in agent frontmatter emit warnings instead of being silently accepted.
24
+ - Skill source directories now allow sibling files (groundwork for future support-file bundling); detected files emit an `info` diagnostic noting that copying is not yet implemented.
25
+
26
+ ### Changed
27
+
28
+ - Source layout: `agents/` and `skills/` directories are gone; everything moved to `artifacts/<name>/<name>.md`.
29
+ - `forge-worker` agent and `forge-worker` skill (previously two files with the same name in separate namespaces) merged into a **single** `forge-worker` artifact rendered as a subagent on every platform. The artificial "thin agent loads a skill" indirection is removed.
30
+ - `package.json` `files` now ships `artifacts/` instead of `agents/` + `skills/`.
31
+ - Refreshing a managed-unmodified file is **silent** — no warning emitted. Previously every existing destination produced an indiscriminate `OVERWRITE_FORCED`.
32
+ - Stale-managed files the user edited are now **backed up and deleted** during `update` (when `--yes`/`--force`); previously they were left on disk with a `CHECKSUM_MISMATCH` warning. Interactive runs prompt before backing up + deleting.
33
+ - OpenCode `permissions` keys switched from boolean (`true`/`false`) to explicit strings (`allow`/`deny`); orchestrator now explicitly denies file/code operations.
34
+ - Adapters consume a unified `CanonicalArtifact` type (with `body` field) instead of separate `CanonicalAgent`/`CanonicalSkill` types.
35
+ - Frontmatter parsing now uses the `yaml` library (replaces the hand-rolled parser), with stricter spec compliance and better edge-case handling (colons in descriptions, horizontal rules in bodies, BOM, CRLF).
36
+ - Claude agent `tools` and skill `allowed-tools` are emitted as comma-separated strings (matches Claude Code's native format).
37
+ - Development tooling migrated from npm to **pnpm**: `packageManager: "pnpm@11.1.1"` pinned via Corepack (no global install needed); `pnpm.onlyBuiltDependencies: []` allowlist is explicit so postinstall script blocking is documented behavior. The published package is unaffected — consumers still install via `npx`.
38
+
39
+ ### Removed
40
+
41
+ - `CanonicalAgent` and `CanonicalSkill` types (replaced by `CanonicalArtifact`).
42
+ - `agents/` and `skills/` source directories.
43
+ - `DESTINATION_EXISTS` error and `OVERWRITE_FORCED` warning (replaced by status-aware `MANAGED_FILE_OVERWRITE` and `FOREIGN_FILE_OVERWRITE`).
44
+ - `CHECKSUM_MISMATCH` warning during prune (the file is now backed up and deleted on consent).
45
+ - `package-lock.json` (replaced by `pnpm-lock.yaml`).
46
+
47
+ ### Fixed
48
+
49
+ - `forge` orchestrator deployment to Claude Code now actually works: it is installed as a `/forge` skill in the main thread, which retains the `Task` tool and can delegate to the `forge-worker` subagent. The previous subagent install was inert because Claude subagents cannot call `Task`.
50
+ - Installer no longer raises spurious overwrite warnings for files Forge installed itself and that have not been edited.
51
+
52
+ ### Security
53
+
54
+ - Migrated the development workflow to pnpm. pnpm 10+ **blocks postinstall scripts by default** (`onlyBuiltDependencies` allowlist), enforces strict `node_modules` (no phantom dependencies), and uses an auditable text lockfile (`pnpm-lock.yaml`). The npm registry is the same, but install-time defaults are hardened.
55
+ - User edits to Forge-managed files are **always backed up** before being overwritten or deleted, eliminating silent data loss when running `update` against a customized install.
56
+
57
+ ### Migration from 0.2.0
58
+
59
+ - Run `npx @guidobuilds/forge-ai update` (not `install`) after upgrading. `update` prunes the now-orphaned `.claude/agents/forge.md` and the old standalone `forge-worker` skill from the previous layout. Local edits to any of those files are backed up automatically to `~/.forge-ai/backups/`.
60
+ - `install` (without `update`) will leave the orphaned files on disk. They are harmless but unmanaged.
package/README.md CHANGED
@@ -77,7 +77,7 @@ The same operating model is shared across all supported agents so the workflow s
77
77
  The primary installer is the npm CLI:
78
78
 
79
79
  ```sh
80
- npx forge-ai install
80
+ npx @guidobuilds/forge-ai install
81
81
  ```
82
82
 
83
83
  The installer prompts for the target agent platform and whether Forge should be installed globally for your user or locally for the current project.
@@ -85,34 +85,36 @@ The installer prompts for the target agent platform and whether Forge should be
85
85
  To update an existing install:
86
86
 
87
87
  ```sh
88
- npx forge-ai update
88
+ npx @guidobuilds/forge-ai update
89
89
  ```
90
90
 
91
91
  For non-interactive environments:
92
92
 
93
93
  ```sh
94
- npx forge-ai install --platform all --scope user --yes
94
+ npx @guidobuilds/forge-ai install --platform all --scope user --yes
95
95
  ```
96
96
 
97
97
  Preview the files without writing them:
98
98
 
99
99
  ```sh
100
- npx forge-ai install --platform all --scope user --dry-run
100
+ npx @guidobuilds/forge-ai install --platform all --scope user --dry-run
101
101
  ```
102
102
 
103
103
  Validate a local Forge source tree:
104
104
 
105
105
  ```sh
106
- npx forge-ai validate --source .
106
+ npx @guidobuilds/forge-ai validate --source .
107
107
  ```
108
108
 
109
109
  ## Local Development
110
110
 
111
+ Forge uses [pnpm](https://pnpm.io) for development (pinned via `packageManager` in `package.json`, so `corepack enable` is enough — no global install needed). The published package is still consumed by end users via `npx`/npm, unchanged.
112
+
111
113
  From a local checkout:
112
114
 
113
115
  ```sh
114
- npm install
115
- npm run build
116
+ pnpm install
117
+ pnpm run build
116
118
  node bin/forge-ai.mjs install --source . --platform all --scope user
117
119
  ```
118
120
 
@@ -127,11 +129,15 @@ node bin/forge-ai.mjs install --source . --platform all --scope project --dry-ru
127
129
  Run the npm updater:
128
130
 
129
131
  ```sh
130
- npx forge-ai update
132
+ npx @guidobuilds/forge-ai update
131
133
  ```
132
134
 
133
135
  Forge replaces its managed agent and skill definitions in your supported agent configuration directories.
134
136
 
137
+ Forge records installed files in manifests under `~/.forge-ai/` so updates can safely remove files that are no longer bundled. `update` prunes stale managed files by default only when the current file still matches the recorded checksum; use `--no-prune` to keep stale managed files. `--dry-run` previews writes and deletes without changing files or manifests.
138
+
139
+ Forge now routes a single canonical artifact to the right artifact kind per agent: the orchestrator installs as a Claude Code skill (`/forge`) but as an agent on OpenCode and Codex. If you installed an earlier version, run `update` (not `install`) so Forge prunes the now-stale `forge` agent and standalone `forge-worker` skill left by the previous layout.
140
+
135
141
  ## Uninstalling
136
142
 
137
143
  Remove Forge from the agent configuration directories for OpenCode, Codex, or Claude Code by deleting the installed Forge agent and skill entries.
@@ -1,15 +1,25 @@
1
1
  ---
2
2
  name: forge
3
3
  description: Forge orchestrator with dynamic runtime routing and a single worker type
4
+ kind: agent
4
5
  claude:
5
- permissions:
6
- tools: [Task, AskUserQuestion, TodoWrite]
6
+ kind: skill
7
7
  opencode:
8
8
  mode: primary
9
9
  permissions:
10
- task: true
11
- question: true
12
- todowrite: true
10
+ task: allow
11
+ question: allow
12
+ todowrite: allow
13
+ read: deny
14
+ write: deny
15
+ edit: deny
16
+ bash: deny
17
+ glob: deny
18
+ grep: deny
19
+ list: deny
20
+ patch: deny
21
+ skill: allow
22
+ webfetch: deny
13
23
  ---
14
24
 
15
25
  # Role
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: forge-grill
3
+ description: Stress-test a plan or design through Forge orchestration, batching user questions while delegating codebase-answerable work to forge-worker.
4
+ kind: skill
5
+ ---
6
+
7
+ # Forge Grill Skill
8
+
9
+ ## Role
10
+
11
+ Use this skill when the user wants Forge to stress-test, challenge, or "grill" a plan, design, proposal, or implementation approach.
12
+
13
+ You are still the Forge orchestrator: keep the user thread thin, delegate technical and operational work to `forge-worker`, and use the lightest safe workflow that reaches shared understanding.
14
+
15
+ ## Core behavior
16
+
17
+ - Build a decision tree for the plan or design under review.
18
+ - Resolve dependencies between decisions in an order that prevents rework.
19
+ - Challenge assumptions, edge cases, scope boundaries, sequencing, risks, and validation strategy.
20
+ - Prefer shared understanding over volume: ask the fewest high-leverage questions that close the next meaningful branch.
21
+ - Do not perform worker work inline.
22
+
23
+ ## Question policy
24
+
25
+ - Use the `question` tool for questions that require the user's judgment, preference, product intent, approval, or risk tolerance.
26
+ - Group related questions into small batches, usually 2-4 questions, instead of asking one question at a time.
27
+ - For each question or option, include Forge's recommended answer and a brief rationale so the user can accept or correct it quickly.
28
+ - Keep each batch focused on one decision branch or tightly related set of branches.
29
+ - Do not ask the user questions that can be answered by inspecting the repository, existing artifacts, logs, tests, or documentation available in the workspace.
30
+
31
+ ## Delegation policy
32
+
33
+ When a question can be answered by exploring the codebase or existing Forge artifacts, launch `forge-worker` instead of asking the user.
34
+
35
+ Use `forge-worker` for:
36
+
37
+ - repository inspection
38
+ - artifact review
39
+ - validation checks
40
+ - feasibility or integration discovery
41
+ - implementation-plan consistency checks
42
+ - technical risk investigation
43
+
44
+ Keep each worker prompt bounded and explicit about:
45
+
46
+ - the plan/design branch being tested
47
+ - what facts to inspect
48
+ - what is out of scope
49
+ - expected validation or evidence
50
+ - that the worker must not ask the user directly
51
+
52
+ ## Worker contract enforcement
53
+
54
+ Every delegated worker run must return exactly the Forge worker contract:
55
+
56
+ ```text
57
+ STATUS: success|partial|blocked
58
+ WORK_TYPE: inspect|design|plan|build|operate|verify|mixed
59
+ FEATURE_SLUG: <kebab-case>
60
+ ARTIFACTS:
61
+ - <path or None>
62
+ SUMMARY:
63
+ - <point>
64
+ NEXT_RECOMMENDED: inspect|design|plan|build|operate|verify|ask-user|none
65
+ RISKS:
66
+ - <risk or None>
67
+ QUESTIONS:
68
+ 1) <question>
69
+ ```
70
+
71
+ `QUESTIONS` must appear only when `STATUS: blocked`.
72
+
73
+ If a worker response is malformed, request one reformat retry for the same task. If it is malformed again, stop and surface an actionable orchestration error.
74
+
75
+ ## Grill workflow
76
+
77
+ 1. Restate the goal, known constraints, and the plan/design surface being grilled.
78
+ 2. Identify decision branches and separate them into:
79
+ - repo-answerable facts for `forge-worker`
80
+ - user-owned decisions for the `question` tool
81
+ - safe assumptions that can be stated and revisited
82
+ 3. Delegate repo-answerable inspection before asking the user about the same branch.
83
+ 4. Ask small batches of user-owned questions with a recommended answer for each.
84
+ 5. After each batch or worker result, update the decision tree and resolve dependent branches.
85
+ 6. Stop when the remaining unknowns are either resolved, explicitly accepted as risks, or safely deferred.
86
+
87
+ ## Output style to the user
88
+
89
+ - Be direct and rigorous, but not performative.
90
+ - Explain why each question matters.
91
+ - Include recommendations in actionable language, such as "Recommended: choose A because...".
92
+ - Make unresolved risk visible before moving to build, plan, or approval-seeking work.
93
+ - If grilling reveals implementation work is needed, route it through the normal Forge worker model instead of doing it inline.
@@ -1,15 +1,35 @@
1
1
  ---
2
2
  name: forge-worker
3
- description: Execute bounded Forge work across inspect, design, plan, build, operate, and verify modes.
3
+ description: Forge universal worker for inspect, design, plan, build, operate, and verify work
4
+ kind: agent
5
+ claude:
6
+ permissions:
7
+ tools: [TodoWrite, Read, Write, Edit, Bash, Glob, Grep, LS, MultiEdit, WebFetch]
8
+ opencode:
9
+ mode: subagent
10
+ permissions:
11
+ todowrite: allow
12
+ read: allow
13
+ write: allow
14
+ edit: allow
15
+ bash: allow
16
+ glob: allow
17
+ grep: allow
18
+ list: allow
19
+ patch: allow
20
+ skill: allow
21
+ webfetch: allow
4
22
  ---
5
23
 
6
- # Forge Worker Skill
24
+ # Forge Worker
7
25
 
8
26
  ## Role
9
27
  Execute only the subgoal assigned by the Forge orchestrator.
10
28
 
11
29
  You are a universal worker derived from Forge's existing explore, design, plan, build, and helper behaviors. Treat those as internal modes, not mandatory phases.
12
30
 
31
+ You are the only worker type in Forge. The orchestrator may launch multiple instances of you in parallel or sequence.
32
+
13
33
  ## Inputs
14
34
 
15
35
  - Orchestrator prompt with the assigned subgoal, constraints, approval context, and expected validation
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: using-forge
3
3
  description: Route work through the lightest safe Forge workflow using dynamic runtime routing.
4
+ kind: skill
4
5
  ---
5
6
 
6
7
  # Using Forge Skill
@@ -0,0 +1,31 @@
1
+ export const knownClaudeTools = new Set([
2
+ 'Read',
3
+ 'Write',
4
+ 'Edit',
5
+ 'MultiEdit',
6
+ 'NotebookEdit',
7
+ 'Bash',
8
+ 'Glob',
9
+ 'Grep',
10
+ 'LS',
11
+ 'Task',
12
+ 'TodoWrite',
13
+ 'WebFetch',
14
+ 'WebSearch',
15
+ 'AskUserQuestion',
16
+ 'ExitPlanMode'
17
+ ]);
18
+ const mcpToolPattern = /^mcp__[A-Za-z0-9_-]+__[A-Za-z0-9_*-]+$/;
19
+ export function isKnownClaudeTool(name) {
20
+ return knownClaudeTools.has(name) || mcpToolPattern.test(name);
21
+ }
22
+ export const knownClaudeModels = new Set([
23
+ 'sonnet',
24
+ 'opus',
25
+ 'haiku',
26
+ 'inherit'
27
+ ]);
28
+ const versionedModelPattern = /^claude-(?:sonnet|opus|haiku)-[A-Za-z0-9.-]+$/;
29
+ export function isKnownClaudeModel(value) {
30
+ return knownClaudeModels.has(value) || versionedModelPattern.test(value);
31
+ }
@@ -1,32 +1,44 @@
1
1
  import { stringifyYaml } from '../frontmatter.js';
2
2
  import { diagnostic } from '../diagnostics.js';
3
- import { isRecord, stringList } from './shared.js';
4
- export function renderClaudeAgent(agent) {
3
+ import { isKnownClaudeModel, isKnownClaudeTool } from './claude-known.js';
4
+ import { isRecord, patternList, stringList } from './shared.js';
5
+ export function renderClaudeAgent(artifact) {
5
6
  const diagnostics = [];
6
- const fm = { name: agent.name, description: agent.description };
7
- if (agent.claude?.model)
8
- fm.model = agent.claude.model;
9
- const permissions = agent.claude?.permissions;
7
+ const fm = { name: artifact.name, description: artifact.description };
8
+ if (artifact.claude?.model) {
9
+ fm.model = artifact.claude.model;
10
+ if (!isKnownClaudeModel(artifact.claude.model)) {
11
+ diagnostics.push(diagnostic('warning', 'CLAUDE_UNKNOWN_MODEL', `Unknown Claude model "${artifact.claude.model}" for ${artifact.name}`, { platform: 'claude' }));
12
+ }
13
+ }
14
+ const permissions = artifact.claude?.permissions;
10
15
  const tools = isRecord(permissions) ? stringList(permissions.tools) : stringList(permissions);
11
- if (tools)
12
- fm.tools = tools;
13
- else if (permissions !== undefined)
14
- diagnostics.push(diagnostic('info', 'CLAUDE_AGENT_TOOLS_IGNORED', `Claude agent permissions must be a tools string list for ${agent.name}`, { platform: 'claude' }));
15
- return { content: `${stringifyYaml(fm)}${agent.definition}\n`, diagnostics };
16
+ if (tools) {
17
+ fm.tools = tools.join(', ');
18
+ for (const tool of tools) {
19
+ if (!isKnownClaudeTool(tool)) {
20
+ diagnostics.push(diagnostic('warning', 'CLAUDE_UNKNOWN_TOOL', `Unknown Claude tool "${tool}" for ${artifact.name}`, { platform: 'claude' }));
21
+ }
22
+ }
23
+ }
24
+ else if (permissions !== undefined) {
25
+ diagnostics.push(diagnostic('info', 'CLAUDE_AGENT_TOOLS_IGNORED', `Claude agent permissions must be a tools string list for ${artifact.name}`, { platform: 'claude' }));
26
+ }
27
+ return { content: `${stringifyYaml(fm)}${artifact.body}\n`, diagnostics };
16
28
  }
17
- export function renderClaudeSkill(skill) {
29
+ export function renderClaudeSkill(artifact) {
18
30
  const diagnostics = [];
19
- const fm = { name: skill.name, description: skill.description };
20
- const permissions = skill.claude?.permissions;
21
- const allowedTools = isRecord(permissions) ? stringList(permissions['allowed-tools']) : undefined;
31
+ const fm = { name: artifact.name, description: artifact.description };
32
+ const permissions = artifact.claude?.permissions;
33
+ const allowedTools = isRecord(permissions) ? patternList(permissions['allowed-tools']) : undefined;
22
34
  if (allowedTools) {
23
- fm['allowed-tools'] = allowedTools;
24
- diagnostics.push(diagnostic('warning', 'CLAUDE_SKILL_ALLOWED_TOOLS', `Claude skill allowed-tools preapproves tools but does not universally restrict them for ${skill.name}`, { platform: 'claude' }));
35
+ fm['allowed-tools'] = allowedTools.join(', ');
36
+ diagnostics.push(diagnostic('warning', 'CLAUDE_SKILL_ALLOWED_TOOLS', `Claude skill allowed-tools preapproves tools but does not universally restrict them for ${artifact.name}`, { platform: 'claude' }));
25
37
  }
26
38
  else if (permissions !== undefined) {
27
- diagnostics.push(diagnostic('info', 'CLAUDE_SKILL_PERMISSIONS_IGNORED', `Claude skill permissions are not emitted for ${skill.name}`, { platform: 'claude' }));
39
+ diagnostics.push(diagnostic('info', 'CLAUDE_SKILL_PERMISSIONS_IGNORED', `Claude skill permissions are not emitted for ${artifact.name}`, { platform: 'claude' }));
28
40
  }
29
- if (skill.claude?.model)
30
- diagnostics.push(diagnostic('info', 'CLAUDE_SKILL_MODEL_IGNORED', `Claude skill model is not emitted for ${skill.name}`, { platform: 'claude' }));
31
- return { content: `${stringifyYaml(fm)}${skill.instructions}\n`, diagnostics };
41
+ if (artifact.claude?.model)
42
+ diagnostics.push(diagnostic('info', 'CLAUDE_SKILL_MODEL_IGNORED', `Claude skill model is not emitted for ${artifact.name}`, { platform: 'claude' }));
43
+ return { content: `${stringifyYaml(fm)}${artifact.body}\n`, diagnostics };
32
44
  }
@@ -2,28 +2,28 @@ import { stringifyYaml } from '../frontmatter.js';
2
2
  import { diagnostic } from '../diagnostics.js';
3
3
  import { isRecord, tomlString } from './shared.js';
4
4
  const safeSandboxModes = new Set(['read-only', 'workspace-write']);
5
- export function renderCodexAgent(agent) {
6
- const diagnostics = [diagnostic('info', 'CODEX_PARTIAL_AGENT_SUPPORT', `Codex agent output is partial and does not generate AGENTS.md or profiles for ${agent.name}`, { platform: 'codex' })];
7
- const lines = [`name = ${tomlString(agent.name)}`, `description = ${tomlString(agent.description)}`, `developer_instructions = ${tomlString(agent.definition)}`];
8
- if (agent.codex?.model)
9
- lines.push(`model = ${tomlString(agent.codex.model)}`);
10
- const permissions = agent.codex?.permissions;
5
+ export function renderCodexAgent(artifact) {
6
+ const diagnostics = [diagnostic('info', 'CODEX_PARTIAL_AGENT_SUPPORT', `Codex agent output is partial and does not generate AGENTS.md or profiles for ${artifact.name}`, { platform: 'codex' })];
7
+ const lines = [`name = ${tomlString(artifact.name)}`, `description = ${tomlString(artifact.description)}`, `developer_instructions = ${tomlString(artifact.body)}`];
8
+ if (artifact.codex?.model)
9
+ lines.push(`model = ${tomlString(artifact.codex.model)}`);
10
+ const permissions = artifact.codex?.permissions;
11
11
  if (isRecord(permissions) && typeof permissions.sandbox_mode === 'string') {
12
12
  if (safeSandboxModes.has(permissions.sandbox_mode))
13
13
  lines.push(`sandbox_mode = ${tomlString(permissions.sandbox_mode)}`);
14
14
  else
15
- diagnostics.push(diagnostic('warning', 'CODEX_UNSAFE_SANDBOX_IGNORED', `Unsafe Codex sandbox_mode ignored for ${agent.name}`, { platform: 'codex' }));
15
+ diagnostics.push(diagnostic('warning', 'CODEX_UNSAFE_SANDBOX_IGNORED', `Unsafe Codex sandbox_mode ignored for ${artifact.name}`, { platform: 'codex' }));
16
16
  }
17
17
  else if (permissions !== undefined) {
18
- diagnostics.push(diagnostic('info', 'CODEX_AGENT_PERMISSIONS_IGNORED', `Codex agent permissions are not emitted for ${agent.name}`, { platform: 'codex' }));
18
+ diagnostics.push(diagnostic('info', 'CODEX_AGENT_PERMISSIONS_IGNORED', `Codex agent permissions are not emitted for ${artifact.name}`, { platform: 'codex' }));
19
19
  }
20
20
  return { content: `${lines.join('\n')}\n`, diagnostics };
21
21
  }
22
- export function renderCodexSkill(skill) {
22
+ export function renderCodexSkill(artifact) {
23
23
  const diagnostics = [];
24
- if (skill.codex?.permissions)
25
- diagnostics.push(diagnostic('info', 'CODEX_SKILL_PERMISSIONS_IGNORED', `Codex skill permissions are not emitted for ${skill.name}`, { platform: 'codex' }));
26
- if (skill.codex?.model)
27
- diagnostics.push(diagnostic('info', 'CODEX_SKILL_MODEL_IGNORED', `Codex skill model is not emitted for ${skill.name}`, { platform: 'codex' }));
28
- return { content: `${stringifyYaml({ name: skill.name, description: skill.description })}${skill.instructions}\n`, diagnostics };
24
+ if (artifact.codex?.permissions)
25
+ diagnostics.push(diagnostic('info', 'CODEX_SKILL_PERMISSIONS_IGNORED', `Codex skill permissions are not emitted for ${artifact.name}`, { platform: 'codex' }));
26
+ if (artifact.codex?.model)
27
+ diagnostics.push(diagnostic('info', 'CODEX_SKILL_MODEL_IGNORED', `Codex skill model is not emitted for ${artifact.name}`, { platform: 'codex' }));
28
+ return { content: `${stringifyYaml({ name: artifact.name, description: artifact.description })}${artifact.body}\n`, diagnostics };
29
29
  }
@@ -1,20 +1,20 @@
1
1
  import { stringifyYaml } from '../frontmatter.js';
2
2
  import { diagnostic } from '../diagnostics.js';
3
- export function renderOpenCodeAgent(agent) {
4
- const fm = { description: agent.description };
5
- if (agent.opencode?.mode)
6
- fm.mode = agent.opencode.mode;
7
- if (agent.opencode?.model)
8
- fm.model = agent.opencode.model;
9
- if (agent.opencode?.permissions)
10
- fm.permission = agent.opencode.permissions;
11
- return { content: `${stringifyYaml(fm)}${agent.definition}\n`, diagnostics: [] };
3
+ export function renderOpenCodeAgent(artifact) {
4
+ const fm = { description: artifact.description };
5
+ if (artifact.opencode?.mode)
6
+ fm.mode = artifact.opencode.mode;
7
+ if (artifact.opencode?.model)
8
+ fm.model = artifact.opencode.model;
9
+ if (artifact.opencode?.permissions)
10
+ fm.permission = artifact.opencode.permissions;
11
+ return { content: `${stringifyYaml(fm)}${artifact.body}\n`, diagnostics: [] };
12
12
  }
13
- export function renderOpenCodeSkill(skill) {
13
+ export function renderOpenCodeSkill(artifact) {
14
14
  const diagnostics = [];
15
- if (skill.opencode?.permissions)
16
- diagnostics.push(diagnostic('info', 'OPENCODE_SKILL_PERMISSIONS_IGNORED', `OpenCode skill permissions are not emitted for ${skill.name}`, { platform: 'opencode' }));
17
- if (skill.opencode?.model)
18
- diagnostics.push(diagnostic('info', 'OPENCODE_SKILL_MODEL_IGNORED', `OpenCode skill model is not emitted for ${skill.name}`, { platform: 'opencode' }));
19
- return { content: `${stringifyYaml({ name: skill.name, description: skill.description })}${skill.instructions}\n`, diagnostics };
15
+ if (artifact.opencode?.permissions)
16
+ diagnostics.push(diagnostic('info', 'OPENCODE_SKILL_PERMISSIONS_IGNORED', `OpenCode skill permissions are not emitted for ${artifact.name}`, { platform: 'opencode' }));
17
+ if (artifact.opencode?.model)
18
+ diagnostics.push(diagnostic('info', 'OPENCODE_SKILL_MODEL_IGNORED', `OpenCode skill model is not emitted for ${artifact.name}`, { platform: 'opencode' }));
19
+ return { content: `${stringifyYaml({ name: artifact.name, description: artifact.description })}${artifact.body}\n`, diagnostics };
20
20
  }
@@ -1,8 +1,13 @@
1
1
  export function isRecord(value) {
2
2
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
3
3
  }
4
+ const toolNamePattern = /^[A-Za-z0-9_*-]+$/;
5
+ const patternBodyPattern = /^[^,\n]+$/;
4
6
  export function stringList(value) {
5
- return Array.isArray(value) && value.every((item) => typeof item === 'string' && /^[A-Za-z0-9_*.,:-]+$/.test(item)) ? value : undefined;
7
+ return Array.isArray(value) && value.every((item) => typeof item === 'string' && toolNamePattern.test(item)) ? value : undefined;
8
+ }
9
+ export function patternList(value) {
10
+ return Array.isArray(value) && value.every((item) => typeof item === 'string' && item.length > 0 && patternBodyPattern.test(item)) ? value : undefined;
6
11
  }
7
12
  export function tomlString(value) {
8
13
  return JSON.stringify(value);