toga-ai 1.0.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/settings.json +119 -0
- package/.claude-plugin/marketplace.json +87 -0
- package/.claude-plugin/plugin.json +22 -0
- package/CLAUDE.md +161 -0
- package/README.md +72 -0
- package/agents/framework-pattern-checker.md +67 -0
- package/agents/harness-optimizer.md +102 -0
- package/agents/knowledge-writer.md +62 -0
- package/agents/php-build-resolver.md +51 -0
- package/agents/php-reviewer.md +51 -0
- package/agents/planner.md +88 -0
- package/agents/session-capture.md +101 -0
- package/agents/sql-reviewer.md +67 -0
- package/contexts/dev.md +43 -0
- package/contexts/research.md +49 -0
- package/contexts/review.md +37 -0
- package/knowledge/1.0/apps/library/INDEX.md +5 -0
- package/knowledge/1.0/apps/library/architecture.md +105 -0
- package/knowledge/1.0/apps/worker/INDEX.md +5 -0
- package/knowledge/1.0/apps/worker/architecture.md +223 -0
- package/knowledge/1.0/standards/backend-php.md +450 -0
- package/knowledge/2.0/apps/_underscore/INDEX.md +6 -0
- package/knowledge/2.0/apps/_underscore/architecture.md +183 -0
- package/knowledge/2.0/apps/_underscore/features/recursive-item-fulfillments.md +111 -0
- package/knowledge/2.0/apps/api2/INDEX.md +5 -0
- package/knowledge/2.0/apps/api2/architecture.md +162 -0
- package/knowledge/2.0/apps/worker2/INDEX.md +6 -0
- package/knowledge/2.0/apps/worker2/architecture.md +127 -0
- package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +135 -0
- package/knowledge/2.0/standards/backend-php.md +710 -0
- package/knowledge/CONVENTIONS.md +117 -0
- package/knowledge/INDEX.md +19 -0
- package/knowledge/clients/.gitkeep +0 -0
- package/knowledge/registry.json +7 -0
- package/knowledge.js +384 -0
- package/mcp-configs/README.md +72 -0
- package/mcp-configs/mcp-servers.json +23 -0
- package/package.json +50 -0
- package/rules/README.md +53 -0
- package/rules/common/coding-style.md +123 -0
- package/rules/common/git-workflow.md +72 -0
- package/rules/common/security.md +118 -0
- package/rules/common/testing.md +74 -0
- package/rules/php/app-framework.md +104 -0
- package/rules/php/underscore-framework.md +111 -0
- package/scripts/harness.js +605 -0
- package/scripts/hooks/evaluate-session.js +55 -0
- package/scripts/hooks/post-edit-validate.js +102 -0
- package/scripts/hooks/session-end.js +13 -0
- package/scripts/hooks/session-start.js +57 -0
- package/scripts/install.js +611 -0
- package/scripts/pre-commit +46 -0
- package/skills/capture/SKILL.md +294 -0
- package/skills/code-review/SKILL.md +140 -0
- package/skills/create-elastic-beanstalk/SKILL.md +217 -0
- package/skills/harness-audit/SKILL.md +152 -0
- package/skills/kickoff/SKILL.md +151 -0
- package/skills/php-patterns/SKILL.md +296 -0
- package/skills/session-resume/SKILL.md +156 -0
- package/skills/session-save/SKILL.md +158 -0
- package/skills/sync-team-skills/SKILL.md +87 -0
- package/sync-skills.js +71 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* toga-harness — harness utility CLI for the TOGA Technology Claude knowledge repo.
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* audit Validate knowledge base, check completeness, output score 0-100
|
|
8
|
+
* sync [path] Sync skills into a project's .claude/skills/
|
|
9
|
+
* status Show knowledge base statistics and last git commit info
|
|
10
|
+
* new-repo Interactive wizard to onboard a new repo into registry.json
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node scripts/harness.js <command> [args]
|
|
14
|
+
* toga-harness <command> [args] (via npm bin)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { execSync, spawnSync } = require('child_process');
|
|
22
|
+
const readline = require('readline');
|
|
23
|
+
|
|
24
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
25
|
+
const KNOWLEDGE_DIR = path.join(REPO_ROOT, 'knowledge');
|
|
26
|
+
const REGISTRY_FILE = path.join(KNOWLEDGE_DIR, 'registry.json');
|
|
27
|
+
const FRAMEWORKS = ['1.0', '2.0'];
|
|
28
|
+
const DOC_TYPES = ['feature', 'client-feature', 'workflow', 'architecture', 'standard'];
|
|
29
|
+
|
|
30
|
+
/* ------------------------------------------------------------------ */
|
|
31
|
+
/* shared utilities */
|
|
32
|
+
/* ------------------------------------------------------------------ */
|
|
33
|
+
|
|
34
|
+
function loadRegistry() {
|
|
35
|
+
if (!fs.existsSync(REGISTRY_FILE)) return [];
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf8'));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error('ERROR: registry.json is not valid JSON: ' + e.message);
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function saveRegistry(registry) {
|
|
45
|
+
fs.writeFileSync(REGISTRY_FILE, JSON.stringify(registry, null, 2) + '\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function walkMd(dir) {
|
|
49
|
+
const results = [];
|
|
50
|
+
if (!fs.existsSync(dir)) return results;
|
|
51
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
52
|
+
const full = path.join(dir, entry.name);
|
|
53
|
+
if (entry.isDirectory()) results.push(...walkMd(full));
|
|
54
|
+
else if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'INDEX.md') {
|
|
55
|
+
results.push(full);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseFrontmatter(content) {
|
|
62
|
+
if (!content.startsWith('---')) return { data: {}, body: content };
|
|
63
|
+
const end = content.indexOf('\n---', 3);
|
|
64
|
+
if (end === -1) return { data: {}, body: content };
|
|
65
|
+
const raw = content.slice(3, end).replace(/^\n/, '');
|
|
66
|
+
const body = content.slice(end + 4).replace(/^\r?\n/, '');
|
|
67
|
+
const data = {};
|
|
68
|
+
const lines = raw.split(/\r?\n/);
|
|
69
|
+
let i = 0;
|
|
70
|
+
while (i < lines.length) {
|
|
71
|
+
const line = lines[i];
|
|
72
|
+
if (!line.trim()) { i++; continue; }
|
|
73
|
+
const m = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
74
|
+
if (!m) { i++; continue; }
|
|
75
|
+
const key = m[1];
|
|
76
|
+
let val = m[2].trim();
|
|
77
|
+
if (val === '') {
|
|
78
|
+
const arr = [];
|
|
79
|
+
let j = i + 1;
|
|
80
|
+
while (j < lines.length && /^\s*-\s+/.test(lines[j])) {
|
|
81
|
+
arr.push(lines[j].replace(/^\s*-\s+/, '').trim().replace(/^["']|["']$/g, ''));
|
|
82
|
+
j++;
|
|
83
|
+
}
|
|
84
|
+
data[key] = arr;
|
|
85
|
+
i = j;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (val.startsWith('[') && val.endsWith(']')) {
|
|
89
|
+
const inner = val.slice(1, -1).trim();
|
|
90
|
+
data[key] = inner === '' ? [] : inner.split(',').map((s) => s.trim().replace(/^["']|["']$/g, ''));
|
|
91
|
+
} else {
|
|
92
|
+
data[key] = val.replace(/^["']|["']$/g, '');
|
|
93
|
+
}
|
|
94
|
+
i++;
|
|
95
|
+
}
|
|
96
|
+
return { data, body };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function allDocs() {
|
|
100
|
+
const dirs = FRAMEWORKS.map((fw) => path.join(KNOWLEDGE_DIR, fw))
|
|
101
|
+
.concat([path.join(KNOWLEDGE_DIR, 'clients')]);
|
|
102
|
+
return dirs.flatMap(walkMd).map((file) => {
|
|
103
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
104
|
+
const { data, body } = parseFrontmatter(content);
|
|
105
|
+
const rel = path.relative(KNOWLEDGE_DIR, file).split(path.sep).join('/');
|
|
106
|
+
return { file, rel, data, body };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function gitLastCommitDate(targetPath) {
|
|
111
|
+
try {
|
|
112
|
+
const result = spawnSync('git', [
|
|
113
|
+
'-C', REPO_ROOT, 'log', '-1', '--format=%ci', '--', targetPath
|
|
114
|
+
], { encoding: 'utf8' });
|
|
115
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
116
|
+
return result.stdout.trim().split(' ')[0]; // YYYY-MM-DD
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function runValidate() {
|
|
125
|
+
const result = spawnSync('node', [path.join(REPO_ROOT, 'knowledge.js'), 'validate'], {
|
|
126
|
+
encoding: 'utf8',
|
|
127
|
+
cwd: REPO_ROOT
|
|
128
|
+
});
|
|
129
|
+
return {
|
|
130
|
+
stdout: result.stdout || '',
|
|
131
|
+
stderr: result.stderr || '',
|
|
132
|
+
exitCode: result.status || 0
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ------------------------------------------------------------------ */
|
|
137
|
+
/* command: audit */
|
|
138
|
+
/* ------------------------------------------------------------------ */
|
|
139
|
+
|
|
140
|
+
function cmdAudit() {
|
|
141
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
142
|
+
console.log('toga-harness audit');
|
|
143
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
144
|
+
console.log('');
|
|
145
|
+
|
|
146
|
+
const checks = []; // { label, pass, detail }
|
|
147
|
+
const warnings = []; // { label, detail }
|
|
148
|
+
|
|
149
|
+
// --- CHECK 1: validate ---
|
|
150
|
+
process.stdout.write('Running knowledge.js validate... ');
|
|
151
|
+
const validateResult = runValidate();
|
|
152
|
+
const validatePassed = validateResult.exitCode === 0 &&
|
|
153
|
+
!validateResult.stdout.includes('FAILED') &&
|
|
154
|
+
!validateResult.stderr.includes('ERROR');
|
|
155
|
+
const validateErrors = (validateResult.stdout + validateResult.stderr)
|
|
156
|
+
.split('\n')
|
|
157
|
+
.filter((l) => l.includes('ERROR'))
|
|
158
|
+
.length;
|
|
159
|
+
|
|
160
|
+
console.log(validatePassed ? '✓ PASS' : '✗ FAIL (' + validateErrors + ' errors)');
|
|
161
|
+
if (!validatePassed) {
|
|
162
|
+
const errorLines = (validateResult.stdout + validateResult.stderr)
|
|
163
|
+
.split('\n')
|
|
164
|
+
.filter((l) => l.trim())
|
|
165
|
+
.slice(0, 10)
|
|
166
|
+
.map((l) => ' ' + l)
|
|
167
|
+
.join('\n');
|
|
168
|
+
console.log(errorLines);
|
|
169
|
+
}
|
|
170
|
+
checks.push({
|
|
171
|
+
label: 'validate: knowledge base integrity',
|
|
172
|
+
pass: validatePassed,
|
|
173
|
+
detail: validatePassed ? 'No errors' : validateErrors + ' error(s) found'
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// --- CHECK 2: registry has entries ---
|
|
177
|
+
const registry = loadRegistry();
|
|
178
|
+
const registryOk = registry.length > 0;
|
|
179
|
+
checks.push({
|
|
180
|
+
label: 'registry.json: has entries',
|
|
181
|
+
pass: registryOk,
|
|
182
|
+
detail: registry.length + ' repo(s) registered'
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// --- CHECK 3: each registered repo has architecture.md ---
|
|
186
|
+
const missingArch = [];
|
|
187
|
+
for (const entry of registry) {
|
|
188
|
+
const archPath = path.join(KNOWLEDGE_DIR, entry.framework, 'apps', entry.repo, 'architecture.md');
|
|
189
|
+
if (!fs.existsSync(archPath)) {
|
|
190
|
+
missingArch.push(entry.framework + '/apps/' + entry.repo + '/architecture.md');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const archOk = missingArch.length === 0;
|
|
194
|
+
checks.push({
|
|
195
|
+
label: 'all registered repos have architecture.md',
|
|
196
|
+
pass: archOk,
|
|
197
|
+
detail: archOk ? 'All repos covered' : 'Missing: ' + missingArch.join(', ')
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// --- CHECK 4: INDEX.md files are not hand-edited ---
|
|
201
|
+
const handEditedIndexFiles = [];
|
|
202
|
+
for (const fw of FRAMEWORKS) {
|
|
203
|
+
const appsDir = path.join(KNOWLEDGE_DIR, fw, 'apps');
|
|
204
|
+
if (!fs.existsSync(appsDir)) continue;
|
|
205
|
+
for (const repo of fs.readdirSync(appsDir, { withFileTypes: true })
|
|
206
|
+
.filter((e) => e.isDirectory()).map((e) => e.name)) {
|
|
207
|
+
const indexPath = path.join(appsDir, repo, 'INDEX.md');
|
|
208
|
+
if (!fs.existsSync(indexPath)) continue;
|
|
209
|
+
const content = fs.readFileSync(indexPath, 'utf8');
|
|
210
|
+
// INDEX.md files generated by knowledge.js start with "# <repo>" and have no "Auto-generated" note
|
|
211
|
+
// We detect hand-edits if content doesn't start with the expected heading pattern
|
|
212
|
+
// and doesn't have the table structure we produce
|
|
213
|
+
if (!content.includes('| Doc |') && content.trim().length > 50) {
|
|
214
|
+
handEditedIndexFiles.push(fw + '/apps/' + repo + '/INDEX.md');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Also check master INDEX.md
|
|
219
|
+
const masterIndex = path.join(KNOWLEDGE_DIR, 'INDEX.md');
|
|
220
|
+
if (fs.existsSync(masterIndex)) {
|
|
221
|
+
const masterContent = fs.readFileSync(masterIndex, 'utf8');
|
|
222
|
+
if (!masterContent.includes('Auto-generated')) {
|
|
223
|
+
warnings.push({
|
|
224
|
+
label: 'knowledge/INDEX.md may be hand-edited',
|
|
225
|
+
detail: 'Expected "Auto-generated" notice not found'
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const indexOk = handEditedIndexFiles.length === 0;
|
|
230
|
+
checks.push({
|
|
231
|
+
label: 'INDEX.md files appear auto-generated (not hand-edited)',
|
|
232
|
+
pass: indexOk,
|
|
233
|
+
detail: indexOk ? 'All INDEX.md files look correct' : 'Possibly hand-edited: ' + handEditedIndexFiles.join(', ')
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// --- CHECK 5: clients/ directory exists ---
|
|
237
|
+
const clientsDir = path.join(KNOWLEDGE_DIR, 'clients');
|
|
238
|
+
const clientsOk = fs.existsSync(clientsDir);
|
|
239
|
+
checks.push({
|
|
240
|
+
label: 'clients/ directory exists',
|
|
241
|
+
pass: clientsOk,
|
|
242
|
+
detail: clientsOk ? 'clients/ found' : 'clients/ missing — client knowledge not started'
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// --- CHECK 6: 1.0/standards/ has at least one file ---
|
|
246
|
+
const standards10Dir = path.join(KNOWLEDGE_DIR, '1.0', 'standards');
|
|
247
|
+
const standards10Files = fs.existsSync(standards10Dir)
|
|
248
|
+
? fs.readdirSync(standards10Dir).filter((f) => f.endsWith('.md'))
|
|
249
|
+
: [];
|
|
250
|
+
const standards10Ok = standards10Files.length > 0;
|
|
251
|
+
checks.push({
|
|
252
|
+
label: '1.0/standards/ has at least one standard',
|
|
253
|
+
pass: standards10Ok,
|
|
254
|
+
detail: standards10Ok
|
|
255
|
+
? standards10Files.length + ' standard(s): ' + standards10Files.join(', ')
|
|
256
|
+
: '1.0/standards/ is empty or missing'
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// --- CHECK 7: 2.0/standards/ has at least one file ---
|
|
260
|
+
const standards20Dir = path.join(KNOWLEDGE_DIR, '2.0', 'standards');
|
|
261
|
+
const standards20Files = fs.existsSync(standards20Dir)
|
|
262
|
+
? fs.readdirSync(standards20Dir).filter((f) => f.endsWith('.md'))
|
|
263
|
+
: [];
|
|
264
|
+
const standards20Ok = standards20Files.length > 0;
|
|
265
|
+
checks.push({
|
|
266
|
+
label: '2.0/standards/ has at least one standard',
|
|
267
|
+
pass: standards20Ok,
|
|
268
|
+
detail: standards20Ok
|
|
269
|
+
? standards20Files.length + ' standard(s): ' + standards20Files.join(', ')
|
|
270
|
+
: '2.0/standards/ is empty or missing'
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// --- CHECK 8: all docs have required frontmatter ---
|
|
274
|
+
const docs = allDocs();
|
|
275
|
+
const missingFrontmatter = [];
|
|
276
|
+
for (const doc of docs) {
|
|
277
|
+
const required = ['title', 'framework', 'type', 'status'];
|
|
278
|
+
const missing = required.filter((f) => !doc.data[f]);
|
|
279
|
+
if (missing.length > 0) {
|
|
280
|
+
missingFrontmatter.push(doc.rel + ' (missing: ' + missing.join(', ') + ')');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const frontmatterOk = missingFrontmatter.length === 0;
|
|
284
|
+
checks.push({
|
|
285
|
+
label: 'all docs have required frontmatter',
|
|
286
|
+
pass: frontmatterOk,
|
|
287
|
+
detail: frontmatterOk
|
|
288
|
+
? docs.length + ' docs checked'
|
|
289
|
+
: missingFrontmatter.slice(0, 3).join('; ') + (missingFrontmatter.length > 3 ? ' ...' : '')
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// --- Scoring ---
|
|
293
|
+
console.log('');
|
|
294
|
+
console.log('Check results:');
|
|
295
|
+
const passed = checks.filter((c) => c.pass).length;
|
|
296
|
+
const total = checks.length;
|
|
297
|
+
for (const c of checks) {
|
|
298
|
+
const icon = c.pass ? ' ✓' : ' ✗';
|
|
299
|
+
console.log(icon + ' [' + (c.pass ? 'PASS' : 'FAIL') + '] ' + c.label);
|
|
300
|
+
if (!c.pass && c.detail) console.log(' → ' + c.detail);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (warnings.length > 0) {
|
|
304
|
+
console.log('');
|
|
305
|
+
console.log('Warnings:');
|
|
306
|
+
for (const w of warnings) {
|
|
307
|
+
console.log(' ⚠ ' + w.label);
|
|
308
|
+
if (w.detail) console.log(' → ' + w.detail);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const score = Math.round((passed / total) * 100);
|
|
313
|
+
console.log('');
|
|
314
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
315
|
+
console.log('Score: ' + score + '/100 (' + passed + '/' + total + ' checks passed)');
|
|
316
|
+
console.log('');
|
|
317
|
+
|
|
318
|
+
// Top 3 action items
|
|
319
|
+
const failing = checks.filter((c) => !c.pass);
|
|
320
|
+
if (failing.length > 0) {
|
|
321
|
+
console.log('Top action items:');
|
|
322
|
+
const top3 = failing.slice(0, 3);
|
|
323
|
+
top3.forEach((c, i) => {
|
|
324
|
+
console.log(' ' + (i + 1) + '. Fix: ' + c.label);
|
|
325
|
+
if (c.detail) console.log(' ' + c.detail);
|
|
326
|
+
});
|
|
327
|
+
} else {
|
|
328
|
+
console.log('✓ Knowledge base is healthy. No action items.');
|
|
329
|
+
}
|
|
330
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
331
|
+
|
|
332
|
+
process.exitCode = score < 60 ? 1 : 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* ------------------------------------------------------------------ */
|
|
336
|
+
/* command: sync */
|
|
337
|
+
/* ------------------------------------------------------------------ */
|
|
338
|
+
|
|
339
|
+
function cmdSync(args) {
|
|
340
|
+
const projectPath = args[0] || null;
|
|
341
|
+
const syncScript = path.join(REPO_ROOT, 'sync-skills.js');
|
|
342
|
+
if (!fs.existsSync(syncScript)) {
|
|
343
|
+
console.error('ERROR: sync-skills.js not found at ' + syncScript);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
const spawnArgs = [syncScript];
|
|
347
|
+
if (projectPath) spawnArgs.push(projectPath);
|
|
348
|
+
const result = spawnSync('node', spawnArgs, {
|
|
349
|
+
cwd: process.cwd(),
|
|
350
|
+
stdio: 'inherit',
|
|
351
|
+
encoding: 'utf8'
|
|
352
|
+
});
|
|
353
|
+
process.exitCode = result.status || 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* ------------------------------------------------------------------ */
|
|
357
|
+
/* command: status */
|
|
358
|
+
/* ------------------------------------------------------------------ */
|
|
359
|
+
|
|
360
|
+
function cmdStatus() {
|
|
361
|
+
const registry = loadRegistry();
|
|
362
|
+
const docs = allDocs();
|
|
363
|
+
|
|
364
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
365
|
+
console.log('toga-harness status — Knowledge Base Overview');
|
|
366
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
367
|
+
console.log('');
|
|
368
|
+
|
|
369
|
+
// Registered repos
|
|
370
|
+
console.log('Registered repos: ' + registry.length);
|
|
371
|
+
for (const e of registry) {
|
|
372
|
+
const docCount = docs.filter((d) => d.data.repo === e.repo && d.data.framework === e.framework).length;
|
|
373
|
+
const role = e.role === 'core' ? ' [core]' : '';
|
|
374
|
+
console.log(' ' + e.framework + '/' + e.repo + ' (' + e.project + ')' + role + ' — ' + docCount + ' doc(s)');
|
|
375
|
+
}
|
|
376
|
+
console.log('');
|
|
377
|
+
|
|
378
|
+
// Docs per framework
|
|
379
|
+
for (const fw of FRAMEWORKS) {
|
|
380
|
+
const fwDocs = docs.filter((d) => d.data.framework === fw);
|
|
381
|
+
const byType = {};
|
|
382
|
+
for (const d of fwDocs) {
|
|
383
|
+
const t = d.data.type || 'unknown';
|
|
384
|
+
byType[t] = (byType[t] || 0) + 1;
|
|
385
|
+
}
|
|
386
|
+
const typeStr = Object.entries(byType).map(([k, v]) => v + ' ' + k).join(', ') || 'none';
|
|
387
|
+
console.log('Framework ' + fw + ': ' + fwDocs.length + ' doc(s) (' + typeStr + ')');
|
|
388
|
+
}
|
|
389
|
+
console.log('');
|
|
390
|
+
|
|
391
|
+
// Docs per client
|
|
392
|
+
const clientsDir = path.join(KNOWLEDGE_DIR, 'clients');
|
|
393
|
+
if (fs.existsSync(clientsDir)) {
|
|
394
|
+
const clients = fs.readdirSync(clientsDir, { withFileTypes: true })
|
|
395
|
+
.filter((e) => e.isDirectory())
|
|
396
|
+
.map((e) => e.name);
|
|
397
|
+
if (clients.length === 0) {
|
|
398
|
+
console.log('Clients: none (clients/ exists but is empty)');
|
|
399
|
+
} else {
|
|
400
|
+
console.log('Clients: ' + clients.length);
|
|
401
|
+
for (const client of clients) {
|
|
402
|
+
const clientDir = path.join(clientsDir, client);
|
|
403
|
+
const clientDocs = walkMd(clientDir);
|
|
404
|
+
console.log(' ' + client + ': ' + clientDocs.length + ' doc(s)');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
console.log('Clients: clients/ directory not found');
|
|
409
|
+
}
|
|
410
|
+
console.log('');
|
|
411
|
+
|
|
412
|
+
// Total doc count
|
|
413
|
+
console.log('Total knowledge docs: ' + docs.length);
|
|
414
|
+
console.log('');
|
|
415
|
+
|
|
416
|
+
// Last git commit date on knowledge/ files
|
|
417
|
+
const lastCommit = gitLastCommitDate(KNOWLEDGE_DIR);
|
|
418
|
+
if (lastCommit) {
|
|
419
|
+
console.log('Last knowledge/ commit: ' + lastCommit);
|
|
420
|
+
} else {
|
|
421
|
+
console.log('Last knowledge/ commit: (git unavailable or no commits yet)');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Skills count
|
|
425
|
+
const skillsDir = path.join(REPO_ROOT, 'skills');
|
|
426
|
+
if (fs.existsSync(skillsDir)) {
|
|
427
|
+
const skillNames = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
428
|
+
.filter((e) => e.isDirectory())
|
|
429
|
+
.map((e) => e.name);
|
|
430
|
+
console.log('Skills available: ' + skillNames.length + ' (' + skillNames.join(', ') + ')');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* ------------------------------------------------------------------ */
|
|
437
|
+
/* command: new-repo (interactive wizard) */
|
|
438
|
+
/* ------------------------------------------------------------------ */
|
|
439
|
+
|
|
440
|
+
function prompt(rl, question) {
|
|
441
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function cmdNewRepo() {
|
|
445
|
+
const registry = loadRegistry();
|
|
446
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
447
|
+
|
|
448
|
+
console.log('');
|
|
449
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
450
|
+
console.log('New Repo Onboarding Wizard');
|
|
451
|
+
console.log('This will add a new entry to knowledge/registry.json');
|
|
452
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
453
|
+
console.log('');
|
|
454
|
+
|
|
455
|
+
let repo = '';
|
|
456
|
+
while (!repo) {
|
|
457
|
+
repo = (await prompt(rl, '1. Repo name (exact on-disk folder/repo name, e.g. worker2): ')).trim();
|
|
458
|
+
if (!repo) console.log(' Repo name is required.');
|
|
459
|
+
else if (registry.find((r) => r.repo === repo)) {
|
|
460
|
+
console.log(' WARNING: "' + repo + '" is already in registry.json.');
|
|
461
|
+
const confirm = await prompt(rl, ' Overwrite? (y/N): ');
|
|
462
|
+
if (confirm.trim().toLowerCase() !== 'y') {
|
|
463
|
+
repo = '';
|
|
464
|
+
console.log(' OK, enter a different name.');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
let framework = '';
|
|
470
|
+
while (!['1.0', '2.0'].includes(framework)) {
|
|
471
|
+
framework = (await prompt(rl, '2. Framework (1.0 or 2.0): ')).trim();
|
|
472
|
+
if (!['1.0', '2.0'].includes(framework)) {
|
|
473
|
+
console.log(' Must be exactly "1.0" or "2.0".');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
let project = '';
|
|
478
|
+
while (!project) {
|
|
479
|
+
project = (await prompt(rl, '3. Project name (human-readable, e.g. "Worker"): ')).trim();
|
|
480
|
+
if (!project) console.log(' Project name is required.');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
let role = '';
|
|
484
|
+
while (!['core', 'app'].includes(role)) {
|
|
485
|
+
role = (await prompt(rl, '4. Role ("core" for the framework base, "app" for everything else): ')).trim().toLowerCase();
|
|
486
|
+
if (!['core', 'app'].includes(role)) {
|
|
487
|
+
console.log(' Must be "core" or "app".');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const existingRepos = registry.map((r) => r.repo).filter((r) => r !== repo);
|
|
492
|
+
let dependsOn = [];
|
|
493
|
+
if (existingRepos.length > 0) {
|
|
494
|
+
console.log(' Known repos: ' + existingRepos.join(', '));
|
|
495
|
+
const depsInput = (await prompt(rl, '5. dependsOn (comma-separated repos beyond framework core, or leave blank): ')).trim();
|
|
496
|
+
if (depsInput) {
|
|
497
|
+
dependsOn = depsInput.split(',').map((s) => s.trim()).filter(Boolean);
|
|
498
|
+
const unknown = dependsOn.filter((d) => !existingRepos.includes(d));
|
|
499
|
+
if (unknown.length > 0) {
|
|
500
|
+
console.log(' WARNING: unknown repo(s) in dependsOn: ' + unknown.join(', '));
|
|
501
|
+
console.log(' These will be added anyway — register them separately if needed.');
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
console.log('5. dependsOn: no other repos registered yet, skipping.');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const localPath = (await prompt(rl, '6. Local path on this machine (e.g. C:\\WWW\\2.0\\worker2 or /home/user/projects/worker2): ')).trim();
|
|
509
|
+
|
|
510
|
+
rl.close();
|
|
511
|
+
|
|
512
|
+
// Build the new entry
|
|
513
|
+
const newEntry = { repo, project, framework, role, dependsOn };
|
|
514
|
+
|
|
515
|
+
// Confirm
|
|
516
|
+
console.log('');
|
|
517
|
+
console.log('New registry entry:');
|
|
518
|
+
console.log(JSON.stringify(newEntry, null, 2));
|
|
519
|
+
if (localPath) {
|
|
520
|
+
console.log('');
|
|
521
|
+
console.log('Local path (for Claude memory only — NOT committed): ' + localPath);
|
|
522
|
+
console.log('Add to Claude memory as: repo-path-' + repo + ' = ' + localPath);
|
|
523
|
+
}
|
|
524
|
+
console.log('');
|
|
525
|
+
|
|
526
|
+
// Use a synchronous readline for the final confirm since we closed async rl
|
|
527
|
+
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
528
|
+
await new Promise((resolve) => {
|
|
529
|
+
rl2.question('Append to registry.json? (Y/n): ', (answer) => {
|
|
530
|
+
rl2.close();
|
|
531
|
+
if (answer.trim().toLowerCase() === 'n') {
|
|
532
|
+
console.log('Aborted — registry not changed.');
|
|
533
|
+
resolve();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Remove existing entry for this repo if present
|
|
538
|
+
const filtered = registry.filter((r) => r.repo !== repo);
|
|
539
|
+
filtered.push(newEntry);
|
|
540
|
+
saveRegistry(filtered);
|
|
541
|
+
console.log('');
|
|
542
|
+
console.log('✓ registry.json updated with "' + repo + '"');
|
|
543
|
+
|
|
544
|
+
// Run validate
|
|
545
|
+
console.log('Running validate...');
|
|
546
|
+
const result = spawnSync('node', [path.join(REPO_ROOT, 'knowledge.js'), 'validate'], {
|
|
547
|
+
encoding: 'utf8',
|
|
548
|
+
cwd: REPO_ROOT,
|
|
549
|
+
stdio: 'inherit'
|
|
550
|
+
});
|
|
551
|
+
if (result.status !== 0) {
|
|
552
|
+
console.error('');
|
|
553
|
+
console.error('ERROR: validate failed after adding the repo.');
|
|
554
|
+
console.error('Please fix the errors before continuing.');
|
|
555
|
+
process.exitCode = 1;
|
|
556
|
+
} else {
|
|
557
|
+
console.log('');
|
|
558
|
+
console.log('Next steps:');
|
|
559
|
+
console.log(' 1. Save Claude memory: repo-path-' + repo + ' = ' + (localPath || '<local path>'));
|
|
560
|
+
console.log(' 2. Create knowledge/' + framework + '/apps/' + repo + '/architecture.md when you start work');
|
|
561
|
+
console.log(' 3. Run /capture after your first session to populate features/');
|
|
562
|
+
}
|
|
563
|
+
resolve();
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/* ------------------------------------------------------------------ */
|
|
569
|
+
/* dispatch */
|
|
570
|
+
/* ------------------------------------------------------------------ */
|
|
571
|
+
|
|
572
|
+
async function main() {
|
|
573
|
+
const [, , cmd, ...rest] = process.argv;
|
|
574
|
+
|
|
575
|
+
switch (cmd) {
|
|
576
|
+
case 'audit':
|
|
577
|
+
cmdAudit();
|
|
578
|
+
break;
|
|
579
|
+
case 'sync':
|
|
580
|
+
cmdSync(rest);
|
|
581
|
+
break;
|
|
582
|
+
case 'status':
|
|
583
|
+
cmdStatus();
|
|
584
|
+
break;
|
|
585
|
+
case 'new-repo':
|
|
586
|
+
await cmdNewRepo();
|
|
587
|
+
break;
|
|
588
|
+
default:
|
|
589
|
+
console.log('toga-harness — TOGA Technology Claude harness utilities');
|
|
590
|
+
console.log('');
|
|
591
|
+
console.log('Usage: node scripts/harness.js <command>');
|
|
592
|
+
console.log('');
|
|
593
|
+
console.log('Commands:');
|
|
594
|
+
console.log(' audit Validate + completeness check, score 0-100');
|
|
595
|
+
console.log(' sync [path] Sync skills into a project .claude/skills/');
|
|
596
|
+
console.log(' status Show knowledge base statistics');
|
|
597
|
+
console.log(' new-repo Interactive wizard to onboard a new repo');
|
|
598
|
+
process.exitCode = 1;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
main().catch((err) => {
|
|
603
|
+
console.error('Unexpected error: ' + err.message);
|
|
604
|
+
process.exit(1);
|
|
605
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const COUNTER_FILE = path.join(os.tmpdir(), 'toga-session-counter.json');
|
|
9
|
+
const PATTERN_INTERVAL = 25;
|
|
10
|
+
|
|
11
|
+
function loadCounter() {
|
|
12
|
+
try {
|
|
13
|
+
if (fs.existsSync(COUNTER_FILE)) {
|
|
14
|
+
return JSON.parse(fs.readFileSync(COUNTER_FILE, 'utf8'));
|
|
15
|
+
}
|
|
16
|
+
} catch (e) {
|
|
17
|
+
// Corrupt counter file — start fresh
|
|
18
|
+
}
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function saveCounter(data) {
|
|
23
|
+
try {
|
|
24
|
+
fs.writeFileSync(COUNTER_FILE, JSON.stringify(data, null, 2));
|
|
25
|
+
} catch (e) {
|
|
26
|
+
// Non-fatal — counter persistence is best-effort
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function main() {
|
|
31
|
+
const pid = String(process.ppid || process.pid);
|
|
32
|
+
const counters = loadCounter();
|
|
33
|
+
|
|
34
|
+
if (!counters[pid]) {
|
|
35
|
+
counters[pid] = 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
counters[pid] += 1;
|
|
39
|
+
const count = counters[pid];
|
|
40
|
+
|
|
41
|
+
saveCounter(counters);
|
|
42
|
+
|
|
43
|
+
// Output reminder on every 25th call for this session PID
|
|
44
|
+
if (count % PATTERN_INTERVAL === 0) {
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log('Pattern opportunity: This session (' + count + ' responses) may contain');
|
|
47
|
+
console.log('extractable patterns worth capturing. Run /learn to distill them into');
|
|
48
|
+
console.log('the knowledge base, or /capture to save session learnings.');
|
|
49
|
+
console.log('');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
main();
|