showdar-skills 0.7.0 → 0.8.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 +31 -0
- package/MIGRATION.md +20 -0
- package/README.md +62 -0
- package/bin/showdar.js +41 -2
- package/config/custom-workflow.schema.json +57 -0
- package/config/pack.schema.json +54 -0
- package/config/project-overrides.schema.json +68 -0
- package/package.json +3 -2
- package/src/extension-catalog.js +196 -0
- package/src/project.js +239 -1
- package/src/validate-pack.js +353 -0
- package/src/workflow-state.js +65 -33
- package/src/workflow-trace.js +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [0.8.0]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Local extension packs: static declarative directories (`pack.json` metadata only, no content hash) installed from a local directory or workspace-relative path.
|
|
12
|
+
- Custom workflows composed from built-in primitive stages (`vendor-name` IDs), with canonical skip reasons/policies and the frozen completion contract.
|
|
13
|
+
- Immutable deterministic extension catalog snapshots (`createExtensionCatalog`): no global registry, input-order independent, duplicate/collision rejecting.
|
|
14
|
+
- Pack-local and project-local profiles; the six built-in profiles remain primitive-only and immutable.
|
|
15
|
+
- User-owned `.showdar/overrides.json` for descriptions, discovery hints, guidance, custom-workflow policy refinement, and new project profiles.
|
|
16
|
+
- CLI: `showdar add-pack <path>`, `showdar remove-pack <name>`, `showdar add-workflow <path>`, `showdar init --pack <path>`, `showdar list --extensions`.
|
|
17
|
+
- Full-tree pack hashing: Showdar computes SHA-256 over the validated source tree at install and records it in `.showdar.json` (`extensions.packs[].hash`); docs-only edits change the hash as source identity, not as a behavior claim.
|
|
18
|
+
- Opt-in custom workflow evaluation (`npm run eval:custom-workflows`); never part of `npm run eval` or the release gate.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Workflow-state APIs accept an optional explicit extension-catalog context; omitted context preserves built-in behavior exactly. State schema stays `schemaVersion: 1`.
|
|
23
|
+
- Trace projection accepts `input.extensionCatalog` as validation context only; event types, envelope, and ordering are unchanged.
|
|
24
|
+
- Manifest v2 gains an optional additive `extensions` object (`packs[]`, `customWorkflows[]`, `overrides` metadata). Existing manifests without it remain valid.
|
|
25
|
+
|
|
26
|
+
### Security
|
|
27
|
+
|
|
28
|
+
- Built-in workflow policy (stages, required stages, skips, completion, identity) and built-in profile definitions are protected and non-overrideable.
|
|
29
|
+
- Recursive structured authority-key rejection (`primaryCapability`, `authorizedAction`, `mutationPermission`, `routeAuthority` and all canonical `FORBIDDEN_AUTHORITY_KEYS`); ordinary prose is not censored.
|
|
30
|
+
- Path/symlink/ownership protection on pack install, removal, and override reads; foreign files are never silently overwritten or deleted.
|
|
31
|
+
- Local-only source model: no network, Git, npm/registry, or executable hooks. Tarball, URL, and Git pack sources are rejected in 0.8.
|
|
32
|
+
- Extensions cannot create capabilities, grant authority, modify Phase 6G, mutate built-ins, or execute code.
|
|
33
|
+
|
|
34
|
+
### Migration
|
|
35
|
+
|
|
36
|
+
- No migration required. Existing 0.7 installs, manifests (v2), checkpoints (schemaVersion 1), profiles, and adapters work unchanged. Extensions and overrides are opt-in; no mandatory action for existing users.
|
|
37
|
+
|
|
7
38
|
## [0.7.0]
|
|
8
39
|
|
|
9
40
|
### Added
|
package/MIGRATION.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
# Migrating to 0.8.0
|
|
2
|
+
|
|
3
|
+
0.8.0 adds local declarative extensibility (extension packs, custom
|
|
4
|
+
workflows, user-owned project overrides) over the unchanged 0.7
|
|
5
|
+
runtime. No user action is required.
|
|
6
|
+
|
|
7
|
+
- Existing 0.7 installs and configs remain valid: manifest stays
|
|
8
|
+
version 2 (the `extensions` object is optional and additive),
|
|
9
|
+
workflow state stays schemaVersion 1, and existing checkpoints remain
|
|
10
|
+
valid.
|
|
11
|
+
- Built-in workflows (`showdar-feature`, `showdar-bugfix`,
|
|
12
|
+
`showdar-release`, `showdar-incident`), the six built-in profiles,
|
|
13
|
+
adapters, and Phase 6G authority are unchanged.
|
|
14
|
+
- Extensions are opt-in: `add-pack`, `add-workflow`, `init --pack`,
|
|
15
|
+
`list --extensions`. Project overrides in `.showdar/overrides.json`
|
|
16
|
+
are user-owned; Showdar reads and validates the file but never
|
|
17
|
+
rewrites or deletes it.
|
|
18
|
+
- Pack sources in 0.8 are local directories/workspace paths only;
|
|
19
|
+
tarball, URL, Git, and npm/registry sources are rejected.
|
|
20
|
+
|
|
1
21
|
# Migrating to 0.7.0
|
|
2
22
|
|
|
3
23
|
0.7.0 adds deterministic workflow trace projection
|
package/README.md
CHANGED
|
@@ -557,6 +557,64 @@ is idempotent, preserves the configured profile, supports `--ai`/`--scope`
|
|
|
557
557
|
overrides, and refuses to overwrite a foreign same-name skill directory that
|
|
558
558
|
Showdar does not own.
|
|
559
559
|
|
|
560
|
+
## Extensions (0.8.0)
|
|
561
|
+
|
|
562
|
+
Extension packs are local, static, declarative directories installed from a
|
|
563
|
+
local directory or workspace-relative path. A pack carries `pack.json`
|
|
564
|
+
metadata (name, version, skills, workflows, pack-local profiles), skill
|
|
565
|
+
directories, custom workflow definitions, and docs. Packs contain no
|
|
566
|
+
executable hooks, lifecycle scripts, or remote code.
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
showdar add-pack ../acme-pack
|
|
570
|
+
showdar list --extensions
|
|
571
|
+
showdar remove-pack acme
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Pack skill IDs use the `vendor/skill` namespace (for example,
|
|
575
|
+
`acme/lint`); the `showdar-` prefix is reserved for built-ins. Skill
|
|
576
|
+
`domains` are lowercase kebab-case discovery hints only (at most 8 per
|
|
577
|
+
skill) — they never create capabilities, routes, or authority.
|
|
578
|
+
|
|
579
|
+
Custom workflows compose built-in primitive stages under a `vendor-name`
|
|
580
|
+
ID (for example, `acme-release`). Stages, skip rules, and completion
|
|
581
|
+
policy follow the same frozen contracts as built-in workflows; custom
|
|
582
|
+
workflows cannot define new primitives, authority, evidence kinds, or
|
|
583
|
+
state schemas. Workflow state remains `schemaVersion: 1` and the trace
|
|
584
|
+
projection is unchanged.
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
showdar add-workflow ./workflows/acme-release.json
|
|
588
|
+
showdar init --pack ../acme-pack
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
Project overrides live in the user-owned `.showdar/overrides.json` file:
|
|
592
|
+
skill descriptions, discovery hints, advisory guidance text, custom
|
|
593
|
+
workflow policy refinement, and new project-owned profiles. Showdar reads
|
|
594
|
+
and validates the file but never rewrites or deletes it; built-in
|
|
595
|
+
workflow semantics and the six built-in profiles cannot be overridden.
|
|
596
|
+
Pack-local profiles select pack-owned skills and workflows only.
|
|
597
|
+
|
|
598
|
+
Showdar computes a full-tree SHA-256 over the validated pack source at
|
|
599
|
+
install and records it in `.showdar.json` (`extensions.packs[].hash`).
|
|
600
|
+
The hash is source-tree identity — a docs-only edit changes it without
|
|
601
|
+
implying any behavior change. Drift means the source tree differs from
|
|
602
|
+
the recorded installation source.
|
|
603
|
+
|
|
604
|
+
Extensions cannot create capabilities, grant authority, modify Phase 6G,
|
|
605
|
+
change built-in workflow semantics or profiles, or execute arbitrary
|
|
606
|
+
code. Supported sources are local directories and workspace-relative
|
|
607
|
+
paths; tarball, URL, Git, and npm/registry sources are rejected in 0.8.
|
|
608
|
+
|
|
609
|
+
Opt-in custom workflow evaluation (never part of the release gate):
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
node scripts/generate-custom-eval-fixture.mjs
|
|
613
|
+
node scripts/custom-workflows-eval.mjs \
|
|
614
|
+
--scenarios .tmp/custom-eval-fixture/custom-scenarios \
|
|
615
|
+
--pack .tmp/custom-eval-fixture/acme-pack/pack.json
|
|
616
|
+
```
|
|
617
|
+
|
|
560
618
|
## Routing
|
|
561
619
|
|
|
562
620
|
Showdar routes each request through progressive disclosure: the host discovers
|
|
@@ -591,7 +649,11 @@ Product behavior notes:
|
|
|
591
649
|
```bash
|
|
592
650
|
showdar init [--scope <project|global>] --ai <target> --profile <profile>
|
|
593
651
|
showdar add <skill> [--ai <target>] [--scope <project|global>]
|
|
652
|
+
showdar add-pack <local-path>
|
|
653
|
+
showdar remove-pack <name>
|
|
654
|
+
showdar add-workflow <local-path>
|
|
594
655
|
showdar list
|
|
656
|
+
showdar list --extensions
|
|
595
657
|
showdar status [--scope <project|global>]
|
|
596
658
|
showdar doctor [--scope <project|global>]
|
|
597
659
|
showdar validate
|
package/bin/showdar.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { AI_TARGETS, PRIMITIVE_COUNT, PROFILE_ALIASES, PROFILES, SKILLS, TOTAL_COUNT, WORKFLOW_COUNT, canonicalProfile, isDeprecatedProfile, resolveProfile } from '../src/catalog.js';
|
|
7
|
-
import { addSkill, globalManifestPath, initGlobal, initProject, inspectGlobal, inspectProject, removeGlobal, removeProject } from '../src/project.js';
|
|
7
|
+
import { addPack, addSkill, addWorkflow, globalManifestPath, initGlobal, initProject, inspectGlobal, inspectProject, listExtensions, removeGlobal, removePack, removeProject } from '../src/project.js';
|
|
8
8
|
import { validateRepository } from '../src/validate.js';
|
|
9
9
|
|
|
10
10
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -43,7 +43,7 @@ function printHelp(version, command = null) {
|
|
|
43
43
|
console.log(`Showdar Skills ${version}\n\nUsage:\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n\nExamples:\n showdar add debug\n showdar add showdar-security\n showdar add test --ai cursor\n showdar add review --scope global --ai claude\n\nDefault scope: project. Default AI target: universal, or the configured .showdar.json value when present.`);
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
|
-
console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>]\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list\n showdar remove ${scopeUsage}\n\nDefaults: scope project, profile full, AI target universal.\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
|
|
46
|
+
console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>] [--pack <local-path>]\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n showdar add-pack <local-path>\n showdar remove-pack <name>\n showdar add-workflow <local-path>\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n\nExtension packs accept local directories/workspace paths only; tarball, URL, Git, and registry sources are rejected.\n\nDefaults: scope project, profile full, AI target universal.\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async function main() {
|
|
@@ -62,11 +62,44 @@ async function main() {
|
|
|
62
62
|
const scope = ['init', 'status', 'doctor', 'remove', 'add'].includes(command) ? scopeAfter(args) : null;
|
|
63
63
|
|
|
64
64
|
if (command === 'list') {
|
|
65
|
+
if (args.includes('--extensions')) {
|
|
66
|
+
const result = await listExtensions({ cwd: projectRoot });
|
|
67
|
+
console.log('Packs:');
|
|
68
|
+
for (const pack of result.packs) console.log(` ${pack.name}@${pack.version} ${pack.hash}`);
|
|
69
|
+
console.log('Custom workflows:');
|
|
70
|
+
for (const workflow of result.customWorkflows) console.log(` ${workflow.id} [${workflow.source}] ${workflow.path}`);
|
|
71
|
+
console.log(`Project overrides: ${result.overrides.present ? result.overrides.status : 'absent'}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
65
74
|
console.log(`Profiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\n\nSkills:`);
|
|
66
75
|
for (const skill of SKILLS) console.log(` ${skill.id} [${skill.domain}] ${skill.description}`);
|
|
67
76
|
return;
|
|
68
77
|
}
|
|
69
78
|
|
|
79
|
+
if (command === 'add-pack') {
|
|
80
|
+
const packSource = args[1];
|
|
81
|
+
if (!packSource) throw new Error('Pack source is required. Usage: showdar add-pack <local-path>');
|
|
82
|
+
const result = await addPack({ cwd: projectRoot, source: packSource, packageVersion: version });
|
|
83
|
+
console.log(`Showdar pack added.\nPack: ${result.pack}\nFiles: ${result.files}\nHash: ${result.hash}\nPath: ${result.destination}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (command === 'remove-pack') {
|
|
88
|
+
const packName = args[1];
|
|
89
|
+
if (!packName) throw new Error('Pack name is required. Usage: showdar remove-pack <name>');
|
|
90
|
+
await removePack({ cwd: projectRoot, name: packName });
|
|
91
|
+
console.log(`Showdar pack removed: ${packName}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (command === 'add-workflow') {
|
|
96
|
+
const workflowSource = args[1];
|
|
97
|
+
if (!workflowSource) throw new Error('Workflow source is required. Usage: showdar add-workflow <local-path>');
|
|
98
|
+
const result = await addWorkflow({ cwd: projectRoot, source: workflowSource });
|
|
99
|
+
console.log(`Showdar workflow added.\nWorkflow: ${result.workflow}\nPath: ${result.path}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
70
103
|
if (command === 'validate') {
|
|
71
104
|
const result = await validateRepository(packageRoot);
|
|
72
105
|
if (result.ok) console.log(`Showdar validation OK (${PRIMITIVE_COUNT} primitives, ${WORKFLOW_COUNT} workflows, ${TOTAL_COUNT} total).`);
|
|
@@ -86,9 +119,15 @@ async function main() {
|
|
|
86
119
|
const ai = valueAfter(args, '--ai', 'universal');
|
|
87
120
|
const skillIds = resolveProfile(requestedProfile);
|
|
88
121
|
if (isDeprecatedProfile(requestedProfile)) console.warn(`Warning: profile "${requestedProfile}" is deprecated; use "${profile}".`);
|
|
122
|
+
const packSource = valueAfter(args, '--pack', null);
|
|
123
|
+
if (packSource && scope === 'global') throw new Error('--pack is only supported with project scope in 0.8.');
|
|
89
124
|
const result = scope === 'global'
|
|
90
125
|
? await initGlobal({ homeRoot: homedir(), packageRoot, profile, ai, skillIds, packageVersion: version })
|
|
91
126
|
: await initProject({ projectRoot, packageRoot, profile, ai, skillIds, packageVersion: version });
|
|
127
|
+
if (packSource) {
|
|
128
|
+
const pack = await addPack({ cwd: projectRoot, source: packSource, packageVersion: version });
|
|
129
|
+
console.log(`Pack: ${pack.pack}@${pack.version ?? ''} Hash: ${pack.hash}`);
|
|
130
|
+
}
|
|
92
131
|
console.log(`Showdar Skills installed.\nScope: ${scope}\nProfile: ${profile}\nAI: ${ai}\nTargets: ${result.targets.join(', ')}\nSkills: ${result.skills}\nOpenCode commands: ${result.commands}`);
|
|
93
132
|
if (scope === 'project') {
|
|
94
133
|
console.log(`Requested: ${result.requestedSkills}\nInstalled in project: ${result.installedSkills}\nSatisfied by global: ${result.satisfiedByGlobal}\nSkipped duplicate copies: ${result.skippedDuplicates}`);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Showdar custom workflow definition",
|
|
4
|
+
"description": "Custom workflows compose built-in primitive skills only. Stages, skip enums, completion policy, and state schema are frozen contracts.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["id", "stages"],
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"id": { "type": "string", "pattern": "^[a-z][a-z0-9]*-[a-z][a-z0-9-]*$" },
|
|
10
|
+
"description": { "type": "string", "minLength": 30 },
|
|
11
|
+
"stages": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"minItems": 1,
|
|
14
|
+
"uniqueItems": true,
|
|
15
|
+
"items": { "type": "string", "pattern": "^showdar-[a-z][a-z0-9-]*$" }
|
|
16
|
+
},
|
|
17
|
+
"allowedSkips": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"required": ["stage", "reason", "policy", "evidence"],
|
|
22
|
+
"additionalProperties": false,
|
|
23
|
+
"properties": {
|
|
24
|
+
"stage": { "type": "string", "pattern": "^showdar-[a-z][a-z0-9-]*$" },
|
|
25
|
+
"reason": { "enum": ["behavior-defined", "local-low-risk", "no-ux-decision", "known-root-cause", "readiness-only", "no-ops-authority", "no-security-risk"] },
|
|
26
|
+
"policy": { "enum": ["adaptive-skip", "known-root-cause", "readiness-only", "no-ops-authority", "no-security-risk", "behavior-defined", "local-low-risk", "no-ux-decision"] },
|
|
27
|
+
"evidence": { "type": "array", "items": { "type": "string" } }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"requiredStages": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": { "type": "string", "pattern": "^showdar-[a-z][a-z0-9-]*$" }
|
|
34
|
+
},
|
|
35
|
+
"completionPolicy": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"required": ["allSelectedStagesAccounted", "noBlockers", "requiredVerificationSatisfied", "noNegativeEvidence"],
|
|
38
|
+
"additionalProperties": false,
|
|
39
|
+
"properties": {
|
|
40
|
+
"allSelectedStagesAccounted": { "type": "boolean", "const": true },
|
|
41
|
+
"noBlockers": { "type": "boolean", "const": true },
|
|
42
|
+
"requiredVerificationSatisfied": { "type": "boolean", "const": true },
|
|
43
|
+
"noNegativeEvidence": { "type": "boolean", "const": true }
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"workflowStateCompat": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"required": ["schemaVersion", "selectableStages", "skipRules"],
|
|
49
|
+
"additionalProperties": false,
|
|
50
|
+
"properties": {
|
|
51
|
+
"schemaVersion": { "const": 1 },
|
|
52
|
+
"selectableStages": { "type": "array", "items": { "type": "string" } },
|
|
53
|
+
"skipRules": { "type": "object" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Showdar extension pack manifest",
|
|
4
|
+
"description": "Metadata only. pack.json MUST NOT contain the authoritative pack-content hash; Showdar computes hashTree() at install and records it in managed manifest state.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["name", "version", "skills"],
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"name": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
10
|
+
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
|
|
11
|
+
"description": { "type": "string" },
|
|
12
|
+
"skills": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"required": ["id", "path"],
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"properties": {
|
|
19
|
+
"id": { "type": "string", "pattern": "^[a-z][a-z0-9]*/[a-z][a-z0-9-]*$" },
|
|
20
|
+
"path": { "type": "string", "minLength": 1 },
|
|
21
|
+
"description": { "type": "string" },
|
|
22
|
+
"domains": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"minItems": 1,
|
|
25
|
+
"maxItems": 8,
|
|
26
|
+
"items": { "type": "string", "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", "minLength": 2, "maxLength": 40 }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"workflows": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": ["id", "path"],
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"properties": {
|
|
38
|
+
"id": { "type": "string", "pattern": "^[a-z][a-z0-9]*-[a-z][a-z0-9-]*$" },
|
|
39
|
+
"path": { "type": "string", "minLength": 1 }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"profiles": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"patternProperties": {
|
|
46
|
+
"^[a-z][a-z0-9-]*$": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"items": { "type": "string" }
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"additionalProperties": false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Showdar project overrides (.showdar/overrides.json)",
|
|
4
|
+
"description": "User-owned project extension configuration. Built-in workflow policy and built-in profile definitions are protected and MUST NOT appear here as semantic overrides.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {
|
|
8
|
+
"version": { "const": 1 },
|
|
9
|
+
"skillDescriptions": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"patternProperties": {
|
|
12
|
+
"^showdar-[a-z][a-z0-9-]*$": { "type": "string", "minLength": 1 }
|
|
13
|
+
},
|
|
14
|
+
"additionalProperties": false
|
|
15
|
+
},
|
|
16
|
+
"skillDomains": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"patternProperties": {
|
|
19
|
+
"^showdar-[a-z][a-z0-9-]*$": { "type": "string", "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", "minLength": 2, "maxLength": 40 }
|
|
20
|
+
},
|
|
21
|
+
"additionalProperties": false
|
|
22
|
+
},
|
|
23
|
+
"workflowPolicy": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"description": "Custom workflows only. Built-in showdar-* workflow IDs are rejected.",
|
|
26
|
+
"patternProperties": {
|
|
27
|
+
"^[a-z][a-z0-9]*-[a-z][a-z0-9-]*$": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"properties": {
|
|
31
|
+
"stages": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"minItems": 1,
|
|
34
|
+
"uniqueItems": true,
|
|
35
|
+
"items": { "type": "string", "pattern": "^showdar-[a-z][a-z0-9-]*$" }
|
|
36
|
+
},
|
|
37
|
+
"skipRules": { "type": "object" },
|
|
38
|
+
"requiredStages": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": { "type": "string", "pattern": "^showdar-[a-z][a-z0-9-]*$" }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"additionalProperties": false
|
|
46
|
+
},
|
|
47
|
+
"guidance": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"description": "Separate advisory text. MUST NOT replace SKILL.md semantics or claim authority.",
|
|
50
|
+
"patternProperties": {
|
|
51
|
+
"^showdar-[a-z][a-z0-9-]*$": { "type": "string", "minLength": 1 }
|
|
52
|
+
},
|
|
53
|
+
"additionalProperties": false
|
|
54
|
+
},
|
|
55
|
+
"profiles": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"description": "New project-owned profiles only. MUST NOT mutate built-in profile definitions.",
|
|
58
|
+
"patternProperties": {
|
|
59
|
+
"^[a-z][a-z0-9-]*$": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"minItems": 1,
|
|
62
|
+
"items": { "type": "string" }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"additionalProperties": false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "showdar-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Production-grade software engineering lifecycle skills for coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "showdar": "./bin/showdar.js" },
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
"eval": "npm run eval:retrieval && npm run eval:workflows",
|
|
12
12
|
"eval:retrieval": "node scripts/retrieval-eval.mjs",
|
|
13
13
|
"eval:workflows": "node scripts/workflow-observability-eval.mjs",
|
|
14
|
+
"eval:custom-workflows": "node scripts/custom-workflows-eval.mjs",
|
|
14
15
|
"check": "npm test && npm run validate && npm pack --dry-run",
|
|
15
16
|
"smoke": "node scripts/package-smoke.mjs",
|
|
16
17
|
"release:check": "node scripts/check-release-version.mjs"
|
|
17
18
|
},
|
|
18
19
|
"engines": { "node": ">=20" },
|
|
19
|
-
"files": ["bin", "src", "skills", "commands", "router", "bundles", "profiles", "engine", "README.md", "LICENSE", "CHANGELOG.md", "MIGRATION.md"],
|
|
20
|
+
"files": ["bin", "src", "config", "skills", "commands", "router", "bundles", "profiles", "engine", "README.md", "LICENSE", "CHANGELOG.md", "MIGRATION.md"],
|
|
20
21
|
"keywords": ["agent-skills", "coding-agents", "codex", "opencode", "claude-code", "software-engineering", "developer-tools", "requirements", "qa", "security", "devops", "workflow"],
|
|
21
22
|
"license": "MIT",
|
|
22
23
|
"repository": {
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { SKILLS, WORKFLOW_SKILLS, PROFILES, PROFILE_ALIASES, getSkill as getBuiltinSkill, getWorkflow as getBuiltinWorkflow } from './catalog.js';
|
|
2
|
+
import { SELECTABLE_STAGES, SKIP_RULES, REQUIRED_STAGES } from './workflow-state.js';
|
|
3
|
+
import {
|
|
4
|
+
validatePackManifest,
|
|
5
|
+
validateCustomWorkflowDoc,
|
|
6
|
+
validateProjectOverridesDoc,
|
|
7
|
+
validatePackSkillId,
|
|
8
|
+
validateCustomWorkflowId,
|
|
9
|
+
validateBuiltinSkillId,
|
|
10
|
+
containsForbiddenAuthorityKey,
|
|
11
|
+
} from './validate-pack.js';
|
|
12
|
+
|
|
13
|
+
function deepFreeze(value) {
|
|
14
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
15
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
16
|
+
Object.freeze(value);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sortedEntries(record) {
|
|
22
|
+
return Object.entries(record).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function builtinSnapshot() {
|
|
26
|
+
const skills = [...SKILLS].sort((a, b) => (a.id < b.id ? -1 : 1)).map((s) => deepFreeze({ ...s }));
|
|
27
|
+
const workflows = [...WORKFLOW_SKILLS].sort((a, b) => (a.id < b.id ? -1 : 1)).map((w) => deepFreeze({ ...w }));
|
|
28
|
+
const profiles = Object.fromEntries(sortedEntries(PROFILES).map(([name, members]) => [name, Object.freeze([...members].sort())]));
|
|
29
|
+
return {
|
|
30
|
+
skills: Object.freeze(skills),
|
|
31
|
+
workflows: Object.freeze(workflows),
|
|
32
|
+
profiles: Object.freeze(profiles),
|
|
33
|
+
overrides: Object.freeze({ skillDescriptions: Object.freeze({}), skillDomains: Object.freeze({}), guidance: Object.freeze({}) }),
|
|
34
|
+
selectableStages: Object.freeze(Object.fromEntries(sortedEntries(SELECTABLE_STAGES).map(([id, stages]) => [id, Object.freeze([...stages])]))),
|
|
35
|
+
skipRules: Object.freeze(Object.fromEntries(sortedEntries(SKIP_RULES).map(([id, rules]) => [id, deepFreeze(JSON.parse(JSON.stringify(rules)))]))),
|
|
36
|
+
requiredStages: Object.freeze(Object.fromEntries(sortedEntries(REQUIRED_STAGES).map(([id, stages]) => [id, Object.freeze([...stages])]))),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const BUILTIN_SNAPSHOT = deepFreeze(builtinSnapshot());
|
|
41
|
+
|
|
42
|
+
const BUILTIN_SKILL_IDS = new Set(SKILLS.map((s) => s.id));
|
|
43
|
+
const BUILTIN_WORKFLOW_IDS = new Set(WORKFLOW_SKILLS.map((w) => w.id));
|
|
44
|
+
const BUILTIN_PROFILE_NAMES = new Set(Object.keys(PROFILES));
|
|
45
|
+
|
|
46
|
+
export function createExtensionCatalog({ packs = [], projectOverrides = null } = {}) {
|
|
47
|
+
const errors = [];
|
|
48
|
+
if (!Array.isArray(packs)) return { ok: false, errors: ['packs must be an array'] };
|
|
49
|
+
if (projectOverrides !== null && (typeof projectOverrides !== 'object' || Array.isArray(projectOverrides))) {
|
|
50
|
+
return { ok: false, errors: ['projectOverrides must be an object or null'] };
|
|
51
|
+
}
|
|
52
|
+
const sortedPacks = [...packs].sort((a, b) => String(a?.manifest?.name ?? '').localeCompare(String(b?.manifest?.name ?? '')));
|
|
53
|
+
const skillById = new Map();
|
|
54
|
+
const workflowById = new Map();
|
|
55
|
+
const packProfiles = {};
|
|
56
|
+
const customWorkflows = new Map();
|
|
57
|
+
const seenSkillIds = new Set([...BUILTIN_SKILL_IDS, ...BUILTIN_WORKFLOW_IDS]);
|
|
58
|
+
|
|
59
|
+
for (const pack of sortedPacks) {
|
|
60
|
+
if (!pack || typeof pack !== 'object') { errors.push('pack entry must be an object'); continue; }
|
|
61
|
+
const manifest = pack.manifest;
|
|
62
|
+
const manifestResult = validatePackManifest(manifest);
|
|
63
|
+
if (!manifestResult.ok) { errors.push(...manifestResult.errors.map((e) => `pack ${manifest?.name ?? '?'}: ${e}`)); continue; }
|
|
64
|
+
const ownedIds = new Set([...(manifest.skills ?? []).map((s) => s.id), ...((manifest.workflows ?? []).map((w) => w.id))]);
|
|
65
|
+
for (const skill of manifest.skills) {
|
|
66
|
+
if (seenSkillIds.has(skill.id)) { errors.push(`duplicate skill id: ${skill.id}`); continue; }
|
|
67
|
+
seenSkillIds.add(skill.id);
|
|
68
|
+
skillById.set(skill.id, deepFreeze({
|
|
69
|
+
id: skill.id,
|
|
70
|
+
kind: 'pack',
|
|
71
|
+
pack: manifest.name,
|
|
72
|
+
description: skill.description ?? '',
|
|
73
|
+
domains: Object.freeze([...(skill.domains ?? [])].sort()),
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
for (const workflow of manifest.workflows ?? []) {
|
|
77
|
+
if (seenSkillIds.has(workflow.id)) { errors.push(`duplicate workflow id: ${workflow.id}`); continue; }
|
|
78
|
+
const doc = { ...(pack.workflows?.[workflow.id] ?? {}), id: workflow.id };
|
|
79
|
+
const docErrors = validateCustomWorkflowDoc(doc, `pack ${manifest.name} workflow ${workflow.id}`);
|
|
80
|
+
if (docErrors.length) { errors.push(...docErrors); continue; }
|
|
81
|
+
const authorityHit = containsForbiddenAuthorityKey(doc);
|
|
82
|
+
if (authorityHit) { errors.push(`pack ${manifest.name} workflow ${workflow.id} contains forbidden authority key at ${authorityHit}`); continue; }
|
|
83
|
+
seenSkillIds.add(workflow.id);
|
|
84
|
+
customWorkflows.set(workflow.id, { pack: manifest.name, doc: deepFreeze(JSON.parse(JSON.stringify(doc))) });
|
|
85
|
+
workflowById.set(workflow.id, deepFreeze({
|
|
86
|
+
id: workflow.id,
|
|
87
|
+
kind: 'custom-workflow',
|
|
88
|
+
pack: manifest.name,
|
|
89
|
+
description: doc.description ?? '',
|
|
90
|
+
stages: Object.freeze([...doc.stages].sort()),
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
for (const [name, members] of Object.entries(manifest.profiles ?? {})) {
|
|
94
|
+
if (BUILTIN_PROFILE_NAMES.has(name) || packProfiles[name]) { errors.push(`duplicate profile name: ${name}`); continue; }
|
|
95
|
+
for (const member of members) {
|
|
96
|
+
if (!ownedIds.has(member)) errors.push(`pack ${manifest.name} profile ${name} references non-pack-owned id: ${member}`);
|
|
97
|
+
}
|
|
98
|
+
packProfiles[name] = Object.freeze([...members].sort());
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let overridesResult = { ok: true, errors: [] };
|
|
103
|
+
let overrides = { skillDescriptions: {}, skillDomains: {}, guidance: {} };
|
|
104
|
+
if (projectOverrides) {
|
|
105
|
+
overridesResult = validateProjectOverridesDoc(projectOverrides, { knownIds: BUILTIN_SKILL_IDS });
|
|
106
|
+
if (!overridesResult.ok) errors.push(...overridesResult.errors.map((e) => `overrides: ${e}`));
|
|
107
|
+
else {
|
|
108
|
+
overrides = {
|
|
109
|
+
skillDescriptions: { ...(projectOverrides.skillDescriptions ?? {}) },
|
|
110
|
+
skillDomains: { ...(projectOverrides.skillDomains ?? {}) },
|
|
111
|
+
guidance: { ...(projectOverrides.guidance ?? {}) },
|
|
112
|
+
};
|
|
113
|
+
for (const id of Object.keys(projectOverrides.workflowPolicy ?? {})) {
|
|
114
|
+
if (!customWorkflows.has(id)) errors.push(`overrides workflowPolicy references unknown custom workflow: ${id}`);
|
|
115
|
+
}
|
|
116
|
+
for (const [name, members] of Object.entries(projectOverrides.profiles ?? {})) {
|
|
117
|
+
if (BUILTIN_PROFILE_NAMES.has(name)) { errors.push(`overrides MUST NOT mutate built-in profile: ${name}`); continue; }
|
|
118
|
+
for (const member of members) {
|
|
119
|
+
if (!BUILTIN_SKILL_IDS.has(member) && !BUILTIN_WORKFLOW_IDS.has(member) && !seenSkillIds.has(member)) {
|
|
120
|
+
errors.push(`overrides profile ${name} references unknown id: ${member}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (errors.length) return { ok: false, errors: [...new Set(errors)].sort() };
|
|
128
|
+
|
|
129
|
+
const skills = [...SKILLS.map((s) => ({ ...s })), ...[...skillById.values()].map((s) => ({ ...s }))].sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
130
|
+
const workflows = [...WORKFLOW_SKILLS.map((w) => ({ ...w })), ...[...workflowById.values()].map((w) => ({ ...w }))].sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
131
|
+
const selectableStages = { ...SELECTABLE_STAGES };
|
|
132
|
+
const skipRules = JSON.parse(JSON.stringify(SKIP_RULES));
|
|
133
|
+
const requiredStages = { ...REQUIRED_STAGES };
|
|
134
|
+
for (const [id, { doc }] of [...customWorkflows.entries()].sort(([a], [b]) => (a < b ? -1 : 1))) {
|
|
135
|
+
const refinement = projectOverrides?.workflowPolicy?.[id];
|
|
136
|
+
const stages = refinement?.stages ?? doc.stages;
|
|
137
|
+
const required = refinement?.requiredStages ?? doc.requiredStages ?? [];
|
|
138
|
+
const compat = doc.workflowStateCompat;
|
|
139
|
+
const skip = compat ? compat.skipRules : Object.fromEntries(
|
|
140
|
+
(doc.allowedSkips ?? []).map((s) => [s.stage, { reason: s.reason, policy: s.policy, evidence: s.evidence }]),
|
|
141
|
+
);
|
|
142
|
+
const refinedSkip = { ...skip, ...(refinement?.skipRules ?? {}) };
|
|
143
|
+
selectableStages[id] = [...stages];
|
|
144
|
+
requiredStages[id] = [...required];
|
|
145
|
+
skipRules[id] = refinedSkip;
|
|
146
|
+
}
|
|
147
|
+
const profiles = {
|
|
148
|
+
...Object.fromEntries(sortedEntries(PROFILES).map(([name, members]) => [name, [...members].sort()])),
|
|
149
|
+
...Object.fromEntries(sortedEntries(packProfiles).map(([name, members]) => [name, [...members]])),
|
|
150
|
+
...Object.fromEntries(sortedEntries(projectOverrides?.profiles ?? {}).map(([name, members]) => [name, [...members].sort()])),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const snapshot = {
|
|
154
|
+
skills: Object.freeze(skills.map((s) => deepFreeze(s))),
|
|
155
|
+
workflows: Object.freeze(workflows.map((w) => deepFreeze(w))),
|
|
156
|
+
profiles: deepFreeze(profiles),
|
|
157
|
+
overrides: deepFreeze(JSON.parse(JSON.stringify(overrides))),
|
|
158
|
+
selectableStages: deepFreeze(JSON.parse(JSON.stringify(selectableStages))),
|
|
159
|
+
skipRules: deepFreeze(JSON.parse(JSON.stringify(skipRules))),
|
|
160
|
+
requiredStages: deepFreeze(JSON.parse(JSON.stringify(requiredStages))),
|
|
161
|
+
};
|
|
162
|
+
return { ok: true, errors: [], value: deepFreeze(snapshot) };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function getCatalogSkill(catalog, id) {
|
|
166
|
+
const snapshot = catalog ?? BUILTIN_SNAPSHOT;
|
|
167
|
+
const skill = snapshot.skills.find((s) => s.id === id) ?? null;
|
|
168
|
+
if (!skill) return getBuiltinSkill(id);
|
|
169
|
+
const description = snapshot.overrides?.skillDescriptions?.[id] ?? skill.description;
|
|
170
|
+
const domain = snapshot.overrides?.skillDomains?.[id];
|
|
171
|
+
const descChanged = description !== skill.description;
|
|
172
|
+
const domainChanged = domain !== undefined && domain !== skill.domain;
|
|
173
|
+
if (!descChanged && !domainChanged) return skill;
|
|
174
|
+
return { ...skill, ...(descChanged ? { description } : {}), ...(domainChanged ? { domain } : {}) };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getCatalogWorkflow(catalog, id) {
|
|
178
|
+
const snapshot = catalog ?? BUILTIN_SNAPSHOT;
|
|
179
|
+
return snapshot.workflows.find((w) => w.id === id) ?? getBuiltinWorkflow(id);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function resolveCatalogProfile(catalog, profile) {
|
|
183
|
+
const snapshot = catalog ?? BUILTIN_SNAPSHOT;
|
|
184
|
+
const canonical = PROFILE_ALIASES[profile] ?? profile;
|
|
185
|
+
if (snapshot.profiles[canonical]) return [...snapshot.profiles[canonical]];
|
|
186
|
+
if (Object.hasOwn(PROFILE_ALIASES, profile) && snapshot.profiles[PROFILE_ALIASES[profile]]) return [...snapshot.profiles[PROFILE_ALIASES[profile]]];
|
|
187
|
+
throw new Error(`Unknown profile "${profile}".`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const extensionCatalogAPI = {
|
|
191
|
+
createExtensionCatalog,
|
|
192
|
+
getCatalogSkill,
|
|
193
|
+
getCatalogWorkflow,
|
|
194
|
+
resolveCatalogProfile,
|
|
195
|
+
BUILTIN_SNAPSHOT,
|
|
196
|
+
};
|