create-harness-vibe-coding 0.8.0 → 0.8.2

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 (37) hide show
  1. package/README-CN.md +40 -24
  2. package/README.md +44 -20
  3. package/package.json +1 -1
  4. package/src/generator.js +1 -1
  5. package/src/index.js +211 -11
  6. package/src/prompts.js +1 -1
  7. package/templates/common/.claude/agents/tdd-guide.md +55 -0
  8. package/templates/common/.claude/settings.json +22 -0
  9. package/templates/common/.claude/skills/tdd/SKILL.md +30 -0
  10. package/templates/common/.claude/skills/wf-auto/SKILL.md +107 -0
  11. package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +39 -0
  12. package/templates/common/.claude/skills/wf-max/SKILL.md +10 -2
  13. package/templates/common/.claude/skills/wf-remove/SKILL.md +12 -5
  14. package/templates/common/.claude/skills/wf-update/SKILL.md +12 -5
  15. package/templates/common/.codex/hooks.json +59 -37
  16. package/templates/common/.harness-version +48 -25
  17. package/templates/common/AGENTS.md +5 -5
  18. package/templates/common/CLAUDE.md +12 -17
  19. package/templates/common/Harness/ECC-GUIDE.md +246 -0
  20. package/templates/common/Harness/README.md +129 -130
  21. package/templates/common/Harness/TDD-GUIDE.md +83 -0
  22. package/templates/common/Harness/WF-AUTO-SPARK.md +297 -0
  23. package/templates/common/Harness/WF-AUTO.md +508 -0
  24. package/templates/common/Harness/WF-MAX.md +24 -0
  25. package/templates/common/Harness/context-loading.md +38 -1
  26. package/templates/common/Harness/dispatch.md +40 -40
  27. package/templates/common/Harness/subagents.md +7 -21
  28. package/templates/common/Harness/tasks/_template/NAMING.md +47 -0
  29. package/templates/common/MEMORY.md +73 -66
  30. package/templates/common/SETUP.md +98 -63
  31. package/templates/common/scripts/validate-harness.mjs +92 -58
  32. package/templates/common/scripts/wf-mode-hook.mjs +895 -318
  33. package/templates/common/scripts/wf-remove.mjs +301 -81
  34. package/templates/common/scripts/wf-statusline.ps1 +62 -38
  35. package/templates/common/scripts/wf-statusline.sh +67 -48
  36. package/templates/common/scripts/wf-update-check.mjs +179 -81
  37. package/templates/optional/catalog.json +5 -5
