@slamb2k/mad-skills 2.0.45 → 2.0.47
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 +15 -0
- package/.claude-plugin/plugin.json +1 -1
- package/hooks/lib/banner.cjs +0 -1
- package/hooks/lib/config.cjs +13 -0
- package/hooks/lib/git-checks.cjs +12 -6
- package/hooks/lib/plugin-health.cjs +107 -0
- package/hooks/lib/state.cjs +63 -4
- package/hooks/lib/utils.cjs +1 -0
- package/hooks/session-guard.cjs +53 -8
- package/package.json +1 -1
- package/skills/brace/SKILL.md +92 -125
- package/skills/brace/assets/gitignore-template +4 -0
- package/skills/brace/references/branch-protection-steps.md +135 -0
- package/skills/brace/references/claude-md-template.md +2 -0
- package/skills/brace/references/phase-prompts.md +111 -0
- package/skills/brace/references/plugin-tuning-steps.md +125 -0
- package/skills/brace/references/report-template.md +9 -0
- package/skills/manifest.json +2 -2
|
@@ -18,6 +18,21 @@
|
|
|
18
18
|
"category": "development",
|
|
19
19
|
"homepage": "https://github.com/slamb2k/mad-skills",
|
|
20
20
|
"tags": ["planning", "tdd", "architecture", "llm-review", "implementation"]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "squiz",
|
|
24
|
+
"description": "Browser automation that works on Windows, WSL, remote VMs, and macOS — no Chrome extension required. Token-efficient agent-browser CLI with context isolation.",
|
|
25
|
+
"author": {
|
|
26
|
+
"name": "slamb2k",
|
|
27
|
+
"url": "https://github.com/slamb2k"
|
|
28
|
+
},
|
|
29
|
+
"source": {
|
|
30
|
+
"source": "github",
|
|
31
|
+
"repo": "slamb2k/squiz"
|
|
32
|
+
},
|
|
33
|
+
"category": "browser-automation",
|
|
34
|
+
"homepage": "https://github.com/slamb2k/squiz",
|
|
35
|
+
"tags": ["browser", "automation", "testing", "scraping", "screenshots"]
|
|
21
36
|
}
|
|
22
37
|
]
|
|
23
38
|
}
|
package/hooks/lib/banner.cjs
CHANGED
|
@@ -13,7 +13,6 @@ const BANNER_LINES = [
|
|
|
13
13
|
"::::: ##:.:: ##: ##.... ##: ##:::: ##::::'##::: ##: ##:. ##::: ##:: ##::::::: ##:::::::'##::: ##:::::::",
|
|
14
14
|
"::::: ##:::: ##: ##:::: ##: ########:::::. ######:: ##::. ##:'####: ########: ########:. ######::::::::",
|
|
15
15
|
"::::::..:::::..::..:::::..::........:::::::......:::..::::..::....::........::........:::......:::::::::",
|
|
16
|
-
'::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::',
|
|
17
16
|
];
|
|
18
17
|
|
|
19
18
|
const SEPARATOR = '\u2500'.repeat(70);
|
package/hooks/lib/config.cjs
CHANGED
|
@@ -53,4 +53,17 @@ module.exports = {
|
|
|
53
53
|
pythonFiles: ['pyproject.toml', 'requirements.txt', 'setup.py'],
|
|
54
54
|
|
|
55
55
|
taskList: { minCommits: 20, minFiles: 30 },
|
|
56
|
+
|
|
57
|
+
pluginHealth: {
|
|
58
|
+
claudeMem: {
|
|
59
|
+
maxObservations: 20,
|
|
60
|
+
maxSessionCount: 5,
|
|
61
|
+
recommendedSkipTools: [
|
|
62
|
+
'Read', 'Glob', 'Grep', 'ToolSearch', 'Agent', 'WebSearch', 'WebFetch',
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
hookify: {
|
|
66
|
+
rulePattern: /^hookify\..*\.local\.md$/,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
56
69
|
};
|
package/hooks/lib/git-checks.cjs
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { existsSync } = require('fs');
|
|
4
|
-
const { join, dirname, basename } = require('path');
|
|
4
|
+
const { join, dirname, basename, resolve } = require('path');
|
|
5
5
|
const config = require('./config.cjs');
|
|
6
6
|
const { git, readJson, countFiles } = require('./utils.cjs');
|
|
7
7
|
|
|
8
|
+
/** Normalize path separators for reliable comparison on Windows. */
|
|
9
|
+
function normalizePath(p) {
|
|
10
|
+
return resolve(p).replace(/\\/g, '/');
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* Validate git repository state.
|
|
10
15
|
* Returns { gitRoot: string|null }.
|
|
@@ -25,7 +30,7 @@ function checkGit(projectDir, output) {
|
|
|
25
30
|
return { gitRoot: null };
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
if (gitRoot !== projectDir) {
|
|
33
|
+
if (normalizePath(gitRoot) !== normalizePath(projectDir)) {
|
|
29
34
|
checkNestedGit(projectDir, gitRoot, output);
|
|
30
35
|
}
|
|
31
36
|
|
|
@@ -33,11 +38,12 @@ function checkGit(projectDir, output) {
|
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
function checkNestedGit(projectDir, gitRoot, output) {
|
|
36
|
-
// Calculate depth
|
|
41
|
+
// Calculate depth — normalize separators for reliable comparison on Windows
|
|
37
42
|
let depth = 0;
|
|
38
|
-
let check = projectDir;
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
let check = normalizePath(projectDir);
|
|
44
|
+
const normalizedRoot = normalizePath(gitRoot);
|
|
45
|
+
while (check !== normalizedRoot && check !== '/' && depth < 20) {
|
|
46
|
+
check = normalizePath(dirname(check));
|
|
41
47
|
depth++;
|
|
42
48
|
}
|
|
43
49
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { join } = require('path');
|
|
4
|
+
const { homedir } = require('os');
|
|
5
|
+
const { readdirSync } = require('fs');
|
|
6
|
+
const config = require('./config.cjs');
|
|
7
|
+
const state = require('./state.cjs');
|
|
8
|
+
const { readJson } = require('./utils.cjs');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Plugin Health — detect performance anti-patterns in companion plugins.
|
|
12
|
+
*
|
|
13
|
+
* Checks:
|
|
14
|
+
* - Hookify enabled with no rules (pure overhead)
|
|
15
|
+
* - claude-mem missing SKIP_TOOLS for read-only tools
|
|
16
|
+
* - claude-mem context injection too high (when OMC also active)
|
|
17
|
+
*
|
|
18
|
+
* Signals are weight=0 (informational only, won't trigger staleness prompt).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function checkPluginHealth(projectDir, output) {
|
|
22
|
+
const prefs = state.loadPrefs(projectDir);
|
|
23
|
+
if (prefs.pluginHealthDismissed) return;
|
|
24
|
+
|
|
25
|
+
const settings = readJson(join(homedir(), '.claude', 'settings.json'));
|
|
26
|
+
if (!settings) return; // No global settings — nothing to check
|
|
27
|
+
|
|
28
|
+
const plugins = settings.enabledPlugins || {};
|
|
29
|
+
|
|
30
|
+
checkHookify(projectDir, plugins, output);
|
|
31
|
+
checkClaudeMem(plugins, output);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── hookify ──────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function checkHookify(projectDir, plugins, output) {
|
|
37
|
+
const hookifyKey = Object.keys(plugins).find(k => k.includes('hookify'));
|
|
38
|
+
if (!hookifyKey || plugins[hookifyKey] !== true) return; // Not enabled
|
|
39
|
+
|
|
40
|
+
// Count rule files in project .claude/ directory
|
|
41
|
+
let ruleCount = 0;
|
|
42
|
+
try {
|
|
43
|
+
const projectClaudeDir = join(projectDir, '.claude');
|
|
44
|
+
const files = readdirSync(projectClaudeDir);
|
|
45
|
+
ruleCount = files.filter(f => config.pluginHealth.hookify.rulePattern.test(f)).length;
|
|
46
|
+
} catch { /* directory doesn't exist — zero rules */ }
|
|
47
|
+
|
|
48
|
+
// Also check global .claude/ directory
|
|
49
|
+
try {
|
|
50
|
+
const globalClaudeDir = join(homedir(), '.claude');
|
|
51
|
+
const files = readdirSync(globalClaudeDir);
|
|
52
|
+
ruleCount += files.filter(f => config.pluginHealth.hookify.rulePattern.test(f)).length;
|
|
53
|
+
} catch { /* noop */ }
|
|
54
|
+
|
|
55
|
+
if (ruleCount === 0) {
|
|
56
|
+
output.signals.push(
|
|
57
|
+
'\u26A0 Hookify enabled but no rules configured \u2014 fires on every tool call for nothing. Run /brace or disable in settings',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── claude-mem ───────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function checkClaudeMem(plugins, output) {
|
|
65
|
+
const memKey = Object.keys(plugins).find(k => k.includes('claude-mem'));
|
|
66
|
+
if (!memKey || plugins[memKey] !== true) return; // Not enabled
|
|
67
|
+
|
|
68
|
+
const memSettings = readJson(join(homedir(), '.claude-mem', 'settings.json'));
|
|
69
|
+
if (!memSettings) return; // No settings file
|
|
70
|
+
|
|
71
|
+
// Check SKIP_TOOLS for read-only tools
|
|
72
|
+
const currentSkip = (memSettings.CLAUDE_MEM_SKIP_TOOLS || '')
|
|
73
|
+
.split(',')
|
|
74
|
+
.map(t => t.trim())
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
|
|
77
|
+
const missing = config.pluginHealth.claudeMem.recommendedSkipTools
|
|
78
|
+
.filter(t => !currentSkip.includes(t));
|
|
79
|
+
|
|
80
|
+
if (missing.length > 0) {
|
|
81
|
+
output.signals.push(
|
|
82
|
+
`\u26A0 claude-mem observing read-only tools (${missing.join(', ')}) \u2014 run /brace to optimise`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check context injection levels (only flag if OMC is also active)
|
|
87
|
+
const omcKey = Object.keys(plugins).find(k => k.includes('oh-my-claudecode'));
|
|
88
|
+
const omcEnabled = omcKey && plugins[omcKey] === true;
|
|
89
|
+
|
|
90
|
+
if (omcEnabled) {
|
|
91
|
+
const obs = parseInt(memSettings.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
|
|
92
|
+
if (!isNaN(obs) && obs > config.pluginHealth.claudeMem.maxObservations) {
|
|
93
|
+
output.signals.push(
|
|
94
|
+
`\u26A0 claude-mem CONTEXT_OBSERVATIONS=${obs} (recommended \u226420) \u2014 may bloat session context`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const sessions = parseInt(memSettings.CLAUDE_MEM_CONTEXT_SESSION_COUNT, 10);
|
|
99
|
+
if (!isNaN(sessions) && sessions > config.pluginHealth.claudeMem.maxSessionCount) {
|
|
100
|
+
output.signals.push(
|
|
101
|
+
`\u26A0 claude-mem CONTEXT_SESSION_COUNT=${sessions} (recommended \u22645) \u2014 may bloat session context`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { checkPluginHealth };
|
package/hooks/lib/state.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync } = require('fs');
|
|
3
|
+
const { mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync, renameSync } = require('fs');
|
|
4
4
|
const { join } = require('path');
|
|
5
5
|
const { createHash } = require('crypto');
|
|
6
6
|
const { homedir } = require('os');
|
|
@@ -19,13 +19,45 @@ function statePath(projectDir) {
|
|
|
19
19
|
return join(STATE_DIR, `${projectKey(projectDir)}.json`);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** Atomic write: write to .tmp then rename to avoid partial reads. */
|
|
22
23
|
function save(projectDir, data) {
|
|
23
24
|
ensureDir();
|
|
24
|
-
|
|
25
|
+
const target = statePath(projectDir);
|
|
26
|
+
const tmp = `${target}.tmp`;
|
|
27
|
+
writeFileSync(tmp, JSON.stringify({
|
|
25
28
|
...data,
|
|
26
29
|
projectDir,
|
|
27
30
|
timestamp: Date.now(),
|
|
28
31
|
}, null, 2));
|
|
32
|
+
try {
|
|
33
|
+
renameSync(tmp, target);
|
|
34
|
+
} catch {
|
|
35
|
+
// Rename failed (rare on Windows if target locked) — fall back to direct write
|
|
36
|
+
writeFileSync(target, readFileSync(tmp, 'utf-8'));
|
|
37
|
+
try { unlinkSync(tmp); } catch { /* noop */ }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Write an in-progress marker so remind() knows to wait. */
|
|
42
|
+
function saveInProgress(projectDir) {
|
|
43
|
+
ensureDir();
|
|
44
|
+
const target = statePath(projectDir);
|
|
45
|
+
const tmp = `${target}.tmp`;
|
|
46
|
+
writeFileSync(tmp, JSON.stringify({
|
|
47
|
+
status: 'in-progress',
|
|
48
|
+
projectDir,
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
}, null, 2));
|
|
51
|
+
try {
|
|
52
|
+
renameSync(tmp, target);
|
|
53
|
+
} catch {
|
|
54
|
+
writeFileSync(target, JSON.stringify({
|
|
55
|
+
status: 'in-progress',
|
|
56
|
+
projectDir,
|
|
57
|
+
timestamp: Date.now(),
|
|
58
|
+
}, null, 2));
|
|
59
|
+
try { unlinkSync(tmp); } catch { /* noop */ }
|
|
60
|
+
}
|
|
29
61
|
}
|
|
30
62
|
|
|
31
63
|
function load(projectDir) {
|
|
@@ -42,13 +74,40 @@ function clear(projectDir) {
|
|
|
42
74
|
try { unlinkSync(statePath(projectDir)); } catch { /* noop */ }
|
|
43
75
|
}
|
|
44
76
|
|
|
45
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Dedup: true if check ran (or is running) within the last `seconds`.
|
|
79
|
+
* Also returns true if a background check is in-progress.
|
|
80
|
+
*/
|
|
46
81
|
function isRecentlyChecked(projectDir, seconds = 5) {
|
|
47
82
|
const data = load(projectDir);
|
|
48
83
|
if (!data) return false;
|
|
84
|
+
if (data.status === 'in-progress') return true;
|
|
49
85
|
return (Date.now() - data.timestamp) < seconds * 1000;
|
|
50
86
|
}
|
|
51
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Wait for the background check to complete.
|
|
90
|
+
* Polls until state file has actual results (not in-progress), or timeout.
|
|
91
|
+
* Returns loaded data or null on timeout / no data.
|
|
92
|
+
*/
|
|
93
|
+
function waitForReady(projectDir, timeoutMs = 4000, intervalMs = 200) {
|
|
94
|
+
const deadline = Date.now() + timeoutMs;
|
|
95
|
+
while (Date.now() < deadline) {
|
|
96
|
+
const data = load(projectDir);
|
|
97
|
+
if (!data) return null; // No state file at all — nothing pending
|
|
98
|
+
if (data.status === 'in-progress') {
|
|
99
|
+
// Check for stale in-progress marker (background worker crashed)
|
|
100
|
+
if (Date.now() - data.timestamp > 30000) return null;
|
|
101
|
+
// Busy-wait with sleep
|
|
102
|
+
const sleepUntil = Date.now() + intervalMs;
|
|
103
|
+
while (Date.now() < sleepUntil) { /* spin */ }
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
return data; // Ready — has actual results
|
|
107
|
+
}
|
|
108
|
+
return null; // Timed out
|
|
109
|
+
}
|
|
110
|
+
|
|
52
111
|
// ─── persistent per-project preferences ───────────────────────────────
|
|
53
112
|
|
|
54
113
|
function prefsPath(projectDir) {
|
|
@@ -99,4 +158,4 @@ function clearPendingBuild(projectDir) {
|
|
|
99
158
|
try { unlinkSync(pendingBuildPath(projectDir)); } catch { /* noop */ }
|
|
100
159
|
}
|
|
101
160
|
|
|
102
|
-
module.exports = { save, load, clear, isRecentlyChecked, loadPrefs, savePrefs, savePendingBuild, loadPendingBuild, clearPendingBuild };
|
|
161
|
+
module.exports = { save, saveInProgress, load, clear, isRecentlyChecked, waitForReady, loadPrefs, savePrefs, savePendingBuild, loadPendingBuild, clearPendingBuild };
|
package/hooks/lib/utils.cjs
CHANGED
package/hooks/session-guard.cjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
const { existsSync } = require('fs');
|
|
21
21
|
const { join } = require('path');
|
|
22
|
+
const { spawn } = require('child_process');
|
|
22
23
|
|
|
23
24
|
const config = require('./lib/config.cjs');
|
|
24
25
|
const state = require('./lib/state.cjs');
|
|
@@ -27,12 +28,14 @@ const { getBanner } = require('./lib/banner.cjs');
|
|
|
27
28
|
const { checkGit } = require('./lib/git-checks.cjs');
|
|
28
29
|
const { checkTaskList } = require('./lib/task-checks.cjs');
|
|
29
30
|
const { checkStaleness } = require('./lib/staleness.cjs');
|
|
31
|
+
const { checkPluginHealth } = require('./lib/plugin-health.cjs');
|
|
30
32
|
|
|
31
33
|
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
32
34
|
const CLAUDE_MD = join(PROJECT_DIR, 'CLAUDE.md');
|
|
33
35
|
|
|
34
36
|
// ─── check ─────────────────────────────────────────────────────────────
|
|
35
|
-
// Runs at SessionStart.
|
|
37
|
+
// Runs at SessionStart. Spawns background worker and exits immediately
|
|
38
|
+
// so the Claude Code UI stays responsive.
|
|
36
39
|
|
|
37
40
|
function check() {
|
|
38
41
|
// Dedup: skip if recently checked (handles dual global+project registration)
|
|
@@ -41,6 +44,28 @@ function check() {
|
|
|
41
44
|
return;
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
// Write in-progress marker immediately (also serves as dedup guard)
|
|
48
|
+
state.saveInProgress(PROJECT_DIR);
|
|
49
|
+
|
|
50
|
+
// Emit empty response — SessionStart returns instantly
|
|
51
|
+
console.log(JSON.stringify({}));
|
|
52
|
+
|
|
53
|
+
// Spawn background worker for heavy checks
|
|
54
|
+
// windowsHide: true prevents a console window from flashing on Windows
|
|
55
|
+
const worker = spawn(process.execPath, [__filename, 'check-bg'], {
|
|
56
|
+
detached: true,
|
|
57
|
+
stdio: 'ignore',
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: PROJECT_DIR },
|
|
60
|
+
});
|
|
61
|
+
worker.unref();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── check-bg ──────────────────────────────────────────────────────────
|
|
65
|
+
// Runs as a detached background process. Performs all validation and
|
|
66
|
+
// writes results to the state file for remind() to pick up.
|
|
67
|
+
|
|
68
|
+
function checkBackground() {
|
|
44
69
|
const output = new OutputBuilder();
|
|
45
70
|
|
|
46
71
|
// Banner — shown once per session at start
|
|
@@ -62,7 +87,7 @@ function check() {
|
|
|
62
87
|
'"Skip" \u2014 continue without one',
|
|
63
88
|
],
|
|
64
89
|
);
|
|
65
|
-
|
|
90
|
+
saveState(output);
|
|
66
91
|
return;
|
|
67
92
|
}
|
|
68
93
|
|
|
@@ -83,7 +108,10 @@ function check() {
|
|
|
83
108
|
// 4) Pending build check
|
|
84
109
|
checkPendingBuild(PROJECT_DIR, output);
|
|
85
110
|
|
|
86
|
-
// 5)
|
|
111
|
+
// 5) Plugin health check
|
|
112
|
+
checkPluginHealth(PROJECT_DIR, output);
|
|
113
|
+
|
|
114
|
+
// 6) Staleness summary
|
|
87
115
|
if (output.score >= config.staleness.threshold) {
|
|
88
116
|
output.blank();
|
|
89
117
|
output.add(`[SESSION GUARD] \u26A0\uFE0F CLAUDE.md appears STALE (score: ${output.score}/${config.staleness.threshold})`);
|
|
@@ -105,14 +133,15 @@ function check() {
|
|
|
105
133
|
output.signals.forEach(sig => output.add(` ${sig}`));
|
|
106
134
|
}
|
|
107
135
|
|
|
108
|
-
|
|
136
|
+
saveState(output);
|
|
109
137
|
}
|
|
110
138
|
|
|
111
139
|
// ─── remind ────────────────────────────────────────────────────────────
|
|
112
140
|
// Runs at UserPromptSubmit. Re-emits pending context from check, once.
|
|
113
141
|
|
|
114
142
|
function remind() {
|
|
115
|
-
|
|
143
|
+
// Wait for background check to complete (polls up to 4s at 200ms intervals)
|
|
144
|
+
const pending = state.waitForReady(PROJECT_DIR);
|
|
116
145
|
|
|
117
146
|
if (!pending || !pending.context) {
|
|
118
147
|
console.log(JSON.stringify({}));
|
|
@@ -256,8 +285,8 @@ function checkPendingBuild(projectDir, output) {
|
|
|
256
285
|
|
|
257
286
|
// ─── helpers ───────────────────────────────────────────────────────────
|
|
258
287
|
|
|
259
|
-
|
|
260
|
-
|
|
288
|
+
/** Save check results to state file (used by background worker, no stdout). */
|
|
289
|
+
function saveState(output) {
|
|
261
290
|
state.save(PROJECT_DIR, {
|
|
262
291
|
context: output.parts.join('\n'),
|
|
263
292
|
score: output.score,
|
|
@@ -276,6 +305,15 @@ switch (command) {
|
|
|
276
305
|
case 'remind':
|
|
277
306
|
remind();
|
|
278
307
|
break;
|
|
308
|
+
case 'check-bg':
|
|
309
|
+
try {
|
|
310
|
+
checkBackground();
|
|
311
|
+
} catch {
|
|
312
|
+
// Background worker failed — clear in-progress marker so remind()
|
|
313
|
+
// doesn't hang waiting. Graceful degradation: no context this session.
|
|
314
|
+
state.clear(PROJECT_DIR);
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
279
317
|
case 'dismiss-brace': {
|
|
280
318
|
const prefs = state.loadPrefs(PROJECT_DIR);
|
|
281
319
|
prefs.braceDismissed = true;
|
|
@@ -290,8 +328,15 @@ switch (command) {
|
|
|
290
328
|
console.log(`Rig prompt dismissed for ${PROJECT_DIR}`);
|
|
291
329
|
break;
|
|
292
330
|
}
|
|
331
|
+
case 'dismiss-plugin-health': {
|
|
332
|
+
const prefs = state.loadPrefs(PROJECT_DIR);
|
|
333
|
+
prefs.pluginHealthDismissed = true;
|
|
334
|
+
state.savePrefs(PROJECT_DIR, prefs);
|
|
335
|
+
console.log(`Plugin health checks dismissed for ${PROJECT_DIR}`);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
293
338
|
default:
|
|
294
339
|
console.error(`Session Guard v${config.version}`);
|
|
295
|
-
console.error('Usage: node session-guard.js <check|remind|dismiss-brace|dismiss-rig>');
|
|
340
|
+
console.error('Usage: node session-guard.js <check|remind|dismiss-brace|dismiss-rig|dismiss-plugin-health>');
|
|
296
341
|
process.exit(1);
|
|
297
342
|
}
|
package/package.json
CHANGED
package/skills/brace/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: brace
|
|
3
3
|
description: 'Initialize any project directory with a standard scaffold for AI-assisted development. Creates specs/ and context/ directories, a project CLAUDE.md with development workflow and guardrails, .gitignore, and branch protection. Recommends claude-mem for persistent memory. Idempotent — safe to run on existing projects. Triggers: "init project", "setup brace", "brace", "initialize", "bootstrap", "scaffold".'
|
|
4
|
-
argument-hint: "[--force]"
|
|
4
|
+
argument-hint: "[--force] [--skip-plugin-tuning]"
|
|
5
5
|
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, Agent, AskUserQuestion
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -75,6 +75,7 @@ Report format: `references/report-template.md`
|
|
|
75
75
|
|
|
76
76
|
Parse optional flags from the request:
|
|
77
77
|
- `--force` — Overwrite existing files without prompting
|
|
78
|
+
- `--skip-plugin-tuning` — Skip Phase 7 plugin performance tuning
|
|
78
79
|
|
|
79
80
|
---
|
|
80
81
|
|
|
@@ -85,6 +86,7 @@ Before starting, check all dependencies in this table:
|
|
|
85
86
|
| Dependency | Type | Check | Required | Resolution | Detail |
|
|
86
87
|
|-----------|------|-------|----------|------------|--------|
|
|
87
88
|
| claude-mem | plugin | — | no | ask | `claude plugin install claude-mem` |
|
|
89
|
+
| oh-my-claudecode | plugin | — | no | ask | `claude plugin install oh-my-claudecode` |
|
|
88
90
|
|
|
89
91
|
For each row, in order:
|
|
90
92
|
1. Run the Check command (for cli/npm) or test file existence (for agent/skill)
|
|
@@ -97,6 +99,11 @@ For each row, in order:
|
|
|
97
99
|
- **fallback**: notify user with Detail, continue with degraded behavior
|
|
98
100
|
4. After all checks: summarize what's available and what's degraded
|
|
99
101
|
|
|
102
|
+
**Plugin detection:** For plugin dependencies (Type = plugin), check
|
|
103
|
+
`~/.claude/settings.json` → `enabledPlugins` for a key containing the plugin
|
|
104
|
+
name set to `true`. Store results as `PLUGIN_STATE` (`claude_mem_installed`,
|
|
105
|
+
`omc_installed`, `hookify_installed`) for use in Phase 4 and Phase 7.
|
|
106
|
+
|
|
100
107
|
1. Capture **FLAGS** from the user's request
|
|
101
108
|
|
|
102
109
|
---
|
|
@@ -246,6 +253,11 @@ Before sending the prompt, substitute these variables:
|
|
|
246
253
|
- `{GITIGNORE_CONTENT}` — read from `assets/gitignore-template`
|
|
247
254
|
- `{GLOBAL_PREFERENCES_CONTENT}` — read from `assets/global-preferences-template.md`
|
|
248
255
|
(the section between BEGIN TEMPLATE and END TEMPLATE)
|
|
256
|
+
- `{PLUGIN_ROLE_SEPARATION}` — if both claude-mem AND oh-my-claudecode are
|
|
257
|
+
detected as enabled (from Phase 7 PLUGIN_REPORT, or by checking
|
|
258
|
+
`~/.claude/settings.json`), substitute with the content from
|
|
259
|
+
`references/plugin-tuning-steps.md#plugin-role-separation-content`.
|
|
260
|
+
Otherwise, substitute with an empty string.
|
|
249
261
|
|
|
250
262
|
Parse SCAFFOLD_REPORT. If status is "failed", report to user and stop.
|
|
251
263
|
|
|
@@ -288,150 +300,105 @@ Parse VERIFY_REPORT. Present the final summary using the format in
|
|
|
288
300
|
|
|
289
301
|
**Only if the project is a git repo** (from SCAN_REPORT `git_initialized`).
|
|
290
302
|
|
|
291
|
-
Detect the default branch and hosting platform
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
default_branch=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
|
|
295
|
-
|
|
296
|
-
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
|
|
297
|
-
if echo "$REMOTE_URL" | grep -qiE 'dev\.azure\.com|visualstudio\.com'; then
|
|
298
|
-
PLATFORM="azdo"
|
|
299
|
-
elif echo "$REMOTE_URL" | grep -qi 'github\.com'; then
|
|
300
|
-
PLATFORM="github"
|
|
301
|
-
else
|
|
302
|
-
PLATFORM="unknown"
|
|
303
|
-
fi
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### GitHub
|
|
303
|
+
Detect the default branch and hosting platform (GitHub, Azure DevOps, or
|
|
304
|
+
unknown). Platform detection and all CLI/REST commands are in
|
|
305
|
+
`references/branch-protection-steps.md`.
|
|
307
306
|
|
|
308
|
-
|
|
309
|
-
1. Check for existing branch protection via `gh api repos/{owner}/{repo}/branches/{default_branch}/protection` (404 = unprotected)
|
|
310
|
-
2. If unprotected, ask via AskUserQuestion:
|
|
307
|
+
### Flow
|
|
311
308
|
|
|
312
|
-
|
|
313
|
-
|
|
309
|
+
1. Detect platform from `git remote get-url origin`
|
|
310
|
+
2. **GitHub:** Check existing protection via `gh api`. If unprotected, ask via
|
|
311
|
+
AskUserQuestion:
|
|
314
312
|
- "Yes, require PR reviews (Recommended)" — require 1 approval, block force push
|
|
315
313
|
- "Skip" — leave unprotected
|
|
314
|
+
3. **Azure DevOps:** Extract org/project from remote URL. Check existing
|
|
315
|
+
policies via `az repos` CLI or REST fallback. If no minimum reviewer
|
|
316
|
+
policy, ask via AskUserQuestion (same options as GitHub).
|
|
317
|
+
4. **Unknown platform:** Skip and report.
|
|
316
318
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
gh api repos/{owner}/{repo}/branches/{default_branch}/protection \
|
|
320
|
-
-X PUT -f required_pull_request_reviews='{"required_approving_review_count":1}' \
|
|
321
|
-
-f enforce_admins=false \
|
|
322
|
-
-f restrictions=null \
|
|
323
|
-
-f required_status_checks=null \
|
|
324
|
-
-F allow_force_pushes=false \
|
|
325
|
-
-F allow_deletions=false
|
|
326
|
-
```
|
|
319
|
+
If user accepts, apply using the procedures in
|
|
320
|
+
`references/branch-protection-steps.md`.
|
|
327
321
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
If `PLATFORM == azdo`:
|
|
331
|
-
|
|
332
|
-
Extract org and project from the remote URL (same pattern as `/ship`):
|
|
333
|
-
```bash
|
|
334
|
-
if echo "$REMOTE_URL" | grep -q 'dev\.azure\.com'; then
|
|
335
|
-
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*dev\.azure\.com/\([^/]*\)/.*|\1|p')
|
|
336
|
-
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*dev\.azure\.com/[^/]*/\([^/]*\)/.*|\1|p')
|
|
337
|
-
AZDO_ORG_URL="https://dev.azure.com/$AZDO_ORG"
|
|
338
|
-
elif echo "$REMOTE_URL" | grep -q 'vs-ssh\.visualstudio\.com'; then
|
|
339
|
-
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*vs-ssh\.visualstudio\.com:v3/\([^/]*\)/.*|\1|p')
|
|
340
|
-
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*vs-ssh\.visualstudio\.com:v3/[^/]*/\([^/]*\)/.*|\1|p')
|
|
341
|
-
AZDO_ORG_URL="https://dev.azure.com/$AZDO_ORG"
|
|
342
|
-
elif echo "$REMOTE_URL" | grep -q 'visualstudio\.com'; then
|
|
343
|
-
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*//\([^.]*\)\.visualstudio\.com.*|\1|p')
|
|
344
|
-
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*/\([^/]*\)/_git/.*|\1|p')
|
|
345
|
-
AZDO_ORG_URL="https://${AZDO_ORG}.visualstudio.com"
|
|
346
|
-
fi
|
|
347
|
-
# URL-decode for CLI/display; keep URL-safe versions for REST API paths
|
|
348
|
-
AZDO_PROJECT_URL_SAFE="$AZDO_PROJECT"
|
|
349
|
-
AZDO_ORG=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$AZDO_ORG'))")
|
|
350
|
-
AZDO_PROJECT=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$AZDO_PROJECT_URL_SAFE'))")
|
|
351
|
-
REPO_NAME=$(basename -s .git "$REMOTE_URL")
|
|
352
|
-
```
|
|
322
|
+
Include result in the final report under a "🔒 Branch protection" section.
|
|
353
323
|
|
|
354
|
-
|
|
324
|
+
---
|
|
355
325
|
|
|
356
|
-
|
|
357
|
-
otherwise fall back to REST API:
|
|
326
|
+
## Phase 7: Plugin Performance Tuning
|
|
358
327
|
|
|
359
|
-
|
|
360
|
-
```bash
|
|
361
|
-
az repos policy list \
|
|
362
|
-
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
363
|
-
--repository-id "$REPO_NAME" --branch "$default_branch" \
|
|
364
|
-
--query "[].type.displayName" -o tsv
|
|
365
|
-
```
|
|
328
|
+
**Skip this phase if `--skip-plugin-tuning` flag is set.**
|
|
366
329
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
# Get repository ID first
|
|
371
|
-
REPO_ID=$(curl -s -H "$AUTH" \
|
|
372
|
-
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/git/repositories/$REPO_NAME?api-version=7.0" \
|
|
373
|
-
| jq -r '.id')
|
|
374
|
-
# List branch policies
|
|
375
|
-
curl -s -H "$AUTH" \
|
|
376
|
-
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/policy/configurations?api-version=7.0" \
|
|
377
|
-
| jq "[.value[] | select(.settings.scope[]?.refName == \"refs/heads/$default_branch\" and .settings.scope[]?.repositoryId == \"$REPO_ID\")]"
|
|
378
|
-
```
|
|
330
|
+
Detect companion Claude Code plugins and recommend performance optimisations.
|
|
331
|
+
This phase modifies user-level settings files (`~/.claude/settings.json`,
|
|
332
|
+
`~/.claude-mem/settings.json`) — not repo-level files.
|
|
379
333
|
|
|
380
|
-
|
|
334
|
+
If `--force` is set, apply all recommendations without prompting.
|
|
381
335
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
336
|
+
**Plugin presence guards:** Only audit plugins detected as installed during
|
|
337
|
+
pre-flight (`PLUGIN_STATE`). Skip H1/H2 if hookify is absent. Skip M1/M2/M3
|
|
338
|
+
if claude-mem is absent. Skip M2 if OMC is absent (M2 requires both). The
|
|
339
|
+
`{PLUGIN_ROLE_SEPARATION}` content in CLAUDE.md is only injected when both
|
|
340
|
+
claude-mem and OMC are confirmed enabled.
|
|
386
341
|
|
|
387
|
-
|
|
342
|
+
If no companion plugins are installed at all, output:
|
|
343
|
+
```
|
|
344
|
+
━━ 7 · Plugin Performance ━━━━━━━━━━━━━━━━━━━━
|
|
345
|
+
⏭️ No companion plugins installed — skipping
|
|
346
|
+
```
|
|
347
|
+
and skip to the report.
|
|
388
348
|
|
|
389
|
-
|
|
390
|
-
```bash
|
|
391
|
-
REPO_ID=$(az repos show --repository "$REPO_NAME" \
|
|
392
|
-
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
393
|
-
--query 'id' -o tsv)
|
|
394
|
-
|
|
395
|
-
az repos policy approver-count create \
|
|
396
|
-
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
397
|
-
--repository-id "$REPO_ID" --branch "$default_branch" \
|
|
398
|
-
--minimum-approver-count 1 \
|
|
399
|
-
--creator-vote-counts false \
|
|
400
|
-
--allow-downvotes false \
|
|
401
|
-
--reset-on-source-push true \
|
|
402
|
-
--blocking true --enabled true
|
|
403
|
-
```
|
|
349
|
+
### Detection
|
|
404
350
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
\"resetOnSourcePush\": true,
|
|
418
|
-
\"scope\": [{
|
|
419
|
-
\"repositoryId\": \"$REPO_ID\",
|
|
420
|
-
\"refName\": \"refs/heads/$default_branch\",
|
|
421
|
-
\"matchKind\": \"exact\"
|
|
422
|
-
}]
|
|
423
|
-
}
|
|
424
|
-
}"
|
|
425
|
-
```
|
|
351
|
+
Launch **Bash** subagent (**haiku**):
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
Task(
|
|
355
|
+
subagent_type: "Bash",
|
|
356
|
+
model: "haiku",
|
|
357
|
+
description: "Detect installed plugins and audit performance",
|
|
358
|
+
prompt: <read from references/phase-prompts.md#phase-7>
|
|
359
|
+
)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Parse PLUGIN_REPORT.
|
|
426
363
|
|
|
427
|
-
###
|
|
364
|
+
### Present Findings
|
|
428
365
|
|
|
429
|
-
If
|
|
366
|
+
If PLUGIN_REPORT contains zero findings, output:
|
|
430
367
|
```
|
|
431
|
-
|
|
368
|
+
━━ 7 · Plugin Performance ━━━━━━━━━━━━━━━━━━━━
|
|
369
|
+
⏭️ No plugin optimisations needed
|
|
432
370
|
```
|
|
371
|
+
Skip to the report.
|
|
433
372
|
|
|
434
|
-
|
|
373
|
+
Otherwise, present findings with **AskUserQuestion**:
|
|
374
|
+
|
|
375
|
+
Options:
|
|
376
|
+
- "Apply all recommendations (Recommended)"
|
|
377
|
+
- "Let me choose which to apply"
|
|
378
|
+
- "Skip plugin tuning"
|
|
379
|
+
|
|
380
|
+
If "Let me choose", present individual findings as multi-select.
|
|
381
|
+
|
|
382
|
+
### Audit Rules
|
|
383
|
+
|
|
384
|
+
| Code | Check | Condition | Severity |
|
|
385
|
+
|------|-------|-----------|----------|
|
|
386
|
+
| H1 | Hookify: no rules | enabled + zero `hookify.*.local.md` files | high |
|
|
387
|
+
| H2 | Hookify: python3 broken | enabled + `python3 --version` fails | high |
|
|
388
|
+
| M1 | claude-mem: read-only tools | SKIP_TOOLS missing Read/Glob/Grep/ToolSearch/Agent/WebSearch/WebFetch | medium |
|
|
389
|
+
| M2 | claude-mem: high context | observations > 10 or sessions > 3, AND OMC also enabled | medium |
|
|
390
|
+
| M3 | claude-mem: provider=claude | provider is "claude" (SDK spawn known-broken) | low |
|
|
391
|
+
|
|
392
|
+
### Apply Approved Changes
|
|
393
|
+
|
|
394
|
+
For each approved finding, follow the procedures in
|
|
395
|
+
`references/plugin-tuning-steps.md`. Each script is idempotent — re-reads
|
|
396
|
+
the target file before writing and checks current value before modifying.
|
|
397
|
+
|
|
398
|
+
If both claude-mem AND oh-my-claudecode are detected as enabled, also inject
|
|
399
|
+
the Plugin Role Separation section into the project CLAUDE.md (see
|
|
400
|
+
`references/plugin-tuning-steps.md#plugin-role-separation-content`).
|
|
401
|
+
Skip if section already exists (idempotent).
|
|
435
402
|
|
|
436
403
|
---
|
|
437
404
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Branch Protection Steps
|
|
2
|
+
|
|
3
|
+
Procedural reference for Phase 6 — platform-specific branch protection commands.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Platform Detection
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
default_branch=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
|
|
11
|
+
|
|
12
|
+
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
|
|
13
|
+
if echo "$REMOTE_URL" | grep -qiE 'dev\.azure\.com|visualstudio\.com'; then
|
|
14
|
+
PLATFORM="azdo"
|
|
15
|
+
elif echo "$REMOTE_URL" | grep -qi 'github\.com'; then
|
|
16
|
+
PLATFORM="github"
|
|
17
|
+
else
|
|
18
|
+
PLATFORM="unknown"
|
|
19
|
+
fi
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## GitHub
|
|
25
|
+
|
|
26
|
+
### Check existing protection
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
gh api repos/{owner}/{repo}/branches/{default_branch}/protection
|
|
30
|
+
```
|
|
31
|
+
404 = unprotected.
|
|
32
|
+
|
|
33
|
+
### Apply protection
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
gh api repos/{owner}/{repo}/branches/{default_branch}/protection \
|
|
37
|
+
-X PUT -f required_pull_request_reviews='{"required_approving_review_count":1}' \
|
|
38
|
+
-f enforce_admins=false \
|
|
39
|
+
-f restrictions=null \
|
|
40
|
+
-f required_status_checks=null \
|
|
41
|
+
-F allow_force_pushes=false \
|
|
42
|
+
-F allow_deletions=false
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Azure DevOps
|
|
48
|
+
|
|
49
|
+
### Extract org and project
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
if echo "$REMOTE_URL" | grep -q 'dev\.azure\.com'; then
|
|
53
|
+
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*dev\.azure\.com/\([^/]*\)/.*|\1|p')
|
|
54
|
+
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*dev\.azure\.com/[^/]*/\([^/]*\)/.*|\1|p')
|
|
55
|
+
AZDO_ORG_URL="https://dev.azure.com/$AZDO_ORG"
|
|
56
|
+
elif echo "$REMOTE_URL" | grep -q 'vs-ssh\.visualstudio\.com'; then
|
|
57
|
+
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*vs-ssh\.visualstudio\.com:v3/\([^/]*\)/.*|\1|p')
|
|
58
|
+
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*vs-ssh\.visualstudio\.com:v3/[^/]*/\([^/]*\)/.*|\1|p')
|
|
59
|
+
AZDO_ORG_URL="https://dev.azure.com/$AZDO_ORG"
|
|
60
|
+
elif echo "$REMOTE_URL" | grep -q 'visualstudio\.com'; then
|
|
61
|
+
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*//\([^.]*\)\.visualstudio\.com.*|\1|p')
|
|
62
|
+
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*/\([^/]*\)/_git/.*|\1|p')
|
|
63
|
+
AZDO_ORG_URL="https://${AZDO_ORG}.visualstudio.com"
|
|
64
|
+
fi
|
|
65
|
+
# URL-decode for CLI/display; keep URL-safe versions for REST API paths
|
|
66
|
+
AZDO_PROJECT_URL_SAFE="$AZDO_PROJECT"
|
|
67
|
+
AZDO_ORG=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$AZDO_ORG'))")
|
|
68
|
+
AZDO_PROJECT=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$AZDO_PROJECT_URL_SAFE'))")
|
|
69
|
+
REPO_NAME=$(basename -s .git "$REMOTE_URL")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If org/project extraction fails, report ⚠️ and skip branch policies.
|
|
73
|
+
|
|
74
|
+
### Check existing policies
|
|
75
|
+
|
|
76
|
+
**CLI:**
|
|
77
|
+
```bash
|
|
78
|
+
az repos policy list \
|
|
79
|
+
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
80
|
+
--repository-id "$REPO_NAME" --branch "$default_branch" \
|
|
81
|
+
--query "[].type.displayName" -o tsv
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**REST fallback:**
|
|
85
|
+
```bash
|
|
86
|
+
AUTH="Authorization: Basic $(printf ":%s" "$PAT" | base64 | tr -d '\n')"
|
|
87
|
+
# Get repository ID first
|
|
88
|
+
REPO_ID=$(curl -s -H "$AUTH" \
|
|
89
|
+
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/git/repositories/$REPO_NAME?api-version=7.0" \
|
|
90
|
+
| jq -r '.id')
|
|
91
|
+
# List branch policies
|
|
92
|
+
curl -s -H "$AUTH" \
|
|
93
|
+
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/policy/configurations?api-version=7.0" \
|
|
94
|
+
| jq "[.value[] | select(.settings.scope[]?.refName == \"refs/heads/$default_branch\" and .settings.scope[]?.repositoryId == \"$REPO_ID\")]"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Create minimum reviewer policy
|
|
98
|
+
|
|
99
|
+
**CLI:**
|
|
100
|
+
```bash
|
|
101
|
+
REPO_ID=$(az repos show --repository "$REPO_NAME" \
|
|
102
|
+
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
103
|
+
--query 'id' -o tsv)
|
|
104
|
+
|
|
105
|
+
az repos policy approver-count create \
|
|
106
|
+
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
107
|
+
--repository-id "$REPO_ID" --branch "$default_branch" \
|
|
108
|
+
--minimum-approver-count 1 \
|
|
109
|
+
--creator-vote-counts false \
|
|
110
|
+
--allow-downvotes false \
|
|
111
|
+
--reset-on-source-push true \
|
|
112
|
+
--blocking true --enabled true
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**REST fallback:**
|
|
116
|
+
```bash
|
|
117
|
+
curl -s -X POST -H "$AUTH" -H "Content-Type: application/json" \
|
|
118
|
+
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/policy/configurations?api-version=7.0" \
|
|
119
|
+
-d "{
|
|
120
|
+
\"isEnabled\": true,
|
|
121
|
+
\"isBlocking\": true,
|
|
122
|
+
\"type\": {\"id\": \"fa4e907d-c16b-4a4c-9dfa-4906e5d171dd\"},
|
|
123
|
+
\"settings\": {
|
|
124
|
+
\"minimumApproverCount\": 1,
|
|
125
|
+
\"creatorVoteCounts\": false,
|
|
126
|
+
\"allowDownvotes\": false,
|
|
127
|
+
\"resetOnSourcePush\": true,
|
|
128
|
+
\"scope\": [{
|
|
129
|
+
\"repositoryId\": \"$REPO_ID\",
|
|
130
|
+
\"refName\": \"refs/heads/$default_branch\",
|
|
131
|
+
\"matchKind\": \"exact\"
|
|
132
|
+
}]
|
|
133
|
+
}
|
|
134
|
+
}"
|
|
135
|
+
```
|
|
@@ -49,6 +49,8 @@ MCP tools for search, timeline, and observation management. Claude Code's
|
|
|
49
49
|
built-in auto memory (`~/.claude/projects/<project>/memory/MEMORY.md`)
|
|
50
50
|
handles curated facts.
|
|
51
51
|
|
|
52
|
+
{PLUGIN_ROLE_SEPARATION}
|
|
53
|
+
|
|
52
54
|
{UNIVERSAL_PRINCIPLES}
|
|
53
55
|
|
|
54
56
|
## Branch Discipline
|
|
@@ -170,6 +170,10 @@ empty string (principles are in the global config instead).
|
|
|
170
170
|
|
|
171
171
|
{GITIGNORE_CONTENT}
|
|
172
172
|
|
|
173
|
+
### Plugin Role Separation Content
|
|
174
|
+
|
|
175
|
+
{PLUGIN_ROLE_SEPARATION}
|
|
176
|
+
|
|
173
177
|
### Global Preferences Content
|
|
174
178
|
|
|
175
179
|
{GLOBAL_PREFERENCES_CONTENT}
|
|
@@ -254,3 +258,110 @@ VERIFY_REPORT:
|
|
|
254
258
|
legacy_cleaned: true|false|not_applicable
|
|
255
259
|
issues: [any problems found]
|
|
256
260
|
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Phase 7: Plugin Performance Detection
|
|
265
|
+
|
|
266
|
+
**Agent:** Bash | **Model:** haiku
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
Detect installed Claude Code plugins and audit their performance configuration.
|
|
270
|
+
|
|
271
|
+
Limit PLUGIN_REPORT to 30 lines maximum.
|
|
272
|
+
|
|
273
|
+
## Checks
|
|
274
|
+
|
|
275
|
+
1. **Read plugin registry**
|
|
276
|
+
SETTINGS_FILE="$HOME/.claude/settings.json"
|
|
277
|
+
if [ ! -f "$SETTINGS_FILE" ]; then
|
|
278
|
+
echo "no_settings_file"
|
|
279
|
+
echo "PLUGIN_REPORT:"
|
|
280
|
+
echo " settings_file_found: false"
|
|
281
|
+
echo " findings: none"
|
|
282
|
+
exit 0
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
Use node to parse JSON and extract installed/enabled plugin status:
|
|
286
|
+
node -e "
|
|
287
|
+
const s = JSON.parse(require('fs').readFileSync('$HOME/.claude/settings.json','utf8'));
|
|
288
|
+
const p = s.enabledPlugins || {};
|
|
289
|
+
const hookify = Object.keys(p).find(k => k.includes('hookify'));
|
|
290
|
+
const mem = Object.keys(p).find(k => k.includes('claude-mem'));
|
|
291
|
+
const omc = Object.keys(p).find(k => k.includes('oh-my-claudecode'));
|
|
292
|
+
console.log('hookify_installed:' + !!hookify);
|
|
293
|
+
console.log('hookify_enabled:' + (hookify && p[hookify] === true));
|
|
294
|
+
console.log('claude_mem_installed:' + !!mem);
|
|
295
|
+
console.log('claude_mem_enabled:' + (mem && p[mem] === true));
|
|
296
|
+
console.log('omc_installed:' + !!omc);
|
|
297
|
+
console.log('omc_enabled:' + (omc && p[omc] === true));
|
|
298
|
+
"
|
|
299
|
+
Record: hookify_installed/enabled, claude_mem_installed/enabled, omc_installed/enabled
|
|
300
|
+
|
|
301
|
+
If a plugin is not installed (not in enabledPlugins at all), skip its
|
|
302
|
+
entire audit section below and report all its fields as "N/A".
|
|
303
|
+
|
|
304
|
+
2. **Hookify audit** (only if hookify_enabled == true)
|
|
305
|
+
a. Check python3 availability:
|
|
306
|
+
python3 --version >/dev/null 2>&1 && echo "python3_available:true" || echo "python3_available:false"
|
|
307
|
+
Record: python3_available
|
|
308
|
+
|
|
309
|
+
b. Count hookify rule files:
|
|
310
|
+
RULE_COUNT=0
|
|
311
|
+
for f in "$HOME/.claude"/hookify.*.local.md .claude/hookify.*.local.md; do
|
|
312
|
+
[ -f "$f" ] && RULE_COUNT=$((RULE_COUNT + 1))
|
|
313
|
+
done
|
|
314
|
+
echo "hookify_rule_count:$RULE_COUNT"
|
|
315
|
+
Record: hookify_rule_count
|
|
316
|
+
|
|
317
|
+
c. Determine findings:
|
|
318
|
+
- If python3_available == false → finding H2
|
|
319
|
+
- If hookify_rule_count == 0 → finding H1
|
|
320
|
+
|
|
321
|
+
3. **claude-mem audit** (only if claude_mem_enabled == true)
|
|
322
|
+
MEM_SETTINGS="$HOME/.claude-mem/settings.json"
|
|
323
|
+
if [ -f "$MEM_SETTINGS" ]; then
|
|
324
|
+
node -e "
|
|
325
|
+
const s = JSON.parse(require('fs').readFileSync('$HOME/.claude-mem/settings.json','utf8'));
|
|
326
|
+
console.log('skip_tools:' + (s.CLAUDE_MEM_SKIP_TOOLS || ''));
|
|
327
|
+
console.log('observations:' + (s.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50'));
|
|
328
|
+
console.log('session_count:' + (s.CLAUDE_MEM_CONTEXT_SESSION_COUNT || '10'));
|
|
329
|
+
console.log('provider:' + (s.CLAUDE_MEM_PROVIDER || 'claude'));
|
|
330
|
+
console.log('has_openrouter_key:' + !!(s.CLAUDE_MEM_OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY));
|
|
331
|
+
"
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
a. Check SKIP_TOOLS for read-only tools:
|
|
335
|
+
Required: Read,Glob,Grep,ToolSearch,Agent,WebSearch,WebFetch
|
|
336
|
+
For each, check if it appears in the skip_tools value.
|
|
337
|
+
Record: missing_skip_tools (comma-separated list, or "none")
|
|
338
|
+
If any missing → finding M1
|
|
339
|
+
|
|
340
|
+
b. Check context injection levels (only if omc_enabled == true):
|
|
341
|
+
If observations > 10 OR session_count > 3 → finding M2
|
|
342
|
+
Record: current_observations, current_session_count
|
|
343
|
+
|
|
344
|
+
c. Check provider:
|
|
345
|
+
If provider == "claude" → finding M3
|
|
346
|
+
Record: current_provider
|
|
347
|
+
|
|
348
|
+
## Output Format
|
|
349
|
+
|
|
350
|
+
PLUGIN_REPORT:
|
|
351
|
+
settings_file_found: true|false
|
|
352
|
+
hookify_installed: true|false
|
|
353
|
+
hookify_enabled: true|false
|
|
354
|
+
hookify_python3_available: true|false|N/A
|
|
355
|
+
hookify_rule_count: {number}|N/A
|
|
356
|
+
claude_mem_installed: true|false
|
|
357
|
+
claude_mem_enabled: true|false
|
|
358
|
+
claude_mem_skip_tools: {current value}|N/A
|
|
359
|
+
claude_mem_missing_skip_tools: {list}|none|N/A
|
|
360
|
+
claude_mem_observations: {number}|N/A
|
|
361
|
+
claude_mem_session_count: {number}|N/A
|
|
362
|
+
claude_mem_provider: {value}|N/A
|
|
363
|
+
claude_mem_has_openrouter_key: true|false|N/A
|
|
364
|
+
omc_installed: true|false
|
|
365
|
+
omc_enabled: true|false
|
|
366
|
+
findings: {comma-separated list of H1,H2,M1,M2,M3 or "none"}
|
|
367
|
+
```
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Plugin Tuning Steps
|
|
2
|
+
|
|
3
|
+
Procedural reference for Phase 7 — `node -e` scripts for each audit rule.
|
|
4
|
+
Each script is idempotent: re-reads the target file before writing and
|
|
5
|
+
checks current value before modifying.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## H1/H2: Disable Hookify
|
|
10
|
+
|
|
11
|
+
Target: `~/.claude/settings.json`
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
node -e "
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const p = require('os').homedir() + '/.claude/settings.json';
|
|
17
|
+
const s = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
18
|
+
const key = Object.keys(s.enabledPlugins || {}).find(k => k.includes('hookify'));
|
|
19
|
+
if (key && s.enabledPlugins[key] !== false) {
|
|
20
|
+
s.enabledPlugins[key] = false;
|
|
21
|
+
fs.writeFileSync(p, JSON.stringify(s, null, 2) + '\n');
|
|
22
|
+
console.log('Disabled hookify');
|
|
23
|
+
} else {
|
|
24
|
+
console.log('Hookify already disabled — skipped');
|
|
25
|
+
}
|
|
26
|
+
"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## M1: Add Read-Only Tools to SKIP_TOOLS
|
|
32
|
+
|
|
33
|
+
Target: `~/.claude-mem/settings.json`
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node -e "
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const p = require('os').homedir() + '/.claude-mem/settings.json';
|
|
39
|
+
const s = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
40
|
+
const required = ['Read','Glob','Grep','ToolSearch','Agent','WebSearch','WebFetch'];
|
|
41
|
+
const current = (s.CLAUDE_MEM_SKIP_TOOLS || '').split(',').map(t => t.trim()).filter(Boolean);
|
|
42
|
+
const missing = required.filter(t => !current.includes(t));
|
|
43
|
+
if (missing.length > 0) {
|
|
44
|
+
s.CLAUDE_MEM_SKIP_TOOLS = [...new Set([...current, ...missing])].join(',');
|
|
45
|
+
fs.writeFileSync(p, JSON.stringify(s, null, 2) + '\n');
|
|
46
|
+
console.log('Added to SKIP_TOOLS: ' + missing.join(', '));
|
|
47
|
+
} else {
|
|
48
|
+
console.log('All read-only tools already in SKIP_TOOLS — skipped');
|
|
49
|
+
}
|
|
50
|
+
"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## M2: Reduce Context Injection
|
|
56
|
+
|
|
57
|
+
Target: `~/.claude-mem/settings.json`
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
node -e "
|
|
61
|
+
const fs = require('fs');
|
|
62
|
+
const p = require('os').homedir() + '/.claude-mem/settings.json';
|
|
63
|
+
const s = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
64
|
+
let changed = false;
|
|
65
|
+
if (parseInt(s.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50') > 10) {
|
|
66
|
+
s.CLAUDE_MEM_CONTEXT_OBSERVATIONS = '10';
|
|
67
|
+
changed = true;
|
|
68
|
+
}
|
|
69
|
+
if (parseInt(s.CLAUDE_MEM_CONTEXT_SESSION_COUNT || '10') > 3) {
|
|
70
|
+
s.CLAUDE_MEM_CONTEXT_SESSION_COUNT = '3';
|
|
71
|
+
changed = true;
|
|
72
|
+
}
|
|
73
|
+
if (changed) {
|
|
74
|
+
fs.writeFileSync(p, JSON.stringify(s, null, 2) + '\n');
|
|
75
|
+
console.log('Reduced context injection: observations=10, sessions=3');
|
|
76
|
+
} else {
|
|
77
|
+
console.log('Context injection already optimal — skipped');
|
|
78
|
+
}
|
|
79
|
+
"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## M3: Switch Provider to OpenRouter
|
|
85
|
+
|
|
86
|
+
Target: `~/.claude-mem/settings.json`
|
|
87
|
+
|
|
88
|
+
Pre-check: verify OpenRouter API key exists. If missing, warn and skip.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
node -e "
|
|
92
|
+
const fs = require('fs');
|
|
93
|
+
const p = require('os').homedir() + '/.claude-mem/settings.json';
|
|
94
|
+
const s = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
95
|
+
if (s.CLAUDE_MEM_PROVIDER === 'claude') {
|
|
96
|
+
if (!s.CLAUDE_MEM_OPENROUTER_API_KEY && !process.env.OPENROUTER_API_KEY) {
|
|
97
|
+
console.log('SKIP: No OpenRouter API key configured');
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
s.CLAUDE_MEM_PROVIDER = 'openrouter';
|
|
101
|
+
fs.writeFileSync(p, JSON.stringify(s, null, 2) + '\n');
|
|
102
|
+
console.log('Switched provider to openrouter');
|
|
103
|
+
} else {
|
|
104
|
+
console.log('Provider already ' + (s.CLAUDE_MEM_PROVIDER || 'unset') + ' — skipped');
|
|
105
|
+
}
|
|
106
|
+
"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Plugin Role Separation Content
|
|
112
|
+
|
|
113
|
+
Inject after `## Memory` in the project CLAUDE.md when both claude-mem AND
|
|
114
|
+
oh-my-claudecode are enabled. Skip if `### Plugin Role Separation` already
|
|
115
|
+
exists (idempotent).
|
|
116
|
+
|
|
117
|
+
```markdown
|
|
118
|
+
### Plugin Role Separation (claude-mem + OMC)
|
|
119
|
+
|
|
120
|
+
- **claude-mem** = passive memory layer (auto-capture, recall, search). Let it run silently.
|
|
121
|
+
- **OMC project memory** = active, curated checkpoints and project status.
|
|
122
|
+
- **OMC wiki** = architectural decisions, bug solutions, patterns.
|
|
123
|
+
- Searching past work → OMC project memory first, then claude-mem for older/cross-project.
|
|
124
|
+
- Code navigation → OMC LSP tools. Planning → OMC /plan or /autopilot.
|
|
125
|
+
```
|
|
@@ -26,6 +26,14 @@ Present this summary after verification completes.
|
|
|
26
26
|
│ {✅} tools/memory/ Legacy memory scripts
|
|
27
27
|
│ {✅} memory/ Legacy memory directory
|
|
28
28
|
│
|
|
29
|
+
│ 🔌 Plugin Tuning (only if Phase 7 ran)
|
|
30
|
+
│ {✅|⏭️} Hookify: {disabled / already disabled / not installed / skipped / N/A}
|
|
31
|
+
│ {✅|⏭️} claude-mem SKIP_TOOLS: {optimised / already optimal / not installed / skipped / N/A}
|
|
32
|
+
│ {✅|⏭️} claude-mem context: {reduced / already optimal / not installed / skipped / N/A}
|
|
33
|
+
│ {✅|⏭️} claude-mem provider: {switched / already optimal / not installed / skipped / N/A}
|
|
34
|
+
│ {⏭️} oh-my-claudecode: {installed / not installed}
|
|
35
|
+
│ {✅|⏭️} Plugin role separation: {injected / already present / not applicable / skipped}
|
|
36
|
+
│
|
|
29
37
|
│ ⚠️ Notes
|
|
30
38
|
│ {any warnings or skipped items}
|
|
31
39
|
│
|
|
@@ -33,6 +41,7 @@ Present this summary after verification completes.
|
|
|
33
41
|
│ 1. Review CLAUDE.md and customise for your project
|
|
34
42
|
│ 2. Add domain knowledge to context/
|
|
35
43
|
│ 3. Run /speccy to design your first feature
|
|
44
|
+
│ 4. Restart Claude Code to activate plugin changes (if any applied)
|
|
36
45
|
│
|
|
37
46
|
└─────────────────────────────────────────────────
|
|
38
47
|
```
|
package/skills/manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-
|
|
2
|
+
"generated": "2026-04-17T04:59:26.749Z",
|
|
3
3
|
"count": 10,
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
6
6
|
"name": "brace",
|
|
7
7
|
"directory": "brace",
|
|
8
8
|
"description": "'Initialize any project directory with a standard scaffold for AI-assisted development. Creates specs/ and context/ directories, a project CLAUDE.md with development workflow and guardrails, .gitignore, and branch protection. Recommends claude-mem for persistent memory. Idempotent — safe to run on existing projects. Triggers: \"init project\", \"setup brace\", \"brace\", \"initialize\", \"bootstrap\", \"scaffold\".'",
|
|
9
|
-
"lines":
|
|
9
|
+
"lines": 427,
|
|
10
10
|
"hasScripts": false,
|
|
11
11
|
"hasReferences": true,
|
|
12
12
|
"hasAssets": true,
|