takomi 2.1.26 → 2.1.28

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/src/cli.js CHANGED
@@ -33,6 +33,8 @@ import {
33
33
  initGlobalStore,
34
34
  getManifest,
35
35
  writeManifest,
36
+ buildStoreSkillsReconcilePlan,
37
+ buildStoreWorkflowsReconcilePlan,
36
38
  populateSkills,
37
39
  populateWorkflows,
38
40
  populateAgentYamls,
@@ -43,7 +45,26 @@ import {
43
45
  import { runDoctor } from './doctor.js';
44
46
  import { ensurePiInstalled, ensurePiSubagentsInstalled, launchTakomiHarness, printPiInstallResult, printPiSubagentsInstallResult, updatePiManagedPackages, printPiManagedPackageUpdateResult } from './pi-harness.js';
45
47
  import { installPiHarnessAssets, printPiInstallSummary, syncPiHarnessAssets, validatePiHarnessInstall } from './pi-installer.js';
46
- import { installBundledSkills, printSkillsInstallSummary, validateSkillsInstall } from './skills-installer.js';
48
+ import { offerPiOptionalFeatures } from './pi-optional-features.js';
49
+ import {
50
+ buildSkillsReconcilePlan,
51
+ getInstalledTakomiSkillNames,
52
+ installBundledSkills,
53
+ printSkillsInstallSummary,
54
+ validateSkillsInstall,
55
+ SKILLS_ROOT,
56
+ } from './skills-installer.js';
57
+ import {
58
+ colorCategory,
59
+ CORE_SKILLS,
60
+ getSkillChoices,
61
+ getSkillsForCategories,
62
+ getUncategorizedSkills,
63
+ getValidCoreSkills,
64
+ listBundledSkillNames,
65
+ SKILL_CATEGORIES,
66
+ } from './skills-catalog.js';
67
+ import { promptSkillCategoryTui } from './skills-selection-tui.js';
47
68
  import { notifyIfTakomiUpdateAvailable, printTakomiUpdateStatus, upgradeTakomiPackage } from './update-check.js';
48
69
  import { printTakomiStats } from './takomi-stats.js';
49
70
 
@@ -172,13 +193,7 @@ async function init() {
172
193
  // Handle Skills
173
194
  if (response.skillMode === 'core') {
174
195
  console.log(pc.green('✔ Downloading Core Skills...'));
175
- const coreSkills = [
176
- 'takomi',
177
- 'ai-sdk', 'code-review', 'component-analysis',
178
- 'nextjs-standards', 'security-audit', 'spawn-task',
179
- 'stitch', 'sync-docs'
180
- ];
181
- await copySpecificSkills(coreSkills, skillsDest);
196
+ await copySpecificSkills(CORE_SKILLS, skillsDest);
182
197
  } else if (response.skillMode === 'all') {
183
198
  console.log(pc.green('✔ Downloading all skills...'));
184
199
  await copyAllSkills(skillsDest);
@@ -219,11 +234,182 @@ async function init() {
219
234
  }
220
235
  }
221
236
 
237
+ function formatCoreSkillsSummary() {
238
+ return CORE_SKILLS.join(', ');
239
+ }
240
+
241
+ async function promptCustomSkillSelection(initialSelected = [], options = {}) {
242
+ const tuiSelection = await promptSkillCategoryTui({
243
+ initialSelected,
244
+ title: options.title || 'Custom Skills Selection',
245
+ });
246
+ if (Array.isArray(tuiSelection)) return tuiSelection;
247
+ if (tuiSelection === null) return null;
248
+
249
+ const initialSet = new Set(initialSelected);
250
+ const uncategorized = await getUncategorizedSkills();
251
+ const categoryChoices = SKILL_CATEGORIES.map((category) => ({
252
+ title: colorCategory(category),
253
+ value: category.id,
254
+ selected: category.skills.some((skill) => initialSet.has(skill)),
255
+ description: category.description,
256
+ }));
257
+
258
+ if (uncategorized.length) {
259
+ categoryChoices.push({
260
+ title: pc.dim('[Other / Uncategorized]'),
261
+ value: '__uncategorized',
262
+ selected: uncategorized.some((skill) => initialSet.has(skill)),
263
+ description: 'Bundled skills that are not assigned to a curated category yet.',
264
+ });
265
+ }
266
+
267
+ const categoryResponse = await prompts({
268
+ type: 'multiselect',
269
+ name: 'categories',
270
+ message: 'Select skill categories:',
271
+ choices: categoryChoices,
272
+ hint: '- Space to select. Return to continue',
273
+ });
274
+
275
+ if (!categoryResponse.categories) return null;
276
+
277
+ const selected = new Set();
278
+ const selectedCategories = categoryResponse.categories;
279
+
280
+ for (const categoryId of selectedCategories) {
281
+ const category = SKILL_CATEGORIES.find((item) => item.id === categoryId);
282
+ const skillNames = categoryId === '__uncategorized'
283
+ ? uncategorized
284
+ : (await getSkillsForCategories([categoryId]));
285
+ const categoryHadInitialSelection = skillNames.some((skill) => initialSet.has(skill));
286
+ const defaultSelected = options.selectAllForNewCategories && !categoryHadInitialSelection
287
+ ? skillNames
288
+ : skillNames.filter((skill) => initialSet.has(skill));
289
+
290
+ const skillResponse = await prompts({
291
+ type: 'multiselect',
292
+ name: 'skills',
293
+ message: `Select skills in ${category?.title || 'Other / Uncategorized'}:`,
294
+ choices: await getSkillChoices(skillNames, defaultSelected),
295
+ hint: '- Space to select. Return to continue',
296
+ });
297
+
298
+ if (!skillResponse.skills) return null;
299
+ for (const skill of skillResponse.skills) selected.add(skill);
300
+ }
301
+
302
+ return [...selected].sort();
303
+ }
304
+
305
+ async function confirmSkillRemovalIfNeeded(selectedSkills, mode) {
306
+ const plan = await buildSkillsReconcilePlan(selectedSkills);
307
+ if (plan.toRemove.length === 0) return true;
308
+
309
+ console.log(pc.yellow('\nTakomi will remove deselected Takomi-managed skills only:'));
310
+ for (const skill of plan.toRemove) console.log(pc.dim(` - ${skill}`));
311
+ console.log(pc.dim('\nManual skills and modified Takomi skills are preserved.'));
312
+
313
+ const response = await prompts({
314
+ type: 'confirm',
315
+ name: 'confirm',
316
+ message: `Remove ${plan.toRemove.length} Takomi-managed skill${plan.toRemove.length === 1 ? '' : 's'} for "${mode}"?`,
317
+ initial: false,
318
+ });
319
+
320
+ return Boolean(response.confirm);
321
+ }
322
+
323
+ async function promptSkillsInstallSelection() {
324
+ const envMode = process.env.TAKOMI_SKILLS_MODE;
325
+ if (envMode) {
326
+ const mode = envMode.toLowerCase();
327
+ if (mode === 'core') return { mode, selectedSkills: await getValidCoreSkills() };
328
+ if (mode === 'all') return { mode, selectedSkills: await listBundledSkillNames() };
329
+ if (mode === 'none') return { mode, selectedSkills: [] };
330
+ if (mode === 'leave-as-is' || mode === 'leave') return { mode: 'leave-as-is', leaveAsIs: true };
331
+ }
332
+
333
+ const installed = await getInstalledTakomiSkillNames();
334
+ const hasTakomiSkills = installed.length > 0;
335
+ const choices = hasTakomiSkills
336
+ ? [
337
+ { title: 'Leave As Is', value: 'leave-as-is', selected: true, description: `Recommended: keep ${installed.length} Takomi-managed skill${installed.length === 1 ? '' : 's'} unchanged.` },
338
+ { title: 'Present Custom', value: 'present-custom', description: 'Review your current Takomi-managed skills and adjust selections.' },
339
+ { title: 'Core Skills', value: 'core', description: `[Recommended defaults] ${formatCoreSkillsSummary()}` },
340
+ { title: 'Custom', value: 'custom', description: 'Choose categories and individual skills.' },
341
+ { title: 'All Skills', value: 'all', description: 'Install every bundled Takomi skill.' },
342
+ { title: 'None', value: 'none', description: 'Disable Takomi-managed skills after confirmation.' },
343
+ ]
344
+ : [
345
+ { title: 'Core Skills', value: 'core', selected: true, description: `[Recommended defaults] ${formatCoreSkillsSummary()}` },
346
+ { title: 'Custom', value: 'custom', description: 'Choose categories and individual skills.' },
347
+ { title: 'All Skills', value: 'all', description: 'Install every bundled Takomi skill.' },
348
+ { title: 'None', value: 'none', description: 'Do not install Takomi skills.' },
349
+ ];
350
+
351
+ const response = await prompts({
352
+ type: 'select',
353
+ name: 'mode',
354
+ message: 'Skills Installation',
355
+ choices,
356
+ });
357
+
358
+ if (!response.mode) return null;
359
+
360
+ if (response.mode === 'leave-as-is') {
361
+ return { mode: response.mode, leaveAsIs: true, ownedCount: installed.length };
362
+ }
363
+
364
+ let selectedSkills = [];
365
+ if (response.mode === 'core') {
366
+ selectedSkills = await getValidCoreSkills();
367
+ } else if (response.mode === 'all') {
368
+ const confirmAll = await prompts({
369
+ type: 'confirm',
370
+ name: 'confirm',
371
+ message: 'Install every bundled skill? This may add skill discovery noise in supported harnesses.',
372
+ initial: false,
373
+ });
374
+ if (!confirmAll.confirm) return { mode: 'leave-as-is', leaveAsIs: true, ownedCount: installed.length };
375
+ selectedSkills = await listBundledSkillNames();
376
+ } else if (response.mode === 'none') {
377
+ selectedSkills = [];
378
+ } else if (response.mode === 'present-custom') {
379
+ const customSelection = await promptCustomSkillSelection(installed, { selectAllForNewCategories: true, title: 'Present Custom Skills' });
380
+ if (!customSelection) return null;
381
+ selectedSkills = customSelection;
382
+ } else if (response.mode === 'custom') {
383
+ const customSelection = await promptCustomSkillSelection([], { selectAllForNewCategories: true, title: 'Custom Skills Selection' });
384
+ if (!customSelection) return null;
385
+ selectedSkills = customSelection;
386
+ }
387
+
388
+ const confirmedRemoval = await confirmSkillRemovalIfNeeded(selectedSkills, response.mode);
389
+ if (!confirmedRemoval) {
390
+ return { mode: 'leave-as-is', leaveAsIs: true, ownedCount: installed.length };
391
+ }
392
+
393
+ return { mode: response.mode, selectedSkills };
394
+ }
395
+
222
396
  async function installSkillsTarget() {
223
397
  console.log(pc.magenta('🧰 Takomi Skills Install\n'));
224
398
  try {
225
- const result = await installBundledSkills(program.version());
226
- const validation = await validateSkillsInstall();
399
+ const selection = await promptSkillsInstallSelection();
400
+ if (!selection) return;
401
+ if (selection.leaveAsIs) {
402
+ printSkillsInstallSummary({
403
+ leaveAsIs: true,
404
+ targetRoot: SKILLS_ROOT,
405
+ ownedCount: selection.ownedCount || (await getInstalledTakomiSkillNames()).length,
406
+ });
407
+ console.log(pc.dim('\nGlobal skills were not changed.\n'));
408
+ return;
409
+ }
410
+
411
+ const result = await installBundledSkills(program.version(), selection);
412
+ const validation = await validateSkillsInstall(selection.selectedSkills);
227
413
  printSkillsInstallSummary(result, validation);
228
414
  console.log(pc.dim('\nGlobal skills are ready for Pi or other supported harnesses.\n'));
229
415
  } catch (error) {
@@ -232,12 +418,13 @@ async function installSkillsTarget() {
232
418
  }
233
419
  }
234
420
 
235
- async function installAllTargets() {
236
- await installPiTarget();
421
+ async function installAllTargets(options = {}) {
422
+ await installPiTarget(options);
237
423
  await installSkillsTarget();
238
424
  }
239
425
 
240
- async function installPiTarget() {
426
+ async function installPiTarget(options = {}) {
427
+ const { offerOptionalFeatures = true } = options;
241
428
  console.log(pc.magenta('🧭 Pi Harness Preflight\n'));
242
429
  const report = await runDoctor({ version: program.version() });
243
430
 
@@ -247,7 +434,7 @@ async function installPiTarget() {
247
434
  printPiInstallResult(installResult);
248
435
  if (!installResult.ok) {
249
436
  console.log(pc.yellow('\nPi harness install stopped because Pi could not be installed.'));
250
- console.log(pc.dim('Retry manually with: npm install -g @mariozechner/pi-coding-agent\n'));
437
+ console.log(pc.dim('Retry manually with: npm install -g @earendil-works/pi-coding-agent\n'));
251
438
  return;
252
439
  }
253
440
  }
@@ -268,6 +455,10 @@ async function installPiTarget() {
268
455
  const result = await installPiHarnessAssets(program.version());
269
456
  const validation = await validatePiHarnessInstall();
270
457
  printPiInstallSummary(result, validation);
458
+ if (offerOptionalFeatures) {
459
+ console.log(pc.cyan('\n🧩 Optional Takomi Pi feature packs...\n'));
460
+ await offerPiOptionalFeatures({ interactive: true });
461
+ }
271
462
  console.log(pc.cyan('\n📦 Checking Pi-managed package updates...\n'));
272
463
  printPiManagedPackageUpdateResult(await updatePiManagedPackages());
273
464
  console.log(pc.dim('\nNext: cd <project> && takomi\n'));
@@ -277,6 +468,16 @@ async function installPiTarget() {
277
468
  }
278
469
  }
279
470
 
471
+ async function installPiOptionalFeaturesTarget() {
472
+ console.log(pc.magenta('🧩 Takomi Optional Pi Features\n'));
473
+ const pi = await ensurePiInstalled();
474
+ printPiInstallResult(pi);
475
+ if (!pi.ok) return;
476
+ await offerPiOptionalFeatures({ interactive: true });
477
+ console.log(pc.cyan('\n📦 Checking Pi-managed package updates...\n'));
478
+ printPiManagedPackageUpdateResult(await updatePiManagedPackages());
479
+ }
480
+
280
481
  async function syncPiTarget() {
281
482
  console.log(pc.magenta('📡 Takomi Pi Sync\n'));
282
483
  try {
@@ -294,8 +495,18 @@ async function syncPiTarget() {
294
495
  async function syncSkillsTarget() {
295
496
  console.log(pc.magenta('📡 Takomi Skills Sync\n'));
296
497
  try {
297
- const result = await installBundledSkills(program.version());
298
- const validation = await validateSkillsInstall();
498
+ const selection = await promptSkillsInstallSelection();
499
+ if (!selection) return;
500
+ if (selection.leaveAsIs) {
501
+ printSkillsInstallSummary({
502
+ leaveAsIs: true,
503
+ targetRoot: SKILLS_ROOT,
504
+ ownedCount: selection.ownedCount || (await getInstalledTakomiSkillNames()).length,
505
+ });
506
+ return;
507
+ }
508
+ const result = await installBundledSkills(program.version(), selection);
509
+ const validation = await validateSkillsInstall(selection.selectedSkills);
299
510
  printSkillsInstallSummary(result, validation);
300
511
  } catch (error) {
301
512
  console.log(pc.red('\nSkills sync failed.'));
@@ -347,7 +558,7 @@ async function upgrade(target = 'all') {
347
558
  }
348
559
 
349
560
  if (normalizedTarget === 'pi') {
350
- await installPiTarget();
561
+ await installPiTarget({ offerOptionalFeatures: false });
351
562
  return;
352
563
  }
353
564
 
@@ -356,13 +567,13 @@ async function upgrade(target = 'all') {
356
567
  return;
357
568
  }
358
569
 
359
- await installAllTargets();
570
+ await installAllTargets({ offerOptionalFeatures: false });
360
571
  console.log(pc.magenta('\n✨ Fully upgraded. Next: run `takomi` from your project.\n'));
361
572
  }
362
573
 
363
574
  function printUnsupportedInstallTarget(target) {
364
575
  console.log(pc.yellow(`Unsupported install target: ${target}`));
365
- console.log(pc.dim('Supported targets right now: pi, skills, all'));
576
+ console.log(pc.dim('Supported targets right now: pi, pi-features, skills, all'));
366
577
  console.log(pc.dim('Use plain "takomi install" for the existing interactive global installer.\n'));
367
578
  }
368
579
 
@@ -386,6 +597,10 @@ async function install(target) {
386
597
  await installSkillsTarget();
387
598
  return;
388
599
  }
600
+ if (target === 'pi-features' || target === 'features' || target === 'optional') {
601
+ await installPiOptionalFeaturesTarget();
602
+ return;
603
+ }
389
604
  if (target === 'all') {
390
605
  await installAllTargets();
391
606
  return;
@@ -426,6 +641,11 @@ async function install(target) {
426
641
  if (!harnessResponse.harnesses || harnessResponse.harnesses.length === 0) return;
427
642
 
428
643
  const selectedHarnesses = detected.filter(h => harnessResponse.harnesses.includes(h.id));
644
+ const existingStoreManifest = await getManifest();
645
+ const existingStoreOwnedSkills = Object.keys(existingStoreManifest.bundledOwned?.skills || {});
646
+ const existingStoreOwnedWorkflows = Object.keys(existingStoreManifest.bundledOwned?.workflows || {});
647
+ const hasExistingStoreSkills = existingStoreOwnedSkills.length > 0;
648
+ const hasExistingStoreWorkflows = existingStoreOwnedWorkflows.length > 0;
429
649
 
430
650
  // 3. Ask what to install
431
651
  const contentResponse = await prompts([
@@ -447,13 +667,15 @@ async function install(target) {
447
667
  name: 'skillMode',
448
668
  message: 'Which skill pack?',
449
669
  choices: [
450
- { title: 'Core (Takomi + essentials)', value: 'core', description: 'takomi, ai-sdk, code-review...' },
670
+ ...(hasExistingStoreSkills ? [{ title: 'Leave As Is (Recommended)', value: 'leave-as-is', description: `Keep ${existingStoreOwnedSkills.length} Takomi-managed store skill${existingStoreOwnedSkills.length === 1 ? '' : 's'} unchanged.` }] : []),
671
+ ...(hasExistingStoreSkills ? [{ title: 'Present Custom', value: 'present-custom', description: 'Review current Takomi-managed store skills and adjust selections.' }] : []),
672
+ { title: 'Core (Recommended defaults)', value: 'core', description: 'takomi, sync-docs, ai-sdk, git-commit-generation...' },
451
673
  { title: `All (${(await getSkills()).length} skills)`, value: 'all' },
452
674
  { title: 'Custom selection', value: 'custom' },
453
675
  ],
454
676
  },
455
677
  {
456
- type: (prev, values) => values.skillMode === 'custom' ? 'multiselect' : null,
678
+ type: null,
457
679
  name: 'selectedSkills',
458
680
  message: 'Select skills:',
459
681
  choices: async () => {
@@ -468,6 +690,7 @@ async function install(target) {
468
690
  name: 'workflowMode',
469
691
  message: 'Which workflow pack?',
470
692
  choices: [
693
+ ...(hasExistingStoreWorkflows ? [{ title: 'Leave As Is (Recommended)', value: 'leave-as-is', description: `Keep ${existingStoreOwnedWorkflows.length} Takomi-managed workflow${existingStoreOwnedWorkflows.length === 1 ? '' : 's'} unchanged.` }] : []),
471
694
  { title: 'Core (16 essentials)', value: 'core', description: 'vibe-build, mode-architect...' },
472
695
  { title: `All (${(await getWorkflows()).length} workflows)`, value: 'all' },
473
696
  { title: 'All (Exclude Legacy)', value: 'no-legacy' },
@@ -495,19 +718,73 @@ async function install(target) {
495
718
 
496
719
  // 5. Populate store from package assets
497
720
  if (contentResponse.content.includes('skills')) {
498
- const skillMode = contentResponse.skillMode === 'custom'
499
- ? contentResponse.selectedSkills
500
- : contentResponse.skillMode;
501
- const skills = await populateSkills(skillMode);
502
- console.log(pc.green(` ✔ ${skills.length} skills loaded into store`));
721
+ let skillMode = contentResponse.skillMode;
722
+ if (contentResponse.skillMode === 'leave-as-is') {
723
+ console.log(pc.green(` ✔ Left ${existingStoreOwnedSkills.length} Takomi-managed store skill${existingStoreOwnedSkills.length === 1 ? '' : 's'} unchanged`));
724
+ } else {
725
+ if (contentResponse.skillMode === 'present-custom') {
726
+ const customSelection = await promptCustomSkillSelection(existingStoreOwnedSkills, { selectAllForNewCategories: true, title: 'Present Global Store Custom Skills' });
727
+ if (!customSelection) return;
728
+ skillMode = customSelection;
729
+ } else if (contentResponse.skillMode === 'custom') {
730
+ const customSelection = await promptCustomSkillSelection([], { selectAllForNewCategories: true, title: 'Global Store Custom Skills' });
731
+ if (!customSelection) return;
732
+ skillMode = customSelection;
733
+ }
734
+
735
+ const plan = await buildStoreSkillsReconcilePlan(skillMode);
736
+ if (plan.toRemove.length > 0) {
737
+ console.log(pc.yellow('\nTakomi will remove deselected Takomi-managed skills from the global store only:'));
738
+ for (const skill of plan.toRemove) console.log(pc.dim(` - ${skill}`));
739
+ console.log(pc.dim('\nManual/imported store skills and modified Takomi skills are preserved.'));
740
+ const confirmStorePrune = await prompts({
741
+ type: 'confirm',
742
+ name: 'confirm',
743
+ message: `Remove ${plan.toRemove.length} Takomi-managed store skill${plan.toRemove.length === 1 ? '' : 's'}?`,
744
+ initial: false,
745
+ });
746
+ if (!confirmStorePrune.confirm) {
747
+ console.log(pc.green(' ✔ Left global store skills unchanged'));
748
+ skillMode = null;
749
+ }
750
+ }
751
+
752
+ if (skillMode) {
753
+ const skills = await populateSkills(skillMode);
754
+ console.log(pc.green(` ✔ ${skills.length} skills loaded into store`));
755
+ }
756
+ }
503
757
  }
504
758
 
505
759
  if (contentResponse.content.includes('workflows')) {
506
- const workflowMode = contentResponse.workflowMode === 'custom'
760
+ let workflowMode = contentResponse.workflowMode === 'custom'
507
761
  ? contentResponse.selectedWorkflows
508
762
  : contentResponse.workflowMode;
509
- const workflows = await populateWorkflows(workflowMode);
510
- console.log(pc.green(` ✔ ${workflows.length} workflows loaded into store`));
763
+ if (contentResponse.workflowMode === 'leave-as-is') {
764
+ console.log(pc.green(` ✔ Left ${existingStoreOwnedWorkflows.length} Takomi-managed workflow${existingStoreOwnedWorkflows.length === 1 ? '' : 's'} unchanged`));
765
+ } else {
766
+ const plan = await buildStoreWorkflowsReconcilePlan(workflowMode);
767
+ if (plan.toRemove.length > 0) {
768
+ console.log(pc.yellow('\nTakomi will remove deselected Takomi-managed workflows from the global store only:'));
769
+ for (const workflow of plan.toRemove) console.log(pc.dim(` - ${workflow}`));
770
+ console.log(pc.dim('\nManual/imported store workflows and modified Takomi workflows are preserved.'));
771
+ const confirmWorkflowPrune = await prompts({
772
+ type: 'confirm',
773
+ name: 'confirm',
774
+ message: `Remove ${plan.toRemove.length} Takomi-managed workflow${plan.toRemove.length === 1 ? '' : 's'}?`,
775
+ initial: false,
776
+ });
777
+ if (!confirmWorkflowPrune.confirm) {
778
+ console.log(pc.green(' ✔ Left global store workflows unchanged'));
779
+ workflowMode = null;
780
+ }
781
+ }
782
+
783
+ if (workflowMode) {
784
+ const workflows = await populateWorkflows(workflowMode);
785
+ console.log(pc.green(` ✔ ${workflows.length} workflows loaded into store`));
786
+ }
787
+ }
511
788
  }
512
789
 
513
790
  if (contentResponse.content.includes('yamls')) {
@@ -517,13 +794,21 @@ async function install(target) {
517
794
 
518
795
  // 6. Sync store to all selected harnesses
519
796
  console.log(pc.cyan('\n📡 Syncing to harnesses...\n'));
520
- await syncToAllHarnesses(selectedHarnesses, STORE_PATH);
797
+ let manifest = await getManifest();
798
+ const syncSummary = await syncToAllHarnesses(selectedHarnesses, STORE_PATH, {
799
+ useOwnership: true,
800
+ owned: manifest.harnessOwned,
801
+ });
521
802
 
522
803
  // 7. Update manifest
523
- const manifest = await getManifest();
804
+ manifest = await getManifest();
524
805
  manifest.linkedHarnesses = selectedHarnesses.map(h => h.id);
525
806
  manifest.installed.skills = await getStoreSkills();
526
807
  manifest.installed.workflows = await getStoreWorkflows();
808
+ manifest.harnessOwned = manifest.harnessOwned || {};
809
+ for (const harness of selectedHarnesses) {
810
+ manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
811
+ }
527
812
  await writeManifest(manifest);
528
813
 
529
814
  // 8. Summary
@@ -594,10 +879,17 @@ async function sync(target) {
594
879
  const selectedHarnesses = detected.filter(h => response.harnesses.includes(h.id));
595
880
 
596
881
  console.log(pc.cyan('\n📡 Syncing from global store...\n'));
597
- await syncToAllHarnesses(selectedHarnesses, STORE_PATH);
882
+ const syncSummary = await syncToAllHarnesses(selectedHarnesses, STORE_PATH, {
883
+ useOwnership: true,
884
+ owned: manifest.harnessOwned,
885
+ });
598
886
 
599
887
  // Update manifest with current harnesses
600
888
  manifest.linkedHarnesses = [...new Set([...manifest.linkedHarnesses, ...response.harnesses])];
889
+ manifest.harnessOwned = manifest.harnessOwned || {};
890
+ for (const harness of selectedHarnesses) {
891
+ manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
892
+ }
601
893
  await writeManifest(manifest);
602
894
 
603
895
  const skills = await getStoreSkills();
@@ -813,6 +1105,7 @@ Primary flow:
813
1105
 
814
1106
  Examples:
815
1107
  takomi setup pi Set up the Pi harness
1108
+ takomi setup pi-features Add optional Pi feature packs
816
1109
  takomi setup skills Install bundled skills
817
1110
  takomi setup project
818
1111
  takomi refresh One-command maintenance
@@ -825,7 +1118,7 @@ Legacy aliases still work:
825
1118
 
826
1119
  program
827
1120
  .command('setup [target]')
828
- .description('Set up Takomi: guided setup, or setup pi|skills|project|all')
1121
+ .description('Set up Takomi: guided setup, or setup pi|pi-features|skills|project|all')
829
1122
  .action(async (target) => {
830
1123
  if (target === 'project') {
831
1124
  await init();