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.
Files changed (62) hide show
  1. package/.claude/settings.json +119 -0
  2. package/.claude-plugin/marketplace.json +87 -0
  3. package/.claude-plugin/plugin.json +22 -0
  4. package/CLAUDE.md +161 -0
  5. package/README.md +72 -0
  6. package/agents/framework-pattern-checker.md +67 -0
  7. package/agents/harness-optimizer.md +102 -0
  8. package/agents/knowledge-writer.md +62 -0
  9. package/agents/php-build-resolver.md +51 -0
  10. package/agents/php-reviewer.md +51 -0
  11. package/agents/planner.md +88 -0
  12. package/agents/session-capture.md +101 -0
  13. package/agents/sql-reviewer.md +67 -0
  14. package/contexts/dev.md +43 -0
  15. package/contexts/research.md +49 -0
  16. package/contexts/review.md +37 -0
  17. package/knowledge/1.0/apps/library/INDEX.md +5 -0
  18. package/knowledge/1.0/apps/library/architecture.md +105 -0
  19. package/knowledge/1.0/apps/worker/INDEX.md +5 -0
  20. package/knowledge/1.0/apps/worker/architecture.md +223 -0
  21. package/knowledge/1.0/standards/backend-php.md +450 -0
  22. package/knowledge/2.0/apps/_underscore/INDEX.md +6 -0
  23. package/knowledge/2.0/apps/_underscore/architecture.md +183 -0
  24. package/knowledge/2.0/apps/_underscore/features/recursive-item-fulfillments.md +111 -0
  25. package/knowledge/2.0/apps/api2/INDEX.md +5 -0
  26. package/knowledge/2.0/apps/api2/architecture.md +162 -0
  27. package/knowledge/2.0/apps/worker2/INDEX.md +6 -0
  28. package/knowledge/2.0/apps/worker2/architecture.md +127 -0
  29. package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +135 -0
  30. package/knowledge/2.0/standards/backend-php.md +710 -0
  31. package/knowledge/CONVENTIONS.md +117 -0
  32. package/knowledge/INDEX.md +19 -0
  33. package/knowledge/clients/.gitkeep +0 -0
  34. package/knowledge/registry.json +7 -0
  35. package/knowledge.js +384 -0
  36. package/mcp-configs/README.md +72 -0
  37. package/mcp-configs/mcp-servers.json +23 -0
  38. package/package.json +50 -0
  39. package/rules/README.md +53 -0
  40. package/rules/common/coding-style.md +123 -0
  41. package/rules/common/git-workflow.md +72 -0
  42. package/rules/common/security.md +118 -0
  43. package/rules/common/testing.md +74 -0
  44. package/rules/php/app-framework.md +104 -0
  45. package/rules/php/underscore-framework.md +111 -0
  46. package/scripts/harness.js +605 -0
  47. package/scripts/hooks/evaluate-session.js +55 -0
  48. package/scripts/hooks/post-edit-validate.js +102 -0
  49. package/scripts/hooks/session-end.js +13 -0
  50. package/scripts/hooks/session-start.js +57 -0
  51. package/scripts/install.js +611 -0
  52. package/scripts/pre-commit +46 -0
  53. package/skills/capture/SKILL.md +294 -0
  54. package/skills/code-review/SKILL.md +140 -0
  55. package/skills/create-elastic-beanstalk/SKILL.md +217 -0
  56. package/skills/harness-audit/SKILL.md +152 -0
  57. package/skills/kickoff/SKILL.md +151 -0
  58. package/skills/php-patterns/SKILL.md +296 -0
  59. package/skills/session-resume/SKILL.md +156 -0
  60. package/skills/session-save/SKILL.md +158 -0
  61. package/skills/sync-team-skills/SKILL.md +87 -0
  62. 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();