@hegemonart/get-design-done 1.45.0 → 1.47.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +97 -0
- package/README.md +4 -0
- package/SKILL.md +5 -1
- package/dist/claude-code/.claude/skills/figma-extract/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/graphify/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/list-pins/SKILL.md +27 -0
- package/dist/claude-code/.claude/skills/live/SKILL.md +98 -0
- package/dist/claude-code/.claude/skills/pin/SKILL.md +37 -0
- package/dist/claude-code/.claude/skills/unpin/SKILL.md +31 -0
- package/package.json +3 -1
- package/reference/live-mode-integration.md +80 -0
- package/reference/registry.json +14 -0
- package/reference/schemas/events.schema.json +1 -1
- package/reference/schemas/live-session.schema.json +64 -0
- package/reference/skill-metadata.md +117 -0
- package/scripts/lib/live/bandit-feed.cjs +64 -0
- package/scripts/lib/live/events.cjs +86 -0
- package/scripts/lib/live/harness-mode.cjs +93 -0
- package/scripts/lib/live/postcheck.cjs +158 -0
- package/scripts/lib/live/runtime.cjs +233 -0
- package/scripts/lib/live/scope-guard.cjs +145 -0
- package/scripts/lib/live/session-store.cjs +364 -0
- package/scripts/lib/manifest/schemas/skills.schema.json +42 -1
- package/scripts/lib/manifest/skills.json +415 -83
- package/scripts/lib/pin/cli.cjs +145 -0
- package/scripts/lib/pin/harness-detect.cjs +75 -0
- package/scripts/lib/pin/store.cjs +288 -0
- package/skills/figma-extract/SKILL.md +1 -1
- package/skills/graphify/SKILL.md +1 -1
- package/skills/list-pins/SKILL.md +27 -0
- package/skills/live/SKILL.md +98 -0
- package/skills/pin/SKILL.md +37 -0
- package/skills/unpin/SKILL.md +31 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/pin/cli.cjs — Phase 46 (Skill UX Polish).
|
|
4
|
+
*
|
|
5
|
+
* Thin CLI over scripts/lib/pin/store.cjs. projectRoot is always process.cwd().
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node cli.cjs pin <skill> [--user]
|
|
9
|
+
* node cli.cjs unpin <skill>
|
|
10
|
+
* node cli.cjs list
|
|
11
|
+
*
|
|
12
|
+
* Exit codes:
|
|
13
|
+
* 0 action succeeded (at least one file written/removed, or a non-empty list)
|
|
14
|
+
* 1 nothing done (no harness dirs / nothing to remove / empty list)
|
|
15
|
+
* 2 error (bad usage, unknown skill, unexpected failure)
|
|
16
|
+
*
|
|
17
|
+
* Dependency-free CommonJS. Ships in the npm package; runtime-safe.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const { pinSkill, unpinSkill, listPins } = require('./store.cjs');
|
|
23
|
+
|
|
24
|
+
function out(msg) {
|
|
25
|
+
process.stdout.write(msg + '\n');
|
|
26
|
+
}
|
|
27
|
+
function err(msg) {
|
|
28
|
+
process.stderr.write(msg + '\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function usage() {
|
|
32
|
+
return [
|
|
33
|
+
'gdd pin - manage pinned skill aliases across installed harness dirs',
|
|
34
|
+
'',
|
|
35
|
+
'Usage:',
|
|
36
|
+
' node cli.cjs pin <skill> [--user]',
|
|
37
|
+
' node cli.cjs unpin <skill>',
|
|
38
|
+
' node cli.cjs list',
|
|
39
|
+
'',
|
|
40
|
+
'Exit codes: 0 ok / 1 nothing done / 2 error.',
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runPin(skillId, opts) {
|
|
45
|
+
const projectRoot = process.cwd();
|
|
46
|
+
let res;
|
|
47
|
+
try {
|
|
48
|
+
res = pinSkill({ projectRoot, skillId, user: Boolean(opts.user) });
|
|
49
|
+
} catch (e) {
|
|
50
|
+
err(`pin: ${e.message}`);
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
if (res.written.length === 0) {
|
|
54
|
+
err(`pin: no harness skills dirs found under ${projectRoot}${opts.user ? '' : ' (try --user to create them)'}.`);
|
|
55
|
+
for (const s of res.skipped) err(` skipped ${s.config_dir}: ${s.reason}`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
out(`Pinned "${skillId}" into ${res.written.length} harness dir(s):`);
|
|
59
|
+
for (const w of res.written) out(` ${w.config_dir} -> ${path.relative(projectRoot, w.path)}`);
|
|
60
|
+
for (const s of res.skipped) err(` skipped ${s.config_dir}: ${s.reason}`);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runUnpin(skillId) {
|
|
65
|
+
const projectRoot = process.cwd();
|
|
66
|
+
let res;
|
|
67
|
+
try {
|
|
68
|
+
res = unpinSkill({ projectRoot, skillId });
|
|
69
|
+
} catch (e) {
|
|
70
|
+
err(`unpin: ${e.message}`);
|
|
71
|
+
return 2;
|
|
72
|
+
}
|
|
73
|
+
for (const r of res.refused) err(` refused ${r.config_dir}: ${r.reason}`);
|
|
74
|
+
if (res.removed.length === 0) {
|
|
75
|
+
err(`unpin: no pinned "${skillId}" stubs removed.`);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
out(`Unpinned "${skillId}" from ${res.removed.length} harness dir(s):`);
|
|
79
|
+
for (const r of res.removed) out(` ${r.config_dir} -> ${path.relative(projectRoot, r.path)}`);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runList() {
|
|
84
|
+
const projectRoot = process.cwd();
|
|
85
|
+
let pins;
|
|
86
|
+
try {
|
|
87
|
+
pins = listPins(projectRoot);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
err(`list: ${e.message}`);
|
|
90
|
+
return 2;
|
|
91
|
+
}
|
|
92
|
+
if (pins.length === 0) {
|
|
93
|
+
out('No pinned skills found.');
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
out(`Pinned skills (${pins.length}):`);
|
|
97
|
+
for (const p of pins) {
|
|
98
|
+
out(` [${p.config_dir}] ${p.alias} -> source=${p.source} (pinned ${p.pinnedAt})`);
|
|
99
|
+
}
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Pure entry point. argv is the slice AFTER node + script (process.argv.slice(2)).
|
|
105
|
+
* Returns an exit code; never calls process.exit (so tests can call it directly).
|
|
106
|
+
*/
|
|
107
|
+
function main(argv) {
|
|
108
|
+
const args = Array.isArray(argv) ? argv.slice() : [];
|
|
109
|
+
const cmd = args.shift();
|
|
110
|
+
|
|
111
|
+
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
112
|
+
out(usage());
|
|
113
|
+
return cmd ? 0 : 2;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (cmd === 'list') {
|
|
117
|
+
if (args.length) { err(`list: unexpected argument "${args[0]}"`); return 2; }
|
|
118
|
+
return runList();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (cmd === 'pin' || cmd === 'unpin') {
|
|
122
|
+
const opts = { user: false };
|
|
123
|
+
const positionals = [];
|
|
124
|
+
for (const a of args) {
|
|
125
|
+
if (a === '--user') opts.user = true;
|
|
126
|
+
else if (a.startsWith('--')) { err(`${cmd}: unknown flag ${a}`); return 2; }
|
|
127
|
+
else positionals.push(a);
|
|
128
|
+
}
|
|
129
|
+
const skillId = positionals[0];
|
|
130
|
+
if (!skillId) { err(`${cmd}: missing <skill> argument`); return 2; }
|
|
131
|
+
if (positionals.length > 1) { err(`${cmd}: unexpected argument "${positionals[1]}"`); return 2; }
|
|
132
|
+
if (cmd === 'unpin' && opts.user) { err('unpin: --user is not valid for unpin'); return 2; }
|
|
133
|
+
return cmd === 'pin' ? runPin(skillId, opts) : runUnpin(skillId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
err(`unknown command: ${cmd}`);
|
|
137
|
+
err(usage());
|
|
138
|
+
return 2;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (require.main === module) {
|
|
142
|
+
process.exit(main(process.argv.slice(2)));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { main };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/pin/harness-detect.cjs — Phase 46 (Skill UX Polish).
|
|
4
|
+
*
|
|
5
|
+
* Locates the per-harness `skills/` directories under a project root so the pin
|
|
6
|
+
* store knows where to write / scan pinned skill stubs. Each harness record in
|
|
7
|
+
* scripts/lib/manifest/harnesses.cjs carries a `config_dir` (e.g. ".claude",
|
|
8
|
+
* ".cursor", ".codex"); the candidate skills dir for a harness is
|
|
9
|
+
* `<projectRoot>/<config_dir>/skills`.
|
|
10
|
+
*
|
|
11
|
+
* Two surfaces:
|
|
12
|
+
* detectHarnessSkillDirs(projectRoot) -> only the candidates that EXIST on disk
|
|
13
|
+
* harnessSkillDirCandidates(projectRoot)-> ALL candidates (existing or not), for
|
|
14
|
+
* the --user / create flows that may need
|
|
15
|
+
* to materialize a missing dir.
|
|
16
|
+
*
|
|
17
|
+
* Dependency-free CommonJS. Cross-platform: all path joins go through `path`,
|
|
18
|
+
* never a hardcoded separator. Ships inside the npm package, so it must stay
|
|
19
|
+
* runtime-safe (no dev-only requires).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
const harnesses = require('../manifest/harnesses.cjs');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the full candidate list (one entry per harness record), regardless of
|
|
29
|
+
* whether the directory currently exists. The candidate skills dir for a harness
|
|
30
|
+
* is `<projectRoot>/<config_dir>/skills`.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} projectRoot absolute or relative project root
|
|
33
|
+
* @returns {Array<{ id: string, config_dir: string, skillsDir: string }>}
|
|
34
|
+
*/
|
|
35
|
+
function harnessSkillDirCandidates(projectRoot) {
|
|
36
|
+
if (!projectRoot || typeof projectRoot !== 'string') {
|
|
37
|
+
throw new TypeError('harnessSkillDirCandidates: projectRoot must be a non-empty string');
|
|
38
|
+
}
|
|
39
|
+
const out = [];
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
for (const h of harnesses) {
|
|
42
|
+
if (!h || !h.config_dir) continue;
|
|
43
|
+
// De-dupe on config_dir so two records pointing at the same dir don't double up.
|
|
44
|
+
if (seen.has(h.config_dir)) continue;
|
|
45
|
+
seen.add(h.config_dir);
|
|
46
|
+
out.push({
|
|
47
|
+
id: h.id,
|
|
48
|
+
config_dir: h.config_dir,
|
|
49
|
+
skillsDir: path.join(projectRoot, h.config_dir, 'skills'),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Filter the candidate list to the harness skills dirs that actually exist as
|
|
57
|
+
* directories under projectRoot.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} projectRoot absolute or relative project root
|
|
60
|
+
* @returns {Array<{ id: string, config_dir: string, skillsDir: string }>}
|
|
61
|
+
*/
|
|
62
|
+
function detectHarnessSkillDirs(projectRoot) {
|
|
63
|
+
return harnessSkillDirCandidates(projectRoot).filter((c) => {
|
|
64
|
+
try {
|
|
65
|
+
return fs.statSync(c.skillsDir).isDirectory();
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
detectHarnessSkillDirs,
|
|
74
|
+
harnessSkillDirCandidates,
|
|
75
|
+
};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/pin/store.cjs — Phase 46 (Skill UX Polish).
|
|
4
|
+
*
|
|
5
|
+
* Core for "pinning" a gdd skill: writing a small standalone shortcut alias
|
|
6
|
+
* (a SKILL.md stub) into every installed harness `skills/` dir so the skill is
|
|
7
|
+
* directly discoverable as its own command in each runtime, plus the inverse
|
|
8
|
+
* (unpin) and an inventory (listPins).
|
|
9
|
+
*
|
|
10
|
+
* The pin marker is exactly:
|
|
11
|
+
* <!-- gdd-pinned-skill source=<skillId> -->
|
|
12
|
+
* and is the FIRST line of every pinned stub. unpin only ever deletes files
|
|
13
|
+
* carrying this marker, so a hand-written / unrelated SKILL.md is never removed.
|
|
14
|
+
*
|
|
15
|
+
* Metadata (name, description, argument-hint, tools) is pulled from the manifest
|
|
16
|
+
* SoT via readSkills() — NEVER scraped from live frontmatter — so a pinned stub
|
|
17
|
+
* always reflects the canonical record.
|
|
18
|
+
*
|
|
19
|
+
* Writes are atomic: contents go to `<dest>.tmp` then fs.renameSync to the final
|
|
20
|
+
* path (rename is atomic within a filesystem), so a crash mid-write never leaves
|
|
21
|
+
* a half-written SKILL.md.
|
|
22
|
+
*
|
|
23
|
+
* Dependency-free CommonJS. Cross-platform via `path`. Ships in the npm package,
|
|
24
|
+
* so it stays runtime-safe (no dev-only requires).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
const { readSkills } = require('../manifest/index.cjs');
|
|
31
|
+
const { detectHarnessSkillDirs, harnessSkillDirCandidates } = require('./harness-detect.cjs');
|
|
32
|
+
|
|
33
|
+
const MARKER_PREFIX = '<!-- gdd-pinned-skill source=';
|
|
34
|
+
const MARKER_SUFFIX = ' -->';
|
|
35
|
+
|
|
36
|
+
/** Build the exact marker line for a skill id. */
|
|
37
|
+
function markerFor(skillId) {
|
|
38
|
+
return `${MARKER_PREFIX}${skillId}${MARKER_SUFFIX}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract the `source=<id>` skill id from a marker line, or null if the line is
|
|
43
|
+
* not a gdd pin marker. Tolerates surrounding whitespace.
|
|
44
|
+
*/
|
|
45
|
+
function parseMarker(line) {
|
|
46
|
+
if (typeof line !== 'string') return null;
|
|
47
|
+
const trimmed = line.trim();
|
|
48
|
+
if (!trimmed.startsWith(MARKER_PREFIX) || !trimmed.endsWith(MARKER_SUFFIX)) return null;
|
|
49
|
+
const inner = trimmed.slice(MARKER_PREFIX.length, trimmed.length - MARKER_SUFFIX.length);
|
|
50
|
+
const id = inner.trim();
|
|
51
|
+
return id.length ? id : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** First non-empty line of a text blob (trimmed), or '' if none. */
|
|
55
|
+
function firstNonEmptyLine(text) {
|
|
56
|
+
const lines = String(text).replace(/\r\n/g, '\n').split('\n');
|
|
57
|
+
for (const l of lines) {
|
|
58
|
+
if (l.trim().length) return l;
|
|
59
|
+
}
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Look up a skill record from the manifest SoT by id, or null. */
|
|
64
|
+
function lookupSkill(skillId) {
|
|
65
|
+
const { skills } = readSkills();
|
|
66
|
+
for (const r of skills || []) {
|
|
67
|
+
if (r && r.name === skillId) return r;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Double-quote a YAML scalar, escaping backslashes and quotes. */
|
|
73
|
+
function quote(s) {
|
|
74
|
+
return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Render the pinned stub contents for a skill record. Layout:
|
|
79
|
+
* <marker line>
|
|
80
|
+
* ---
|
|
81
|
+
* name: gdd-<id>
|
|
82
|
+
* description: "<desc>"
|
|
83
|
+
* argument-hint: "<hint>" (only when the record has one)
|
|
84
|
+
* tools: <tools> (only when the record has tools)
|
|
85
|
+
* ---
|
|
86
|
+
* <one-line body pointing at the source skill>
|
|
87
|
+
*
|
|
88
|
+
* `name` mirrors the generator: `gdd-<id>` unless the record overrides via
|
|
89
|
+
* `frontmatter_name`.
|
|
90
|
+
*/
|
|
91
|
+
function renderStub(skillId, rec) {
|
|
92
|
+
const fmName = rec.frontmatter_name || `gdd-${skillId}`;
|
|
93
|
+
const lines = [];
|
|
94
|
+
lines.push(markerFor(skillId));
|
|
95
|
+
lines.push('---');
|
|
96
|
+
lines.push(`name: ${fmName}`);
|
|
97
|
+
lines.push(`description: ${quote(rec.description || '')}`);
|
|
98
|
+
if (rec.argument_hint != null && String(rec.argument_hint).length) {
|
|
99
|
+
lines.push(`argument-hint: ${quote(rec.argument_hint)}`);
|
|
100
|
+
}
|
|
101
|
+
if (rec.tools != null && String(rec.tools).length) {
|
|
102
|
+
lines.push(`tools: ${rec.tools}`);
|
|
103
|
+
}
|
|
104
|
+
lines.push('---');
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push(`Pinned alias for the gdd \`${skillId}\` skill. Run the canonical \`${fmName}\` skill; this stub only makes it directly discoverable in this harness.`);
|
|
107
|
+
lines.push('');
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Atomic write: write to `<dest>.tmp` then rename into place. */
|
|
112
|
+
function atomicWrite(dest, contents) {
|
|
113
|
+
const dir = path.dirname(dest);
|
|
114
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
115
|
+
const tmp = `${dest}.tmp`;
|
|
116
|
+
fs.writeFileSync(tmp, contents, 'utf8');
|
|
117
|
+
try {
|
|
118
|
+
fs.renameSync(tmp, dest);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
// Clean up the temp file on failure so we never leave a stray .tmp behind.
|
|
121
|
+
try { fs.unlinkSync(tmp); } catch { /* ignore */ }
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Pin a skill across harness skills dirs.
|
|
128
|
+
*
|
|
129
|
+
* @param {object} args
|
|
130
|
+
* @param {string} args.projectRoot
|
|
131
|
+
* @param {string} args.skillId
|
|
132
|
+
* @param {Array<string>} [args.harnesses] optional allow-list of config_dir or harness id; when omitted, all detected dirs
|
|
133
|
+
* @param {boolean} [args.user] when true, materialize ALL candidate dirs (not just existing ones)
|
|
134
|
+
* @returns {{ skillId: string, written: Array<{ id, config_dir, path }>, skipped: Array<{ id, config_dir, reason }> }}
|
|
135
|
+
*/
|
|
136
|
+
function pinSkill(args) {
|
|
137
|
+
const { projectRoot, skillId } = args || {};
|
|
138
|
+
if (!projectRoot) throw new TypeError('pinSkill: projectRoot is required');
|
|
139
|
+
if (!skillId) throw new TypeError('pinSkill: skillId is required');
|
|
140
|
+
|
|
141
|
+
const rec = lookupSkill(skillId);
|
|
142
|
+
if (!rec) {
|
|
143
|
+
throw new Error(`pinSkill: "${skillId}" is not a known skill in scripts/lib/manifest/skills.json`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const all = args.user
|
|
147
|
+
? harnessSkillDirCandidates(projectRoot)
|
|
148
|
+
: detectHarnessSkillDirs(projectRoot);
|
|
149
|
+
|
|
150
|
+
const filter = Array.isArray(args.harnesses) && args.harnesses.length
|
|
151
|
+
? new Set(args.harnesses)
|
|
152
|
+
: null;
|
|
153
|
+
const targets = filter
|
|
154
|
+
? all.filter((c) => filter.has(c.config_dir) || filter.has(c.id))
|
|
155
|
+
: all;
|
|
156
|
+
|
|
157
|
+
const contents = renderStub(skillId, rec);
|
|
158
|
+
const written = [];
|
|
159
|
+
const skipped = [];
|
|
160
|
+
for (const t of targets) {
|
|
161
|
+
const dest = path.join(t.skillsDir, skillId, 'SKILL.md');
|
|
162
|
+
try {
|
|
163
|
+
atomicWrite(dest, contents);
|
|
164
|
+
written.push({ id: t.id, config_dir: t.config_dir, path: dest });
|
|
165
|
+
} catch (e) {
|
|
166
|
+
skipped.push({ id: t.id, config_dir: t.config_dir, reason: e.message });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { skillId, written, skipped };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Unpin a skill: delete pinned stubs across harness dirs. REFUSES (skips with a
|
|
174
|
+
* warning) any SKILL.md whose first non-empty line is not the gdd pin marker, so
|
|
175
|
+
* a hand-authored skill is never deleted.
|
|
176
|
+
*
|
|
177
|
+
* @param {object} args
|
|
178
|
+
* @param {string} args.projectRoot
|
|
179
|
+
* @param {string} args.skillId
|
|
180
|
+
* @returns {{ skillId: string, removed: Array<{ id, config_dir, path }>, refused: Array<{ id, config_dir, path, reason }>, missing: Array<{ id, config_dir, path }> }}
|
|
181
|
+
*/
|
|
182
|
+
function unpinSkill(args) {
|
|
183
|
+
const { projectRoot, skillId } = args || {};
|
|
184
|
+
if (!projectRoot) throw new TypeError('unpinSkill: projectRoot is required');
|
|
185
|
+
if (!skillId) throw new TypeError('unpinSkill: skillId is required');
|
|
186
|
+
|
|
187
|
+
// Look across every candidate harness dir (existing or not) so we can clean up
|
|
188
|
+
// stubs even if the surrounding harness dir was partially removed.
|
|
189
|
+
const candidates = harnessSkillDirCandidates(projectRoot);
|
|
190
|
+
const removed = [];
|
|
191
|
+
const refused = [];
|
|
192
|
+
const missing = [];
|
|
193
|
+
|
|
194
|
+
for (const c of candidates) {
|
|
195
|
+
const file = path.join(c.skillsDir, skillId, 'SKILL.md');
|
|
196
|
+
let content;
|
|
197
|
+
try {
|
|
198
|
+
content = fs.readFileSync(file, 'utf8');
|
|
199
|
+
} catch {
|
|
200
|
+
missing.push({ id: c.id, config_dir: c.config_dir, path: file });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const marker = parseMarker(firstNonEmptyLine(content));
|
|
204
|
+
if (marker == null) {
|
|
205
|
+
refused.push({
|
|
206
|
+
id: c.id,
|
|
207
|
+
config_dir: c.config_dir,
|
|
208
|
+
path: file,
|
|
209
|
+
reason: 'first non-empty line lacks the gdd-pinned-skill marker - refusing to delete',
|
|
210
|
+
});
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
fs.unlinkSync(file);
|
|
215
|
+
// Remove the now-empty alias dir if nothing else lives there.
|
|
216
|
+
const aliasDir = path.dirname(file);
|
|
217
|
+
try {
|
|
218
|
+
if (fs.readdirSync(aliasDir).length === 0) fs.rmdirSync(aliasDir);
|
|
219
|
+
} catch { /* leave non-empty dir alone */ }
|
|
220
|
+
removed.push({ id: c.id, config_dir: c.config_dir, path: file });
|
|
221
|
+
} catch (e) {
|
|
222
|
+
refused.push({ id: c.id, config_dir: c.config_dir, path: file, reason: e.message });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { skillId, removed, refused, missing };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* List pinned skills across harness skills dirs.
|
|
230
|
+
*
|
|
231
|
+
* Scans each existing harness skills dir for `<alias>/SKILL.md` files whose first
|
|
232
|
+
* non-empty line carries the gdd pin marker.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} projectRoot
|
|
235
|
+
* @returns {Array<{ id: string, config_dir: string, alias: string, source: string, pinnedAt: string }>}
|
|
236
|
+
* `id` is the harness id, `alias` is the on-disk directory name, `source` is the
|
|
237
|
+
* pinned source skill id from the marker, `pinnedAt` is the file mtime ISO string.
|
|
238
|
+
*/
|
|
239
|
+
function listPins(projectRoot) {
|
|
240
|
+
if (!projectRoot) throw new TypeError('listPins: projectRoot is required');
|
|
241
|
+
const out = [];
|
|
242
|
+
for (const dir of detectHarnessSkillDirs(projectRoot)) {
|
|
243
|
+
let entries;
|
|
244
|
+
try {
|
|
245
|
+
entries = fs.readdirSync(dir.skillsDir, { withFileTypes: true });
|
|
246
|
+
} catch {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
for (const e of entries) {
|
|
250
|
+
if (!e.isDirectory()) continue;
|
|
251
|
+
const file = path.join(dir.skillsDir, e.name, 'SKILL.md');
|
|
252
|
+
let content;
|
|
253
|
+
let stat;
|
|
254
|
+
try {
|
|
255
|
+
content = fs.readFileSync(file, 'utf8');
|
|
256
|
+
stat = fs.statSync(file);
|
|
257
|
+
} catch {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const source = parseMarker(firstNonEmptyLine(content));
|
|
261
|
+
if (source == null) continue;
|
|
262
|
+
out.push({
|
|
263
|
+
id: dir.id,
|
|
264
|
+
config_dir: dir.config_dir,
|
|
265
|
+
alias: e.name,
|
|
266
|
+
source,
|
|
267
|
+
pinnedAt: stat.mtime.toISOString(),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Stable order: by config_dir then alias for deterministic output.
|
|
272
|
+
out.sort((a, b) => (a.config_dir === b.config_dir
|
|
273
|
+
? a.alias.localeCompare(b.alias)
|
|
274
|
+
: a.config_dir.localeCompare(b.config_dir)));
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
pinSkill,
|
|
280
|
+
unpinSkill,
|
|
281
|
+
listPins,
|
|
282
|
+
// exported for the CLI + tests
|
|
283
|
+
markerFor,
|
|
284
|
+
parseMarker,
|
|
285
|
+
renderStub,
|
|
286
|
+
MARKER_PREFIX,
|
|
287
|
+
MARKER_SUFFIX,
|
|
288
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gdd-figma-extract
|
|
3
|
-
description: Off-context Figma design-system extraction into a compact local digest (DESIGN.md + tokens.json + components.json). Pulls the file via the Figma REST API and digests it without the raw JSON ever entering the model context.
|
|
3
|
+
description: "Off-context Figma design-system extraction into a compact local digest (DESIGN.md + tokens.json + components.json). Pulls the file via the Figma REST API and digests it without the raw JSON ever entering the model context."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# gdd-figma-extract
|
package/skills/graphify/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gdd-graphify
|
|
3
|
-
description: Manage the Graphify knowledge graph for the current project. Build, query, status, diff. When available, design-planner and design-integration-checker use the graph for pre-search consultation.
|
|
3
|
+
description: "Manage the Graphify knowledge graph for the current project. Build, query, status, diff. When available, design-planner and design-integration-checker use the graph for pre-search consultation."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# gdd-graphify
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gdd-list-pins
|
|
3
|
+
description: "Lists pinned skill aliases per harness with their source skill and pin timestamp. Use when you want to see which gdd skills have been pinned as standalone shortcuts and where."
|
|
4
|
+
tools: Read, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /gdd:list-pins
|
|
8
|
+
|
|
9
|
+
**Role:** Show every pinned skill alias across the installed harness `skills/` directories. For each one, report the harness it lives in, the on-disk alias directory name, the source skill it points at (from the `<!-- gdd-pinned-skill source=<skill> -->` marker), and when it was pinned (the file modification time).
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
1. **Run the list CLI.** Invoke the shipped script (it takes no arguments). The plugin root resolves via `CLAUDE_PLUGIN_ROOT` (falling back to the current directory when that variable is absent):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
node "${CLAUDE_PLUGIN_ROOT:-$(pwd)}/scripts/lib/pin/cli.cjs" list
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The CLI scans each harness `skills/` directory under the current project, finds the stubs carrying the gdd pin marker, and prints one line per pinned alias in the form `[<config-dir>] <alias> -> source=<skill> (pinned <timestamp>)`.
|
|
20
|
+
|
|
21
|
+
2. **Report the result.** Relay the CLI output verbatim. Exit codes: 0 means one or more pinned aliases were found, 1 means none were found (nothing has been pinned yet), 2 means an error.
|
|
22
|
+
|
|
23
|
+
## Do Not
|
|
24
|
+
|
|
25
|
+
- Do not scan the harness directories by hand. The CLI already enforces the marker check, so only genuine gdd pins are listed.
|
|
26
|
+
|
|
27
|
+
## LIST-PINS COMPLETE
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gdd-live
|
|
3
|
+
description: "Live in-browser design mode. The user picks a DOM element on a running dev server (via the Claude Preview MCP), the agent generates N design variants in one batch, they hot-swap in place through HMR or preview_eval using a data-gdd-variant marker, the user accepts or discards, and the whole pick-generate-accept loop persists to .design/live-sessions so it survives a crash or resume. Use when the user wants to iterate on the look of a live component against a real running server, asks to try variants on a page, or runs the live command with a url; falls back to a screenshot-only degraded mode on harnesses without MCP support."
|
|
4
|
+
argument-hint: "[--variants N] [--resume <session-id>] [url]"
|
|
5
|
+
tools: Read, Write, Edit, Bash, Glob, Grep, Task
|
|
6
|
+
user-invocable: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# gdd-live - Live In-Browser Design Mode
|
|
10
|
+
|
|
11
|
+
Pick a DOM element on a running dev server, generate competing design variants, hot-swap them in place, and accept the winner as a real source edit. Every step persists to `.design/live-sessions/<id>.json` so the session survives a crash or a later resume.
|
|
12
|
+
|
|
13
|
+
The browser-side runtime, the harness-mode gate, the session store, the events feed, the post-check, the scope guard, and the bandit feed are all separate modules under `scripts/lib/live/`. This skill describes the loop and names the module that owns each step; it does not import them.
|
|
14
|
+
|
|
15
|
+
For the full surface (the Preview MCP tools, the six `live_*` events, the session file, the bandit feed, degraded mode, the scope guard), see `../../reference/live-mode-integration.md`. For the SKILL.md structural contract, see `../../reference/skill-authoring-contract.md`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Arguments
|
|
20
|
+
|
|
21
|
+
- `[url]` - the page to drive. Optional. When omitted, detect the dev server and use its root.
|
|
22
|
+
- `--variants N` - how many variants to generate per pick. Default 3.
|
|
23
|
+
- `--resume <session-id>` - reattach to an in-progress session in `.design/live-sessions/`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## BOOT
|
|
28
|
+
|
|
29
|
+
1. Probe the Preview MCP per `../../connections/preview.md`: `ToolSearch({ query: "Claude_Preview" })`, then `mcp__Claude_Preview__preview_list`. Empty ToolSearch means the MCP is not loaded.
|
|
30
|
+
2. Resolve the harness live mode. The capability signal is `capability_matrix.mcp_support` in `scripts/lib/manifest/harnesses.json`, projected by `scripts/lib/live/harness-mode.cjs` (`liveModeFor(harnessId)`). A `puppeteer` result means full live mode; a `degraded` result means screenshot-only.
|
|
31
|
+
3. If `mcp_support` is false for this harness, or Preview is unavailable, enter DEGRADED mode and say so plainly: variants are generated and captured as static screenshots, with no in-page hot-swap. Skip the INJECT and PICK steps; generate against the file the user names instead.
|
|
32
|
+
4. Detect the dev server. Look for Vite, Next, Bun, or a static server (check `package.json` scripts plus a `preview_list` entry). Record the server descriptor on the session.
|
|
33
|
+
5. Open or create the session via `scripts/lib/live/session-store.cjs` (`.design/live-sessions/<id>.json`). On `--resume`, load the named session (see RESUME).
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## INJECT
|
|
38
|
+
|
|
39
|
+
Inject the browser runtime once. Read `RUNTIME_JS` from `scripts/lib/live/runtime.cjs` and evaluate it in the page with `mcp__Claude_Preview__preview_eval`. The runtime is an idempotent IIFE bound to `window.__gddLive`, so a re-inject after navigation rebinds the same singleton rather than stacking listeners. It installs the pick handler and the variant-swap helpers, and stamps the live variant on the element via the `data-gdd-variant` attribute.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## PICK
|
|
44
|
+
|
|
45
|
+
1. Arm the picker (`window.__gddLive.pick()`), then guide the user to click the target element. Use `preview_click` and `preview_inspect` to confirm the element and read its computed styles and bounding box.
|
|
46
|
+
2. Read the pick report back. Its fields are documented in `pickReportShape` (selector, tagName, classList, boundingRect, computedStyle subset, current variant). The selector strategy prefers id, then a data-testid, then a tag plus class plus nth-of-type path.
|
|
47
|
+
3. Emit a `live_pick` event through `scripts/lib/live/events.cjs` and append a `pick` entry to the session.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## GENERATE (one batch)
|
|
52
|
+
|
|
53
|
+
1. Load the relevant Phase 45 canonical reference index FIRST, so variants are grounded in real guidance: the domain index that matches the picked element (for example `../../reference/spatial.md` for layout, `../../reference/interaction.md` for components and a11y, `../../reference/color.md` for color, `../../reference/typography.md` for type, `../../reference/motion.md` for animation).
|
|
54
|
+
2. Generate all N variants in ONE batch (default 3), each a distinct, hypothesis-tagged design direction for the picked element. Do not generate them one at a time.
|
|
55
|
+
3. For each variant: write the change atomically to the implicated source file, then make it live. With HMR running, the file write is enough; otherwise apply the variant in place with `window.__gddLive.swapVariant({ n, style, html })`, which sets `data-gdd-variant="n"` and applies the variant's style or markup.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## POST-CHECK
|
|
60
|
+
|
|
61
|
+
Run the post-check on each variant via `scripts/lib/live/postcheck.cjs`, which invokes `gdd-detect`. Show the findings inline next to each variant. A variant that trips a finding is flagged, NOT auto-rejected: the user still decides. Append a `live_postcheck` event per variant.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## ACCEPT / DISCARD
|
|
66
|
+
|
|
67
|
+
- ACCEPT one variant: apply the chosen variant as the canonical source edit, and revert the others in the page (`window.__gddLive.revert()` on each non-chosen element). Emit a `live_accept` event and feed the outcome to the design-variants bandit via `scripts/lib/live/bandit-feed.cjs` (a dev-time signal). Append an `accept` entry.
|
|
68
|
+
- DISCARD: revert every variant in the page back to its captured original and leave the source untouched. Emit a `live_discard` event and append a `discard` entry.
|
|
69
|
+
|
|
70
|
+
Either way, persist the result through `scripts/lib/live/session-store.cjs` before continuing.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## PERSIST
|
|
75
|
+
|
|
76
|
+
Every step (boot, pick, generate, post-check, accept, discard) is written to the session file through `scripts/lib/live/session-store.cjs` as it happens. The on-disk event log uses the `pick`, `generate`, `accept`, `discard` kinds; the telemetry stream uses the six `live_*` event types. Writes are atomic, so an interrupted step never leaves a half-written session.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## RESUME
|
|
81
|
+
|
|
82
|
+
With `--resume <session-id>`, load the named session from `.design/live-sessions/`. Only an `in_progress` session is resumable. Offer the user two choices: continue from the last recorded event (report what that was, for example "last pick was the primary button"), or start fresh (open a new session and leave the old one intact). Never silently replay completed events.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## SCOPE GUARD
|
|
87
|
+
|
|
88
|
+
Never write outside the source files implicated by the picked element. Run every proposed write through `scripts/lib/live/scope-guard.cjs`, which maps the picked selector to its owning source files and rejects edits that fall outside them. If a variant would need a change beyond that scope (a shared token, a parent layout, a new dependency), stop and surface it to the user rather than widening the blast radius.
|
|
89
|
+
|
|
90
|
+
## Constraints
|
|
91
|
+
|
|
92
|
+
- Do NOT edit files outside the picked element's implicated sources (enforced by the scope guard).
|
|
93
|
+
- Do NOT generate variants one at a time; generate the full batch, then swap.
|
|
94
|
+
- Do NOT auto-reject a variant on a post-check finding; flag it and let the user decide.
|
|
95
|
+
- In DEGRADED mode, state up front that hot-swap is unavailable and fall back to screenshots.
|
|
96
|
+
- Persist before every user-facing prompt so a crash never loses accepted work.
|
|
97
|
+
|
|
98
|
+
## LIVE COMPLETE
|