takomi 2.1.35 → 2.1.37

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 (33) hide show
  1. package/.pi/README.md +6 -3
  2. package/.pi/extensions/oauth-router/commands.ts +36 -35
  3. package/.pi/extensions/oauth-router/index.ts +8 -8
  4. package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +40 -7
  5. package/.pi/extensions/takomi-context-manager/diagnostics.ts +534 -55
  6. package/.pi/extensions/takomi-context-manager/index.ts +14 -2
  7. package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +16 -4
  8. package/.pi/extensions/takomi-context-manager/policy-tools.ts +7 -0
  9. package/.pi/extensions/takomi-context-manager/prerequisite-gates.ts +2 -0
  10. package/.pi/extensions/takomi-context-manager/session-state.ts +160 -0
  11. package/.pi/extensions/takomi-context-manager/skill-registry.ts +120 -0
  12. package/.pi/extensions/takomi-context-manager/skill-tools.ts +26 -3
  13. package/.pi/extensions/takomi-context-manager/state.ts +11 -1
  14. package/.pi/extensions/takomi-context-manager/types.ts +9 -1
  15. package/.pi/extensions/takomi-runtime/command-text.ts +2 -1
  16. package/.pi/extensions/takomi-runtime/commands.ts +13 -1
  17. package/.pi/extensions/takomi-runtime/index.ts +68 -1
  18. package/.pi/extensions/takomi-runtime/ui.ts +3 -3
  19. package/.pi/extensions/takomi-subagents/index.ts +37 -4
  20. package/.pi/extensions/takomi-subagents/native-render.ts +84 -7
  21. package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +18 -3
  22. package/.pi/extensions/takomi-subagents/tool-runner.ts +52 -5
  23. package/README.md +14 -7
  24. package/assets/.agent/skills/exam-creator-skill/SKILL.md +182 -0
  25. package/assets/.agent/skills/exam-creator-skill/references/model-routing-policy.md +48 -0
  26. package/assets/.agent/skills/exam-creator-skill/references/workflow-rulebook.md +115 -0
  27. package/package.json +2 -2
  28. package/src/cli.js +134 -13
  29. package/src/doctor.js +7 -1
  30. package/src/harness.js +54 -32
  31. package/src/owned-tree.js +28 -0
  32. package/src/pi-harness.js +149 -24
  33. package/src/store.js +2 -0
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
 
@@ -102,6 +119,100 @@ function getGlobalNodeModulesRoot(home = HOME) {
102
119
  return path.join(home, '.npm-global', 'lib', 'node_modules');
103
120
  }
104
121
 
