@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 +60 -0
- package/README.md +14 -8
- package/{agents → artifacts/forge}/forge.md +15 -5
- package/artifacts/forge-grill/forge-grill.md +93 -0
- package/{skills/forge-worker/SKILL.md → artifacts/forge-worker/forge-worker.md} +22 -2
- package/{skills/using-forge/SKILL.md → artifacts/using-forge/using-forge.md} +1 -0
- package/dist/src/adapters/claude-known.js +31 -0
- package/dist/src/adapters/claude.js +33 -21
- package/dist/src/adapters/codex.js +14 -14
- package/dist/src/adapters/opencode.js +15 -15
- package/dist/src/adapters/shared.js +6 -1
- package/dist/src/cli.js +112 -24
- package/dist/src/discovery.js +16 -18
- package/dist/src/frontmatter.js +8 -68
- package/dist/src/manifest.js +134 -0
- package/dist/src/model.js +3 -0
- package/dist/src/processor.js +88 -45
- package/dist/src/writer.js +13 -1
- package/package.json +12 -12
- package/agents/forge-worker.md +0 -60
- package/skills/forge-build/SKILL.md +0 -80
- package/skills/forge-design/SKILL.md +0 -104
- package/skills/forge-explore/SKILL.md +0 -65
- package/skills/forge-helper/SKILL.md +0 -46
- package/skills/forge-plan/SKILL.md +0 -81
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
6
|
-
tools: [Task, AskUserQuestion, TodoWrite]
|
|
6
|
+
kind: skill
|
|
7
7
|
opencode:
|
|
8
8
|
mode: primary
|
|
9
9
|
permissions:
|
|
10
|
-
task:
|
|
11
|
-
question:
|
|
12
|
-
todowrite:
|
|
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:
|
|
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
|
|
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
|
|
@@ -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 {
|
|
4
|
-
|
|
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:
|
|
7
|
-
if (
|
|
8
|
-
fm.model =
|
|
9
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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(
|
|
29
|
+
export function renderClaudeSkill(artifact) {
|
|
18
30
|
const diagnostics = [];
|
|
19
|
-
const fm = { name:
|
|
20
|
-
const permissions =
|
|
21
|
-
const allowedTools = isRecord(permissions) ?
|
|
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 ${
|
|
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 ${
|
|
39
|
+
diagnostics.push(diagnostic('info', 'CLAUDE_SKILL_PERMISSIONS_IGNORED', `Claude skill permissions are not emitted for ${artifact.name}`, { platform: 'claude' }));
|
|
28
40
|
}
|
|
29
|
-
if (
|
|
30
|
-
diagnostics.push(diagnostic('info', 'CLAUDE_SKILL_MODEL_IGNORED', `Claude skill model is not emitted for ${
|
|
31
|
-
return { content: `${stringifyYaml(fm)}${
|
|
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(
|
|
6
|
-
const diagnostics = [diagnostic('info', 'CODEX_PARTIAL_AGENT_SUPPORT', `Codex agent output is partial and does not generate AGENTS.md or profiles for ${
|
|
7
|
-
const lines = [`name = ${tomlString(
|
|
8
|
-
if (
|
|
9
|
-
lines.push(`model = ${tomlString(
|
|
10
|
-
const 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 ${
|
|
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 ${
|
|
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(
|
|
22
|
+
export function renderCodexSkill(artifact) {
|
|
23
23
|
const diagnostics = [];
|
|
24
|
-
if (
|
|
25
|
-
diagnostics.push(diagnostic('info', 'CODEX_SKILL_PERMISSIONS_IGNORED', `Codex skill permissions are not emitted for ${
|
|
26
|
-
if (
|
|
27
|
-
diagnostics.push(diagnostic('info', 'CODEX_SKILL_MODEL_IGNORED', `Codex skill model is not emitted for ${
|
|
28
|
-
return { content: `${stringifyYaml({ name:
|
|
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(
|
|
4
|
-
const fm = { description:
|
|
5
|
-
if (
|
|
6
|
-
fm.mode =
|
|
7
|
-
if (
|
|
8
|
-
fm.model =
|
|
9
|
-
if (
|
|
10
|
-
fm.permission =
|
|
11
|
-
return { content: `${stringifyYaml(fm)}${
|
|
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(
|
|
13
|
+
export function renderOpenCodeSkill(artifact) {
|
|
14
14
|
const diagnostics = [];
|
|
15
|
-
if (
|
|
16
|
-
diagnostics.push(diagnostic('info', 'OPENCODE_SKILL_PERMISSIONS_IGNORED', `OpenCode skill permissions are not emitted for ${
|
|
17
|
-
if (
|
|
18
|
-
diagnostics.push(diagnostic('info', 'OPENCODE_SKILL_MODEL_IGNORED', `OpenCode skill model is not emitted for ${
|
|
19
|
-
return { content: `${stringifyYaml({ name:
|
|
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' &&
|
|
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);
|