takomi 2.1.35 → 2.1.36

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.
@@ -92,6 +92,73 @@ let activeSubagentLabel: string | undefined;
92
92
  let activeSubagentAgent: string | undefined;
93
93
  let activeSubagentTask: string | undefined;
94
94
  let activeSubagentStatus: string | undefined;
95
+ let delayedPiVersionCheckStarted = false;
96
+
97
+ const PI_LATEST_VERSION_URL = "https://pi.dev/api/latest-version";
98
+
99
+ function parseVersionParts(version: string): number[] | undefined {
100
+ const core = version.trim().replace(/^v/, "").split("-")[0];
101
+ if (!/^\d+(?:\.\d+)*$/.test(core)) return undefined;
102
+ return core.split(".").map((part) => Number.parseInt(part, 10) || 0);
103
+ }
104
+
105
+ function isNewerVersion(candidate: string, current: string): boolean {
106
+ const left = parseVersionParts(candidate);
107
+ const right = parseVersionParts(current);
108
+ if (!left || !right) return candidate.trim() !== current.trim();
109
+ for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
110
+ const a = left[i] ?? 0;
111
+ const b = right[i] ?? 0;
112
+ if (a > b) return true;
113
+ if (a < b) return false;
114
+ }
115
+ return false;
116
+ }
117
+
118
+ async function readCurrentPiVersion(): Promise<string | undefined> {
119
+ const candidates = [
120
+ path.resolve(path.dirname(process.argv[1] ?? ""), "..", "package.json"),
121
+ path.resolve(path.dirname(process.argv[1] ?? ""), "package.json"),
122
+ ];
123
+ for (const candidate of candidates) {
124
+ try {
125
+ const parsed = JSON.parse(await readFile(candidate, "utf8")) as { name?: string; version?: string };
126
+ if (parsed.name === "@earendil-works/pi-coding-agent" && typeof parsed.version === "string") return parsed.version;
127
+ } catch { }
128
+ }
129
+ return undefined;
130
+ }
131
+
132
+ function scheduleDelayedPiVersionCheck(ctx: ExtensionContext): void {
133
+ if (delayedPiVersionCheckStarted) return;
134
+ if (process.env.TAKOMI_DELAYED_PI_VERSION_CHECK !== "1") return;
135
+ if (process.env.TAKOMI_SKIP_DELAYED_PI_VERSION_CHECK === "1" || process.env.PI_OFFLINE === "1") return;
136
+ delayedPiVersionCheckStarted = true;
137
+
138
+ const delayMs = Math.max(0, Number(process.env.TAKOMI_PI_VERSION_CHECK_DELAY_MS || 3000) || 3000);
139
+ const timeoutMs = Math.max(500, Number(process.env.TAKOMI_PI_VERSION_CHECK_TIMEOUT_MS || 2500) || 2500);
140
+ setTimeout(() => {
141
+ void (async () => {
142
+ try {
143
+ const currentVersion = await readCurrentPiVersion();
144
+ if (!currentVersion) return;
145
+ const response = await fetch(PI_LATEST_VERSION_URL, {
146
+ headers: { accept: "application/json", "user-agent": `takomi-delayed-pi-version-check/${currentVersion}` },
147
+ signal: AbortSignal.timeout(timeoutMs),
148
+ });
149
+ if (!response.ok) return;
150
+ const data = await response.json() as { version?: unknown; note?: unknown };
151
+ if (typeof data.version !== "string" || !isNewerVersion(data.version, currentVersion)) return;
152
+ if (ctx.hasUI) {
153
+ const note = typeof data.note === "string" && data.note.trim() ? ` ${data.note.trim()}` : "";
154
+ ctx.ui.notify(`Pi ${data.version} is available (installed: ${currentVersion}). Run: takomi refresh pi.${note}`, "info");
155
+ }
156
+ } catch {
157
+ // Best-effort only. Delayed version checks must never affect startup or usage.
158
+ }
159
+ })();
160
+ }, delayMs).unref?.();
161
+ }
95
162
 
