@hegemonart/get-design-done 1.23.0 → 1.24.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 +36 -7
- package/package.json +2 -1
- package/scripts/install.cjs +164 -116
- package/scripts/lib/adaptive-mode.cjs +170 -0
- package/scripts/lib/bandit-router.cjs +368 -0
- package/scripts/lib/hedge-ensemble.cjs +217 -0
- package/scripts/lib/install/config-dir.cjs +55 -0
- package/scripts/lib/install/installer.cjs +244 -0
- package/scripts/lib/install/interactive.cjs +142 -0
- package/scripts/lib/install/merge.cjs +103 -0
- package/scripts/lib/install/runtimes.cjs +172 -0
- package/scripts/lib/mmr-rerank.cjs +154 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Pure merge / mutation helpers for the multi-runtime installer.
|
|
4
|
+
//
|
|
5
|
+
// mergeClaudeSettings — extracted from the v1.23.5 entrypoint. Adds a
|
|
6
|
+
// marketplace registration + flips enabledPlugins[<plugin>@<marketplace>].
|
|
7
|
+
//
|
|
8
|
+
// removeClaudeSettings — inverse: removes the marketplace + the
|
|
9
|
+
// enabledPlugins entry. Leaves untouched anything we did not write.
|
|
10
|
+
//
|
|
11
|
+
// agentsFileFingerprint — first-line marker we drop into every AGENTS.md /
|
|
12
|
+
// GEMINI.md write so uninstall can confirm the file is plugin-owned.
|
|
13
|
+
|
|
14
|
+
const PLUGIN_FINGERPRINT = 'get-design-done plugin instructions';
|
|
15
|
+
|
|
16
|
+
function mergeClaudeSettings(existing, marketplaceEntry) {
|
|
17
|
+
const next = { ...(existing || {}) };
|
|
18
|
+
|
|
19
|
+
const marketplaces = { ...(next.extraKnownMarketplaces || {}) };
|
|
20
|
+
const desired = {
|
|
21
|
+
source: { source: 'github', repo: marketplaceEntry.repo },
|
|
22
|
+
};
|
|
23
|
+
const marketplaceChanged =
|
|
24
|
+
JSON.stringify(marketplaces[marketplaceEntry.name]) !==
|
|
25
|
+
JSON.stringify(desired);
|
|
26
|
+
marketplaces[marketplaceEntry.name] = desired;
|
|
27
|
+
next.extraKnownMarketplaces = marketplaces;
|
|
28
|
+
|
|
29
|
+
const enabled = { ...(next.enabledPlugins || {}) };
|
|
30
|
+
const enabledKey = `${marketplaceEntry.pluginName}@${marketplaceEntry.name}`;
|
|
31
|
+
const enabledChanged = enabled[enabledKey] !== true;
|
|
32
|
+
enabled[enabledKey] = true;
|
|
33
|
+
next.enabledPlugins = enabled;
|
|
34
|
+
|
|
35
|
+
return { next, changed: marketplaceChanged || enabledChanged };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function removeClaudeSettings(existing, marketplaceEntry) {
|
|
39
|
+
const next = { ...(existing || {}) };
|
|
40
|
+
|
|
41
|
+
const marketplaces = { ...(next.extraKnownMarketplaces || {}) };
|
|
42
|
+
const marketplaceChanged = Object.prototype.hasOwnProperty.call(
|
|
43
|
+
marketplaces,
|
|
44
|
+
marketplaceEntry.name,
|
|
45
|
+
);
|
|
46
|
+
delete marketplaces[marketplaceEntry.name];
|
|
47
|
+
if (Object.keys(marketplaces).length > 0) {
|
|
48
|
+
next.extraKnownMarketplaces = marketplaces;
|
|
49
|
+
} else if ('extraKnownMarketplaces' in next) {
|
|
50
|
+
delete next.extraKnownMarketplaces;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const enabled = { ...(next.enabledPlugins || {}) };
|
|
54
|
+
const enabledKey = `${marketplaceEntry.pluginName}@${marketplaceEntry.name}`;
|
|
55
|
+
const enabledChanged = Object.prototype.hasOwnProperty.call(
|
|
56
|
+
enabled,
|
|
57
|
+
enabledKey,
|
|
58
|
+
);
|
|
59
|
+
delete enabled[enabledKey];
|
|
60
|
+
if (Object.keys(enabled).length > 0) {
|
|
61
|
+
next.enabledPlugins = enabled;
|
|
62
|
+
} else if ('enabledPlugins' in next) {
|
|
63
|
+
delete next.enabledPlugins;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { next, changed: marketplaceChanged || enabledChanged };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function agentsFileFingerprint() {
|
|
70
|
+
return PLUGIN_FINGERPRINT;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildAgentsFileContent(runtime, payloadHeader) {
|
|
74
|
+
const lines = [
|
|
75
|
+
`<!-- ${PLUGIN_FINGERPRINT} -->`,
|
|
76
|
+
'',
|
|
77
|
+
`# ${runtime.displayName} — get-design-done plugin`,
|
|
78
|
+
'',
|
|
79
|
+
'This file was written by `npx @hegemonart/get-design-done`. It loads',
|
|
80
|
+
'the GDD plugin instructions for this runtime. Re-run the installer to',
|
|
81
|
+
'refresh; run `npx @hegemonart/get-design-done --uninstall` to remove.',
|
|
82
|
+
'',
|
|
83
|
+
payloadHeader || '',
|
|
84
|
+
'',
|
|
85
|
+
`Plugin repository: https://github.com/hegemonart/get-design-done`,
|
|
86
|
+
'',
|
|
87
|
+
];
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isPluginOwned(content) {
|
|
92
|
+
if (!content || typeof content !== 'string') return false;
|
|
93
|
+
return content.includes(PLUGIN_FINGERPRINT);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
mergeClaudeSettings,
|
|
98
|
+
removeClaudeSettings,
|
|
99
|
+
agentsFileFingerprint,
|
|
100
|
+
buildAgentsFileContent,
|
|
101
|
+
isPluginOwned,
|
|
102
|
+
PLUGIN_FINGERPRINT,
|
|
103
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Per-runtime install matrix for the get-design-done plugin.
|
|
4
|
+
//
|
|
5
|
+
// Each entry is pure data describing how to install / uninstall the plugin
|
|
6
|
+
// into one runtime. The 14 runtimes listed below are roadmap-locked
|
|
7
|
+
// (Phase 24 D-02). Two `kind`s exist:
|
|
8
|
+
//
|
|
9
|
+
// - `claude-marketplace` — register a marketplace entry + flip
|
|
10
|
+
// `enabledPlugins[<name>@<marketplace>]` in settings.json. Today only
|
|
11
|
+
// Claude Code uses this.
|
|
12
|
+
//
|
|
13
|
+
// - `agents-md` — drop a runtime-specific instructions file (AGENTS.md /
|
|
14
|
+
// GEMINI.md) into the runtime's config directory. Most modern AI coding
|
|
15
|
+
// CLIs follow this convention.
|
|
16
|
+
//
|
|
17
|
+
// Adding a new runtime: append to RUNTIMES below, append the same id to the
|
|
18
|
+
// alphabetised baseline at test-fixture/baselines/phase-24/runtimes.txt.
|
|
19
|
+
|
|
20
|
+
const REPO = 'hegemonart/get-design-done';
|
|
21
|
+
const MARKETPLACE_NAME = 'get-design-done';
|
|
22
|
+
const PLUGIN_NAME = 'get-design-done';
|
|
23
|
+
|
|
24
|
+
const RUNTIMES = Object.freeze([
|
|
25
|
+
{
|
|
26
|
+
id: 'claude',
|
|
27
|
+
displayName: 'Claude Code',
|
|
28
|
+
configDirEnv: 'CLAUDE_CONFIG_DIR',
|
|
29
|
+
configDirFallback: '.claude',
|
|
30
|
+
kind: 'claude-marketplace',
|
|
31
|
+
files: [],
|
|
32
|
+
marketplaceEntry: {
|
|
33
|
+
name: MARKETPLACE_NAME,
|
|
34
|
+
pluginName: PLUGIN_NAME,
|
|
35
|
+
repo: REPO,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'opencode',
|
|
40
|
+
displayName: 'OpenCode',
|
|
41
|
+
configDirEnv: 'OPENCODE_CONFIG_DIR',
|
|
42
|
+
configDirFallback: '.config/opencode',
|
|
43
|
+
kind: 'agents-md',
|
|
44
|
+
files: ['AGENTS.md'],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'gemini',
|
|
48
|
+
displayName: 'Gemini CLI',
|
|
49
|
+
configDirEnv: 'GEMINI_CONFIG_DIR',
|
|
50
|
+
configDirFallback: '.gemini',
|
|
51
|
+
kind: 'agents-md',
|
|
52
|
+
files: ['GEMINI.md'],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'kilo',
|
|
56
|
+
displayName: 'Kilo Code',
|
|
57
|
+
configDirEnv: 'KILO_CONFIG_DIR',
|
|
58
|
+
configDirFallback: '.kilo',
|
|
59
|
+
kind: 'agents-md',
|
|
60
|
+
files: ['AGENTS.md'],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'codex',
|
|
64
|
+
displayName: 'OpenAI Codex CLI',
|
|
65
|
+
configDirEnv: 'CODEX_HOME',
|
|
66
|
+
configDirFallback: '.codex',
|
|
67
|
+
kind: 'agents-md',
|
|
68
|
+
files: ['AGENTS.md'],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'copilot',
|
|
72
|
+
displayName: 'GitHub Copilot CLI',
|
|
73
|
+
configDirEnv: 'COPILOT_CONFIG_DIR',
|
|
74
|
+
configDirFallback: '.copilot',
|
|
75
|
+
kind: 'agents-md',
|
|
76
|
+
files: ['AGENTS.md'],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'cursor',
|
|
80
|
+
displayName: 'Cursor',
|
|
81
|
+
configDirEnv: 'CURSOR_CONFIG_DIR',
|
|
82
|
+
configDirFallback: '.cursor',
|
|
83
|
+
kind: 'agents-md',
|
|
84
|
+
files: ['AGENTS.md'],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'windsurf',
|
|
88
|
+
displayName: 'Windsurf',
|
|
89
|
+
configDirEnv: 'WINDSURF_CONFIG_DIR',
|
|
90
|
+
configDirFallback: '.windsurf',
|
|
91
|
+
kind: 'agents-md',
|
|
92
|
+
files: ['AGENTS.md'],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'antigravity',
|
|
96
|
+
displayName: 'Antigravity',
|
|
97
|
+
configDirEnv: 'ANTIGRAVITY_CONFIG_DIR',
|
|
98
|
+
configDirFallback: '.antigravity',
|
|
99
|
+
kind: 'agents-md',
|
|
100
|
+
files: ['AGENTS.md'],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'augment',
|
|
104
|
+
displayName: 'Augment',
|
|
105
|
+
configDirEnv: 'AUGMENT_CONFIG_DIR',
|
|
106
|
+
configDirFallback: '.augment',
|
|
107
|
+
kind: 'agents-md',
|
|
108
|
+
files: ['AGENTS.md'],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'trae',
|
|
112
|
+
displayName: 'Trae',
|
|
113
|
+
configDirEnv: 'TRAE_CONFIG_DIR',
|
|
114
|
+
configDirFallback: '.trae',
|
|
115
|
+
kind: 'agents-md',
|
|
116
|
+
files: ['AGENTS.md'],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'qwen',
|
|
120
|
+
displayName: 'Qwen Code',
|
|
121
|
+
configDirEnv: 'QWEN_CONFIG_DIR',
|
|
122
|
+
configDirFallback: '.qwen',
|
|
123
|
+
kind: 'agents-md',
|
|
124
|
+
files: ['AGENTS.md'],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'codebuddy',
|
|
128
|
+
displayName: 'CodeBuddy',
|
|
129
|
+
configDirEnv: 'CODEBUDDY_CONFIG_DIR',
|
|
130
|
+
configDirFallback: '.codebuddy',
|
|
131
|
+
kind: 'agents-md',
|
|
132
|
+
files: ['AGENTS.md'],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'cline',
|
|
136
|
+
displayName: 'Cline',
|
|
137
|
+
configDirEnv: 'CLINE_CONFIG_DIR',
|
|
138
|
+
configDirFallback: '.cline',
|
|
139
|
+
kind: 'agents-md',
|
|
140
|
+
files: ['AGENTS.md'],
|
|
141
|
+
},
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
const BY_ID = new Map(RUNTIMES.map((r) => [r.id, r]));
|
|
145
|
+
|
|
146
|
+
function getRuntime(id) {
|
|
147
|
+
const r = BY_ID.get(id);
|
|
148
|
+
if (!r) {
|
|
149
|
+
throw new RangeError(
|
|
150
|
+
`Unknown runtime "${id}". Known: ${[...BY_ID.keys()].join(', ')}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return r;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function listRuntimes() {
|
|
157
|
+
return RUNTIMES;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function listRuntimeIds() {
|
|
161
|
+
return RUNTIMES.map((r) => r.id);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
RUNTIMES,
|
|
166
|
+
REPO,
|
|
167
|
+
MARKETPLACE_NAME,
|
|
168
|
+
PLUGIN_NAME,
|
|
169
|
+
getRuntime,
|
|
170
|
+
listRuntimes,
|
|
171
|
+
listRuntimeIds,
|
|
172
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mmr-rerank.cjs — Maximal Marginal Relevance post-pass on top-K
|
|
3
|
+
* (Plan 23.5-03).
|
|
4
|
+
*
|
|
5
|
+
* Solves the "all 5 surfaced learnings are about the same thing"
|
|
6
|
+
* failure mode in the Phase 14.5 decision-injector. Greedy selection
|
|
7
|
+
* with the standard MMR criterion:
|
|
8
|
+
*
|
|
9
|
+
* nextItem = argmax_{i ∉ selected} λ * relevance(i) − (1 − λ) * max_sim(i, selected)
|
|
10
|
+
*
|
|
11
|
+
* Similarity is token-overlap (Jaccard on case-folded word n-grams,
|
|
12
|
+
* default n=2). No external deps, no embedding API.
|
|
13
|
+
*
|
|
14
|
+
* Pure helper — caller supplies the candidates and a relevance score
|
|
15
|
+
* already computed by upstream (e.g. grep hit count, BM25, or the
|
|
16
|
+
* decision-injector's existing rank function). MMR re-ranks ONLY.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const DEFAULT_LAMBDA = 0.7;
|
|
22
|
+
const DEFAULT_NGRAM = 2;
|
|
23
|
+
const TOKEN_RE = /[\p{L}\p{N}_-]+/gu;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Tokenize a string into case-folded alphanumeric+underscore+dash runs.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} text
|
|
29
|
+
* @returns {string[]}
|
|
30
|
+
*/
|
|
31
|
+
function tokenize(text) {
|
|
32
|
+
if (typeof text !== 'string' || text.length === 0) return [];
|
|
33
|
+
const matches = text.toLowerCase().match(TOKEN_RE);
|
|
34
|
+
return matches ? matches : [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build a Set of word n-grams from a string.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} text
|
|
41
|
+
* @param {number} n
|
|
42
|
+
* @returns {Set<string>}
|
|
43
|
+
*/
|
|
44
|
+
function ngrams(text, n) {
|
|
45
|
+
const toks = tokenize(text);
|
|
46
|
+
if (toks.length < n) return new Set(toks);
|
|
47
|
+
const out = new Set();
|
|
48
|
+
for (let i = 0; i <= toks.length - n; i++) {
|
|
49
|
+
out.add(toks.slice(i, i + n).join(' '));
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Jaccard similarity between two strings on word n-grams.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} a
|
|
58
|
+
* @param {string} b
|
|
59
|
+
* @param {number} [n]
|
|
60
|
+
* @returns {number} 0..1
|
|
61
|
+
*/
|
|
62
|
+
function similarity(a, b, n = DEFAULT_NGRAM) {
|
|
63
|
+
const A = ngrams(a, n);
|
|
64
|
+
const B = ngrams(b, n);
|
|
65
|
+
if (A.size === 0 || B.size === 0) return 0;
|
|
66
|
+
let inter = 0;
|
|
67
|
+
for (const g of A) if (B.has(g)) inter += 1;
|
|
68
|
+
const union = A.size + B.size - inter;
|
|
69
|
+
return union > 0 ? inter / union : 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Re-rank an array of items using the MMR criterion.
|
|
74
|
+
*
|
|
75
|
+
* @param {Array<{text: string, relevance?: number}>} items
|
|
76
|
+
* @param {{lambda?: number, k?: number, ngram?: number, textOf?: (item: object) => string, relevanceOf?: (item: object) => number}} [opts]
|
|
77
|
+
* @returns {Array<object>} subset of input in MMR-selected order
|
|
78
|
+
*/
|
|
79
|
+
function rerank(items, opts = {}) {
|
|
80
|
+
if (!Array.isArray(items)) {
|
|
81
|
+
throw new TypeError('mmr-rerank.rerank: items must be an array');
|
|
82
|
+
}
|
|
83
|
+
if (items.length === 0) return [];
|
|
84
|
+
const lambda = typeof opts.lambda === 'number' ? opts.lambda : DEFAULT_LAMBDA;
|
|
85
|
+
const ngram = typeof opts.ngram === 'number' ? opts.ngram : DEFAULT_NGRAM;
|
|
86
|
+
const k = typeof opts.k === 'number' && opts.k > 0 ? Math.min(opts.k, items.length) : items.length;
|
|
87
|
+
const textOf =
|
|
88
|
+
typeof opts.textOf === 'function'
|
|
89
|
+
? opts.textOf
|
|
90
|
+
: (it) => (typeof it === 'string' ? it : (it && typeof it.text === 'string' ? it.text : ''));
|
|
91
|
+
const relOf =
|
|
92
|
+
typeof opts.relevanceOf === 'function'
|
|
93
|
+
? opts.relevanceOf
|
|
94
|
+
: (it) => {
|
|
95
|
+
if (it && typeof it.relevance === 'number') return it.relevance;
|
|
96
|
+
if (it && typeof it.score === 'number') return it.score;
|
|
97
|
+
return 1;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Pre-tokenize candidates.
|
|
101
|
+
const grams = items.map((it) => ngrams(textOf(it), ngram));
|
|
102
|
+
const relevance = items.map((it) => relOf(it));
|
|
103
|
+
const remaining = items.map((_, i) => i);
|
|
104
|
+
/** @type {number[]} */
|
|
105
|
+
const selected = [];
|
|
106
|
+
|
|
107
|
+
while (selected.length < k && remaining.length > 0) {
|
|
108
|
+
let bestIdx = -1;
|
|
109
|
+
let bestScore = -Infinity;
|
|
110
|
+
for (const i of remaining) {
|
|
111
|
+
let maxSim = 0;
|
|
112
|
+
for (const j of selected) {
|
|
113
|
+
const sim = jaccard(grams[i], grams[j]);
|
|
114
|
+
if (sim > maxSim) maxSim = sim;
|
|
115
|
+
}
|
|
116
|
+
const score = lambda * relevance[i] - (1 - lambda) * maxSim;
|
|
117
|
+
if (score > bestScore) {
|
|
118
|
+
bestScore = score;
|
|
119
|
+
bestIdx = i;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (bestIdx === -1) break;
|
|
123
|
+
selected.push(bestIdx);
|
|
124
|
+
const pos = remaining.indexOf(bestIdx);
|
|
125
|
+
if (pos !== -1) remaining.splice(pos, 1);
|
|
126
|
+
}
|
|
127
|
+
return selected.map((i) => items[i]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Jaccard between two pre-built ngram sets. Faster than calling
|
|
132
|
+
* `similarity()` from the rerank loop.
|
|
133
|
+
*
|
|
134
|
+
* @param {Set<string>} A
|
|
135
|
+
* @param {Set<string>} B
|
|
136
|
+
* @returns {number}
|
|
137
|
+
*/
|
|
138
|
+
function jaccard(A, B) {
|
|
139
|
+
if (A.size === 0 || B.size === 0) return 0;
|
|
140
|
+
let inter = 0;
|
|
141
|
+
for (const g of A) if (B.has(g)) inter += 1;
|
|
142
|
+
const union = A.size + B.size - inter;
|
|
143
|
+
return union > 0 ? inter / union : 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
rerank,
|
|
148
|
+
similarity,
|
|
149
|
+
tokenize,
|
|
150
|
+
ngrams,
|
|
151
|
+
jaccard,
|
|
152
|
+
DEFAULT_LAMBDA,
|
|
153
|
+
DEFAULT_NGRAM,
|
|
154
|
+
};
|