@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,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-metadata
|
|
3
|
+
type: meta-rules
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
phase: 46
|
|
6
|
+
tags: [skill, metadata, frontmatter, single-source-of-truth, generator, description-budget]
|
|
7
|
+
last_updated: 2026-06-02
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Skill Metadata Single Source of Truth
|
|
11
|
+
|
|
12
|
+
`scripts/lib/manifest/skills.json` is the one place skill frontmatter is authored. A
|
|
13
|
+
generator projects it onto every `source/skills/<id>/SKILL.md`, the build step compiles
|
|
14
|
+
those into the shipped trees, and CI gates keep the three copies identical. This doc
|
|
15
|
+
explains the file, the generator, and the description budget. For the structural rules a
|
|
16
|
+
SKILL.md body must follow (line cap, progressive disclosure, frontmatter required fields),
|
|
17
|
+
see `./skill-authoring-contract.md`.
|
|
18
|
+
|
|
19
|
+
## What skills.json is
|
|
20
|
+
|
|
21
|
+
The file is a JSON object: a `schema_version` integer and a `skills` array. Each array
|
|
22
|
+
element is one skill record. Only `name` is required (it must equal the
|
|
23
|
+
`source/skills/<id>/` directory name); every other field is optional and omitted when it
|
|
24
|
+
has no value.
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"schema_version": 1,
|
|
29
|
+
"skills": [
|
|
30
|
+
{
|
|
31
|
+
"name": "health",
|
|
32
|
+
"description": "Reports .design/ artifact health - staleness, missing files, token drift, broken state transitions.",
|
|
33
|
+
"tools": "Read, Bash, Glob, Grep, mcp__gdd_state__get",
|
|
34
|
+
"disable_model_invocation": true
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Recognized record fields: `name`, `description`, `argument_hint`, `tools`,
|
|
41
|
+
`user_invocable`, `disable_model_invocation`, `frontmatter_name` (the `name:` value when it
|
|
42
|
+
is not the default `gdd-<id>`), `extra_frontmatter` (see below), plus `registered_in_phase`
|
|
43
|
+
and `aliases`, which live only in the manifest and never round-trip to frontmatter. The
|
|
44
|
+
schema at `scripts/lib/manifest/schemas/skills.schema.json` validates the shape.
|
|
45
|
+
|
|
46
|
+
## The build chain
|
|
47
|
+
|
|
48
|
+
Metadata flows in one direction, and a `*:check` gate guards each hop:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
skills.json
|
|
52
|
+
-> generate-skill-frontmatter (npm run generate:skill-frontmatter)
|
|
53
|
+
-> source/skills/<id>/SKILL.md
|
|
54
|
+
-> build:skills (npm run build:skills)
|
|
55
|
+
-> skills/ + dist/claude-code/
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`scripts/generate-skill-frontmatter.cjs` rewrites only the frontmatter block of each
|
|
59
|
+
`SKILL.md`, never the markdown body. It has three modes:
|
|
60
|
+
|
|
61
|
+
- forward (no flag): manifest to frontmatter. The default.
|
|
62
|
+
- `--extract`: the reverse direction, reading current frontmatter back into `skills.json`
|
|
63
|
+
to seed or refresh the source of truth. Idempotent with forward.
|
|
64
|
+
- `--check`: the CI drift gate. It writes nothing and exits non-zero when any committed
|
|
65
|
+
frontmatter differs from what the manifest would generate.
|
|
66
|
+
|
|
67
|
+
`scripts/build-skills.cjs` then propagates `source/skills/` into the committed `skills/`
|
|
68
|
+
tree and `dist/claude-code/`; its own check mode asserts that the built output equals the
|
|
69
|
+
committed output. So the contract is: edit `skills.json`, regenerate, build. Never
|
|
70
|
+
hand-edit a managed frontmatter line in `SKILL.md`, because the drift gate will fail.
|
|
71
|
+
|
|
72
|
+
## Managed vs preserved fields
|
|
73
|
+
|
|
74
|
+
The generator owns six frontmatter keys and emits them in this canonical order whenever the
|
|
75
|
+
record carries a value:
|
|
76
|
+
|
|
77
|
+
1. `name`
|
|
78
|
+
2. `description`
|
|
79
|
+
3. `argument-hint`
|
|
80
|
+
4. `tools`
|
|
81
|
+
5. `user-invocable`
|
|
82
|
+
6. `disable-model-invocation`
|
|
83
|
+
|
|
84
|
+
String values for `description` and `argument-hint` are always double-quoted on emit, so
|
|
85
|
+
quoting stays uniform across every skill. Anything outside these six keys is not managed.
|
|
86
|
+
The generator carries it verbatim in the record's `extra_frontmatter` array and re-emits it
|
|
87
|
+
after the managed block. The `quality-gate` skill is the working example: its `color`,
|
|
88
|
+
`model`, `default-tier`, `size_budget`, `parallel-safe`, `typical-duration-seconds`,
|
|
89
|
+
`reads-only`, and multi-line `writes:` block all live in `extra_frontmatter` and survive
|
|
90
|
+
every regeneration untouched.
|
|
91
|
+
|
|
92
|
+
## Description budget
|
|
93
|
+
|
|
94
|
+
Every `description` must be at least 20 and at most 1024 characters. Two validators enforce
|
|
95
|
+
the ceiling in CI:
|
|
96
|
+
|
|
97
|
+
- `scripts/validate-skill-length.cjs` treats `DESC_MAX = 1024` as a blocker (it also blocks
|
|
98
|
+
under 20 characters as under-specification) and is the Phase 28.5 authoring-contract gate.
|
|
99
|
+
- `scripts/lint-agentskills-spec.cjs` rule R4 applies the same 1024-character hard cap from
|
|
100
|
+
the agentskills.io spec angle, and warns past 200 characters.
|
|
101
|
+
|
|
102
|
+
Phase 46 keeps that contract intact and wires `npm run lint:agentskills` in as its own
|
|
103
|
+
explicit CI gate, so the spec lint runs on every change rather than only in-process via the
|
|
104
|
+
doctor aggregator.
|
|
105
|
+
|
|
106
|
+
## How pin consumes it
|
|
107
|
+
|
|
108
|
+
The `pin`, `unpin`, and `list-pins` skills shipping in this phase read `description`,
|
|
109
|
+
`argument_hint`, and `tools` straight from `skills.json`. That manifest is the pin metadata
|
|
110
|
+
catalogue: the pin surface never live-scrapes a `SKILL.md` or re-derives a description at
|
|
111
|
+
runtime. Reading one structured file keeps pin output consistent with what the generator
|
|
112
|
+
emits, and the `aliases` array is reserved for the pin shortcuts those skills honor.
|
|
113
|
+
|
|
114
|
+
## See also
|
|
115
|
+
|
|
116
|
+
- `./skill-authoring-contract.md` for the SKILL.md line cap, description-format guidance,
|
|
117
|
+
required frontmatter fields, and progressive-disclosure rule.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/live/bandit-feed.cjs — Phase 47 (Live Mode) → Phase 38 design-arms bridge.
|
|
4
|
+
*
|
|
5
|
+
* When a user ACCEPTS a generated variant in /gdd:live, that acceptance is a weak,
|
|
6
|
+
* dev-time positive signal for the variant's design pattern. We fold it into the Phase 38
|
|
7
|
+
* `design_arms` posterior store as a WON observation — but at a discounted weight
|
|
8
|
+
* (DEV_TIME_WEIGHT = 0.5) because a developer's pick during authoring is advisory until
|
|
9
|
+
* real production A/B + user-research data arrives (Phase 38 D-03: advisory, never
|
|
10
|
+
* directive). The full posterior math + atomic persistence lives in the design-arms
|
|
11
|
+
* store; we only reuse it (variantKey + observe) and never duplicate the Beta math.
|
|
12
|
+
*
|
|
13
|
+
* Pure, dependency-free CommonJS. Persistence + `baseDir`/`armsPath` test-injection are
|
|
14
|
+
* inherited from the store. Ships in the npm package; requires only the in-repo store.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { variantKey, observe } = require('../ds-arms/design-arms-store.cjs');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The dev-time acceptance weight. Half a win — a developer's accept is a discounted,
|
|
21
|
+
* advisory signal (Phase 38 D-03) until production user-outcome data shifts the arm.
|
|
22
|
+
*/
|
|
23
|
+
const DEV_TIME_WEIGHT = 0.5;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Record an accepted Live Mode variant as a discounted WON observation on its design arm.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} args
|
|
29
|
+
* @param {string} args.componentType The component class (e.g. 'button', 'card').
|
|
30
|
+
* @param {string|object} args.pattern The variant's design pattern (string or object;
|
|
31
|
+
* hashed to the arm key via the store's variantKey).
|
|
32
|
+
* @param {string} [args.label] Human-readable arm label, stored on first observe.
|
|
33
|
+
* @param {number} [args.weight] Override the dev-time weight (defaults to DEV_TIME_WEIGHT).
|
|
34
|
+
* @param {string} [args.projectRoot] Repo root — forwarded to the store as `baseDir` so the
|
|
35
|
+
* arms file resolves under it (testable / hermetic).
|
|
36
|
+
* @param {string} [args.armsPath] Explicit arms-file path override (forwarded to the store).
|
|
37
|
+
* @returns {object} The updated arm posterior (as returned by the store's observe()).
|
|
38
|
+
*/
|
|
39
|
+
function recordAccepted(args = {}) {
|
|
40
|
+
const { componentType, pattern, label, projectRoot, armsPath } = args;
|
|
41
|
+
if (typeof componentType !== 'string' || componentType.length === 0) {
|
|
42
|
+
throw new TypeError('recordAccepted: componentType is required (non-empty string)');
|
|
43
|
+
}
|
|
44
|
+
if (pattern == null) {
|
|
45
|
+
throw new TypeError('recordAccepted: pattern is required');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const weight = typeof args.weight === 'number' && args.weight > 0 ? args.weight : DEV_TIME_WEIGHT;
|
|
49
|
+
const hash = variantKey(componentType, pattern);
|
|
50
|
+
|
|
51
|
+
// Forward store options only when provided so the store keeps its own defaults otherwise.
|
|
52
|
+
const opts = {};
|
|
53
|
+
if (projectRoot) opts.baseDir = projectRoot;
|
|
54
|
+
if (armsPath) opts.armsPath = armsPath;
|
|
55
|
+
|
|
56
|
+
return observe(
|
|
57
|
+
componentType,
|
|
58
|
+
hash,
|
|
59
|
+
{ won: true, weight, source: 'dev_time', label },
|
|
60
|
+
opts,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { recordAccepted, DEV_TIME_WEIGHT };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/live/events.cjs — Phase 47 (Live Mode) telemetry emitter.
|
|
4
|
+
*
|
|
5
|
+
* /gdd:live emits six event types across a session — start, pick, generate, accept,
|
|
6
|
+
* discard, end. This module is the single typed entry point: it validates the `type`
|
|
7
|
+
* against the closed LIVE_EVENT_TYPES allow-list (an unknown live type is a programmer
|
|
8
|
+
* error and is rejected) and appends the event via the in-repo event-chain emitter
|
|
9
|
+
* (scripts/lib/event-chain.cjs), the same write path router_pick uses.
|
|
10
|
+
*
|
|
11
|
+
* The emitted row carries the events.schema.json envelope fields verbatim
|
|
12
|
+
* (type / timestamp / sessionId / payload) so that, projected back to
|
|
13
|
+
* {type, timestamp, sessionId, payload}, it validates against the additive `live_*`
|
|
14
|
+
* seed types in reference/schemas/events.schema.json. `agent` + `outcome` are the
|
|
15
|
+
* chain emitter's two required fields; we set agent='live' and outcome=type.
|
|
16
|
+
*
|
|
17
|
+
* Pure, dependency-free CommonJS:
|
|
18
|
+
* - NO top-level clock: the timestamp comes from an injectable `now` (default
|
|
19
|
+
* `Date.now`), so tests are deterministic.
|
|
20
|
+
* - `baseDir` is injectable (the emitter resolves the chain file relative to it),
|
|
21
|
+
* so tests write to a temp project and never touch the repo's real stream.
|
|
22
|
+
*
|
|
23
|
+
* Ships in the npm package; requires only the in-repo event-chain emitter.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const { appendChainEvent } = require('../event-chain.cjs');
|
|
27
|
+
|
|
28
|
+
/** The six Live Mode event types. Closed allow-list — an unknown type is rejected. */
|
|
29
|
+
const LIVE_EVENT_TYPES = Object.freeze([
|
|
30
|
+
'live_session_start',
|
|
31
|
+
'live_pick',
|
|
32
|
+
'live_generate',
|
|
33
|
+
'live_accept',
|
|
34
|
+
'live_discard',
|
|
35
|
+
'live_session_end',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const LIVE_EVENT_TYPE_SET = new Set(LIVE_EVENT_TYPES);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Emit a Live Mode telemetry event.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} args
|
|
44
|
+
* @param {string} args.projectRoot Repo root — injected as the emitter `baseDir` so the
|
|
45
|
+
* chain file resolves under it (testable / hermetic).
|
|
46
|
+
* @param {string} args.type One of LIVE_EVENT_TYPES. Anything else throws.
|
|
47
|
+
* @param {string} args.sessionId Stable per-session id (correlates the event stream).
|
|
48
|
+
* @param {object} [args.payload] Free-form, event-specific payload (MVP: opaque object).
|
|
49
|
+
* @param {(() => number)} [args.now] Injectable clock returning epoch ms (default Date.now).
|
|
50
|
+
* @returns {{event_id: string, type: string, timestamp: string, sessionId: string, payload: object}}
|
|
51
|
+
* The projected envelope (also the shape that validates against events.schema.json).
|
|
52
|
+
*/
|
|
53
|
+
function emitLiveEvent(args = {}) {
|
|
54
|
+
const { projectRoot, type, sessionId, payload } = args;
|
|
55
|
+
const now = typeof args.now === 'function' ? args.now : Date.now;
|
|
56
|
+
|
|
57
|
+
if (!LIVE_EVENT_TYPE_SET.has(type)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`emitLiveEvent: unknown live event type "${String(type)}". ` +
|
|
60
|
+
`Expected one of: ${LIVE_EVENT_TYPES.join(', ')}.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
|
64
|
+
throw new TypeError('emitLiveEvent: sessionId is required (non-empty string)');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const timestamp = new Date(now()).toISOString();
|
|
68
|
+
const envelopePayload = payload && typeof payload === 'object' ? payload : {};
|
|
69
|
+
|
|
70
|
+
// Mirror the router_pick emit surface: write a chain row carrying the events-schema
|
|
71
|
+
// envelope fields verbatim. agent/outcome are the chain emitter's required fields.
|
|
72
|
+
const event_id = appendChainEvent({
|
|
73
|
+
baseDir: projectRoot,
|
|
74
|
+
agent: 'live',
|
|
75
|
+
outcome: type,
|
|
76
|
+
// Envelope fields preserved verbatim by appendChainEvent (opaque-extras pass-through):
|
|
77
|
+
type,
|
|
78
|
+
timestamp,
|
|
79
|
+
sessionId,
|
|
80
|
+
payload: envelopePayload,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return { event_id, type, timestamp, sessionId, payload: envelopePayload };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { emitLiveEvent, LIVE_EVENT_TYPES };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/live/harness-mode.cjs — Phase 47 (Live Mode) capability gate.
|
|
4
|
+
*
|
|
5
|
+
* `/gdd:live` drives the Claude Preview MCP at runtime (preview_inspect /
|
|
6
|
+
* preview_click / preview_eval / preview_screenshot). That whole loop is only
|
|
7
|
+
* available on a harness whose capability matrix reports `mcp_support: true`.
|
|
8
|
+
* Harnesses without MCP support cannot inject the runtime or read picks back, so
|
|
9
|
+
* live mode degrades to a screenshot-only experience on them.
|
|
10
|
+
*
|
|
11
|
+
* This module is the single source of truth for that gate. It reads the canonical
|
|
12
|
+
* harness record (scripts/lib/manifest/harnesses.cjs, a typed re-export of
|
|
13
|
+
* harnesses.json) and projects each harness onto one of two live-mode strings:
|
|
14
|
+
*
|
|
15
|
+
* 'puppeteer' — full live mode: the harness has mcp_support, the skill probes
|
|
16
|
+
* Preview, injects scripts/lib/live/runtime.cjs, and hot-swaps
|
|
17
|
+
* variants in place. (Named for the Preview MCP's Playwright /
|
|
18
|
+
* puppeteer-class browser driver, not a bundled dependency: there
|
|
19
|
+
* is NO bundled puppeteer; the skill drives the MCP.)
|
|
20
|
+
* 'degraded' — screenshot-only: no mcp_support, so no live injection. The skill
|
|
21
|
+
* falls back to generating variants and capturing static
|
|
22
|
+
* screenshots, and says so up front.
|
|
23
|
+
*
|
|
24
|
+
* Design constraints (mirror the other scripts/lib/live/* modules):
|
|
25
|
+
* - Pure, dependency-free apart from the manifest re-export. No fs, no network,
|
|
26
|
+
* no Date / Math.random. Cross-platform (no path work, no OS calls).
|
|
27
|
+
* - The harness list is injectable so tests can pass a fixture; it defaults to
|
|
28
|
+
* the real manifest.
|
|
29
|
+
*
|
|
30
|
+
* Exports:
|
|
31
|
+
* - liveModeFor(harnessId, harnesses?) -> 'puppeteer' | 'degraded'
|
|
32
|
+
* - degradedHarnesses(harnesses?) -> string[] of ids in degraded mode
|
|
33
|
+
* - isMcpSupported(harnessId, harnesses?) -> boolean
|
|
34
|
+
* - MODE_FULL / MODE_DEGRADED -> the two mode string constants
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const MODE_FULL = 'puppeteer';
|
|
38
|
+
const MODE_DEGRADED = 'degraded';
|
|
39
|
+
|
|
40
|
+
/** Lazily resolve the default harness list (the typed re-export of harnesses.json). */
|
|
41
|
+
function defaultHarnesses() {
|
|
42
|
+
return require('../manifest/harnesses.cjs');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Find a harness record by id in a list. Returns undefined when absent. */
|
|
46
|
+
function findHarness(harnessId, harnesses) {
|
|
47
|
+
const list = Array.isArray(harnesses) ? harnesses : defaultHarnesses();
|
|
48
|
+
return list.find((h) => h && h.id === harnessId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* True when the named harness reports `capability_matrix.mcp_support === true`.
|
|
53
|
+
* Unknown harness ids and harnesses missing the flag are treated as false (no
|
|
54
|
+
* MCP -> no live injection).
|
|
55
|
+
*/
|
|
56
|
+
function isMcpSupported(harnessId, harnesses) {
|
|
57
|
+
const h = findHarness(harnessId, harnesses);
|
|
58
|
+
return Boolean(h && h.capability_matrix && h.capability_matrix.mcp_support === true);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Live mode for a harness: 'puppeteer' when mcp_support is true, else 'degraded'.
|
|
63
|
+
* An unknown harness id degrades (fail safe to screenshot-only).
|
|
64
|
+
*
|
|
65
|
+
* @param {string} harnessId
|
|
66
|
+
* @param {Array=} harnesses Optional injected list; defaults to the manifest.
|
|
67
|
+
* @returns {'puppeteer'|'degraded'}
|
|
68
|
+
*/
|
|
69
|
+
function liveModeFor(harnessId, harnesses) {
|
|
70
|
+
return isMcpSupported(harnessId, harnesses) ? MODE_FULL : MODE_DEGRADED;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The ids of every harness currently in degraded (screenshot-only) live mode,
|
|
75
|
+
* in manifest order.
|
|
76
|
+
*
|
|
77
|
+
* @param {Array=} harnesses Optional injected list; defaults to the manifest.
|
|
78
|
+
* @returns {string[]}
|
|
79
|
+
*/
|
|
80
|
+
function degradedHarnesses(harnesses) {
|
|
81
|
+
const list = Array.isArray(harnesses) ? harnesses : defaultHarnesses();
|
|
82
|
+
return list
|
|
83
|
+
.filter((h) => h && h.id && !isMcpSupported(h.id, list))
|
|
84
|
+
.map((h) => h.id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
liveModeFor,
|
|
89
|
+
degradedHarnesses,
|
|
90
|
+
isMcpSupported,
|
|
91
|
+
MODE_FULL,
|
|
92
|
+
MODE_DEGRADED,
|
|
93
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/live/postcheck.cjs — Phase 47 (Live Mode) variant post-check.
|
|
4
|
+
*
|
|
5
|
+
* After /gdd:live generates a design variant, we run that variant's source through the
|
|
6
|
+
* in-repo gdd-detect engine (Phase 41) and surface its anti-pattern findings on the
|
|
7
|
+
* variant card. Per the Live Mode spec, an `error`-severity finding is FLAGGED but NOT
|
|
8
|
+
* auto-rejected — the human still chooses; the detector is advisory at dev-time. So
|
|
9
|
+
* `autoReject` is ALWAYS false; we only surface counts + a compact card summary.
|
|
10
|
+
*
|
|
11
|
+
* Pure, dependency-free CommonJS. We reuse the detect engine's `scanContent` +`RULES`
|
|
12
|
+
* (NOT a CLI shell-out, NOT a temp-file write) so post-checking a generated variant is
|
|
13
|
+
* fully in-memory and hermetically testable. The engine is the single source of truth
|
|
14
|
+
* for the finding shape ({ruleId, category, name, severity, file, line, column, match,
|
|
15
|
+
* references}); we never reimplement rule matching.
|
|
16
|
+
*
|
|
17
|
+
* Ships in the npm package (scripts/lib/** is in package.json `files`), so it requires
|
|
18
|
+
* only the in-repo detect engine — no runtime dependency, no network, no optional dep.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
const { scanContent, run, RULES } = require('../detect/engine.cjs');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Normalize the caller's `files`/`content` input into a list of
|
|
26
|
+
* `{ path, content, ext }` units to scan in-memory.
|
|
27
|
+
*
|
|
28
|
+
* Accepts either:
|
|
29
|
+
* - `content`: a raw string (single anonymous unit), or
|
|
30
|
+
* - `files`: an array of `{ path, content }` (or `{ name, content }`) units, or
|
|
31
|
+
* an object map of `{ "<path>": "<content>" }`.
|
|
32
|
+
*
|
|
33
|
+
* @param {{files?: any, content?: string}} input
|
|
34
|
+
* @returns {{path: string, content: string, ext: string}[]}
|
|
35
|
+
*/
|
|
36
|
+
function normalizeUnits(input) {
|
|
37
|
+
const units = [];
|
|
38
|
+
if (input && typeof input.content === 'string') {
|
|
39
|
+
const p = input.path || input.file || 'variant';
|
|
40
|
+
units.push({ path: p, content: input.content, ext: path.extname(p).toLowerCase() });
|
|
41
|
+
}
|
|
42
|
+
const files = input && input.files;
|
|
43
|
+
if (Array.isArray(files)) {
|
|
44
|
+
for (const f of files) {
|
|
45
|
+
if (!f) continue;
|
|
46
|
+
if (typeof f === 'string') continue; // a bare path with no content is not scannable in-memory
|
|
47
|
+
const p = f.path || f.name || f.file || 'variant';
|
|
48
|
+
const content = typeof f.content === 'string' ? f.content : '';
|
|
49
|
+
units.push({ path: p, content, ext: path.extname(p).toLowerCase() });
|
|
50
|
+
}
|
|
51
|
+
} else if (files && typeof files === 'object') {
|
|
52
|
+
for (const p of Object.keys(files)) {
|
|
53
|
+
const content = typeof files[p] === 'string' ? files[p] : '';
|
|
54
|
+
units.push({ path: p, content, ext: path.extname(p).toLowerCase() });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return units;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Post-check a generated variant against the gdd-detect anti-pattern rule set.
|
|
62
|
+
*
|
|
63
|
+
* Prefers an in-memory scan of the supplied variant source via the detect engine's
|
|
64
|
+
* `scanContent`. When NO inline content is supplied but a `projectRoot` is given,
|
|
65
|
+
* falls back to the documented programmatic engine form `run(projectRoot)` so the
|
|
66
|
+
* caller can still post-check files already written to disk.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} args
|
|
69
|
+
* @param {string} [args.projectRoot] Repo root — used for the on-disk fallback and as the
|
|
70
|
+
* `cwd` so finding `file` paths are repo-relative.
|
|
71
|
+
* @param {Array|object} [args.files] Variant files: `[{path, content}]` or `{ "<path>": "<content>" }`.
|
|
72
|
+
* @param {string} [args.content] A single raw variant source string.
|
|
73
|
+
* @returns {{findings: object[], errorCount: number, warnCount: number, autoReject: boolean}}
|
|
74
|
+
*/
|
|
75
|
+
function postCheckVariant(args = {}) {
|
|
76
|
+
const { projectRoot } = args;
|
|
77
|
+
const units = normalizeUnits(args);
|
|
78
|
+
|
|
79
|
+
let findings = [];
|
|
80
|
+
if (units.length > 0) {
|
|
81
|
+
const cwd = projectRoot || process.cwd();
|
|
82
|
+
for (const u of units) {
|
|
83
|
+
// Make the reported `file` repo-relative when projectRoot is known, mirroring the
|
|
84
|
+
// engine's own relativization in run().
|
|
85
|
+
let rel = u.path;
|
|
86
|
+
if (projectRoot && path.isAbsolute(u.path)) {
|
|
87
|
+
rel = path.relative(cwd, u.path).split(path.sep).join('/') || u.path;
|
|
88
|
+
}
|
|
89
|
+
const hits = scanContent(u.content, { path: rel, ext: u.ext }, RULES);
|
|
90
|
+
findings.push(...hits);
|
|
91
|
+
}
|
|
92
|
+
// Match the engine's deterministic ordering (file, line, column, ruleId).
|
|
93
|
+
findings.sort(
|
|
94
|
+
(a, b) =>
|
|
95
|
+
a.file.localeCompare(b.file) ||
|
|
96
|
+
a.line - b.line ||
|
|
97
|
+
a.column - b.column ||
|
|
98
|
+
a.ruleId.localeCompare(b.ruleId),
|
|
99
|
+
);
|
|
100
|
+
} else if (projectRoot) {
|
|
101
|
+
// No inline content — fall back to the documented programmatic engine form against
|
|
102
|
+
// files already on disk under projectRoot.
|
|
103
|
+
const result = run(projectRoot, { cwd: projectRoot });
|
|
104
|
+
findings = result.findings;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let errorCount = 0;
|
|
108
|
+
let warnCount = 0;
|
|
109
|
+
for (const f of findings) {
|
|
110
|
+
if (f.severity === 'error') errorCount += 1;
|
|
111
|
+
else warnCount += 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Spec D: error-severity is flagged, NOT auto-rejected. autoReject is always false.
|
|
115
|
+
return { findings, errorCount, warnCount, autoReject: false };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Compact, single-line summary of a findings array for a variant card.
|
|
120
|
+
* Always returns a string (never null/undefined) so the skill can render it verbatim.
|
|
121
|
+
*
|
|
122
|
+
* [] → "clean — no anti-patterns"
|
|
123
|
+
* [1 error, 2 warn] → "2 issue(s): 1 error, 2 warn — BAN-06, BAN-01, BAN-08"
|
|
124
|
+
*
|
|
125
|
+
* Rule ids are de-duplicated and listed in first-seen order, capped so the card stays compact.
|
|
126
|
+
*
|
|
127
|
+
* @param {object[]} findings
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
function summarizeForCard(findings) {
|
|
131
|
+
const list = Array.isArray(findings) ? findings : [];
|
|
132
|
+
if (list.length === 0) return 'clean — no anti-patterns';
|
|
133
|
+
|
|
134
|
+
let errors = 0;
|
|
135
|
+
let warns = 0;
|
|
136
|
+
const ids = [];
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
for (const f of list) {
|
|
139
|
+
if (f && f.severity === 'error') errors += 1;
|
|
140
|
+
else warns += 1;
|
|
141
|
+
const id = f && f.ruleId;
|
|
142
|
+
if (id && !seen.has(id)) {
|
|
143
|
+
seen.add(id);
|
|
144
|
+
ids.push(id);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const MAX_IDS = 5;
|
|
149
|
+
const shownIds = ids.slice(0, MAX_IDS);
|
|
150
|
+
const idTail = ids.length > MAX_IDS ? `, +${ids.length - MAX_IDS} more` : '';
|
|
151
|
+
const parts = [];
|
|
152
|
+
if (errors) parts.push(`${errors} error`);
|
|
153
|
+
if (warns) parts.push(`${warns} warn`);
|
|
154
|
+
|
|
155
|
+
return `${list.length} issue(s): ${parts.join(', ')} — ${shownIds.join(', ')}${idTail}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { postCheckVariant, summarizeForCard, normalizeUnits };
|