96
163
  const ThinkingSchema = Type.Union([
97
164
  Type.Literal("off"),
@@ -821,7 +888,6 @@ export default function takomiRuntime(pi: ExtensionAPI) {
821
888
  case "review":
822
889
  state.autoOrch = false;
823
890
  state.planMode = true;
824
- state.launchMode = "manual";
825
891
  state.role = "review";
826
892
  state.stage = undefined;
827
893
  state.workflow = undefined;
@@ -1365,6 +1431,7 @@ ${stateJson}`
1365
1431
 
1366
1432
  pi.on("session_start", async (_event, ctx) => {
1367
1433
  runtimeCtx = ctx;
1434
+ scheduleDelayedPiVersionCheck(ctx);
1368
1435
  activeProfile = await loadTakomiProfile(ctx.cwd);
1369
1436
  activeSubagentLabel = undefined;
1370
1437
  activeSubagentAgent = undefined;
@@ -81,9 +81,9 @@ export function renderRuntimeStatus(theme: Theme, state: RuntimeHudState): strin
81
81
  const sourceLabel = source === "manual" ? theme.fg("warning", "manual") : theme.fg("success", source);
82
82
  const role = state.stage && state.role !== state.stage ? theme.fg("dim", state.role) : "";
83
83
  const plan = state.planMode ? theme.fg("warning", "plan") : theme.fg("dim", "direct");
84
- const gate = state.launchMode === "manual" ? theme.fg("warning", "review-gate") : "";
84
+ const gate = state.launchMode === "manual" ? theme.fg("warning", "review-gate") : theme.fg("success", "auto-gate");
85
85
  const subagents = state.subagentsEnabled === false ? theme.fg("error", "subagents:off") : "";
86
- return [theme.fg("accent", "Takomi"), sourceLabel, stageBadge, role, gate || plan, subagents].filter(Boolean).join(" ");
86
+ return [theme.fg("accent", "Takomi"), sourceLabel, stageBadge, role, gate, plan, subagents].filter(Boolean).join(" ");
87
87
  }
88
88
 
89
89
  export function renderRuntimeWidget(theme: Theme, state: RuntimeHudState): string[] {
@@ -95,7 +95,7 @@ export function renderRuntimeWidget(theme: Theme, state: RuntimeHudState): strin
95
95
  sourceLabel,
96
96
  badge(theme, primary, stageTone(state.stage)),
97
97
  state.stage && state.role !== state.stage ? theme.fg("dim", `role:${state.role}`) : "",
98
- state.launchMode === "manual" ? theme.fg("warning", "review-gate") : "",
98
+ state.launchMode === "manual" ? theme.fg("warning", "review-gate") : theme.fg("success", "auto-gate"),
99
99
  state.planMode ? theme.fg("warning", "plan") : theme.fg("dim", "direct"),
100
100
  state.subagentsEnabled === false ? theme.fg("error", "subagents:off") : theme.fg("dim", "subagents:on"),
101
101
  state.workflow ? theme.fg("dim", `wf:${state.workflow}`) : "",
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
3
  import { renderTakomiSubagentCall, renderTakomiSubagentResult } from "./native-render";
4
+ import { loadPiSubagentsInternals } from "./pi-subagents-internal";
4
5
  import { executeTakomiSubagentTool } from "./tool-runner";
5
6
 
6
7
  const ChecklistItemSchema = Type.Object({
@@ -74,6 +75,11 @@ function registerSubagentTool(pi: ExtensionAPI): void {
74
75
  });
75
76
  }
76
77
 
77
- export default function takomiSubagents(pi: ExtensionAPI) {
78
+ export default async function takomiSubagents(pi: ExtensionAPI) {
79
+ // Preload the native pi-subagents renderer before tool registration. Rendering
80
+ // callbacks are synchronous, so if we only lazy-load internals during execution
81
+ // a completed takomi_subagent result can fall back to Takomi's plain text
82
+ // renderer and lose the native compact/expanded details shown by `subagent`.
83
+ await loadPiSubagentsInternals();
78
84
  registerSubagentTool(pi);
79
85
  }
@@ -38,12 +38,16 @@ function resultText(result: ToolResult): string {
38
38
  : JSON.stringify((result as any)?.details ?? {}, null, 2);
39
39
  }
40
40
 
41
- function summarizeCollapsedResult(text: string, status: string, theme: Theme): string {
41
+ function extractPolicyNames(text: string): string[] {
42
42
  const policyMatch = text.match(/Required policies:\n((?:- .+\n?)+)/);
43
- const policies = policyMatch?.[1]
43
+ return policyMatch?.[1]
44
44
  ?.split("\n")
45
45
  .map((line) => line.replace(/^[-\s]+/, "").trim())
46
46
  .filter(Boolean) ?? [];
47
+ }
48
+
49
+ function summarizeCollapsedResult(text: string, status: string, theme: Theme): string {
50
+ const policies = extractPolicyNames(text);
47
51
  const lineCount = text ? text.split(/\r?\n/).length : 0;
48
52
  const label = policies.length > 0
49
53
  ? `policy context loaded: ${policies.join(", ")}`
@@ -53,22 +57,89 @@ function summarizeCollapsedResult(text: string, status: string, theme: Theme): s
53
57
  return `${theme.fg(color, `${icon} takomi_subagent ${status}`)}${theme.fg("dim", ` · ${label} (ctrl+o to expand)`)}`;
54
58
  }
55
59
 
60
+ function isPolicyGateBlock(text: string): boolean {
61
+ return /^Blocked\s+takomi_subagent:\s+required policy context had not been loaded yet\./m.test(text)
62
+ && /\nRequired policies:\n/.test(text)
63
+ && /\nLoaded policy context:\n/.test(text);
64
+ }
65
+
66
+ function renderPolicyGateBlock(text: string, expanded: boolean | undefined, theme: Theme): string {
67
+ const policies = extractPolicyNames(text);
68
+ const lineCount = text ? text.split(/\r?\n/).length : 0;
69
+ const policyLabel = policies.length ? policies.join(", ") : "required policy";
70
+ if (!expanded) {
71
+ return [
72
+ theme.fg("warning", "⚠ takomi_subagent blocked"),
73
+ theme.fg("dim", `Required policy context loaded for this session: ${policyLabel}.`),
74
+ theme.fg("dim", `Retry the original tool call. ${lineCount} policy lines hidden (ctrl+o to expand).`),
75
+ ].join("\n");
76
+ }
77
+ return [
78
+ theme.fg("warning", "⚠ takomi_subagent blocked"),
79
+ theme.fg("dim", "Policy context was loaded and passed back to the model; retry the original call."),
80
+ theme.fg("dim", "Press ctrl+o again to collapse."),
81
+ "",
82
+ text,
83
+ ].join("\n");
84
+ }
85
+
56
86
  function recentOutputLines(value: unknown): string[] {
57
87
  if (Array.isArray(value)) return value.map(String).filter(Boolean).slice(-3);
58
88
  if (typeof value === "string") return value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-3);
59
89
  return [];
60
90
  }
61
91
 
92
+ function normalizeLiveStatus(value: string | undefined): string | undefined {
93
+ if (!value) return undefined;
94
+ if (value === "complete") return "completed";
95
+ if (value === "in-progress") return "running";
96
+ if (value === "timed-out") return "failed";
97
+ return value;
98
+ }
99
+
100
+ function rawLiveStatus(row: any): string {
101
+ const explicit = normalizeLiveStatus(
102
+ typeof row?.progress?.status === "string" ? row.progress.status
103
+ : typeof row?.status === "string" ? row.status
104
+ : undefined,
105
+ );
106
+ if (explicit) return explicit;
107
+
108
+ const exitCode = typeof row?.exitCode === "number" ? row.exitCode
109
+ : typeof row?.code === "number" ? row.code
110
+ : undefined;
111
+ if (exitCode === 0) return "completed";
112
+ if (exitCode === undefined || exitCode === null || exitCode === -1) return "running";
113
+ return "failed";
114
+ }
115
+
116
+ function isLiveTerminalStatus(status: string): boolean {
117
+ return ["completed", "failed", "blocked", "detached", "paused"].includes(status);
118
+ }
119
+
120
+ function displayLiveStatus(status: string, allRowsTerminal: boolean): string {
121
+ // Native pi-subagents can emit one last partial update after the child process
122
+ // reaches exitCode 0 but before the takomi_subagent tool result has actually
123
+ // settled. In that window the tool card is still partial/running, so showing a
124
+ // child as "completed" is misleading. Call it finalizing until Pi renders the
125
+ // real final result.
126
+ if (allRowsTerminal && status === "completed") return "finalizing";
127
+ return status;
128
+ }
129
+
62
130
  function livePartialText(result: ToolResult, theme: Theme): string {
63
131
  const details = (result as any)?.details ?? {};
64
132
  const results = Array.isArray(details.results) ? details.results : [];
65
133
  const progress = Array.isArray(details.progress) ? details.progress : [];
134
+ const rows = results.length ? results : progress;
135
+ const rawStatuses = rows.map(rawLiveStatus);
136
+ const allRowsTerminal = rows.length > 0 && rawStatuses.every(isLiveTerminalStatus);
137
+ const titleStatus = allRowsTerminal ? "finalizing" : "running";
66
138
  const lines = [
67
- theme.fg("toolTitle", theme.bold("takomi_subagent running")),
139
+ theme.fg("toolTitle", theme.bold(`takomi_subagent ${titleStatus}`)),
68
140
  theme.fg("dim", "Live detail is bounded while streaming so manual scroll/ctrl+o does not jump on every token."),
69
141
  ];
70
142
 
71
- const rows = results.length ? results : progress;
72
143
  if (!rows.length) {
73
144
  const text = resultText(result).split(/\r?\n/).find((line) => line.trim())?.trim();
74
145
  lines.push(theme.fg("dim", text || "Waiting for subagent progress…"));
@@ -76,7 +147,7 @@ function livePartialText(result: ToolResult, theme: Theme): string {
76
147
  }
77
148
 
78
149
  rows.slice(0, 6).forEach((row: any, index: number) => {
79
- const status = row.status ?? (row.exitCode === 0 ? "completed" : row.exitCode === -1 ? "running" : row.exitCode === undefined ? "running" : "failed");
150
+ const status = displayLiveStatus(rawStatuses[index] ?? rawLiveStatus(row), allRowsTerminal);
80
151
  const agent = row.agent ?? `task ${index + 1}`;
81
152
  const task = String(row.task ?? "").replace(/\s+/g, " ").trim();
82
153
  const currentTool = row.currentTool ?? row.progress?.currentTool;
@@ -93,9 +164,13 @@ function livePartialText(result: ToolResult, theme: Theme): string {
93
164
  }
94
165
 
95
166
  export function renderTakomiSubagentResult(result: ToolResult, options: { expanded?: boolean; isPartial?: boolean }, theme: Theme, context: any): any {
96
- const status = (result as any)?.isError ? "failed" : options.isPartial ? "running" : "completed";
167
+ const status = ((result as any)?.isError || context?.isError) ? "failed" : options.isPartial ? "running" : "completed";
97
168
  const text = resultText(result);
98
169
 
170
+ if (isPolicyGateBlock(text)) {
171
+ return new Text(renderPolicyGateBlock(text, options.expanded, theme), 0, 0);
172
+ }
173
+
99
174
  if (options.isPartial) {
100
175
  if (options.expanded) return new Text(livePartialText(result, theme), 0, 0);
101
176
  return new Text(summarizeCollapsedResult(text, status, theme), 0, 0);
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
- import type { TakomiThinkingLevel } from "../../../src/pi-takomi-core";
4
+ import type { TakomiLaunchMode, TakomiThinkingLevel } from "../../../src/pi-takomi-core";
5
5
  import { loadTakomiProfile } from "../takomi-runtime/profile";
6
6
  import { applyTakomiRoutingDefaults, loadTakomiModelRoutingSnapshot } from "../takomi-runtime/model-routing-defaults";
7
7
  import { resolveAgentName } from "./agent-aliases";
@@ -192,6 +192,17 @@ function withNativeHardStop(result: any, hardStop: { reason: string; message: st
192
192
  },
193
193
  };
194
194
  }
195
+
196
+ function readRuntimeLaunchMode(ctx: ExtensionContext): TakomiLaunchMode | undefined {
197
+ const entries = ctx.sessionManager.getEntries();
198
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
199
+ const entry = entries[i] as { type?: string; customType?: string; data?: { launchMode?: unknown } };
200
+ if (entry.type !== "custom" || entry.customType !== "takomi-runtime-state") continue;
201
+ if (entry.data?.launchMode === "manual" || entry.data?.launchMode === "auto") return entry.data.launchMode;
202
+ }
203
+ return undefined;
204
+ }
205
+
195
206
  function resolveMode(params: TakomiSubagentToolParams): "single" | "parallel" | "chain" | undefined {
196
207
  const hasChain = Boolean(params.chain?.length);
197
208
  const hasParallel = Boolean(params.tasks?.length);
@@ -234,6 +245,7 @@ export async function executeTakomiSubagentTool(
234
245
  return textResult(message, { results: [], agentScope: params.agentScope ?? "both" }, true);
235
246
  }
236
247
  const profile = await loadTakomiProfile(rootCwd);
248
+ const runtimeLaunchMode = readRuntimeLaunchMode(ctx);
237
249
  const agentScope = params.agentScope ?? "both";
238
250
  const agents = discoverTakomiAgents(rootCwd, agentScope);
239
251
  const byName = new Map<string, TakomiAgentConfig>(agents.map((agent) => [agent.name, agent]));
@@ -290,7 +302,7 @@ export async function executeTakomiSubagentTool(
290
302
  }
291
303
  const plan = createTakomiDelegationPlan({
292
304
  source: "takomi-tool",
293
- launchMode: profile.launchMode ?? "auto",
305
+ launchMode: runtimeLaunchMode ?? profile.launchMode ?? "auto",
294
306
  profile,
295
307
  tasks: tasks.map((task, index) => ({
296
308
  id: task.conversationId ?? `direct-${index + 1}`,
package/README.md CHANGED
@@ -63,7 +63,7 @@ Takomi now ships a Pi-native `takomi-context-manager` extension. It reduces prom
63
63
 
64
64
  ### Option A: Global Install (Best for Multi-IDE Users) ⭐
65
65
 
66
- Install once. Use everywhere. Your skills follow you across every AI harness(Antigravity, KiloCode, Windsurf, Cursor, Gemini CLI):
66
+ Install once. Use everywhere. Your skills follow you across detected AI harnesses (Antigravity, Claude Code, Codex, Cursor, Kilo Code, Pi/shared Agent Skills, Windsurf):
67
67
 
68
68
  ```bash
69
69
  # Using pnpm
@@ -184,17 +184,21 @@ npx -y skills add https://github.com/JStaRFilms/VibeCode-Protocol-Suite --skill
184
184
 
185
185
  **One install. Every IDE. Zero friction.**
186
186
 
187
- Takomi v2.0 introduces the **Global Skills Router** — install skills once, and they appear in every AI harness you use. No symlinks. No admin privileges. Works on Mac & Windows.
187
+ Takomi v2.0 introduces the **Global Skills Router** — install skills once into `~/.takomi/`, and Takomi syncs them into each harness' current global skills directory. You can choose symlink/junction mode for one canonical copy, auto fallback, or plain copy mode. Works on Mac & Windows.
188
+
189
+ Note: `SKILL.md` is portable, but `~/.agents/skills` is **not** a universal global skills directory. Pi does load `~/.agents/skills`, and Takomi keeps that as the Pi/shared Agent Skills target. Other harnesses use their own global paths. See `docs/skills-harness-targets.md` for the current checked path table.
188
190
 
189
191
  ### Supported Harnesses
190
192
 
191
193
  | Harness | Global Skills Path | Global Workflows Path |
192
194
  |---|---|---|
193
- | **Antigravity** | `~/.gemini/antigravity/skills/` | `~/.gemini/antigravity/global_workflows/` |
194
- | **KiloCode** | `~/.kilocode/skills/` | `~/.kilocode/workflows/` |
195
+ | **Antigravity** | `~/.gemini/config/skills/` | `~/.gemini/config/global_workflows/` |
196
+ | **Claude Code** | `~/.claude/skills/` | _(skills only)_ |
197
+ | **Codex** | `~/.codex/skills/` | _(skills only)_ |
198
+ | **Cursor** | `~/.cursor/skills/` | _(skills only)_ |
199
+ | **Kilo Code** | `~/.kilocode/skills/` | `~/.kilocode/workflows/` |
200
+ | **Pi / shared Agent Skills** | `~/.agents/skills/` | _(skills only)_ |
195
201
  | **Windsurf** | `~/.codeium/windsurf/skills/` | `~/.codeium/windsurf/global_workflows/` |
196
- | **Global agents-compatible CLIs** _(e.g., Codex, Gemini CLI)_ | `~/.agents/skills/` | _(skills only)_ |
197
- | **Cursor** | `~/.cursor/skills/` | _(uses rules)_ |
198
202
 
199
203
  ### CLI Commands
200
204
 
@@ -505,7 +509,7 @@ Externally sourced skills and optional Pi packages retain credit to their upstre
505
509
  - **Context7**: From [upstash/context7](https://github.com/upstash/context7) — fresh library documentation fetcher.
506
510
  - **Audit Website**: From [squirrelscan/skills](https://github.com/squirrelscan/skills) — professional website auditor.
507
511
  - **Convex Skills**: From [waynesutton/convexskills](https://github.com/waynesutton/convexskills) — complete Convex development suite including **Functions**, **Schema Validation**, **Realtime**, **Agents**, **File Storage**, **Migrations**, **HTTP Actions**, **Cron Jobs**, **Component Authoring**, **Best Practices**, **Security Audit**, **Security Check**, **Avoid Feature Creep**, and **Optimize Agent Context**.
508
- - **Gemini CLI**: Custom VibeCode skill for large-context processing with Gemini 3 Pro.
512
+ - **Anti-Gravity**: Custom skill for running Google's current Anti-Gravity CLI workflow for large-context review and analysis.
509
513
  - **Google Stitch Skills**: From [google-labs-code/stitch-skills](https://github.com/google-labs-code/stitch-skills) — Design-to-code suite including **design-md**, **enhance-prompt**, **stitch-loop**, **react-components**, and **shadcn-ui**.
510
514
  - **Jules**: From [sanjay3290/ai-skills](https://github.com/sanjay3290/ai-skills) — delegate coding tasks to Google Jules AI agent.
511
515
  - **Subagent Execution**: Built on **[`pi-subagents`](https://github.com/nicobailon/pi-subagents)** by **Nico Bailon** — providing the underlying Pi extension for delegated subagent runs (result rendering, live progress, single/parallel/chain execution, session/artifact handling, and related subagent tooling), upon which Takomi adds its own lifecycle orchestration, model-routing policy, and workflow metadata.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takomi",
3
- "version": "2.1.35",
3
+ "version": "2.1.36",
4
4
  "description": "🎯 Stop wrestling with AI. Start building with purpose. The artisan's toolkit for agent workflows, Codex skills, and original Takomi capabilities like 21st.dev integration.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  downloadDirectoryFromGitHub,
24
24
  } from './utils.js';
25
25
  import {
26
+ HARNESS_MAP,
26
27
  detectHarnesses,
27
28
  printHarnessStatus,
28
29
  syncToAllHarnesses,
@@ -393,6 +394,43 @@ async function promptSkillsInstallSelection() {
393
394
  return { mode: response.mode, selectedSkills };
394
395
  }
395
396
 
397
+ function normalizeHarnessSyncMode(value) {
398
+ return ['copy', 'symlink', 'auto'].includes(value) ? value : 'copy';
399
+ }
400
+
401
+ async function promptHarnessSyncMode(initial = 'auto') {
402
+ const normalizedInitial = normalizeHarnessSyncMode(initial);
403
+ const choices = [
404
+ { title: 'Auto (try symlink, copy if blocked)', value: 'auto', description: 'Best fallback when Windows permissions or filesystems block symlinks.' },
405
+ { title: 'Symlink / Junction (single source of truth)', value: 'symlink', description: 'Each managed skill/workflow points back to ~/.takomi; edits update one canonical copy.' },
406
+ { title: 'Copy (independent files)', value: 'copy', description: 'Most compatible; changes do not flow back to ~/.takomi.' },
407
+ ];
408
+ const response = await prompts({
409
+ type: 'select',
410
+ name: 'syncMode',
411
+ message: 'How should Takomi sync store resources into harness folders?',
412
+ choices,
413
+ initial: Math.max(0, choices.findIndex((choice) => choice.value === normalizedInitial)),
414
+ });
415
+ if (!response.syncMode) return null;
416
+ return normalizeHarnessSyncMode(response.syncMode);
417
+ }
418
+
419
+ function collectSyncFailures(syncSummary) {
420
+ return Object.entries(syncSummary || {})
421
+ .filter(([, result]) => result?.ok === false)
422
+ .map(([id, result]) => ({ id, errors: result.errors?.length ? result.errors : ['unknown sync error'] }));
423
+ }
424
+
425
+ function printSyncFailures(failures) {
426
+ if (!failures.length) return;
427
+ console.log(pc.yellow('\nSome harnesses did not fully sync:'));
428
+ for (const failure of failures) {
429
+ console.log(pc.dim(` - ${failure.id}: ${failure.errors.join('; ')}`));
430
+ }
431
+ process.exitCode = 1;
432
+ }
433
+
396
434
  async function installSkillsTarget() {
397
435
  console.log(pc.magenta('🧰 Takomi Skills Install\n'));
398
436
  try {
@@ -411,7 +449,7 @@ async function installSkillsTarget() {
411
449
  const result = await installBundledSkills(program.version(), selection);
412
450
  const validation = await validateSkillsInstall(selection.selectedSkills);
413
451
  printSkillsInstallSummary(result, validation);
414
- console.log(pc.dim('\nGlobal skills are ready for Pi or other supported harnesses.\n'));
452
+ console.log(pc.dim('\nShared skills target is ready. For per-harness global paths, run "takomi setup" or "takomi sync".\n'));
415
453
  } catch (error) {
416
454
  console.log(pc.red('\nSkills install failed.'));
417
455
  console.log(pc.dim(String(error?.message || error)));
@@ -622,7 +660,7 @@ async function install(target) {
622
660
 
623
661
  if (detected.length === 0) {
624
662
  console.log(pc.yellow(' No AI harnesses detected on this machine.'));
625
- console.log(pc.dim(' We support: Antigravity, KiloCode, Windsurf, Codex, Cursor, Gemini CLI'));
663
+ console.log(pc.dim(` We support: ${Object.values(HARNESS_MAP).map(h => h.name).join(', ')}`));
626
664
  console.log(pc.dim(' Run "takomi init" instead for per-project setup.\n'));
627
665
  return;
628
666
  }
@@ -715,6 +753,11 @@ async function install(target) {
715
753
 
716
754
  if (!contentResponse.content) return;
717
755
 
756
+ const syncMode = await promptHarnessSyncMode(
757
+ (hasExistingStoreSkills || hasExistingStoreWorkflows) ? existingStoreManifest.syncMode : 'auto'
758
+ );
759
+ if (!syncMode) return;
760
+
718
761
  try {
719
762
  // 4. Initialize global store
720
763
  console.log(pc.cyan(`\n📦 Creating global store (${STORE_PATH})...\n`));
@@ -802,11 +845,15 @@ async function install(target) {
802
845
  const syncSummary = await syncToAllHarnesses(selectedHarnesses, STORE_PATH, {
803
846
  useOwnership: true,
804
847
  owned: manifest.harnessOwned,
848
+ linkMode: syncMode,
805
849
  });
806
850
 
807
851
  // 7. Update manifest
852
+ const syncFailures = collectSyncFailures(syncSummary);
853
+ const successfulHarnesses = selectedHarnesses.filter(h => !syncFailures.some(failure => failure.id === h.id));
808
854
  manifest = await getManifest();
809
- manifest.linkedHarnesses = selectedHarnesses.map(h => h.id);
855
+ manifest.linkedHarnesses = successfulHarnesses.map(h => h.id);
856
+ manifest.syncMode = syncMode;
810
857
  manifest.installed.skills = await getStoreSkills();
811
858
  manifest.installed.workflows = await getStoreWorkflows();
812
859
  manifest.harnessOwned = manifest.harnessOwned || {};
@@ -814,11 +861,13 @@ async function install(target) {
814
861
  manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
815
862
  }
816
863
  await writeManifest(manifest);
864
+ printSyncFailures(syncFailures);
817
865
 
818
866
  // 8. Summary
819
- console.log(pc.magenta('\n✨ Your command center is live. ✨'));
867
+ console.log(pc.magenta(syncFailures.length ? '\n⚠ Your command center is partially synced. ⚠' : '\n✨ Your command center is live. ✨'));
820
868
  console.log(pc.white(`\n Store: ${STORE_PATH}`));
821
- console.log(pc.white(` Connected: ${selectedHarnesses.map(h => h.name).join(', ')}`));
869
+ console.log(pc.white(` Sync mode: ${syncMode}`));
870
+ console.log(pc.white(` Connected: ${successfulHarnesses.map(h => h.name).join(', ') || 'none'}`));
822
871
  console.log(pc.dim(`\n Add skills: takomi add <github-url>`));
823
872
  console.log(pc.dim(` Sync updates: takomi sync\n`));
824
873
 
@@ -882,23 +931,33 @@ async function sync(target) {
882
931
 
883
932
  const selectedHarnesses = detected.filter(h => response.harnesses.includes(h.id));
884
933
 
885
- console.log(pc.cyan('\n📡 Syncing from global store...\n'));
934
+ const envSyncMode = process.env.TAKOMI_HARNESS_SYNC_MODE;
935
+ const syncMode = normalizeHarnessSyncMode(envSyncMode || manifest.syncMode || 'copy');
936
+ console.log(pc.cyan(`\n📡 Syncing from global store (${syncMode})...\n`));
886
937
  const syncSummary = await syncToAllHarnesses(selectedHarnesses, STORE_PATH, {
887
938
  useOwnership: true,
888
939
  owned: manifest.harnessOwned,
940
+ linkMode: syncMode,
889
941
  });
890
942
 
891
943
  // Update manifest with current harnesses
892
- manifest.linkedHarnesses = [...new Set([...manifest.linkedHarnesses, ...response.harnesses])];
944
+ const syncFailures = collectSyncFailures(syncSummary);
945
+ const successfulHarnessIds = response.harnesses.filter(id => !syncFailures.some(failure => failure.id === id));
946
+ manifest.linkedHarnesses = [...new Set([...manifest.linkedHarnesses, ...successfulHarnessIds])];
947
+ if (!envSyncMode) manifest.syncMode = syncMode;
893
948
  manifest.harnessOwned = manifest.harnessOwned || {};
894
949
  for (const harness of selectedHarnesses) {
895
950
  manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
896
951
  }
897
952
  await writeManifest(manifest);
953
+ printSyncFailures(syncFailures);
898
954
 
899
955
  const skills = await getStoreSkills();
900
956
  const workflows = await getStoreWorkflows();
901
- console.log(pc.magenta(`\n✨ ${skills.length} skills and ${workflows.length} workflows synced to ${selectedHarnesses.length} IDE(s). Ready to build.\n`));
957
+ const syncedCount = successfulHarnessIds.length;
958
+ console.log(pc.magenta(syncFailures.length
959
+ ? `\n⚠ ${skills.length} skills and ${workflows.length} workflows partially synced to ${syncedCount}/${selectedHarnesses.length} IDE(s).\n`
960
+ : `\n✨ ${skills.length} skills and ${workflows.length} workflows synced to ${syncedCount} IDE(s). Ready to build.\n`));
902
961
  }
903
962
 
904
963
  // ─────────────────────────────────────────────────────────────────────────────
@@ -994,8 +1053,21 @@ async function add(url) {
994
1053
  if (syncResponse.doSync) {
995
1054
  const detected = detectHarnesses();
996
1055
  const linked = detected.filter(h => manifest.linkedHarnesses.includes(h.id));
997
- await syncToAllHarnesses(linked, STORE_PATH);
998
- console.log(pc.magenta(`\n✨ Live on ${linked.length} IDE(s). You're armed and ready.\n`));
1056
+ const syncSummary = await syncToAllHarnesses(linked, STORE_PATH, {
1057
+ useOwnership: true,
1058
+ owned: manifest.harnessOwned,
1059
+ linkMode: normalizeHarnessSyncMode(process.env.TAKOMI_HARNESS_SYNC_MODE || manifest.syncMode || 'copy'),
1060
+ });
1061
+ const syncFailures = collectSyncFailures(syncSummary);
1062
+ manifest.harnessOwned = manifest.harnessOwned || {};
1063
+ for (const harness of linked) {
1064
+ manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
1065
+ }
1066
+ await writeManifest(manifest);
1067
+ printSyncFailures(syncFailures);
1068
+ console.log(pc.magenta(syncFailures.length
1069
+ ? `\n⚠ Live on ${linked.length - syncFailures.length}/${linked.length} IDE(s); some harnesses need attention.\n`
1070
+ : `\n✨ Live on ${linked.length} IDE(s). You're armed and ready.\n`));
999
1071
  }
1000
1072
  } else {
1001
1073
  console.log(pc.dim('\n Run "takomi sync" when you want to push these to your IDEs.\n'));
@@ -1022,6 +1094,7 @@ async function harnesses() {
1022
1094
  console.log(pc.white(` Location: ${STORE_PATH}`));
1023
1095
  console.log(pc.white(` Skills: ${skills.length} ready to deploy`));
1024
1096
  console.log(pc.white(` Workflows: ${workflows.length} available`));
1097
+ console.log(pc.white(` Sync mode: ${manifest.syncMode || 'copy'}`));
1025
1098
  console.log(pc.white(` Connected: ${manifest.linkedHarnesses.join(', ') || 'none'}`));
1026
1099
  console.log(pc.dim(` Updated: ${manifest.updatedAt}\n`));
1027
1100
  } else {
@@ -1096,8 +1169,19 @@ async function updateProjectResources() {
1096
1169
  const detected = detectHarnesses();
1097
1170
  const linked = detected.filter(h => manifest.linkedHarnesses.includes(h.id));
1098
1171
  if (linked.length > 0) {
1099
- console.log(pc.cyan('\n📡 Auto-syncing to linked harnesses...\n'));
1100
- await syncToAllHarnesses(linked, STORE_PATH);
1172
+ const syncMode = normalizeHarnessSyncMode(process.env.TAKOMI_HARNESS_SYNC_MODE || manifest.syncMode || 'copy');
1173
+ console.log(pc.cyan(`\n📡 Auto-syncing to linked harnesses (${syncMode})...\n`));
1174
+ const syncSummary = await syncToAllHarnesses(linked, STORE_PATH, {
1175
+ useOwnership: true,
1176
+ owned: manifest.harnessOwned,
1177
+ linkMode: syncMode,
1178
+ });
1179
+ const syncFailures = collectSyncFailures(syncSummary);
1180
+ manifest.harnessOwned = manifest.harnessOwned || {};
1181
+ for (const harness of linked) {
1182
+ manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
1183
+ }
1184
+ printSyncFailures(syncFailures);
1101
1185
  }
1102
1186
  }
1103
1187
 
package/src/harness.js CHANGED
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import pc from 'picocolors';
5
- import { hashPath, normalizeOwnedMap, copyOwnedTree } from './owned-tree.js';
5
+ import { hashPath, normalizeOwnedMap, materializeOwnedTree } from './owned-tree.js';
6
6
 
7
7
  // ─── Cross-Platform Home Directory ───────────────────────────────────────────
8
8
  const HOME = os.homedir();
@@ -34,13 +34,26 @@ function getAppData() {
34
34
  export const HARNESS_MAP = {
35
35
  antigravity: {
36
36
  name: 'Antigravity',
37
- rootPath: path.join(HOME, '.gemini', 'antigravity'),
37
+ rootPath: path.join(HOME, '.gemini', 'config'),
38
38
  detect() {
39
39
  return fs.existsSync(this.rootPath);
40
40
  },
41
41
  targets: {
42
- skills: path.join(HOME, '.gemini', 'antigravity', 'skills'),
43
- workflows: path.join(HOME, '.gemini', 'antigravity', 'global_workflows'),
42
+ skills: path.join(HOME, '.gemini', 'config', 'skills'),
43
+ workflows: path.join(HOME, '.gemini', 'config', 'global_workflows'),
44
+ yamls: null,
45
+ },
46
+ },
47
+
48
+ claude_code: {
49
+ name: 'Claude Code',
50
+ rootPath: path.join(HOME, '.claude'),
51
+ detect() {
52
+ return fs.existsSync(this.rootPath);
53
+ },
54
+ targets: {
55
+ skills: path.join(HOME, '.claude', 'skills'),
56
+ workflows: null,
44
57
  yamls: null,
45
58
  },
46
59
  },
@@ -78,7 +91,20 @@ export const HARNESS_MAP = {
78
91
  name: 'Codex',
79
92
  rootPath: path.join(HOME, '.codex'),
80
93
  detect() {
81
- return fs.existsSync(this.rootPath);
94
+ return fs.existsSync(this.rootPath) || fs.existsSync('/etc/codex');
95
+ },
96
+ targets: {
97
+ skills: path.join(HOME, '.codex', 'skills'),
98
+ workflows: null,
99
+ yamls: null,
100
+ },
101
+ },
102
+
103
+ pi: {
104
+ name: 'Pi / shared Agent Skills',
105
+ rootPath: path.join(HOME, '.pi'),
106
+ detect() {
107
+ return fs.existsSync(this.rootPath) || fs.existsSync(path.join(HOME, '.agents'));
82
108
  },
83
109
  targets: {
84
110
  skills: path.join(HOME, '.agents', 'skills'),
@@ -100,19 +126,7 @@ export const HARNESS_MAP = {
100
126
  },
101
127
  },
102
128
 
103
- gemini_cli: {
104
- name: 'Gemini CLI',
105
- rootPath: path.join(HOME, '.gemini'),
106
- detect() {
107
- // Only match bare Gemini CLI — not Antigravity (which also lives under .gemini)
108
- return fs.existsSync(this.rootPath) && !fs.existsSync(path.join(HOME, '.gemini', 'antigravity'));
109
- },
110
- targets: {
111
- skills: path.join(HOME, '.agents', 'skills'),
112
- workflows: null,
113
- yamls: null,
114
- },
115
- },
129
+
116
130
  };
117
131
 
118
132
  // ─── Detection ───────────────────────────────────────────────────────────────
@@ -163,17 +177,14 @@ export function printHarnessStatus(detected) {
163
177
 
164
178
  /**
165
179
  * Syncs a source directory to a harness target path.
166
- * Uses hard copy (fs.copy) no symlinks.
180
+ * Supports copy, symlink/junction, or auto-fallback materialization.
181
+ * When ownership is supplied, it also prunes previous Takomi-owned items
182
+ * that are no longer in the source while preserving manual or modified files.
167
183
  *
168
184
  * @param {string} sourcePath - Source directory (e.g., ~/.takomi/skills/)
169
- * @param {string} targetPath - Destination directory (e.g., ~/.gemini/antigravity/skills/)
185
+ * @param {string} targetPath - Destination directory (e.g., ~/.gemini/config/skills/)
170
186
  * @param {string} label - Human-readable label for logging
171
- * @returns {Promise<number>} - Number of items copied
172
- */
173
- /**
174
- * Syncs a directory to a target path. When ownership is supplied, it also
175
- * prunes previous Takomi-owned items that are no longer in the source while
176
- * preserving manual or modified files.
187
+ * @returns {Promise<number|object>} - Number of items materialized, or details
177
188
  */
178
189
  export async function syncDirectory(sourcePath, targetPath, label = '', options = {}) {
179
190
  if (!targetPath) return options.returnDetails ? { copied: 0, pruned: 0, preservedManual: [], preservedModified: [], owned: {} } : 0;
@@ -187,7 +198,10 @@ export async function syncDirectory(sourcePath, targetPath, label = '', options
187
198
  const nextOwned = {};
188
199
  const preservedManual = [];
189
200
  const preservedModified = [];
201
+ const symlinkFallbacks = [];
202
+ const linkMode = options.linkMode || process.env.TAKOMI_HARNESS_SYNC_MODE || 'copy';
190
203
  let copied = 0;
204
+ let linked = 0;
191
205
  let pruned = 0;
192
206
 
193
207
  if (options.prune && Object.keys(previousOwned).length > 0) {
@@ -225,29 +239,35 @@ export async function syncDirectory(sourcePath, targetPath, label = '', options
225
239
  await fs.remove(dest);
226
240
  }
227
241
 
228
- await copyOwnedTree(src, dest);
242
+ const materialized = await materializeOwnedTree(src, dest, { linkMode });
229
243
  nextOwned[item] = {
230
244
  hash: await hashPath(dest),
231
245
  targetPath: dest,
246
+ sourcePath: path.resolve(src),
247
+ syncMethod: materialized.method,
232
248
  syncedAt: new Date().toISOString(),
233
249
  };
234
- copied++;
250
+ if (materialized.method === 'symlink') linked++;
251
+ else copied++;
252
+ if (materialized.fallbackError) symlinkFallbacks.push(`${item}: ${materialized.fallbackError}`);
235
253
  }
236
254
 
237
255
  if (label) {
238
256
  const suffix = pruned ? `, removed ${pruned}` : '';
239
- console.log(pc.green(` ✔ ${label} (${copied} items${suffix})`));
257
+ const methodSummary = linked ? `${linked} linked, ${copied} copied` : `${copied} copied`;
258
+ console.log(pc.green(` ✔ ${label} (${methodSummary}${suffix})`));
240
259
  if (preservedManual.length) console.log(pc.yellow(` Preserved manual items: ${preservedManual.join(', ')}`));
241
260
  if (preservedModified.length) console.log(pc.yellow(` Preserved modified Takomi-owned items: ${preservedModified.join(', ')}`));
261
+ if (symlinkFallbacks.length) console.log(pc.yellow(` Symlink fallback copies: ${symlinkFallbacks.join('; ')}`));
242
262
  }
243
263
 
244
- const details = { copied, pruned, preservedManual, preservedModified, owned: Object.fromEntries(Object.entries(nextOwned).sort(([a], [b]) => a.localeCompare(b))) };
245
- return options.returnDetails ? details : copied;
264
+ const details = { copied: copied + linked, linked, copyCount: copied, pruned, preservedManual, preservedModified, symlinkFallbacks, owned: Object.fromEntries(Object.entries(nextOwned).sort(([a], [b]) => a.localeCompare(b))) };
265
+ return options.returnDetails ? details : copied + linked;
246
266
  } catch (error) {
247
267
  if (label) {
248
268
  console.log(pc.red(` ✗ ${label}: ${error.message}`));
249
269
  }
250
- return options.returnDetails ? { copied: 0, pruned: 0, preservedManual: [], preservedModified: [], owned: normalizeOwnedMap(options.owned), ok: false, error: error.message } : 0;
270
+ return options.returnDetails ? { copied: 0, linked: 0, copyCount: 0, pruned: 0, preservedManual: [], preservedModified: [], symlinkFallbacks: [], owned: normalizeOwnedMap(options.owned), ok: false, error: error.message } : 0;
251
271
  }
252
272
  }
253
273
 
@@ -309,6 +329,7 @@ export async function syncToHarness(harness, storePath, options = {}) {
309
329
  preserveManual: useOwnership,
310
330
  prune: useOwnership,
311
331
  returnDetails: useOwnership,
332
+ linkMode: options.linkMode,
312
333
  },
313
334
  );
314
335
  if (useOwnership) {
@@ -335,6 +356,7 @@ export async function syncToHarness(harness, storePath, options = {}) {
335
356
  preserveManual: useOwnership,
336
357
  prune: useOwnership,
337
358
  returnDetails: useOwnership,
359
+ linkMode: options.linkMode,
338
360
  },
339
361
  );
340
362
  if (useOwnership) {
package/src/owned-tree.js CHANGED
@@ -84,3 +84,31 @@ export async function copyOwnedTree(src, dest, options = {}) {
84
84
  },
85
85
  });
86
86
  }
87
+
88
+ export async function linkOwnedTree(src, dest) {
89
+ const resolvedSrc = path.resolve(src);
90
+ const stat = await fs.lstat(resolvedSrc);
91
+ const type = stat.isDirectory()
92
+ ? (process.platform === 'win32' ? 'junction' : 'dir')
93
+ : 'file';
94
+ await fs.ensureDir(path.dirname(dest));
95
+ await fs.symlink(resolvedSrc, dest, type);
96
+ }
97
+
98
+ export async function materializeOwnedTree(src, dest, options = {}) {
99
+ const linkMode = options.linkMode || 'copy';
100
+ if (linkMode === 'symlink' || linkMode === 'auto') {
101
+ try {
102
+ await linkOwnedTree(src, dest);
103
+ return { method: 'symlink' };
104
+ } catch (error) {
105
+ if (linkMode === 'symlink') throw error;
106
+ await fs.remove(dest).catch(() => {});
107
+ await copyOwnedTree(src, dest, options.copyOptions || {});
108
+ return { method: 'copy', fallbackError: error.message };
109
+ }
110
+ }
111
+
112
+ await copyOwnedTree(src, dest, options.copyOptions || {});
113
+ return { method: 'copy' };
114
+ }
package/src/pi-harness.js CHANGED
@@ -15,6 +15,28 @@ function getPathEntries() {
15
15
  }
16
16
 
17
17
  function commandExists(command) {
18
+ const pathEntries = getPathEntries();
19
+
20
+ // Avoid shelling out on the hot launch path. On Windows, `where pi` is often
21
+ // ~100ms by itself; a direct PATH scan is much cheaper and good enough for
22
+ // npm/cmd shims. Keep `where`/`which` as a fallback for edge cases.
23
+ const manualCandidates = process.platform === 'win32'
24
+ ? (() => {
25
+ const hasExtension = /\.(cmd|exe|bat|com)$/i.test(command);
26
+ const extensions = hasExtension
27
+ ? ['']
28
+ : (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
29
+ .split(';')
30
+ .filter(Boolean)
31
+ .sort((a, b) => (a.toLowerCase() === '.cmd' ? -1 : b.toLowerCase() === '.cmd' ? 1 : 0));
32
+ return pathEntries.flatMap((entry) => extensions.map((extension) => path.join(entry, `${command}${extension.toLowerCase()}`)));
33
+ })()
34
+ : pathEntries.map((entry) => path.join(entry, command));
35
+
36
+ for (const candidate of manualCandidates) {
37
+ if (fs.existsSync(candidate)) return { found: true, path: candidate };
38
+ }
39
+
18
40
  const probe = process.platform === 'win32'
19
41
  ? spawnSync('where', [command], { stdio: 'pipe', encoding: 'utf8' })
20
42
  : spawnSync('which', [command], { stdio: 'pipe', encoding: 'utf8' });
@@ -27,11 +49,6 @@ function commandExists(command) {
27
49
  return { found: true, path: preferred || matches[0] || null };
28
50
  }
29
51
 
30
- for (const entry of getPathEntries()) {
31
- const candidate = path.join(entry, process.platform === 'win32' ? `${command}.cmd` : command);
32
- if (fs.existsSync(candidate)) return { found: true, path: candidate };
33
- }
34
-
35
52
  return { found: false, path: null };
36
53
  }
37
54
 
@@ -140,12 +157,17 @@ export async function inspectPiSubagentsDependency(home = HOME) {
140
157
  };
141
158
  }
142
159
 
143
- export async function detectPiCommand() {
160
+ export async function detectPiCommand(options = {}) {
161
+ const { includeVersion = true } = options;
144
162
  const binary = commandExists('pi');
145
163
  if (!binary.found) {
146
164
  return { installed: false, path: null, version: null };
147
165
  }
148
166
 
167
+ if (!includeVersion) {
168
+ return { installed: true, path: binary.path, version: null };
169
+ }
170
+
149
171
  const versionProbe = process.platform === 'win32'
150
172
  ? spawnSync('powershell', ['-NoProfile', '-Command', `& '${(binary.path || 'pi').replace(/'/g, "''")}' --version`], { stdio: 'pipe', encoding: 'utf8' })
151
173
  : spawnSync(binary.path || 'pi', ['--version'], { stdio: 'pipe', encoding: 'utf8' });
@@ -381,7 +403,7 @@ export async function updatePiManagedPackages({ timeoutMs } = {}) {
381
403
  return { ok: true, changed: false, report: 'Skipped Pi extension package update because TAKOMI_SKIP_PI_PACKAGE_UPDATE=1.' };
382
404
  }
383
405
 
384
- const pi = await detectPiCommand();
406
+ const pi = await detectPiCommand({ includeVersion: false });
385
407
  if (!pi.installed) return { ok: false, changed: false, report: 'Pi is not installed.' };
386
408
 
387
409
  // Only reconcile Pi-managed extension/package entries here. Plain `pi update`
@@ -458,14 +480,22 @@ function printFirstRunGuidance(reason) {
458
480
  }
459
481
 
460
482
  export async function launchTakomiHarness(cwd = process.cwd()) {
461
- const report = await inspectPiHarnessEnvironment(cwd);
462
-
463
- if (!report.pi.installed) {
483
+ // Keep the common `takomi` path lean. A full environment inspection shells out
484
+ // to `pi --version`, which costs multiple seconds on Windows before Pi even
485
+ // starts. For launch we only need to know the command exists and the required
486
+ // global Takomi extensions are present; `takomi doctor` still performs the
487
+ // complete diagnostic check.
488
+ const [pi, installed] = await Promise.all([
489
+ detectPiCommand({ includeVersion: false }),
490
+ inspectInstalledTakomiPiHarness(),
491
+ ]);
492
+
493
+ if (!pi.installed) {
464
494
  printFirstRunGuidance('Pi is not installed yet.');
465
495
  return 1;
466
496
  }
467
497
 
468
- if (!report.installed.runtimeInstalled || !report.installed.subagentsInstalled) {
498
+ if (!installed.runtimeInstalled || !installed.subagentsInstalled) {
469
499
  printFirstRunGuidance('Takomi Pi harness is not fully installed yet.');
470
500
  return 1;
471
501
  }
@@ -473,22 +503,21 @@ export async function launchTakomiHarness(cwd = process.cwd()) {
473
503
  const env = {
474
504
  ...process.env,
475
505
  TAKOMI_HARNESS: '1',
506
+ // Avoid Pi's blocking startup version check on this wrapped launch path.
507
+ // Takomi runtime schedules its own UI-safe delayed check after session_start.
508
+ PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK ?? '1',
509
+ TAKOMI_DELAYED_PI_VERSION_CHECK: process.env.TAKOMI_DELAYED_PI_VERSION_CHECK ?? '1',
476
510
  };
477
511
 
478
512
  return await new Promise((resolve) => {
479
- const child = process.platform === 'win32'
480
- ? spawn('cmd.exe', ['/d', '/s', '/c', 'pi'], {
481
- cwd,
482
- stdio: 'inherit',
483
- env,
484
- shell: false,
485
- })
486
- : spawn(report.pi.path || 'pi', [], {
487
- cwd,
488
- stdio: 'inherit',
489
- env,
490
- shell: false,
491
- });
513
+ const resolved = resolveCommandForSpawn(pi.path || 'pi', []);
514
+ const child = spawn(resolved.command, resolved.args, {
515
+ cwd,
516
+ stdio: 'inherit',
517
+ env,
518
+ shell: false,
519
+ windowsHide: false,
520
+ });
492
521
 
493
522
  child.on('close', (code) => resolve(code ?? 0));
494
523
  child.on('error', () => resolve(1));
package/src/store.js CHANGED
@@ -17,6 +17,7 @@ function createDefaultManifest() {
17
17
  createdAt: new Date().toISOString(),
18
18
  updatedAt: new Date().toISOString(),
19
19
  linkedHarnesses: [],
20
+ syncMode: 'copy',
20
21
  installed: {
21
22
  skills: [],
22
23
  workflows: [],
@@ -38,6 +39,7 @@ function normalizeManifest(manifest) {
38
39
  workflows: normalizeOwnedMap(manifest?.bundledOwned?.workflows),
39
40
  };
40
41
  normalized.harnessOwned = manifest?.harnessOwned || {};
42
+ normalized.syncMode = manifest?.syncMode || 'copy';
41
43
  return normalized;
42
44
  }
43
45