@@ -1,48 +1,67 @@
1
- #!/bin/bash
2
- # wf-statusline.sh — Unix statusline badge for WF-MAX / WF-REVIEW
3
- # Reads Harness/.runtime/current-mode.json and outputs a colored badge.
4
- #
5
- # Usage in .claude/settings.json:
6
- # "statusLine": { "type": "command", "command": "bash Harness/scripts/wf-statusline.sh" }
7
- #
8
- # Security: refuses symlinks, validates JSON, caps read size.
9
-
10
- MODE_FILE="$(dirname "$0")/../.runtime/current-mode.json"
11
-
12
- # Resolve relative path to absolute (from project root or script location)
13
- if [ ! -f "$MODE_FILE" ]; then
14
- # Try relative to CWD (project root)
15
- MODE_FILE="Harness/.runtime/current-mode.json"
16
- fi
17
-
18
- [ -L "$MODE_FILE" ] && exit 0 # Refuse symlinks
19
- [ ! -f "$MODE_FILE" ] && exit 0 # Missing file
20
-
21
- # Read with python for safe JSON parse (jq may not be installed).
22
- # Pass file path as argv to avoid shell injection via MODE_FILE content.
23
- MODE=$(python3 -c "
24
- import json, os, sys
25
- try:
26
- fpath = sys.argv[1]
27
- if os.path.getsize(fpath) > 4096: sys.exit(1)
28
- with open(fpath, 'r') as f:
29
- d = json.load(f)
30
- if d.get('active') and d.get('role') == 'ceo':
31
- print(d.get('mode','') + '|' + d.get('phase',''))
32
- except: pass
33
- " "$MODE_FILE" 2>/dev/null)
34
-
35
- [ -z "$MODE" ] && exit 0
36
-
37
- MODE_NAME="${MODE%%|*}"
38
- MODE_PHASE="${MODE##*|}"
39
-
40
- case "$MODE_NAME" in
41
- wf-max)
42
- PHASE_SHORT=$(echo "$MODE_PHASE" | sed 's/W._/W/')
43
- printf '\033[38;5;75m[WF-MAX:%s]\033[0m' "$PHASE_SHORT"
44
- ;;
45
- wf-review)
46
- printf '\033[38;5;178m[WF-REVIEW]\033[0m'
47
- ;;
48
- esac
1
+ #!/bin/bash
2
+ # wf-statusline.sh — Unix statusline badge for WF-MAX / WF-REVIEW
3
+ # Displays mode + agentRole from Harness/.runtime/current-mode.json
4
+ # Role-aware: shows all roles (CEO/MGR/WRK/REV), not just CEO.
5
+ #
6
+ # Usage in .claude/settings.json:
7
+ # "statusLine": { "type": "command", "command": "bash Harness/scripts/wf-statusline.sh" }
8
+ #
9
+ # Security: refuses symlinks, validates JSON, caps read size.
10
+
11
+ MODE_FILE="$(dirname "$0")/../.runtime/current-mode.json"
12
+
13
+ # Resolve relative path to absolute (from project root or script location)
14
+ if [ ! -f "$MODE_FILE" ]; then
15
+ MODE_FILE="Harness/.runtime/current-mode.json"
16
+ fi
17
+
18
+ [ -L "$MODE_FILE" ] && exit 0 # Refuse symlinks
19
+ [ ! -f "$MODE_FILE" ] && exit 0 # Missing file
20
+
21
+ # Read with python for safe JSON parse (jq may not be installed).
22
+ MODE=$(python3 -c "
23
+ import json, os, sys
24
+ try:
25
+ fpath = sys.argv[1]
26
+ if os.path.getsize(fpath) > 16384: sys.exit(1)
27
+ with open(fpath, 'r') as f:
28
+ d = json.load(f)
29
+ if not d.get('active'): sys.exit(1)
30
+ agent_role = d.get('agentRole') or d.get('role') or ''
31
+ role_short = {'ceo':'CEO','manager':'MGR','worker':'WRK','reviewer':'REV'}.get(agent_role, '')
32
+ print(d.get('mode','') + '|' + d.get('phase','') + '|' + role_short)
33
+ except: pass
34
+ " "$MODE_FILE" 2>/dev/null)
35
+
36
+ [ -z "$MODE" ] && exit 0
37
+
38
+ MODE_NAME="${MODE%%|*}"
39
+ REST="${MODE#*|}"
40
+ MODE_PHASE="${REST%%|*}"
41
+ MODE_ROLE="${REST##*|}"
42
+
43
+ case "$MODE_NAME" in
44
+ wf-max)
45
+ PHASE_SHORT=$(echo "$MODE_PHASE" | sed 's/W._/W/')
46
+ if [ -n "$MODE_ROLE" ]; then
47
+ printf '\033[38;5;75m[WF-MAX:%s:%s]\033[0m' "$MODE_ROLE" "$PHASE_SHORT"
48
+ else
49
+ printf '\033[38;5;75m[WF-MAX:%s]\033[0m' "$PHASE_SHORT"
50
+ fi
51
+ ;;
52
+ wf-review)
53
+ printf '\033[38;5;178m[WF-REVIEW]\033[0m'
54
+ ;;
55
+ wf-auto-spark)
56
+ printf '\033[38;5;213m[SPARK]\033[0m'
57
+ ;;
58
+ wf-auto)
59
+ printf '\033[38;5;111m[WF-AUTO]\033[0m'
60
+ ;;
61
+ wf)
62
+ printf '\033[38;5;108m[WF]\033[0m'
63
+ ;;
64
+ wf-learn)
65
+ printf '\033[38;5;222m[WF-LEARN]\033[0m'
66
+ ;;
67
+ esac
@@ -4,21 +4,22 @@
4
4
  * Fetches remote checksums, compares locally, classifies all files instantly.