122
+ async function readJsonIfExists(filePath) {
123
+ try {
124
+ if (await fs.pathExists(filePath)) return await fs.readJson(filePath);
125
+ } catch {}
126
+ return null;
127
+ }
128
+
129
+ async function writeJsonWithBackup(filePath, value, backupLabel = 'takomi-backup') {
130
+ await fs.ensureDir(path.dirname(filePath));
131
+ if (await fs.pathExists(filePath)) {
132
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
133
+ await fs.copy(filePath, `${filePath}.${backupLabel}-${stamp}`);
134
+ }
135
+ await fs.writeJson(filePath, value, { spaces: 2 });
136
+ }
137
+
138
+ function removeRawPiSubagentsPackageEntries(settings) {
139
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) return { changed: false, settings };
140
+ if (!Array.isArray(settings.packages)) return { changed: false, settings };
141
+ const before = settings.packages.length;
142
+ const nextPackages = settings.packages.filter((entry) => entry !== 'npm:pi-subagents' && entry !== 'pi-subagents');
143
+ if (nextPackages.length === before) return { changed: false, settings };
144
+ return { changed: true, settings: { ...settings, packages: nextPackages } };
145
+ }
146
+
147
+ export async function inspectRawPiSubagentsActivation({ cwd = process.cwd(), home = HOME } = {}) {
148
+ const user = getPiGlobalTargets(home);
149
+ const project = getProjectPiTargets(cwd);
150
+ const legacyExtension = path.join(user.extensions, 'subagent');
151
+ const findings = [];
152
+
153
+ for (const [scope, filePath] of [['user', user.settings], ['project', project.settings]]) {
154
+ const settings = await readJsonIfExists(filePath);
155
+ const packages = Array.isArray(settings?.packages) ? settings.packages : [];
156
+ if (packages.includes('npm:pi-subagents') || packages.includes('pi-subagents')) {
157
+ findings.push({ type: 'settings-package', scope, path: filePath });
158
+ }
159
+ }
160
+
161
+ if (await fs.pathExists(path.join(legacyExtension, 'index.ts')) || await fs.pathExists(path.join(legacyExtension, 'index.js'))) {
162
+ findings.push({ type: 'legacy-extension', scope: 'user', path: legacyExtension });
163
+ }
164
+
165
+ return { findings };
166
+ }
167
+
168
+ export async function disableRawPiSubagentsActivation({ cwd = process.cwd(), home = HOME } = {}) {
169
+ const user = getPiGlobalTargets(home);
170
+ const project = getProjectPiTargets(cwd);
171
+ const actions = [];
172
+
173
+ for (const [scope, filePath] of [['user', user.settings], ['project', project.settings]]) {
174
+ const settings = await readJsonIfExists(filePath);
175
+ const result = removeRawPiSubagentsPackageEntries(settings);
176
+ if (result.changed) {
177
+ await writeJsonWithBackup(filePath, result.settings, 'takomi-disable-raw-pi-subagents');
178
+ actions.push({ type: 'settings-package-removed', scope, path: filePath });
179
+ }
180
+ }
181
+
182
+ const legacyExtension = path.join(user.extensions, 'subagent');
183
+ if (await fs.pathExists(path.join(legacyExtension, 'index.ts')) || await fs.pathExists(path.join(legacyExtension, 'index.js'))) {
184
+ const disabledRoot = path.join(user.root, 'disabled-extensions');
185
+ await fs.ensureDir(disabledRoot);
186
+ let dest = path.join(disabledRoot, 'subagent');
187
+ if (await fs.pathExists(dest)) {
188
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
189
+ dest = path.join(disabledRoot, `subagent-${stamp}`);
190
+ }
191
+ await fs.move(legacyExtension, dest, { overwrite: false });
192
+ actions.push({ type: 'legacy-extension-moved', scope: 'user', path: legacyExtension, disabledPath: dest });
193
+ }
194
+
195
+ return { actions };
196
+ }
197
+
198
+ export function formatRawPiSubagentsFindings(findings = []) {
199
+ if (!findings.length) return 'No raw pi-subagents activation found.';
200
+ return findings.map((finding) => {
201
+ if (finding.type === 'settings-package') return `- ${finding.scope} settings package entry: ${finding.path}`;
202
+ if (finding.type === 'legacy-extension') return `- legacy raw subagent extension: ${finding.path}`;
203
+ return `- ${finding.path || JSON.stringify(finding)}`;
204
+ }).join('\n');
205
+ }
206
+
207
+ export function formatRawPiSubagentsDisableActions(actions = []) {
208
+ if (!actions.length) return 'No raw pi-subagents activation changes were needed.';
209
+ return actions.map((action) => {
210
+ if (action.type === 'settings-package-removed') return `- removed raw pi-subagents package from ${action.scope} settings: ${action.path}`;
211
+ if (action.type === 'legacy-extension-moved') return `- moved raw subagent extension out of Pi extensions: ${action.path} -> ${action.disabledPath}`;
212
+ return `- ${action.path || JSON.stringify(action)}`;
213
+ }).join('\n');
214
+ }
215
+
105
216
  export async function inspectPiSubagentsDependency(home = HOME) {
106
217
  const pkg = await getPackageJson();
107
218
  const declaredVersion = pkg?.dependencies?.['pi-subagents'] || null;
@@ -140,12 +251,17 @@ export async function inspectPiSubagentsDependency(home = HOME) {
140
251
  };
141
252
  }
142
253
 
