azclaude-copilot 0.4.39 → 0.5.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 +2 -2
- package/README.md +9 -7
- package/bin/cli.js +53 -1
- package/package.json +2 -2
- package/templates/CLAUDE.md +35 -1
- package/templates/agents/cc-cli-integrator.md +5 -0
- package/templates/agents/cc-template-author.md +7 -0
- package/templates/agents/cc-test-maintainer.md +5 -0
- package/templates/agents/code-reviewer.md +11 -0
- package/templates/agents/constitution-guard.md +9 -0
- package/templates/agents/devops-engineer.md +9 -0
- package/templates/agents/loop-controller.md +7 -0
- package/templates/agents/milestone-builder.md +7 -0
- package/templates/agents/orchestrator-init.md +9 -1
- package/templates/agents/orchestrator.md +8 -0
- package/templates/agents/problem-architect.md +29 -1
- package/templates/agents/qa-engineer.md +9 -0
- package/templates/agents/security-auditor.md +9 -0
- package/templates/agents/spec-reviewer.md +9 -0
- package/templates/agents/test-writer.md +11 -0
- package/templates/capabilities/manifest.md +2 -0
- package/templates/capabilities/shared/context-inoculation.md +39 -0
- package/templates/capabilities/shared/reward-hack-detection.md +32 -0
- package/templates/commands/audit.md +8 -0
- package/templates/commands/ghost-test.md +99 -0
- package/templates/commands/inoculate.md +76 -0
- package/templates/commands/sentinel.md +3 -0
- package/templates/commands/ship.md +6 -0
- package/templates/commands/test.md +10 -0
- package/templates/hooks/post-tool-use.js +341 -277
- package/templates/hooks/pre-tool-use.js +344 -292
- package/templates/hooks/stop.js +198 -151
- package/templates/hooks/user-prompt.js +369 -163
- package/templates/scripts/statusline.sh +105 -0
- package/templates/skills/agent-creator/SKILL.md +11 -0
- package/templates/skills/architecture-advisor/SKILL.md +21 -16
- package/templates/skills/debate/SKILL.md +5 -0
- package/templates/skills/env-scanner/SKILL.md +5 -0
- package/templates/skills/frontend-design/SKILL.md +5 -0
- package/templates/skills/mcp/SKILL.md +3 -0
- package/templates/skills/security/SKILL.md +3 -0
- package/templates/skills/session-guard/SKILL.md +3 -0
- package/templates/skills/skill-creator/SKILL.md +12 -0
- package/templates/skills/test-first/SKILL.md +5 -0
package/templates/hooks/stop.js
CHANGED
|
@@ -1,151 +1,198 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
/**
|
|
4
|
-
* AZCLAUDE — Stop hook
|
|
5
|
-
* Runs at end of every session.
|
|
6
|
-
* 1. Migrates "In progress" entries → "Done this session"
|
|
7
|
-
* 2. Stamps goals.md with today's date
|
|
8
|
-
* 3. Writes friction stub if /persist was not run
|
|
9
|
-
* Works on: Windows (PowerShell/CMD/Git Bash), macOS, Linux.
|
|
10
|
-
*/
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
const os = require('os');
|
|
14
|
-
|
|
15
|
-
// ── Hook profile gate ───────────────────────────────────────────────────────
|
|
16
|
-
// AZCLAUDE_HOOK_PROFILE=minimal|standard|strict (default: standard)
|
|
17
|
-
const HOOK_PROFILE = process.env.AZCLAUDE_HOOK_PROFILE || 'standard';
|
|
18
|
-
|
|
19
|
-
const cfg = process.env.AZCLAUDE_CFG || '.claude';
|
|
20
|
-
// Guard: cfg must resolve inside the project root
|
|
21
|
-
if (path.resolve(cfg).indexOf(process.cwd()) !== 0) process.exit(0);
|
|
22
|
-
const goalsPath = path.join(cfg, 'memory', 'goals.md');
|
|
23
|
-
|
|
24
|
-
if (!fs.existsSync(goalsPath)) process.exit(0);
|
|
25
|
-
|
|
26
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
27
|
-
let content = fs.readFileSync(goalsPath, 'utf8');
|
|
28
|
-
|
|
29
|
-
// ── Migrate "In progress" → "Done this session" ──────────────────────────────
|
|
30
|
-
const IN_PROGRESS = '## In progress';
|
|
31
|
-
const DONE = '## Done this session';
|
|
32
|
-
|
|
33
|
-
if (content.includes(IN_PROGRESS)) {
|
|
34
|
-
const lines = content.split('\n');
|
|
35
|
-
const ipIdx = lines.findIndex(l => l.trim() === IN_PROGRESS);
|
|
36
|
-
const doneIdx = lines.findIndex(l => l.trim() === DONE);
|
|
37
|
-
|
|
38
|
-
// Collect entries under ## In progress (lines starting with "- " until next ##)
|
|
39
|
-
const ipEntries = [];
|
|
40
|
-
for (let i = ipIdx + 1; i < lines.length; i++) {
|
|
41
|
-
if (lines[i].startsWith('## ')) break;
|
|
42
|
-
if (lines[i].startsWith('- ')) ipEntries.push(lines[i]);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (ipEntries.length > 0) {
|
|
46
|
-
// Remove ## In progress section entirely
|
|
47
|
-
const withoutIP = [];
|
|
48
|
-
let skip = false;
|
|
49
|
-
for (const line of lines) {
|
|
50
|
-
if (line.trim() === IN_PROGRESS) { skip = true; continue; }
|
|
51
|
-
if (skip && line.startsWith('## ')) skip = false;
|
|
52
|
-
if (!skip) withoutIP.push(line);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Add entries to ## Done this session
|
|
56
|
-
const dIdx = withoutIP.findIndex(l => l.trim() === DONE);
|
|
57
|
-
if (dIdx !== -1) {
|
|
58
|
-
withoutIP.splice(dIdx + 1, 0, ...ipEntries);
|
|
59
|
-
} else {
|
|
60
|
-
// No Done section — create one
|
|
61
|
-
withoutIP.push('', DONE, ...ipEntries);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
content = withoutIP.join('\n');
|
|
65
|
-
} else {
|
|
66
|
-
// Empty In progress — just remove the heading + any trailing blank lines
|
|
67
|
-
content = content.replace(new RegExp('\\n' + IN_PROGRESS + '\\n(\\n)*', 'g'), '\n');
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ── Trim "Done this session" to max 20 entries (overflow → archive) ──────────
|
|
72
|
-
const DONE_KEEP = 20;
|
|
73
|
-
const trimLines = content.split('\n');
|
|
74
|
-
const dTrimIdx = trimLines.findIndex(l => l.trim() === DONE);
|
|
75
|
-
if (dTrimIdx !== -1) {
|
|
76
|
-
const doneEntries = [];
|
|
77
|
-
for (let i = dTrimIdx + 1; i < trimLines.length; i++) {
|
|
78
|
-
if (trimLines[i].startsWith('## ')) break;
|
|
79
|
-
if (trimLines[i].startsWith('- ')) doneEntries.push({ line: trimLines[i], idx: i });
|
|
80
|
-
}
|
|
81
|
-
if (doneEntries.length > DONE_KEEP) {
|
|
82
|
-
const toArchive = doneEntries.slice(DONE_KEEP);
|
|
83
|
-
const archivePath = path.join(cfg, 'memory', 'sessions', `${today}-edits.md`);
|
|
84
|
-
try { fs.mkdirSync(path.join(cfg, 'memory', 'sessions'), { recursive: true }); } catch (_) {}
|
|
85
|
-
const header = `\n<!-- archived: ${today} source: stop -->\n`;
|
|
86
|
-
const payload = toArchive.map(e => e.line).join('\n') + '\n';
|
|
87
|
-
try { fs.appendFileSync(archivePath, header + payload); } catch (_) {}
|
|
88
|
-
const archivedSet = new Set(toArchive.map(e => e.idx));
|
|
89
|
-
content = trimLines.filter((_, i) => !archivedSet.has(i)).join('\n');
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── Stamp today's date ────────────────────────────────────────────────────────
|
|
94
|
-
content = content.replace(/^Updated: .*/m, `Updated: ${today}`);
|
|
95
|
-
try { fs.writeFileSync(goalsPath, content); } catch (_) {}
|
|
96
|
-
|
|
97
|
-
// ── Prune old checkpoints — keep 5 most recent, delete the rest ──────────────
|
|
98
|
-
// Older checkpoints are superseded by goals.md "Current threads" entries.
|
|
99
|
-
const checkpointDir = path.join(cfg, 'memory', 'checkpoints');
|
|
100
|
-
if (fs.existsSync(checkpointDir)) {
|
|
101
|
-
try {
|
|
102
|
-
const cpFiles = fs.readdirSync(checkpointDir)
|
|
103
|
-
.filter(f => f.endsWith('.md'))
|
|
104
|
-
.sort()
|
|
105
|
-
.reverse(); // newest first (YYYY-MM-DD-HH-MM.md sorts correctly)
|
|
106
|
-
const MAX_CHECKPOINTS = 5;
|
|
107
|
-
for (const f of cpFiles.slice(MAX_CHECKPOINTS)) {
|
|
108
|
-
try { fs.unlinkSync(path.join(checkpointDir, f)); } catch (_) {}
|
|
109
|
-
}
|
|
110
|
-
} catch (_) {}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ── Session
|
|
114
|
-
const
|
|
115
|
-
if (fs.existsSync(
|
|
116
|
-
try {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* AZCLAUDE — Stop hook
|
|
5
|
+
* Runs at end of every session.
|
|
6
|
+
* 1. Migrates "In progress" entries → "Done this session"
|
|
7
|
+
* 2. Stamps goals.md with today's date
|
|
8
|
+
* 3. Writes friction stub if /persist was not run
|
|
9
|
+
* Works on: Windows (PowerShell/CMD/Git Bash), macOS, Linux.
|
|
10
|
+
*/
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
// ── Hook profile gate ───────────────────────────────────────────────────────
|
|
16
|
+
// AZCLAUDE_HOOK_PROFILE=minimal|standard|strict (default: standard)
|
|
17
|
+
const HOOK_PROFILE = process.env.AZCLAUDE_HOOK_PROFILE || 'standard';
|
|
18
|
+
|
|
19
|
+
const cfg = process.env.AZCLAUDE_CFG || '.claude';
|
|
20
|
+
// Guard: cfg must resolve inside the project root
|
|
21
|
+
if (path.resolve(cfg).indexOf(process.cwd()) !== 0) process.exit(0);
|
|
22
|
+
const goalsPath = path.join(cfg, 'memory', 'goals.md');
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(goalsPath)) process.exit(0);
|
|
25
|
+
|
|
26
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
27
|
+
let content = fs.readFileSync(goalsPath, 'utf8');
|
|
28
|
+
|
|
29
|
+
// ── Migrate "In progress" → "Done this session" ──────────────────────────────
|
|
30
|
+
const IN_PROGRESS = '## In progress';
|
|
31
|
+
const DONE = '## Done this session';
|
|
32
|
+
|
|
33
|
+
if (content.includes(IN_PROGRESS)) {
|
|
34
|
+
const lines = content.split('\n');
|
|
35
|
+
const ipIdx = lines.findIndex(l => l.trim() === IN_PROGRESS);
|
|
36
|
+
const doneIdx = lines.findIndex(l => l.trim() === DONE);
|
|
37
|
+
|
|
38
|
+
// Collect entries under ## In progress (lines starting with "- " until next ##)
|
|
39
|
+
const ipEntries = [];
|
|
40
|
+
for (let i = ipIdx + 1; i < lines.length; i++) {
|
|
41
|
+
if (lines[i].startsWith('## ')) break;
|
|
42
|
+
if (lines[i].startsWith('- ')) ipEntries.push(lines[i]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (ipEntries.length > 0) {
|
|
46
|
+
// Remove ## In progress section entirely
|
|
47
|
+
const withoutIP = [];
|
|
48
|
+
let skip = false;
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (line.trim() === IN_PROGRESS) { skip = true; continue; }
|
|
51
|
+
if (skip && line.startsWith('## ')) skip = false;
|
|
52
|
+
if (!skip) withoutIP.push(line);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add entries to ## Done this session
|
|
56
|
+
const dIdx = withoutIP.findIndex(l => l.trim() === DONE);
|
|
57
|
+
if (dIdx !== -1) {
|
|
58
|
+
withoutIP.splice(dIdx + 1, 0, ...ipEntries);
|
|
59
|
+
} else {
|
|
60
|
+
// No Done section — create one
|
|
61
|
+
withoutIP.push('', DONE, ...ipEntries);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
content = withoutIP.join('\n');
|
|
65
|
+
} else {
|
|
66
|
+
// Empty In progress — just remove the heading + any trailing blank lines
|
|
67
|
+
content = content.replace(new RegExp('\\n' + IN_PROGRESS + '\\n(\\n)*', 'g'), '\n');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Trim "Done this session" to max 20 entries (overflow → archive) ──────────
|
|
72
|
+
const DONE_KEEP = 20;
|
|
73
|
+
const trimLines = content.split('\n');
|
|
74
|
+
const dTrimIdx = trimLines.findIndex(l => l.trim() === DONE);
|
|
75
|
+
if (dTrimIdx !== -1) {
|
|
76
|
+
const doneEntries = [];
|
|
77
|
+
for (let i = dTrimIdx + 1; i < trimLines.length; i++) {
|
|
78
|
+
if (trimLines[i].startsWith('## ')) break;
|
|
79
|
+
if (trimLines[i].startsWith('- ')) doneEntries.push({ line: trimLines[i], idx: i });
|
|
80
|
+
}
|
|
81
|
+
if (doneEntries.length > DONE_KEEP) {
|
|
82
|
+
const toArchive = doneEntries.slice(DONE_KEEP);
|
|
83
|
+
const archivePath = path.join(cfg, 'memory', 'sessions', `${today}-edits.md`);
|
|
84
|
+
try { fs.mkdirSync(path.join(cfg, 'memory', 'sessions'), { recursive: true }); } catch (_) {}
|
|
85
|
+
const header = `\n<!-- archived: ${today} source: stop -->\n`;
|
|
86
|
+
const payload = toArchive.map(e => e.line).join('\n') + '\n';
|
|
87
|
+
try { fs.appendFileSync(archivePath, header + payload); } catch (_) {}
|
|
88
|
+
const archivedSet = new Set(toArchive.map(e => e.idx));
|
|
89
|
+
content = trimLines.filter((_, i) => !archivedSet.has(i)).join('\n');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Stamp today's date ────────────────────────────────────────────────────────
|
|
94
|
+
content = content.replace(/^Updated: .*/m, `Updated: ${today}`);
|
|
95
|
+
try { fs.writeFileSync(goalsPath, content); } catch (_) {}
|
|
96
|
+
|
|
97
|
+
// ── Prune old checkpoints — keep 5 most recent, delete the rest ──────────────
|
|
98
|
+
// Older checkpoints are superseded by goals.md "Current threads" entries.
|
|
99
|
+
const checkpointDir = path.join(cfg, 'memory', 'checkpoints');
|
|
100
|
+
if (fs.existsSync(checkpointDir)) {
|
|
101
|
+
try {
|
|
102
|
+
const cpFiles = fs.readdirSync(checkpointDir)
|
|
103
|
+
.filter(f => f.endsWith('.md'))
|
|
104
|
+
.sort()
|
|
105
|
+
.reverse(); // newest first (YYYY-MM-DD-HH-MM.md sorts correctly)
|
|
106
|
+
const MAX_CHECKPOINTS = 5;
|
|
107
|
+
for (const f of cpFiles.slice(MAX_CHECKPOINTS)) {
|
|
108
|
+
try { fs.unlinkSync(path.join(checkpointDir, f)); } catch (_) {}
|
|
109
|
+
}
|
|
110
|
+
} catch (_) {}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Session duration ────────────────────────────────────────────────────────
|
|
114
|
+
const sessionStartPath = path.join(os.tmpdir(), `.azclaude-session-start-${process.ppid || process.pid}`);
|
|
115
|
+
if (fs.existsSync(sessionStartPath)) {
|
|
116
|
+
try {
|
|
117
|
+
const startIso = fs.readFileSync(sessionStartPath, 'utf8').trim();
|
|
118
|
+
const startMs = new Date(startIso).getTime();
|
|
119
|
+
const durationMs = Date.now() - startMs;
|
|
120
|
+
const mins = Math.round(durationMs / 60000);
|
|
121
|
+
const hours = Math.floor(mins / 60);
|
|
122
|
+
const remMins = mins % 60;
|
|
123
|
+
const durationStr = hours > 0 ? `${hours}h ${remMins}m` : `${mins}m`;
|
|
124
|
+
process.stdout.write(`\nSession duration: ${durationStr}\n`);
|
|
125
|
+
} catch (_) {}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Tool-use summary ────────────────────────────────────────────────────────
|
|
129
|
+
const obsPath = path.join(cfg, 'memory', 'reflexes', 'observations.jsonl');
|
|
130
|
+
if (fs.existsSync(obsPath)) {
|
|
131
|
+
try {
|
|
132
|
+
const sid = process.ppid || process.pid;
|
|
133
|
+
const obsLines = fs.readFileSync(obsPath, 'utf8').split('\n').filter(Boolean);
|
|
134
|
+
const counts = {};
|
|
135
|
+
for (const line of obsLines) {
|
|
136
|
+
try {
|
|
137
|
+
const o = JSON.parse(line);
|
|
138
|
+
if (o.session == sid && o.tool) {
|
|
139
|
+
counts[o.tool] = (counts[o.tool] || 0) + 1;
|
|
140
|
+
}
|
|
141
|
+
} catch (_) {}
|
|
142
|
+
}
|
|
143
|
+
const parts = Object.entries(counts)
|
|
144
|
+
.sort((a, b) => b[1] - a[1])
|
|
145
|
+
.map(([t, n]) => `${n} ${t}`)
|
|
146
|
+
.slice(0, 6);
|
|
147
|
+
if (parts.length > 0) {
|
|
148
|
+
process.stdout.write(`Tools: ${parts.join(', ')}\n`);
|
|
149
|
+
}
|
|
150
|
+
} catch (_) {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Session security summary ──────────────────────────────────────────────────
|
|
154
|
+
const seclogPath = path.join(os.tmpdir(), `.azclaude-seclog-${process.ppid || process.pid}`);
|
|
155
|
+
if (fs.existsSync(seclogPath)) {
|
|
156
|
+
try {
|
|
157
|
+
const events = fs.readFileSync(seclogPath, 'utf8')
|
|
158
|
+
.split('\n').filter(Boolean)
|
|
159
|
+
.map(l => { try { return JSON.parse(l); } catch (_) { return null; } })
|
|
160
|
+
.filter(Boolean);
|
|
161
|
+
const blocks = events.filter(e => e.level === 'block');
|
|
162
|
+
const warns = events.filter(e => e.level === 'warn');
|
|
163
|
+
if (blocks.length > 0 || warns.length > 0) {
|
|
164
|
+
const b = blocks.length, w = warns.length;
|
|
165
|
+
process.stdout.write(`\n🔒 Security: ${b} block${b !== 1 ? 's' : ''}, ${w} warning${w !== 1 ? 's' : ''} this session\n`);
|
|
166
|
+
blocks.forEach(e => process.stdout.write(` ✗ BLOCKED [${e.rule}] ${e.target || ''}\n`));
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
warns.forEach(e => { if (!seen.has(e.rule)) { seen.add(e.rule); process.stdout.write(` ⚠ WARNED [${e.rule}]\n`); } });
|
|
169
|
+
} else {
|
|
170
|
+
process.stdout.write('\n🔒 Security: clean session — 0 events\n');
|
|
171
|
+
}
|
|
172
|
+
try { fs.unlinkSync(seclogPath); } catch (_) {} // cleanup
|
|
173
|
+
} catch (_) {}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Clean ALL session temp files ─────────────────────────────────────────────
|
|
177
|
+
const sid = process.ppid || process.pid;
|
|
178
|
+
const tempPatterns = [
|
|
179
|
+
`edit-count`, `sec`, `secseq`, `seclog`, `seq`, `rapid`, `diff`, `cleanup-done`, `session-start`
|
|
180
|
+
];
|
|
181
|
+
for (const pat of tempPatterns) {
|
|
182
|
+
const fp = path.join(os.tmpdir(), `.azclaude-${pat}-${sid}`);
|
|
183
|
+
try { fs.unlinkSync(fp); } catch (_) {}
|
|
184
|
+
}
|
|
185
|
+
// Also clean the session marker from user-prompt.js
|
|
186
|
+
try { fs.unlinkSync(path.join(os.tmpdir(), `.azclaude-session-${sid}`)); } catch (_) {}
|
|
187
|
+
|
|
188
|
+
// ── Warn if /persist was not run (only in AZCLAUDE projects with obs dir) ──
|
|
189
|
+
const obsDir = path.join(cfg, 'memory', 'sessions');
|
|
190
|
+
if (!fs.existsSync(obsDir)) process.exit(0);
|
|
191
|
+
try {
|
|
192
|
+
const existing = fs.readdirSync(obsDir).filter(f => f.startsWith(today) && f.endsWith('-edits.md'));
|
|
193
|
+
if (existing.length === 0) {
|
|
194
|
+
process.stdout.write('⚠ session state not persisted — run /persist before closing\n');
|
|
195
|
+
}
|
|
196
|
+
} catch (_) {
|
|
197
|
+
// obs dir doesn't exist yet — that's fine
|
|
198
|
+
}
|