5
5
  * Only CONFLICT files need AI/user decision.
6
6
  *
7
- * Usage:
8
- * node Harness/scripts/wf-update-check.mjs # full plan
9
- * node Harness/scripts/wf-update-check.mjs --apply # apply SAFE+MERGE+NEW, report CONFLICT
10
- * node Harness/scripts/wf-update-check.mjs --json # JSON output for AI consumption
11
- */
7
+ * Usage:
8
+ * node Harness/scripts/wf-update-check.mjs # full dry-run plan
9
+ * node Harness/scripts/wf-update-check.mjs --json # JSON output for AI consumption
10
+ * node Harness/scripts/wf-update-check.mjs --apply-safe # apply SAFE+NEW, leave CONFLICT for AI
11
+ * node Harness/scripts/wf-update-check.mjs --apply # apply only when no CONFLICT exists
12
+ */
12
13
 
13
14
  import { readFileSync, writeFileSync, existsSync, mkdirSync, lstatSync } from 'fs';
14
15
  import { createHash } from 'crypto';
15
16
  import { resolve, dirname, sep } from 'path';
16
17
  import { fileURLToPath } from 'url';
17
18
 
18
- const __dirname = dirname(fileURLToPath(import.meta.url));
19
- const ROOT = process.env.WF_ROOT ? resolve(process.env.WF_ROOT) : resolve(__dirname, '..', '..');
20
- const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
21
- const SOURCE_BASE = 'https://raw.githubusercontent.com/zingspark/create-harness-vibe-coding/main/templates/common/';
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const ROOT = process.env.WF_ROOT ? resolve(process.env.WF_ROOT) : resolve(__dirname, '..', '..');
21
+ const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
22
+ const DEFAULT_SOURCE_BASE = 'https://raw.githubusercontent.com/zingspark/create-harness-vibe-coding/main/templates/common/';
22
23
 
23
24
  // ── Tier classification ──────────────────────────────────────────
24
25
 
@@ -60,14 +61,26 @@ function safePath(file) {
60
61
  }
61
62
 
62
63
  /** Canonical normalization for classification matching. */
63
- function canonicalPath(file) {
64
- return file.replace(/\\/g, '/').replace(/\/+/g, '/');
65
- }
66
-
67
- /** Detect template placeholders in remote content. */
68
- function isTemplate(raw) {
69
- return /\{\{[a-zA-Z]+\}\}/.test(raw);
70
- }
64
+ function canonicalPath(file) {
65
+ return file.replace(/\\/g, '/').replace(/\/+/g, '/');
66
+ }
67
+
68
+ function readFlagValue(args, flagName) {
69
+ const idx = args.indexOf(flagName);
70
+ if (idx === -1) return null;
71
+ const value = args[idx + 1];
72
+ if (!value || value.startsWith('--')) return null;
73
+ return value;
74
+ }
75
+
76
+ function normalizeSourceBase(sourceBase) {
77
+ return sourceBase.endsWith('/') ? sourceBase : sourceBase + '/';
78
+ }
79
+
80
+ /** Detect template placeholders in remote content. */
81
+ function isTemplate(raw) {
82
+ return /\{\{[a-zA-Z]+\}\}/.test(raw);
83
+ }
71
84
 
