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.
Files changed (45) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/README.md +9 -7
  4. package/bin/cli.js +53 -1
  5. package/package.json +2 -2
  6. package/templates/CLAUDE.md +35 -1
  7. package/templates/agents/cc-cli-integrator.md +5 -0
  8. package/templates/agents/cc-template-author.md +7 -0
  9. package/templates/agents/cc-test-maintainer.md +5 -0
  10. package/templates/agents/code-reviewer.md +11 -0
  11. package/templates/agents/constitution-guard.md +9 -0
  12. package/templates/agents/devops-engineer.md +9 -0
  13. package/templates/agents/loop-controller.md +7 -0
  14. package/templates/agents/milestone-builder.md +7 -0
  15. package/templates/agents/orchestrator-init.md +9 -1
  16. package/templates/agents/orchestrator.md +8 -0
  17. package/templates/agents/problem-architect.md +29 -1
  18. package/templates/agents/qa-engineer.md +9 -0
  19. package/templates/agents/security-auditor.md +9 -0
  20. package/templates/agents/spec-reviewer.md +9 -0
  21. package/templates/agents/test-writer.md +11 -0
  22. package/templates/capabilities/manifest.md +2 -0
  23. package/templates/capabilities/shared/context-inoculation.md +39 -0
  24. package/templates/capabilities/shared/reward-hack-detection.md +32 -0
  25. package/templates/commands/audit.md +8 -0
  26. package/templates/commands/ghost-test.md +99 -0
  27. package/templates/commands/inoculate.md +76 -0
  28. package/templates/commands/sentinel.md +3 -0
  29. package/templates/commands/ship.md +6 -0
  30. package/templates/commands/test.md +10 -0
  31. package/templates/hooks/post-tool-use.js +341 -277
  32. package/templates/hooks/pre-tool-use.js +344 -292
  33. package/templates/hooks/stop.js +198 -151
  34. package/templates/hooks/user-prompt.js +369 -163
  35. package/templates/scripts/statusline.sh +105 -0
  36. package/templates/skills/agent-creator/SKILL.md +11 -0
  37. package/templates/skills/architecture-advisor/SKILL.md +21 -16
  38. package/templates/skills/debate/SKILL.md +5 -0
  39. package/templates/skills/env-scanner/SKILL.md +5 -0
  40. package/templates/skills/frontend-design/SKILL.md +5 -0
  41. package/templates/skills/mcp/SKILL.md +3 -0
  42. package/templates/skills/security/SKILL.md +3 -0
  43. package/templates/skills/session-guard/SKILL.md +3 -0
  44. package/templates/skills/skill-creator/SKILL.md +12 -0
  45. package/templates/skills/test-first/SKILL.md +5 -0
@@ -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 security summary ──────────────────────────────────────────────────
114
- const seclogPath = path.join(os.tmpdir(), `.azclaude-seclog-${process.ppid || process.pid}`);
115
- if (fs.existsSync(seclogPath)) {
116
- try {
117
- const events = fs.readFileSync(seclogPath, 'utf8')
118
- .split('\n').filter(Boolean)
119
- .map(l => { try { return JSON.parse(l); } catch (_) { return null; } })
120
- .filter(Boolean);
121
- const blocks = events.filter(e => e.level === 'block');
122
- const warns = events.filter(e => e.level === 'warn');
123
- if (blocks.length > 0 || warns.length > 0) {
124
- const b = blocks.length, w = warns.length;
125
- process.stdout.write(`\n🔒 Security: ${b} block${b !== 1 ? 's' : ''}, ${w} warning${w !== 1 ? 's' : ''} this session\n`);
126
- blocks.forEach(e => process.stdout.write(` ✗ BLOCKED [${e.rule}] ${e.target || ''}\n`));
127
- const seen = new Set();
128
- warns.forEach(e => { if (!seen.has(e.rule)) { seen.add(e.rule); process.stdout.write(` ⚠ WARNED [${e.rule}]\n`); } });
129
- } else {
130
- process.stdout.write('\n🔒 Security: clean session — 0 events\n');
131
- }
132
- try { fs.unlinkSync(seclogPath); } catch (_) {} // cleanup
133
- } catch (_) {}
134
- }
135
-
136
- // ── Reset edit counter so checkpoint reminder starts fresh next session ───────
137
- const counterPath = path.join(os.tmpdir(), `.azclaude-edit-count-${process.ppid || process.pid}`);
138
- try { fs.writeFileSync(counterPath, '0'); } catch (_) {}
139
-
140
- // ── Warn if /persist was not run (only in AZCLAUDE projects with obs dir) ──
141
- const obsDir = path.join('ops', 'observations');
142
- if (!fs.existsSync(obsDir)) process.exit(0);
143
- const todayStamp = today.replace(/-/g, '');
144
- try {
145
- const existing = fs.readdirSync(obsDir).filter(f => f.startsWith(todayStamp) && f.endsWith('-friction.md'));
146
- if (existing.length === 0) {
147
- process.stdout.write('⚠ session state not persisted — run /persist before closing\n');
148
- }
149
- } catch (_) {
150
- // obs dir doesn't exist yet — that's fine
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
+ }