143
- export async function detectPiCommand() {
254
+ export async function detectPiCommand(options = {}) {
255
+ const { includeVersion = true } = options;
144
256
  const binary = commandExists('pi');
145
257
  if (!binary.found) {
146
258
  return { installed: false, path: null, version: null };
147
259
  }
148
260
 
261
+ if (!includeVersion) {
262
+ return { installed: true, path: binary.path, version: null };
263
+ }
264
+
149
265
  const versionProbe = process.platform === 'win32'
150
266
  ? spawnSync('powershell', ['-NoProfile', '-Command', `& '${(binary.path || 'pi').replace(/'/g, "''")}' --version`], { stdio: 'pipe', encoding: 'utf8' })
151
267
  : spawnSync(binary.path || 'pi', ['--version'], { stdio: 'pipe', encoding: 'utf8' });
@@ -381,7 +497,7 @@ export async function updatePiManagedPackages({ timeoutMs } = {}) {
381
497
  return { ok: true, changed: false, report: 'Skipped Pi extension package update because TAKOMI_SKIP_PI_PACKAGE_UPDATE=1.' };
382
498
  }
383
499
 
384
- const pi = await detectPiCommand();
500
+ const pi = await detectPiCommand({ includeVersion: false });
385
501
  if (!pi.installed) return { ok: false, changed: false, report: 'Pi is not installed.' };
386
502
 
387
503
  // Only reconcile Pi-managed extension/package entries here. Plain `pi update`
@@ -425,6 +541,7 @@ export async function inspectPiHarnessEnvironment(cwd = process.cwd()) {
425
541
  const bundled = await inspectBundledPiAssets();
426
542
  const installed = await inspectInstalledTakomiPiHarness();
427
543
  const piSubagents = await inspectPiSubagentsDependency();
544
+ const rawPiSubagentsActivation = await inspectRawPiSubagentsActivation({ cwd });
428
545
  const project = getProjectPiTargets(cwd);
429
546
 
430
547
  return {
@@ -432,6 +549,7 @@ export async function inspectPiHarnessEnvironment(cwd = process.cwd()) {
432
549
  bundled,
433
550
  installed,
434
551
  piSubagents,
552
+ rawPiSubagentsActivation,
435
553
  project: {
436
554
  targets: project,
437
555
  settingsPresent: await fs.pathExists(project.settings),
@@ -458,14 +576,22 @@ function printFirstRunGuidance(reason) {
458
576
  }
459
577
 
460
578
  export async function launchTakomiHarness(cwd = process.cwd()) {
461
- const report = await inspectPiHarnessEnvironment(cwd);
462
-
463
- if (!report.pi.installed) {
579
+ // Keep the common `takomi` path lean. A full environment inspection shells out
580
+ // to `pi --version`, which costs multiple seconds on Windows before Pi even
581
+ // starts. For launch we only need to know the command exists and the required
582
+ // global Takomi extensions are present; `takomi doctor` still performs the
583
+ // complete diagnostic check.
584
+ const [pi, installed] = await Promise.all([
585
+ detectPiCommand({ includeVersion: false }),
586
+ inspectInstalledTakomiPiHarness(),
587
+ ]);
588
+
589
+ if (!pi.installed) {
464
590
  printFirstRunGuidance('Pi is not installed yet.');
465
591
  return 1;
466
592
  }
467
593
 
468
- if (!report.installed.runtimeInstalled || !report.installed.subagentsInstalled) {
594
+ if (!installed.runtimeInstalled || !installed.subagentsInstalled) {
469
595
  printFirstRunGuidance('Takomi Pi harness is not fully installed yet.');
470
596
  return 1;
471
597
  }
@@ -473,22 +599,21 @@ export async function launchTakomiHarness(cwd = process.cwd()) {
473
599
  const env = {
474
600
  ...process.env,
475
601
  TAKOMI_HARNESS: '1',
602
+ // Avoid Pi's blocking startup version check on this wrapped launch path.
603
+ // Takomi runtime schedules its own UI-safe delayed check after session_start.
604
+ PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK ?? '1',
605
+ TAKOMI_DELAYED_PI_VERSION_CHECK: process.env.TAKOMI_DELAYED_PI_VERSION_CHECK ?? '1',
476
606
  };
477
607
 
478
608
  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
- });
609
+ const resolved = resolveCommandForSpawn(pi.path || 'pi', []);
610
+ const child = spawn(resolved.command, resolved.args, {
611
+ cwd,
612
+ stdio: 'inherit',
613
+ env,
614
+ shell: false,
615
+ windowsHide: false,
616
+ });
492
617
 
493
618
  child.on('close', (code) => resolve(code ?? 0));
494
619
  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