72
85
  function sha256(content) {
73
86
  return 'sha256-' + createHash('sha256').update(content).digest('hex');
@@ -98,9 +111,13 @@ function classify(file, localHash, storedHash) {
98
111
  return 'CONFLICT'; // modified runtime file — unexpected
99
112
  }
100
113
 
101
- async function fetchRemote(url, timeoutMs = 30000) {
102
- const controller = new AbortController();
103
- const timer = setTimeout(() => controller.abort(), timeoutMs);
114
+ async function fetchRemote(url, timeoutMs = 30000) {
115
+ if (url.startsWith('file://')) {
116
+ return readFileSync(fileURLToPath(url), 'utf-8');
117
+ }
118
+
119
+ const controller = new AbortController();
120
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
104
121
  try {
105
122
  const res = await fetch(url, { signal: controller.signal });
106
123
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
@@ -112,11 +129,13 @@ async function fetchRemote(url, timeoutMs = 30000) {
112
129
 
113
130
  // ── Main ───────────────────────────────────────────────────────────
114
131
 
115
- async function main() {
116
- const args = process.argv.slice(2);
117
- const apply = args.includes('--apply');
118
- const jsonOut = args.includes('--json');
119
- const ignoreVersion = args.includes('--ignore-version') || args.includes('--force-check');
132
+ async function main() {
133
+ const args = process.argv.slice(2);
134
+ const apply = args.includes('--apply');
135
+ const applySafe = args.includes('--apply-safe') || args.includes('--safe-only');
136
+ const jsonOut = args.includes('--json');
137
+ const ignoreVersion = args.includes('--ignore-version') || args.includes('--force-check');
138
+ const sourceBase = normalizeSourceBase(readFlagValue(args, '--source-base') || process.env.WF_SOURCE_BASE || DEFAULT_SOURCE_BASE);
120
139
 
121
140
  // 1. Read local state
122
141
  if (!existsSync(VERSION_FILE)) {
@@ -155,7 +174,7 @@ async function main() {
155
174
  // 2. Fetch remote version file
156
175
  let remoteVersion;
157
176
  try {
158
- const raw = await fetchRemote(SOURCE_BASE + '.harness-version');
177
+ const raw = await fetchRemote(sourceBase + '.harness-version');
159
178
  if (isTemplate(raw)) {
160
179
  if (jsonOut) {
161
180
  console.log(JSON.stringify({ status: 'template-remote', message: 'Remote .harness-version has not been generated yet.' }));
@@ -188,15 +207,21 @@ async function main() {
188
207
  return 0;
189
208
  }
190
209
 
191
- const localGen = localVersion.generator || '0.0.0';
192
- const remoteGen = remoteVersion.generator || '0.0.0';
193
-
194
- if (!ignoreVersion && cmpSemver(remoteGen, localGen) <= 0) {
195
- if (jsonOut) {
196
- console.log(JSON.stringify({ status: 'up-to-date', version: localGen, remote: remoteGen }));
197
- } else if (cmpSemver(remoteGen, localGen) < 0) {
198
- console.log(`⚠ Remote (v${remoteGen}) is OLDER than local (v${localGen}). Downgrade refused.`);
199
- } else {
210
+ const localGen = localVersion.generator || '0.0.0';
211
+ const remoteGen = remoteVersion.generator || '0.0.0';
212
+ const versionCmp = cmpSemver(remoteGen, localGen);
213
+
214
+ if (!ignoreVersion && versionCmp <= 0) {
215
+ if (jsonOut) {
216
+ console.log(JSON.stringify({
217
+ status: versionCmp < 0 ? 'downgrade-refused' : 'up-to-date',
218
+ version: localGen,
219
+ remote: remoteGen,
220
+ sourceBase,
221
+ }));
222
+ } else if (versionCmp < 0) {
223
+ console.log(`⚠ Remote (v${remoteGen}) is OLDER than local (v${localGen}). Downgrade refused.`);
224
+ } else {
200
225
  console.log(`✅ Already up to date (v${localGen})`);
201
226
  }
202
227
  return;
@@ -210,11 +235,54 @@ async function main() {
210
235
  const remoteSources = remoteVersion.sources || {};
211
236
 
212
237
  /** Resolve the remote template-relative path for a dest-keyed file. Falls back to the key itself for back-compat. */
213
- function remotePath(file) {
214
- return remoteSources[file] || file;
215
- }
216
-
217
- const allFiles = new Set([...Object.keys(localChecksums), ...Object.keys(remoteChecksums)]);
238
+ function remotePath(file) {
239
+ return remoteSources[file] || file;
240
+ }
241
+
242
+ function withRemoteMeta(entry) {
243
+ if (!entry || !entry.file || !remoteChecksums[entry.file]) return entry;
244
+ const templateHint = remotePath(entry.file);
245
+ return {
246
+ ...entry,
247
+ templateHint,
248
+ remoteUrl: sourceBase + templateHint,
249
+ };
250
+ }
251
+
252
+ function withConflictActions(entry) {
253
+ return {
254
+ ...withRemoteMeta(entry),
255
+ choices: ['merge', 'keep-local', 'overwrite-from-template'],
256
+ };
257
+ }
258
+
259
+ function buildJsonPlan() {
260
+ return {
261
+ updated: plan.updated.map(withRemoteMeta),
262
+ created: plan.created.map(withRemoteMeta),
263
+ conflict: plan.conflict.map(withConflictActions),
264
+ skipped: plan.skipped.map(withRemoteMeta),
265
+ };
266
+ }
267
+
268
+ function buildAgentHints(jsonPlan) {
269
+ return {
270
+ mode: 'script-first-ai-conflicts',
271
+ dryRunJsonCommand: 'node Harness/scripts/wf-update-check.mjs --json',
272
+ safeApplyCommand: 'node Harness/scripts/wf-update-check.mjs --apply-safe',
273
+ strictApplyCommand: 'node Harness/scripts/wf-update-check.mjs --apply',
274
+ partialUpdate: localVersion.partialUpdate || null,
275
+ aiMergeRequired: jsonPlan.conflict,
276
+ aiMergeRequiredCount: jsonPlan.conflict.length,
277
+ conflictPolicy: 'Use the script for SAFE/NEW files. For each CONFLICT file, compare local content with templateHint/remoteUrl and choose merge, keep-local, or overwrite-from-template.',
278
+ postUpdateCommands: [
279
+ 'node Harness/scripts/validate-harness.mjs --strict',
280
+ 'node Harness/scripts/scan-clean.mjs',
281
+ ],
282
+ };
283
+ }
284
+
285
+ const allFiles = new Set([...Object.keys(localChecksums), ...Object.keys(remoteChecksums)]);
218
286
 
219
287
  const plan = { updated: [], created: [], conflict: [], skipped: [] };
220
288
 
@@ -255,11 +323,15 @@ async function main() {
255
323
 
256
324
  const tier = classify(canonical, localHash, storedHash);
257
325
 
258
- if (tier === 'PRESERVE') {
259
- plan.skipped.push({ file, reason: 'PRESERVE — user data' });
260
- } else if (tier === 'SAFE') {
261
- plan.updated.push({ file, remoteHash });
262
- } else if (tier === 'CONFLICT') {
326
+ if (tier === 'PRESERVE') {
327
+ plan.skipped.push({ file, reason: 'PRESERVE — user data' });
328
+ } else if (tier === 'SAFE') {
329
+ if (localHash === remoteHash) {
330
+ plan.skipped.push({ file, reason: 'already current' });
331
+ } else {
332
+ plan.updated.push({ file, remoteHash });
333
+ }
334
+ } else if (tier === 'CONFLICT') {
263
335
  plan.conflict.push({
264
336
  file,
265
337
  localHash,
@@ -272,20 +344,24 @@ async function main() {
272
344
  }
273
345
  }
274
346
 
275
- // 3. Output
276
- if (jsonOut) {
277
- console.log(JSON.stringify({
278
- status: 'update-available',
279
- from: localGen,
280
- to: remoteGen,
281
- updated: plan.updated.length,
282
- created: plan.created.length,
283
- conflict: plan.conflict.length,
284
- skipped: plan.skipped.length,
285
- plan,
286
- }, null, 2));
287
- return;
288
- }
347
+ // 3. Output
348
+ if (jsonOut) {
349
+ const jsonPlan = buildJsonPlan();
350
+ console.log(JSON.stringify({
351
+ status: localVersion.partialUpdate ? 'partial-update' : 'update-available',
352
+ from: localGen,
353
+ to: remoteGen,
354
+ sourceBase,
355
+ partialUpdate: localVersion.partialUpdate || null,
356
+ updated: plan.updated.length,
357
+ created: plan.created.length,
358
+ conflict: plan.conflict.length,
359
+ skipped: plan.skipped.length,
360
+ plan: jsonPlan,
361
+ agent: buildAgentHints(jsonPlan),
362
+ }, null, 2));
363
+ return;
364
+ }
289
365
 
290
366
  console.log(`\n🔄 Update: v${localGen} → v${remoteGen}`);
291
367
  console.log(` ${plan.updated.length} safe update, ${plan.created.length} new, ${plan.conflict.length} conflict, ${plan.skipped.length} skipped\n`);
@@ -296,10 +372,13 @@ async function main() {
296
372
  for (const c of plan.conflict) {
297
373
  console.log(` 📄 ${c.file} [${c.reason}]`);
298
374
  }
299
- console.log('');
300
- }
301
-
302
- // Show what will be auto-updated
375
+ if (plan.updated.length + plan.created.length > 0) {
376
+ console.log(' Tip: run --apply-safe to apply SAFE/NEW files first, then merge conflicts.');
377
+ }
378
+ console.log('');
379
+ }
380
+
381
+ // Show what will be auto-updated
303
382
  if (plan.updated.length + plan.created.length > 0) {
304
383
  console.log('✅ AUTO (safe to apply):');
305
384
  for (const u of plan.updated) console.log(` ↑ ${u.file}`);
@@ -308,15 +387,18 @@ async function main() {
308
387
  }
309
388
 
310
389
  // 4. Apply if requested
311
- if (apply) {
390
+ if (apply || applySafe) {
312
391
  // Refuse to apply when conflicts exist — must resolve first
313
- if (plan.conflict.length > 0) {
392
+ if (apply && !applySafe && plan.conflict.length > 0) {
314
393
  console.log(`❌ Cannot apply: ${plan.conflict.length} conflicts must be resolved first.`);
315
- console.log(' Resolve conflicts manually, then re-run --apply.');
316
- return plan;
317
- }
318
-
319
- const lexists = existsSync;
394
+ console.log(' Run --apply-safe to apply SAFE/NEW files first, or resolve conflicts manually then re-run --apply.');
395
+ return plan;
396
+ }
397
+ if (applySafe && plan.conflict.length > 0) {
398
+ console.log(`Applying SAFE/NEW files only; ${plan.conflict.length} conflicts will remain for AI/user merge.`);
399
+ }
400
+
401
+ const lexists = existsSync;
320
402
  let applied = 0;
321
403
  let failed = 0;
322
404
 
@@ -328,7 +410,7 @@ async function main() {
328
410
  if (lexists(dest)) {
329
411
  try { if (lstatSync(dest).isSymbolicLink()) { console.error(` ✗ Symlink rejected: ${u.file}`); failed++; continue; } } catch (_) {}
330
412
  }
331
- const content = await fetchRemote(SOURCE_BASE + remotePath(u.file));
413
+ const content = await fetchRemote(sourceBase + remotePath(u.file));
332
414
  const normalized = content.replace(/\r\n/g, '\n');
333
415
  const fetchedHash = sha256(normalized);
334
416
  if (fetchedHash !== u.remoteHash) {
@@ -354,7 +436,7 @@ async function main() {
354
436
  console.error(` ✗ File created since plan: ${c.file} — treating as CONFLICT`);
355
437
  failed++; continue;
356
438
  }
357
- const content = await fetchRemote(SOURCE_BASE + remotePath(c.file));
439
+ const content = await fetchRemote(sourceBase + remotePath(c.file));
358
440
  const normalized = content.replace(/\r\n/g, '\n');
359
441
  const fetchedHash = sha256(normalized);
360
442
  if (fetchedHash !== c.remoteHash) {
@@ -370,15 +452,31 @@ async function main() {
370
452
  }
371
453
  }
372
454
 
373
- // Only update version on complete success
374
- if (failed === 0) {
375
- localVersion.generator = remoteGen;
376
- localVersion.generated = new Date().toISOString();
377
- for (const u of plan.updated) localVersion.checksums[u.file] = u.remoteHash;
378
- for (const c of plan.created) localVersion.checksums[c.file] = c.remoteHash;
379
- writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
380
- console.log(`✅ Applied ${applied} files. Version updated to ${remoteVersion.generator}.`);
381
- } else {
455
+ // Only version-track applied files after complete script success.
456
+ if (failed === 0) {
457
+ const appliedAt = new Date().toISOString();
458
+ localVersion.checksums = localVersion.checksums || {};
459
+ for (const u of plan.updated) localVersion.checksums[u.file] = u.remoteHash;
460
+ for (const c of plan.created) localVersion.checksums[c.file] = c.remoteHash;
461
+ if (plan.conflict.length === 0) {
462
+ localVersion.generator = remoteGen;
463
+ localVersion.generated = appliedAt;
464
+ delete localVersion.partialUpdate;
465
+ } else {
466
+ localVersion.partialUpdate = {
467
+ targetGenerator: remoteGen,
468
+ updatedAt: appliedAt,
469
+ appliedFiles: [...plan.updated, ...plan.created].map(x => x.file),
470
+ conflicts: plan.conflict.map(x => x.file),
471
+ };
472
+ }
473
+ writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
474
+ if (plan.conflict.length === 0) {
475
+ console.log(`✅ Applied ${applied} files. Version updated to ${remoteVersion.generator}.`);
476
+ } else {
477
+ console.log(`✅ Applied ${applied} SAFE/NEW files. Version remains ${localGen}; ${plan.conflict.length} conflicts still need merge.`);
478
+ }
479
+ } else {
382
480
  console.log(`❌ ${failed} failures. NO files were version-tracked. Fix and re-run.`);
383
481
  }
384
482
  }
@@ -46,15 +46,15 @@
46
46
  "title": "Superpowers",
47
47
  "description": "Community skill registry for agent workflows and reusable coding skills.",
48
48
  "installMode": "recommend-only",
49
- "installHint": "run skill discovery first, then install only the selected Superpowers skills after user approval",
49
+ "url": "https://github.com/obra/Superpowers",
50
50
  "tags": ["skills", "community", "subagents"]
51
51
  },
52
52
  {
53
53
  "id": "caveman",
54
54
  "title": "Caveman",
55
- "description": "External workflow/style capability to evaluate for strict, low-abstraction implementation behavior.",
55
+ "description": "External terse-agent capability to evaluate for low-token behavior and memory compression.",
56
56
  "installMode": "recommend-only",
57
- "installHint": "search the configured skill/plugin sources for Caveman and install only after checking provenance",
57
+ "url": "https://github.com/JuliusBrussee/caveman",
58
58
  "tags": ["workflow", "implementation", "style"]
59
59
  },
60
60
  {
@@ -62,7 +62,7 @@
62
62
  "title": "Agent Research",
63
63
  "description": "External research-agent capability for broader product, dependency, and ecosystem investigation.",
64
64
  "installMode": "recommend-only",
65
- "installHint": "use available skill/plugin discovery for agent research and record the chosen source in Harness/MEMORY.md",
65
+ "url": "https://github.com/lingzhi227/agent-research-skills",
66
66
  "tags": ["research", "agents", "discovery"]
67
67
  },
68
68
  {
@@ -70,7 +70,7 @@
70
70
  "title": "Code Graph",
71
71
  "description": "External code graph or repository map capability for dependency and architecture exploration.",
72
72
  "installMode": "recommend-only",
73
- "installHint": "use available code graph tooling only after confirming it supports the project language and privacy constraints",
73
+ "url": "https://github.com/colbymchenry/codegraph",
74
74
  "tags": ["code graph", "architecture", "analysis"]
75
75
  }
76
76
  ]