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.
- package/.pi/README.md +6 -3
- package/.pi/extensions/oauth-router/commands.ts +36 -35
- package/.pi/extensions/oauth-router/index.ts +8 -8
- package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +40 -7
- package/.pi/extensions/takomi-context-manager/diagnostics.ts +534 -55
- package/.pi/extensions/takomi-context-manager/index.ts +14 -2
- package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +16 -4
- package/.pi/extensions/takomi-context-manager/policy-tools.ts +7 -0
- package/.pi/extensions/takomi-context-manager/prerequisite-gates.ts +2 -0
- package/.pi/extensions/takomi-context-manager/session-state.ts +160 -0
- package/.pi/extensions/takomi-context-manager/skill-registry.ts +120 -0
- package/.pi/extensions/takomi-context-manager/skill-tools.ts +26 -3
- package/.pi/extensions/takomi-context-manager/state.ts +11 -1
- package/.pi/extensions/takomi-context-manager/types.ts +9 -1
- package/.pi/extensions/takomi-runtime/command-text.ts +2 -1
- package/.pi/extensions/takomi-runtime/commands.ts +13 -1
- package/.pi/extensions/takomi-runtime/index.ts +68 -1
- package/.pi/extensions/takomi-runtime/ui.ts +3 -3
- package/.pi/extensions/takomi-subagents/index.ts +37 -4
- package/.pi/extensions/takomi-subagents/native-render.ts +84 -7
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +18 -3
- package/.pi/extensions/takomi-subagents/tool-runner.ts +52 -5
- package/README.md +14 -7
- package/assets/.agent/skills/exam-creator-skill/SKILL.md +182 -0
- package/assets/.agent/skills/exam-creator-skill/references/model-routing-policy.md +48 -0
- package/assets/.agent/skills/exam-creator-skill/references/workflow-rulebook.md +115 -0
- package/package.json +2 -2
- package/src/cli.js +134 -13
- package/src/doctor.js +7 -1
- package/src/harness.js +54 -32
- package/src/owned-tree.js +28 -0
- package/src/pi-harness.js +149 -24
- package/src/store.js +2 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Exam Creator Model Routing Policy
|
|
2
|
+
|
|
3
|
+
Use this routing policy for the exam-creation workflow unless the user overrides it.
|
|
4
|
+
|
|
5
|
+
## Terminology Rule
|
|
6
|
+
|
|
7
|
+
When the user says **Gemini**, interpret that as **Gemini via the agy CLI** unless the user explicitly says otherwise.
|
|
8
|
+
|
|
9
|
+
## Default Routing
|
|
10
|
+
|
|
11
|
+
- **Orchestrator:** parent assistant / orchestrator
|
|
12
|
+
- **Drafting agent:** **Gemini via agy CLI**
|
|
13
|
+
- **Marking agent:** **Gemini via agy CLI**
|
|
14
|
+
- **Verification agent:** **GPT**
|
|
15
|
+
|
|
16
|
+
## Why This Is The Default
|
|
17
|
+
|
|
18
|
+
- Drafting and marking are the highest-volume steps.
|
|
19
|
+
- Gemini is preferred there because it is cheaper and faster.
|
|
20
|
+
- Verification is the quality gate, so GPT is preferred for the final correctness check.
|
|
21
|
+
|
|
22
|
+
## Escalation Rule For Drafting
|
|
23
|
+
|
|
24
|
+
Upgrade the **drafting agent** from Gemini via agy CLI to **GPT 5.4 High** when any of these are true:
|
|
25
|
+
|
|
26
|
+
- the source notes are messy or inconsistent
|
|
27
|
+
- the subject requires tighter wording judgment
|
|
28
|
+
- ambiguity risk is high
|
|
29
|
+
- the diagram/question structure is more delicate than usual
|
|
30
|
+
- the paper is Mathematics or English and extra care is desired
|
|
31
|
+
|
|
32
|
+
## Practical Modes
|
|
33
|
+
|
|
34
|
+
### Default cheap/fast mode
|
|
35
|
+
- Orchestrator → parent assistant
|
|
36
|
+
- Drafting → Gemini via agy CLI
|
|
37
|
+
- Marking → Gemini via agy CLI
|
|
38
|
+
- Verification → GPT
|
|
39
|
+
|
|
40
|
+
### Escalated drafting mode
|
|
41
|
+
- Orchestrator → parent assistant
|
|
42
|
+
- Drafting → GPT 5.4 High
|
|
43
|
+
- Marking → Gemini via agy CLI
|
|
44
|
+
- Verification → GPT
|
|
45
|
+
|
|
46
|
+
## Guiding Principle
|
|
47
|
+
|
|
48
|
+
Use **Gemini via agy CLI for drafting and marking by default**, **GPT for verification**, and only upgrade drafting to **GPT 5.4 High** when the paper is tricky enough to justify the extra cost.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# OBHIS Exam Workflow Rulebook
|
|
2
|
+
|
|
3
|
+
This reference captures the current house style agreed during the Mathematics paper workflow.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
Use this rulebook for OBHIS / Olive Blessed Crest Academy exam generation unless the user gives a newer rule.
|
|
8
|
+
|
|
9
|
+
## Terminology Rule
|
|
10
|
+
|
|
11
|
+
When the user says **Gemini**, interpret that as **Gemini via the agy CLI** unless the user explicitly says otherwise.
|
|
12
|
+
|
|
13
|
+
## Workflow Summary
|
|
14
|
+
|
|
15
|
+
1. Gather source materials.
|
|
16
|
+
2. For photographed handwritten pages, run a multimodal-first extraction pass before OCR:
|
|
17
|
+
- use direct model vision on the image files
|
|
18
|
+
- batch images in small ordered groups, usually 3-5 images per model task
|
|
19
|
+
- explicitly forbid OCR/Tesseract/Python preprocessing on the first pass
|
|
20
|
+
- use OCR only as fallback when direct vision is unavailable or fails for a specific page
|
|
21
|
+
3. Confirm subject-specific mark pattern.
|
|
22
|
+
4. Create clean folder structure.
|
|
23
|
+
5. Draft question paper.
|
|
24
|
+
6. Build marking scheme only when requested.
|
|
25
|
+
7. Independently verify.
|
|
26
|
+
8. Incorporate user feedback.
|
|
27
|
+
9. Rerender deliverables.
|
|
28
|
+
|
|
29
|
+
## Folder Structure
|
|
30
|
+
|
|
31
|
+
Preferred output structure:
|
|
32
|
+
|
|
33
|
+
- `Questions/`
|
|
34
|
+
- `questions.md`
|
|
35
|
+
- final exam `.docx`
|
|
36
|
+
- `assets/`
|
|
37
|
+
- all SVG diagrams
|
|
38
|
+
- all PNG diagram copies used for Word export
|
|
39
|
+
- `Marking Scheme/` only when requested
|
|
40
|
+
- `marking-scheme.md`
|
|
41
|
+
- `objective-validation.md`
|
|
42
|
+
- `verification-report.md`
|
|
43
|
+
- marking-scheme `.docx` if needed
|
|
44
|
+
|
|
45
|
+
## Subject Rules
|
|
46
|
+
|
|
47
|
+
### Mathematics and English
|
|
48
|
+
|
|
49
|
+
Default pattern:
|
|
50
|
+
|
|
51
|
+
- exam total = **40 marks**
|
|
52
|
+
- objective count = **50**
|
|
53
|
+
- objective marks = **0.5 each**
|
|
54
|
+
- objective total = **25 marks**
|
|
55
|
+
- theory questions = **5**
|
|
56
|
+
- answer any = **3**
|
|
57
|
+
- theory total = **15 marks**
|
|
58
|
+
- no separate short-answer section by default
|
|
59
|
+
|
|
60
|
+
### Other Subjects
|
|
61
|
+
|
|
62
|
+
Current preference:
|
|
63
|
+
|
|
64
|
+
- may still use open-ended and theory sections
|
|
65
|
+
- still intended to fit the 40-mark exam system
|
|
66
|
+
- exact split should be clarified before final generation if not explicitly stated
|
|
67
|
+
|
|
68
|
+
## Layout Rules
|
|
69
|
+
|
|
70
|
+
- use **Times New Roman**
|
|
71
|
+
- default body size **12 pt**
|
|
72
|
+
- narrow margins
|
|
73
|
+
- compact layout
|
|
74
|
+
- header must be 1-column and should include: school name, academic session, term/exam title, pupil name/date, examination number/time, class, arm, subject, and instructions
|
|
75
|
+
- body is **1-column by default**
|
|
76
|
+
- use 2-column/3-column body only when a dense objective paper genuinely needs it for fit; do not apply columns mechanically to every subject
|
|
77
|
+
- headings should be bold and black, not oversized
|
|
78
|
+
- objective options should appear inline or compactly, not stacked unless unavoidable
|
|
79
|
+
- remove `Total Marks` line unless requested
|
|
80
|
+
- try to keep each paper around **2 pages** where practical
|
|
81
|
+
|
|
82
|
+
## Diagram Rules
|
|
83
|
+
|
|
84
|
+
- store diagrams in `Questions/assets/`
|
|
85
|
+
- simple line/geometry diagrams: prefer SVG masters plus PNG export copies
|
|
86
|
+
- pictorial nursery/primary objects (mango, pot, bucket, moon, table, cup, animals, classroom objects, etc.): prefer Anti-Gravity image generation or another image-generation path instead of rough SVG-only drawings
|
|
87
|
+
- Anti-Gravity can generate images; when using it, instruct whether to generate images, use SVG, or use a hybrid method, and require a report of the method used
|
|
88
|
+
- do not include answer labels from the teacher’s source in the student-facing diagram, e.g. do not label the keyboard as “Keyboard” when the question asks pupils to identify it
|
|
89
|
+
- keep separate source drawings as separate assets unless the question explicitly presents them as one grouped diagram; this helps preserve order and layout
|
|
90
|
+
- for diagram-heavy papers, rebuild the draft directly from the original tagged images instead of relying only on extraction summaries
|
|
91
|
+
- do not show worked/grouped answers in division or counting diagrams when the child is supposed to solve them
|
|
92
|
+
- if pupils are asked to colour an object or flag, leave it as an outline unless the exam explicitly asks for a sample
|
|
93
|
+
- labels must be readable when labels are part of the prompt rather than the answer
|
|
94
|
+
- angle values must not be hidden by edges
|
|
95
|
+
- move text away from lines when needed
|
|
96
|
+
- rerender diagrams if the user reports visibility problems
|
|
97
|
+
|
|
98
|
+
## Review Rules
|
|
99
|
+
|
|
100
|
+
- objective answer validation is mandatory when a marking scheme or answer key is requested
|
|
101
|
+
- marking scheme must match final numbering when requested
|
|
102
|
+
- verification should independently confirm correctness
|
|
103
|
+
- for handwritten sources, infer unclear text aggressively from context before marking it unclear
|
|
104
|
+
- any remaining ambiguous handwriting must be captured in a user-review list with image filename/page reference, question number, and candidate interpretations
|
|
105
|
+
- if a wording ambiguity is found, fix the paper and then realign the scheme when a scheme exists
|
|
106
|
+
|
|
107
|
+
## Feedback Rules
|
|
108
|
+
|
|
109
|
+
- user feedback overrides previous assumptions
|
|
110
|
+
- update future papers to follow the corrected rule
|
|
111
|
+
- if a DOCX is locked, save a versioned file such as `v2`
|
|
112
|
+
|
|
113
|
+
## Notes
|
|
114
|
+
|
|
115
|
+
This rulebook is intentionally opinionated because it represents Johno's workflow. Keep it flexible enough to ask follow-up questions whenever a new subject or mark pattern does not clearly fit the stored defaults.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "takomi",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.37",
|
|
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": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"commander": "^13.1.0",
|
|
51
51
|
"figlet": "^1.8.0",
|
|
52
52
|
"fs-extra": "^11.3.0",
|
|
53
|
-
"pi-subagents": "0.
|
|
53
|
+
"pi-subagents": "0.31.0",
|
|
54
54
|
"picocolors": "^1.1.1",
|
|
55
55
|
"prompts": "^2.4.2"
|
|
56
56
|
},
|
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,
|
|
@@ -43,7 +44,7 @@ import {
|
|
|
43
44
|
isStoreInitialized,
|
|
44
45
|
} from './store.js';
|
|
45
46
|
import { runDoctor } from './doctor.js';
|
|
46
|
-
import { ensurePiInstalled, ensurePiSubagentsInstalled, launchTakomiHarness, printPiInstallResult, printPiSubagentsInstallResult, updatePiCliPackage, updatePiManagedPackages, printPiManagedPackageUpdateResult } from './pi-harness.js';
|
|
47
|
+
import { disableRawPiSubagentsActivation, ensurePiInstalled, ensurePiSubagentsInstalled, formatRawPiSubagentsDisableActions, formatRawPiSubagentsFindings, inspectRawPiSubagentsActivation, launchTakomiHarness, printPiInstallResult, printPiSubagentsInstallResult, updatePiCliPackage, updatePiManagedPackages, printPiManagedPackageUpdateResult } from './pi-harness.js';
|
|
47
48
|
import { installPiHarnessAssets, printPiInstallSummary, syncPiHarnessAssets, validatePiHarnessInstall } from './pi-installer.js';
|
|
48
49
|
import { offerPiOptionalFeatures } from './pi-optional-features.js';
|
|
49
50
|
import {
|
|
@@ -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('\
|
|
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)));
|
|
@@ -423,6 +461,37 @@ async function installAllTargets(options = {}) {
|
|
|
423
461
|
await installSkillsTarget();
|
|
424
462
|
}
|
|
425
463
|
|
|
464
|
+
async function offerRawPiSubagentsHardening({ interactive = process.stdin.isTTY && process.stdout.isTTY } = {}) {
|
|
465
|
+
const audit = await inspectRawPiSubagentsActivation({ cwd: process.cwd() });
|
|
466
|
+
if (!audit.findings.length) return;
|
|
467
|
+
|
|
468
|
+
console.log(pc.yellow('\n⚠ Raw Pi subagents activation detected'));
|
|
469
|
+
console.log(pc.dim(formatRawPiSubagentsFindings(audit.findings)));
|
|
470
|
+
console.log(pc.dim('\nTakomi keeps pi-subagents installed as an internal module, but raw Pi activation exposes the separate `subagent` tool next to `takomi_subagent`.'));
|
|
471
|
+
|
|
472
|
+
let shouldDisable = process.env.TAKOMI_DISABLE_RAW_PI_SUBAGENTS === '1';
|
|
473
|
+
if (interactive && process.env.TAKOMI_DISABLE_RAW_PI_SUBAGENTS !== '1') {
|
|
474
|
+
const response = await prompts({
|
|
475
|
+
type: 'confirm',
|
|
476
|
+
name: 'disable',
|
|
477
|
+
message: 'Disable raw Pi subagents activation so agents only see takomi_subagent?',
|
|
478
|
+
initial: true,
|
|
479
|
+
});
|
|
480
|
+
shouldDisable = response.disable === true;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (!shouldDisable) {
|
|
484
|
+
console.log(pc.yellow(interactive
|
|
485
|
+
? 'Leaving raw Pi subagents activation unchanged.'
|
|
486
|
+
: 'Non-interactive run: leaving raw Pi subagents activation unchanged. Set TAKOMI_DISABLE_RAW_PI_SUBAGENTS=1 to disable automatically.'));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const result = await disableRawPiSubagentsActivation({ cwd: process.cwd() });
|
|
491
|
+
console.log(pc.green('✔ Disabled raw Pi subagents activation'));
|
|
492
|
+
console.log(pc.dim(formatRawPiSubagentsDisableActions(result.actions)));
|
|
493
|
+
}
|
|
494
|
+
|
|
426
495
|
async function installPiTarget(options = {}) {
|
|
427
496
|
const { offerOptionalFeatures = true } = options;
|
|
428
497
|
console.log(pc.magenta('🧭 Pi Harness Preflight\n'));
|
|
@@ -455,12 +524,14 @@ async function installPiTarget(options = {}) {
|
|
|
455
524
|
const result = await installPiHarnessAssets(program.version());
|
|
456
525
|
const validation = await validatePiHarnessInstall();
|
|
457
526
|
printPiInstallSummary(result, validation);
|
|
527
|
+
await offerRawPiSubagentsHardening();
|
|
458
528
|
if (offerOptionalFeatures) {
|
|
459
529
|
console.log(pc.cyan('\n🧩 Optional Takomi Pi feature packs...\n'));
|
|
460
530
|
await offerPiOptionalFeatures({ interactive: true });
|
|
461
531
|
}
|
|
462
532
|
console.log(pc.cyan('\n📦 Checking Pi-managed extension package updates...\n'));
|
|
463
533
|
printPiManagedPackageUpdateResult(await updatePiManagedPackages());
|
|
534
|
+
await offerRawPiSubagentsHardening();
|
|
464
535
|
console.log(pc.dim('\nNext: cd <project> && takomi\n'));
|
|
465
536
|
} catch (error) {
|
|
466
537
|
console.log(pc.red('\nPi harness asset install failed.'));
|
|
@@ -474,8 +545,10 @@ async function installPiOptionalFeaturesTarget() {
|
|
|
474
545
|
printPiInstallResult(pi);
|
|
475
546
|
if (!pi.ok) return;
|
|
476
547
|
await offerPiOptionalFeatures({ interactive: true });
|
|
548
|
+
await offerRawPiSubagentsHardening();
|
|
477
549
|
console.log(pc.cyan('\n📦 Checking Pi-managed extension package updates...\n'));
|
|
478
550
|
printPiManagedPackageUpdateResult(await updatePiManagedPackages());
|
|
551
|
+
await offerRawPiSubagentsHardening();
|
|
479
552
|
}
|
|
480
553
|
|
|
481
554
|
async function syncPiTarget() {
|
|
@@ -484,8 +557,10 @@ async function syncPiTarget() {
|
|
|
484
557
|
const result = await syncPiHarnessAssets(program.version());
|
|
485
558
|
const validation = await validatePiHarnessInstall();
|
|
486
559
|
printPiInstallSummary(result, validation);
|
|
560
|
+
await offerRawPiSubagentsHardening();
|
|
487
561
|
console.log(pc.cyan('\n📦 Checking Pi-managed extension package updates...\n'));
|
|
488
562
|
printPiManagedPackageUpdateResult(await updatePiManagedPackages());
|
|
563
|
+
await offerRawPiSubagentsHardening();
|
|
489
564
|
} catch (error) {
|
|
490
565
|
console.log(pc.red('\nPi sync failed.'));
|
|
491
566
|
console.log(pc.dim(String(error?.message || error)));
|
|
@@ -622,7 +697,7 @@ async function install(target) {
|
|
|
622
697
|
|
|
623
698
|
if (detected.length === 0) {
|
|
624
699
|
console.log(pc.yellow(' No AI harnesses detected on this machine.'));
|
|
625
|
-
console.log(pc.dim(
|
|
700
|
+
console.log(pc.dim(` We support: ${Object.values(HARNESS_MAP).map(h => h.name).join(', ')}`));
|
|
626
701
|
console.log(pc.dim(' Run "takomi init" instead for per-project setup.\n'));
|
|
627
702
|
return;
|
|
628
703
|
}
|
|
@@ -715,6 +790,11 @@ async function install(target) {
|
|
|
715
790
|
|
|
716
791
|
if (!contentResponse.content) return;
|
|
717
792
|
|
|
793
|
+
const syncMode = await promptHarnessSyncMode(
|
|
794
|
+
(hasExistingStoreSkills || hasExistingStoreWorkflows) ? existingStoreManifest.syncMode : 'auto'
|
|
795
|
+
);
|
|
796
|
+
if (!syncMode) return;
|
|
797
|
+
|
|
718
798
|
try {
|
|
719
799
|
// 4. Initialize global store
|
|
720
800
|
console.log(pc.cyan(`\n📦 Creating global store (${STORE_PATH})...\n`));
|
|
@@ -802,11 +882,15 @@ async function install(target) {
|
|
|
802
882
|
const syncSummary = await syncToAllHarnesses(selectedHarnesses, STORE_PATH, {
|
|
803
883
|
useOwnership: true,
|
|
804
884
|
owned: manifest.harnessOwned,
|
|
885
|
+
linkMode: syncMode,
|
|
805
886
|
});
|
|
806
887
|
|
|
807
888
|
// 7. Update manifest
|
|
889
|
+
const syncFailures = collectSyncFailures(syncSummary);
|
|
890
|
+
const successfulHarnesses = selectedHarnesses.filter(h => !syncFailures.some(failure => failure.id === h.id));
|
|
808
891
|
manifest = await getManifest();
|
|
809
|
-
manifest.linkedHarnesses =
|
|
892
|
+
manifest.linkedHarnesses = successfulHarnesses.map(h => h.id);
|
|
893
|
+
manifest.syncMode = syncMode;
|
|
810
894
|
manifest.installed.skills = await getStoreSkills();
|
|
811
895
|
manifest.installed.workflows = await getStoreWorkflows();
|
|
812
896
|
manifest.harnessOwned = manifest.harnessOwned || {};
|
|
@@ -814,11 +898,13 @@ async function install(target) {
|
|
|
814
898
|
manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
|
|
815
899
|
}
|
|
816
900
|
await writeManifest(manifest);
|
|
901
|
+
printSyncFailures(syncFailures);
|
|
817
902
|
|
|
818
903
|
// 8. Summary
|
|
819
|
-
console.log(pc.magenta('\n✨ Your command center is live. ✨'));
|
|
904
|
+
console.log(pc.magenta(syncFailures.length ? '\n⚠ Your command center is partially synced. ⚠' : '\n✨ Your command center is live. ✨'));
|
|
820
905
|
console.log(pc.white(`\n Store: ${STORE_PATH}`));
|
|
821
|
-
console.log(pc.white(`
|
|
906
|
+
console.log(pc.white(` Sync mode: ${syncMode}`));
|
|
907
|
+
console.log(pc.white(` Connected: ${successfulHarnesses.map(h => h.name).join(', ') || 'none'}`));
|
|
822
908
|
console.log(pc.dim(`\n Add skills: takomi add <github-url>`));
|
|
823
909
|
console.log(pc.dim(` Sync updates: takomi sync\n`));
|
|
824
910
|
|
|
@@ -882,23 +968,33 @@ async function sync(target) {
|
|
|
882
968
|
|
|
883
969
|
const selectedHarnesses = detected.filter(h => response.harnesses.includes(h.id));
|
|
884
970
|
|
|
885
|
-
|
|
971
|
+
const envSyncMode = process.env.TAKOMI_HARNESS_SYNC_MODE;
|
|
972
|
+
const syncMode = normalizeHarnessSyncMode(envSyncMode || manifest.syncMode || 'copy');
|
|
973
|
+
console.log(pc.cyan(`\n📡 Syncing from global store (${syncMode})...\n`));
|
|
886
974
|
const syncSummary = await syncToAllHarnesses(selectedHarnesses, STORE_PATH, {
|
|
887
975
|
useOwnership: true,
|
|
888
976
|
owned: manifest.harnessOwned,
|
|
977
|
+
linkMode: syncMode,
|
|
889
978
|
});
|
|
890
979
|
|
|
891
980
|
// Update manifest with current harnesses
|
|
892
|
-
|
|
981
|
+
const syncFailures = collectSyncFailures(syncSummary);
|
|
982
|
+
const successfulHarnessIds = response.harnesses.filter(id => !syncFailures.some(failure => failure.id === id));
|
|
983
|
+
manifest.linkedHarnesses = [...new Set([...manifest.linkedHarnesses, ...successfulHarnessIds])];
|
|
984
|
+
if (!envSyncMode) manifest.syncMode = syncMode;
|
|
893
985
|
manifest.harnessOwned = manifest.harnessOwned || {};
|
|
894
986
|
for (const harness of selectedHarnesses) {
|
|
895
987
|
manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
|
|
896
988
|
}
|
|
897
989
|
await writeManifest(manifest);
|
|
990
|
+
printSyncFailures(syncFailures);
|
|
898
991
|
|
|
899
992
|
const skills = await getStoreSkills();
|
|
900
993
|
const workflows = await getStoreWorkflows();
|
|
901
|
-
|
|
994
|
+
const syncedCount = successfulHarnessIds.length;
|
|
995
|
+
console.log(pc.magenta(syncFailures.length
|
|
996
|
+
? `\n⚠ ${skills.length} skills and ${workflows.length} workflows partially synced to ${syncedCount}/${selectedHarnesses.length} IDE(s).\n`
|
|
997
|
+
: `\n✨ ${skills.length} skills and ${workflows.length} workflows synced to ${syncedCount} IDE(s). Ready to build.\n`));
|
|
902
998
|
}
|
|
903
999
|
|
|
904
1000
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -994,8 +1090,21 @@ async function add(url) {
|
|
|
994
1090
|
if (syncResponse.doSync) {
|
|
995
1091
|
const detected = detectHarnesses();
|
|
996
1092
|
const linked = detected.filter(h => manifest.linkedHarnesses.includes(h.id));
|
|
997
|
-
await syncToAllHarnesses(linked, STORE_PATH
|
|
998
|
-
|
|
1093
|
+
const syncSummary = await syncToAllHarnesses(linked, STORE_PATH, {
|
|
1094
|
+
useOwnership: true,
|
|
1095
|
+
owned: manifest.harnessOwned,
|
|
1096
|
+
linkMode: normalizeHarnessSyncMode(process.env.TAKOMI_HARNESS_SYNC_MODE || manifest.syncMode || 'copy'),
|
|
1097
|
+
});
|
|
1098
|
+
const syncFailures = collectSyncFailures(syncSummary);
|
|
1099
|
+
manifest.harnessOwned = manifest.harnessOwned || {};
|
|
1100
|
+
for (const harness of linked) {
|
|
1101
|
+
manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
|
|
1102
|
+
}
|
|
1103
|
+
await writeManifest(manifest);
|
|
1104
|
+
printSyncFailures(syncFailures);
|
|
1105
|
+
console.log(pc.magenta(syncFailures.length
|
|
1106
|
+
? `\n⚠ Live on ${linked.length - syncFailures.length}/${linked.length} IDE(s); some harnesses need attention.\n`
|
|
1107
|
+
: `\n✨ Live on ${linked.length} IDE(s). You're armed and ready.\n`));
|
|
999
1108
|
}
|
|
1000
1109
|
} else {
|
|
1001
1110
|
console.log(pc.dim('\n Run "takomi sync" when you want to push these to your IDEs.\n'));
|
|
@@ -1022,6 +1131,7 @@ async function harnesses() {
|
|
|
1022
1131
|
console.log(pc.white(` Location: ${STORE_PATH}`));
|
|
1023
1132
|
console.log(pc.white(` Skills: ${skills.length} ready to deploy`));
|
|
1024
1133
|
console.log(pc.white(` Workflows: ${workflows.length} available`));
|
|
1134
|
+
console.log(pc.white(` Sync mode: ${manifest.syncMode || 'copy'}`));
|
|
1025
1135
|
console.log(pc.white(` Connected: ${manifest.linkedHarnesses.join(', ') || 'none'}`));
|
|
1026
1136
|
console.log(pc.dim(` Updated: ${manifest.updatedAt}\n`));
|
|
1027
1137
|
} else {
|
|
@@ -1096,8 +1206,19 @@ async function updateProjectResources() {
|
|
|
1096
1206
|
const detected = detectHarnesses();
|
|
1097
1207
|
const linked = detected.filter(h => manifest.linkedHarnesses.includes(h.id));
|
|
1098
1208
|
if (linked.length > 0) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1209
|
+
const syncMode = normalizeHarnessSyncMode(process.env.TAKOMI_HARNESS_SYNC_MODE || manifest.syncMode || 'copy');
|
|
1210
|
+
console.log(pc.cyan(`\n📡 Auto-syncing to linked harnesses (${syncMode})...\n`));
|
|
1211
|
+
const syncSummary = await syncToAllHarnesses(linked, STORE_PATH, {
|
|
1212
|
+
useOwnership: true,
|
|
1213
|
+
owned: manifest.harnessOwned,
|
|
1214
|
+
linkMode: syncMode,
|
|
1215
|
+
});
|
|
1216
|
+
const syncFailures = collectSyncFailures(syncSummary);
|
|
1217
|
+
manifest.harnessOwned = manifest.harnessOwned || {};
|
|
1218
|
+
for (const harness of linked) {
|
|
1219
|
+
manifest.harnessOwned[harness.id] = syncSummary[harness.id]?.owned || manifest.harnessOwned[harness.id] || {};
|
|
1220
|
+
}
|
|
1221
|
+
printSyncFailures(syncFailures);
|
|
1101
1222
|
}
|
|
1102
1223
|
}
|
|
1103
1224
|
|
package/src/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
-
import { inspectPiHarnessEnvironment } from './pi-harness.js';
|
|
4
|
+
import { formatRawPiSubagentsFindings, inspectPiHarnessEnvironment } from './pi-harness.js';
|
|
5
5
|
import { getStoreSkills, isStoreInitialized } from './store.js';
|
|
6
6
|
import { PI_MANIFEST_PATH } from './pi-installer.js';
|
|
7
7
|
import { SKILLS_MANIFEST_PATH, SKILLS_ROOT } from './skills-installer.js';
|
|
@@ -41,6 +41,7 @@ export async function runDoctor({ version, cwd = process.cwd() } = {}) {
|
|
|
41
41
|
? `${report.piSubagents.globalPackageJson}${report.piSubagents.globalVersion ? ` (${report.piSubagents.globalVersion})` : ''}`
|
|
42
42
|
: 'missing'));
|
|
43
43
|
console.log(status(report.piSubagents.binaryInstalled, 'pi-subagents installer binary', report.piSubagents.binaryPath || 'missing'));
|
|
44
|
+
console.log(status(report.rawPiSubagentsActivation.findings.length === 0, 'Raw Pi subagent tool hidden', report.rawPiSubagentsActivation.findings.length ? 'raw subagent activation detected' : 'yes'));
|
|
44
45
|
console.log(status(report.installed.routingPolicyPresent || report.project.routingPolicyPresent, 'Routing policy', report.project.routingPolicyPresent ? report.project.targets.routingPolicy : report.installed.targets.routingPolicy));
|
|
45
46
|
console.log(status(await fs.pathExists(SKILLS_ROOT), 'Installed skills root', SKILLS_ROOT));
|
|
46
47
|
console.log(status(await fs.pathExists(SKILLS_MANIFEST_PATH), 'Skills install manifest', SKILLS_MANIFEST_PATH));
|
|
@@ -68,6 +69,11 @@ export async function runDoctor({ version, cwd = process.cwd() } = {}) {
|
|
|
68
69
|
console.log(pc.white(' - Install pi-subagents: npm install -g pi-subagents'));
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
if (report.rawPiSubagentsActivation.findings.length) {
|
|
73
|
+
console.log(pc.white(' - Disable raw Pi subagents so models only see takomi_subagent. Run takomi setup pi or set TAKOMI_DISABLE_RAW_PI_SUBAGENTS=1 takomi refresh pi.'));
|
|
74
|
+
console.log(pc.dim(formatRawPiSubagentsFindings(report.rawPiSubagentsActivation.findings).split(/\r?\n/).map((line) => ` ${line}`).join('\n')));
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
if (!report.project.routingPolicyPresent && !report.installed.routingPolicyPresent) {
|
|
72
78
|
console.log(pc.white(' - Add a routing policy at .pi/takomi/model-routing.md when ready.'));
|
|
73
79
|
}
|
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,
|
|
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', '
|
|
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', '
|
|
43
|
-
workflows: path.join(HOME, '.gemini', '
|
|
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
|
-
|
|
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
|
-
*
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|