@opencode_weave/weave 0.6.1 → 0.6.3
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/README.md +5 -0
- package/dist/agents/agent-builder.d.ts +14 -0
- package/dist/agents/builtin-agents.d.ts +16 -0
- package/dist/agents/custom-agent-factory.d.ts +24 -0
- package/dist/agents/dynamic-prompt-builder.d.ts +6 -0
- package/dist/agents/index.d.ts +6 -2
- package/dist/agents/loom/index.d.ts +9 -0
- package/dist/agents/loom/prompt-composer.d.ts +35 -0
- package/dist/agents/model-resolution.d.ts +9 -1
- package/dist/agents/prompt-loader.d.ts +9 -0
- package/dist/agents/prompt-utils.d.ts +2 -0
- package/dist/agents/tapestry/index.d.ts +7 -0
- package/dist/agents/tapestry/prompt-composer.d.ts +24 -0
- package/dist/config/schema.d.ts +112 -0
- package/dist/create-managers.d.ts +3 -0
- package/dist/features/analytics/fingerprint.d.ts +32 -0
- package/dist/features/analytics/index.d.ts +22 -0
- package/dist/features/analytics/session-tracker.d.ts +48 -0
- package/dist/features/analytics/storage.d.ts +28 -0
- package/dist/features/analytics/suggestions.d.ts +10 -0
- package/dist/features/analytics/types.d.ts +104 -0
- package/dist/features/work-state/index.d.ts +1 -1
- package/dist/features/work-state/storage.d.ts +10 -0
- package/dist/features/work-state/types.d.ts +6 -0
- package/dist/hooks/create-hooks.d.ts +2 -0
- package/dist/hooks/work-continuation.d.ts +9 -0
- package/dist/index.js +1112 -173
- package/dist/plugin/plugin-interface.d.ts +3 -0
- package/dist/shared/agent-display-names.d.ts +11 -0
- package/dist/shared/index.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { join as join9 } from "path";
|
|
3
|
+
|
|
1
4
|
// src/config/loader.ts
|
|
2
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
6
|
import { join as join2 } from "node:path";
|
|
@@ -51,15 +54,42 @@ var ExperimentalConfigSchema = z.object({
|
|
|
51
54
|
context_window_warning_threshold: z.number().min(0).max(1).optional(),
|
|
52
55
|
context_window_critical_threshold: z.number().min(0).max(1).optional()
|
|
53
56
|
});
|
|
57
|
+
var DelegationTriggerSchema = z.object({
|
|
58
|
+
domain: z.string(),
|
|
59
|
+
trigger: z.string()
|
|
60
|
+
});
|
|
61
|
+
var CustomAgentConfigSchema = z.object({
|
|
62
|
+
prompt: z.string().optional(),
|
|
63
|
+
prompt_file: z.string().optional(),
|
|
64
|
+
model: z.string().optional(),
|
|
65
|
+
display_name: z.string().optional(),
|
|
66
|
+
mode: z.enum(["subagent", "primary", "all"]).optional(),
|
|
67
|
+
fallback_models: z.array(z.string()).optional(),
|
|
68
|
+
category: z.enum(["exploration", "specialist", "advisor", "utility"]).optional(),
|
|
69
|
+
cost: z.enum(["FREE", "CHEAP", "EXPENSIVE"]).optional(),
|
|
70
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
71
|
+
top_p: z.number().min(0).max(1).optional(),
|
|
72
|
+
maxTokens: z.number().optional(),
|
|
73
|
+
tools: z.record(z.string(), z.boolean()).optional(),
|
|
74
|
+
skills: z.array(z.string()).optional(),
|
|
75
|
+
triggers: z.array(DelegationTriggerSchema).optional(),
|
|
76
|
+
description: z.string().optional()
|
|
77
|
+
});
|
|
78
|
+
var CustomAgentsConfigSchema = z.record(z.string(), CustomAgentConfigSchema);
|
|
79
|
+
var AnalyticsConfigSchema = z.object({
|
|
80
|
+
enabled: z.boolean().optional()
|
|
81
|
+
});
|
|
54
82
|
var WeaveConfigSchema = z.object({
|
|
55
83
|
$schema: z.string().optional(),
|
|
56
84
|
agents: AgentOverridesSchema.optional(),
|
|
85
|
+
custom_agents: CustomAgentsConfigSchema.optional(),
|
|
57
86
|
categories: CategoriesConfigSchema.optional(),
|
|
58
87
|
disabled_hooks: z.array(z.string()).optional(),
|
|
59
88
|
disabled_tools: z.array(z.string()).optional(),
|
|
60
89
|
disabled_agents: z.array(z.string()).optional(),
|
|
61
90
|
disabled_skills: z.array(z.string()).optional(),
|
|
62
91
|
background: BackgroundConfigSchema.optional(),
|
|
92
|
+
analytics: AnalyticsConfigSchema.optional(),
|
|
63
93
|
tmux: TmuxConfigSchema.optional(),
|
|
64
94
|
experimental: ExperimentalConfigSchema.optional()
|
|
65
95
|
});
|
|
@@ -88,6 +118,7 @@ function mergeConfigs(user, project) {
|
|
|
88
118
|
...user,
|
|
89
119
|
...project,
|
|
90
120
|
agents: user.agents || project.agents ? deepMergeObjects(user.agents ?? {}, project.agents ?? {}) : undefined,
|
|
121
|
+
custom_agents: user.custom_agents || project.custom_agents ? deepMergeObjects(user.custom_agents ?? {}, project.custom_agents ?? {}) : undefined,
|
|
91
122
|
categories: user.categories || project.categories ? deepMergeObjects(user.categories ?? {}, project.categories ?? {}) : undefined,
|
|
92
123
|
disabled_hooks: mergeStringArrays(user.disabled_hooks, project.disabled_hooks),
|
|
93
124
|
disabled_tools: mergeStringArrays(user.disabled_tools, project.disabled_tools),
|
|
@@ -173,6 +204,26 @@ var AGENT_DISPLAY_NAMES = {
|
|
|
173
204
|
warp: "warp",
|
|
174
205
|
weft: "weft"
|
|
175
206
|
};
|
|
207
|
+
var BUILTIN_CONFIG_KEYS = new Set(Object.keys(AGENT_DISPLAY_NAMES));
|
|
208
|
+
var reverseDisplayNames = null;
|
|
209
|
+
function getReverseDisplayNames() {
|
|
210
|
+
if (reverseDisplayNames === null) {
|
|
211
|
+
reverseDisplayNames = Object.fromEntries(Object.entries(AGENT_DISPLAY_NAMES).map(([key, displayName]) => [displayName.toLowerCase(), key]));
|
|
212
|
+
}
|
|
213
|
+
return reverseDisplayNames;
|
|
214
|
+
}
|
|
215
|
+
function registerAgentDisplayName(configKey, displayName) {
|
|
216
|
+
if (BUILTIN_CONFIG_KEYS.has(configKey)) {
|
|
217
|
+
throw new Error(`Cannot register display name for "${configKey}": it is a built-in agent name`);
|
|
218
|
+
}
|
|
219
|
+
const reverse = getReverseDisplayNames();
|
|
220
|
+
const existingKey = reverse[displayName.toLowerCase()];
|
|
221
|
+
if (existingKey !== undefined && BUILTIN_CONFIG_KEYS.has(existingKey)) {
|
|
222
|
+
throw new Error(`Display name "${displayName}" is reserved for built-in agent "${existingKey}"`);
|
|
223
|
+
}
|
|
224
|
+
AGENT_DISPLAY_NAMES[configKey] = displayName;
|
|
225
|
+
reverseDisplayNames = null;
|
|
226
|
+
}
|
|
176
227
|
function getAgentDisplayName(configKey) {
|
|
177
228
|
const exactMatch = AGENT_DISPLAY_NAMES[configKey];
|
|
178
229
|
if (exactMatch !== undefined)
|
|
@@ -184,7 +235,6 @@ function getAgentDisplayName(configKey) {
|
|
|
184
235
|
}
|
|
185
236
|
return configKey;
|
|
186
237
|
}
|
|
187
|
-
var REVERSE_DISPLAY_NAMES = Object.fromEntries(Object.entries(AGENT_DISPLAY_NAMES).map(([key, displayName]) => [displayName.toLowerCase(), key]));
|
|
188
238
|
|
|
189
239
|
// src/features/builtin-commands/templates/start-work.ts
|
|
190
240
|
var START_WORK_TEMPLATE = `You are being activated by the /start-work command to execute a Weave plan.
|
|
@@ -467,17 +517,65 @@ class SkillMcpManager {
|
|
|
467
517
|
}
|
|
468
518
|
}
|
|
469
519
|
|
|
470
|
-
// src/agents/
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
520
|
+
// src/agents/dynamic-prompt-builder.ts
|
|
521
|
+
function buildDelegationTable(agents) {
|
|
522
|
+
const rows = [
|
|
523
|
+
"### Delegation Table:",
|
|
524
|
+
""
|
|
525
|
+
];
|
|
526
|
+
for (const agent of agents) {
|
|
527
|
+
for (const trigger of agent.metadata.triggers) {
|
|
528
|
+
rows.push(`- **${trigger.domain}** → \`${agent.name}\` — ${trigger.trigger}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return rows.join(`
|
|
532
|
+
`);
|
|
533
|
+
}
|
|
534
|
+
function buildProjectContextSection(fingerprint) {
|
|
535
|
+
if (!fingerprint)
|
|
536
|
+
return "";
|
|
537
|
+
const parts = [];
|
|
538
|
+
if (fingerprint.primaryLanguage || fingerprint.packageManager) {
|
|
539
|
+
const lang = fingerprint.primaryLanguage ?? "unknown";
|
|
540
|
+
const pm = fingerprint.packageManager;
|
|
541
|
+
const desc = pm ? `a ${lang} project using ${pm}` : `a ${lang} project`;
|
|
542
|
+
parts.push(`This is ${desc}.`);
|
|
543
|
+
}
|
|
544
|
+
const highConfidence = fingerprint.stack.filter((s) => s.confidence === "high");
|
|
545
|
+
if (highConfidence.length > 0) {
|
|
546
|
+
const names = highConfidence.map((s) => s.name).join(", ");
|
|
547
|
+
parts.push(`Detected stack: ${names}.`);
|
|
548
|
+
}
|
|
549
|
+
if (fingerprint.isMonorepo) {
|
|
550
|
+
parts.push("Monorepo structure detected.");
|
|
551
|
+
}
|
|
552
|
+
if (fingerprint.os) {
|
|
553
|
+
const archSuffix = fingerprint.arch ? ` (${fingerprint.arch})` : "";
|
|
554
|
+
parts.push(`Platform: ${fingerprint.os}${archSuffix}.`);
|
|
555
|
+
}
|
|
556
|
+
if (parts.length === 0)
|
|
557
|
+
return "";
|
|
558
|
+
return `<ProjectContext>
|
|
559
|
+
${parts.join(`
|
|
560
|
+
`)}
|
|
561
|
+
</ProjectContext>`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/agents/prompt-utils.ts
|
|
565
|
+
function isAgentEnabled(name, disabled) {
|
|
566
|
+
return !disabled.has(name);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/agents/loom/prompt-composer.ts
|
|
570
|
+
function buildRoleSection() {
|
|
571
|
+
return `<Role>
|
|
475
572
|
Loom — main orchestrator for Weave.
|
|
476
573
|
Plan tasks, coordinate work, and delegate to specialized agents.
|
|
477
574
|
You are the team lead. Understand the request, break it into tasks, delegate intelligently.
|
|
478
|
-
</Role
|
|
479
|
-
|
|
480
|
-
|
|
575
|
+
</Role>`;
|
|
576
|
+
}
|
|
577
|
+
function buildDisciplineSection() {
|
|
578
|
+
return `<Discipline>
|
|
481
579
|
TODO OBSESSION (NON-NEGOTIABLE):
|
|
482
580
|
- 2+ steps → todowrite FIRST, atomic breakdown
|
|
483
581
|
- Mark in_progress before starting (ONE at a time)
|
|
@@ -485,9 +583,10 @@ TODO OBSESSION (NON-NEGOTIABLE):
|
|
|
485
583
|
- NEVER batch completions
|
|
486
584
|
|
|
487
585
|
No todos on multi-step work = INCOMPLETE WORK.
|
|
488
|
-
</Discipline
|
|
489
|
-
|
|
490
|
-
|
|
586
|
+
</Discipline>`;
|
|
587
|
+
}
|
|
588
|
+
function buildSidebarTodosSection() {
|
|
589
|
+
return `<SidebarTodos>
|
|
491
590
|
The user sees a Todo sidebar (~35 char width). Use todowrite strategically:
|
|
492
591
|
|
|
493
592
|
WHEN PLANNING (multi-step work):
|
|
@@ -510,20 +609,60 @@ FORMAT RULES:
|
|
|
510
609
|
- in_progress = yellow highlight — use for ACTIVE work only
|
|
511
610
|
- Prefix delegations with agent name
|
|
512
611
|
- After all work done: mark everything completed (sidebar hides)
|
|
513
|
-
</SidebarTodos
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
- Use
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
- Use
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
612
|
+
</SidebarTodos>`;
|
|
613
|
+
}
|
|
614
|
+
function buildDelegationSection(disabled) {
|
|
615
|
+
const lines = [];
|
|
616
|
+
if (isAgentEnabled("thread", disabled)) {
|
|
617
|
+
lines.push("- Use thread for fast codebase exploration (read-only, cheap)");
|
|
618
|
+
}
|
|
619
|
+
if (isAgentEnabled("spindle", disabled)) {
|
|
620
|
+
lines.push("- Use spindle for external docs and research (read-only)");
|
|
621
|
+
}
|
|
622
|
+
if (isAgentEnabled("pattern", disabled)) {
|
|
623
|
+
lines.push("- Use pattern for detailed planning before complex implementations");
|
|
624
|
+
}
|
|
625
|
+
if (isAgentEnabled("tapestry", disabled)) {
|
|
626
|
+
lines.push("- Use /start-work to hand off to Tapestry for todo-list driven execution of multi-step plans");
|
|
627
|
+
}
|
|
628
|
+
if (isAgentEnabled("shuttle", disabled)) {
|
|
629
|
+
lines.push("- Use shuttle for category-specific specialized work");
|
|
630
|
+
}
|
|
631
|
+
if (isAgentEnabled("weft", disabled)) {
|
|
632
|
+
let weftLine = "- Use Weft for reviewing completed work or validating plans before execution";
|
|
633
|
+
if (isAgentEnabled("warp", disabled)) {
|
|
634
|
+
weftLine += `
|
|
635
|
+
- MUST use Warp for security audits when changes touch auth, crypto, certificates, tokens, signatures, input validation, secrets, passwords, sessions, CORS, CSP, .env files, or OAuth/OIDC/SAML flows — not optional. When in doubt, invoke Warp — false positives (fast APPROVE) are cheap.`;
|
|
636
|
+
}
|
|
637
|
+
lines.push(weftLine);
|
|
638
|
+
} else if (isAgentEnabled("warp", disabled)) {
|
|
639
|
+
lines.push("- MUST use Warp for security audits when changes touch auth, crypto, certificates, tokens, signatures, input validation, secrets, passwords, sessions, CORS, CSP, .env files, or OAuth/OIDC/SAML flows — not optional.");
|
|
640
|
+
}
|
|
641
|
+
lines.push("- Delegate aggressively to keep your context lean");
|
|
642
|
+
return `<Delegation>
|
|
643
|
+
${lines.join(`
|
|
644
|
+
`)}
|
|
645
|
+
</Delegation>`;
|
|
646
|
+
}
|
|
647
|
+
function buildDelegationNarrationSection(disabled = new Set) {
|
|
648
|
+
const hints = [];
|
|
649
|
+
if (isAgentEnabled("pattern", disabled)) {
|
|
650
|
+
hints.push('- Pattern (planning): "This may take a moment — Pattern is researching the codebase and writing a detailed plan..."');
|
|
651
|
+
}
|
|
652
|
+
if (isAgentEnabled("spindle", disabled)) {
|
|
653
|
+
hints.push('- Spindle (web research): "Spindle is fetching external docs — this may take a moment..."');
|
|
654
|
+
}
|
|
655
|
+
if (isAgentEnabled("weft", disabled) || isAgentEnabled("warp", disabled)) {
|
|
656
|
+
hints.push('- Weft/Warp (review): "Running review — this will take a moment..."');
|
|
657
|
+
}
|
|
658
|
+
if (isAgentEnabled("thread", disabled)) {
|
|
659
|
+
hints.push("- Thread (exploration): Fast — no duration hint needed.");
|
|
660
|
+
}
|
|
661
|
+
const hintsBlock = hints.length > 0 ? `
|
|
662
|
+
DURATION HINTS — tell the user when something takes time:
|
|
663
|
+
${hints.join(`
|
|
664
|
+
`)}` : "";
|
|
665
|
+
return `<DelegationNarration>
|
|
527
666
|
EVERY delegation MUST follow this pattern — no exceptions:
|
|
528
667
|
|
|
529
668
|
1. BEFORE delegating: Write a brief message to the user explaining what you're about to do:
|
|
@@ -541,49 +680,80 @@ EVERY delegation MUST follow this pattern — no exceptions:
|
|
|
541
680
|
- "Spindle confirmed the library supports streaming — docs at [url]"
|
|
542
681
|
|
|
543
682
|
4. Mark the delegation todo as "completed" after summarizing results.
|
|
544
|
-
|
|
545
|
-
DURATION HINTS — tell the user when something takes time:
|
|
546
|
-
- Pattern (planning): "This may take a moment — Pattern is researching the codebase and writing a detailed plan..."
|
|
547
|
-
- Spindle (web research): "Spindle is fetching external docs — this may take a moment..."
|
|
548
|
-
- Weft/Warp (review): "Running review — this will take a moment..."
|
|
549
|
-
- Thread (exploration): Fast — no duration hint needed.
|
|
683
|
+
${hintsBlock}
|
|
550
684
|
|
|
551
685
|
The user should NEVER see a blank pause with no explanation. If you're about to call Task, WRITE SOMETHING FIRST.
|
|
552
|
-
</DelegationNarration
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
686
|
+
</DelegationNarration>`;
|
|
687
|
+
}
|
|
688
|
+
function buildPlanWorkflowSection(disabled) {
|
|
689
|
+
const hasWeft = isAgentEnabled("weft", disabled);
|
|
690
|
+
const hasWarp = isAgentEnabled("warp", disabled);
|
|
691
|
+
const hasTapestry = isAgentEnabled("tapestry", disabled);
|
|
692
|
+
const hasPattern = isAgentEnabled("pattern", disabled);
|
|
693
|
+
const steps = [];
|
|
694
|
+
if (hasPattern) {
|
|
695
|
+
steps.push(`1. PLAN: Delegate to Pattern to produce a plan saved to \`.weave/plans/{name}.md\`
|
|
558
696
|
- Pattern researches the codebase, produces a structured plan with \`- [ ]\` checkboxes
|
|
559
|
-
- Pattern ONLY writes .md files in .weave/ — it never writes code
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
- If Weft rejects, send issues back to Pattern for revision
|
|
565
|
-
|
|
566
|
-
|
|
697
|
+
- Pattern ONLY writes .md files in .weave/ — it never writes code`);
|
|
698
|
+
}
|
|
699
|
+
if (hasWeft || hasWarp) {
|
|
700
|
+
const reviewParts = [];
|
|
701
|
+
if (hasWeft) {
|
|
702
|
+
reviewParts.push(` - TRIGGER: Plan touches 3+ files OR has 5+ tasks — Weft review is mandatory`, ` - SKIP ONLY IF: User explicitly says "skip review"`, ` - Weft reads the plan, verifies file references, checks executability`, ` - If Weft rejects, send issues back to Pattern for revision`);
|
|
703
|
+
}
|
|
704
|
+
if (hasWarp) {
|
|
705
|
+
reviewParts.push(` - MANDATORY: If the plan touches security-relevant areas (crypto, auth, certificates, tokens, signatures, or input validation) → also run Warp on the plan`);
|
|
706
|
+
}
|
|
707
|
+
const stepNum = hasPattern ? 2 : 1;
|
|
708
|
+
const reviewerName = hasWeft ? "Weft" : "Warp";
|
|
709
|
+
steps.push(`${stepNum}. REVIEW: Delegate to ${reviewerName} to validate the plan before execution
|
|
710
|
+
${reviewParts.join(`
|
|
711
|
+
`)}`);
|
|
712
|
+
}
|
|
713
|
+
const execStepNum = steps.length + 1;
|
|
714
|
+
if (hasTapestry) {
|
|
715
|
+
steps.push(`${execStepNum}. EXECUTE: Tell the user to run \`/start-work\` to begin execution
|
|
567
716
|
- /start-work loads the plan, creates work state at \`.weave/state.json\`, and switches to Tapestry
|
|
568
|
-
- Tapestry reads the plan and works through tasks, marking checkboxes as it goes
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
717
|
+
- Tapestry reads the plan and works through tasks, marking checkboxes as it goes`);
|
|
718
|
+
}
|
|
719
|
+
const resumeStepNum = steps.length + 1;
|
|
720
|
+
steps.push(`${resumeStepNum}. RESUME: If work was interrupted, \`/start-work\` resumes from the last unchecked task`);
|
|
721
|
+
const notes = [];
|
|
722
|
+
if (hasTapestry && (hasWeft || hasWarp)) {
|
|
723
|
+
notes.push(`Note: Tapestry runs Weft and Warp reviews directly after completing all tasks — Loom does not need to gate this.`);
|
|
724
|
+
}
|
|
725
|
+
notes.push(`When to use this workflow vs. direct execution:
|
|
574
726
|
- USE plan workflow: Large features, multi-file refactors, anything with 5+ steps or architectural decisions
|
|
575
|
-
- SKIP plan workflow: Quick fixes, single-file changes, simple questions
|
|
576
|
-
|
|
727
|
+
- SKIP plan workflow: Quick fixes, single-file changes, simple questions`);
|
|
728
|
+
return `<PlanWorkflow>
|
|
729
|
+
For complex tasks that benefit from structured planning before execution:
|
|
730
|
+
|
|
731
|
+
${steps.join(`
|
|
732
|
+
`)}
|
|
577
733
|
|
|
578
|
-
|
|
579
|
-
Two review modes — different rules for each:
|
|
734
|
+
${notes.join(`
|
|
580
735
|
|
|
736
|
+
`)}
|
|
737
|
+
</PlanWorkflow>`;
|
|
738
|
+
}
|
|
739
|
+
function buildReviewWorkflowSection(disabled) {
|
|
740
|
+
const hasWeft = isAgentEnabled("weft", disabled);
|
|
741
|
+
const hasWarp = isAgentEnabled("warp", disabled);
|
|
742
|
+
const hasTapestry = isAgentEnabled("tapestry", disabled);
|
|
743
|
+
if (!hasWeft && !hasWarp)
|
|
744
|
+
return "";
|
|
745
|
+
const parts = [];
|
|
746
|
+
parts.push("Two review modes — different rules for each:");
|
|
747
|
+
if (hasTapestry) {
|
|
748
|
+
parts.push(`
|
|
581
749
|
**Post-Plan-Execution Review:**
|
|
582
750
|
- Handled directly by Tapestry — Tapestry invokes Weft and Warp after completing all tasks.
|
|
583
|
-
- Loom does not need to intervene
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
-
|
|
751
|
+
- Loom does not need to intervene.`);
|
|
752
|
+
}
|
|
753
|
+
parts.push(`
|
|
754
|
+
**Ad-Hoc Review (non-plan work):**`);
|
|
755
|
+
if (hasWeft) {
|
|
756
|
+
parts.push(`- Delegate to Weft to review the changes
|
|
587
757
|
- Weft is read-only and approval-biased — it rejects only for real problems
|
|
588
758
|
- If Weft approves: proceed confidently
|
|
589
759
|
- If Weft rejects: address the specific blocking issues, then re-review
|
|
@@ -596,25 +766,83 @@ When to invoke ad-hoc Weft:
|
|
|
596
766
|
When to skip ad-hoc Weft:
|
|
597
767
|
- Single-file trivial changes
|
|
598
768
|
- User explicitly says "skip review"
|
|
599
|
-
- Simple question-answering (no code changes)
|
|
600
|
-
|
|
769
|
+
- Simple question-answering (no code changes)`);
|
|
770
|
+
}
|
|
771
|
+
if (hasWarp) {
|
|
772
|
+
parts.push(`
|
|
601
773
|
MANDATORY — If ANY changed file touches crypto, auth, certificates, tokens, signatures, or input validation:
|
|
602
774
|
→ MUST run Warp in parallel with Weft. This is NOT optional.
|
|
603
775
|
→ Failure to invoke Warp for security-relevant changes is a workflow violation.
|
|
604
776
|
- Warp is read-only and skeptical-biased — it rejects when security is at risk
|
|
605
777
|
- Warp self-triages: if no security-relevant changes, it fast-exits with APPROVE
|
|
606
|
-
- If Warp rejects: address the specific security issues before shipping
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
778
|
+
- If Warp rejects: address the specific security issues before shipping`);
|
|
779
|
+
}
|
|
780
|
+
return `<ReviewWorkflow>
|
|
781
|
+
${parts.join(`
|
|
782
|
+
`)}
|
|
783
|
+
</ReviewWorkflow>`;
|
|
784
|
+
}
|
|
785
|
+
function buildStyleSection() {
|
|
786
|
+
return `<Style>
|
|
610
787
|
- Start immediately. No preamble acknowledgments (e.g., "Sure!", "Great question!").
|
|
611
788
|
- Delegation narration is NOT an acknowledgment — always narrate before/after delegating.
|
|
612
789
|
- Dense > verbose.
|
|
613
790
|
- Match user's communication style.
|
|
614
|
-
</Style
|
|
791
|
+
</Style>`;
|
|
792
|
+
}
|
|
793
|
+
function buildCustomAgentDelegationSection(customAgents, disabled) {
|
|
794
|
+
const enabledAgents = customAgents.filter((a) => isAgentEnabled(a.name, disabled));
|
|
795
|
+
if (enabledAgents.length === 0)
|
|
796
|
+
return "";
|
|
797
|
+
const table = buildDelegationTable(enabledAgents);
|
|
798
|
+
return `<CustomDelegation>
|
|
799
|
+
Custom agents available for delegation:
|
|
800
|
+
|
|
801
|
+
${table}
|
|
802
|
+
|
|
803
|
+
Delegate to these agents when their domain matches the task. Use the same delegation pattern as built-in agents.
|
|
804
|
+
</CustomDelegation>`;
|
|
805
|
+
}
|
|
806
|
+
function composeLoomPrompt(options = {}) {
|
|
807
|
+
const disabled = options.disabledAgents ?? new Set;
|
|
808
|
+
const fingerprint = options.fingerprint;
|
|
809
|
+
const customAgents = options.customAgents ?? [];
|
|
810
|
+
const sections = [
|
|
811
|
+
buildRoleSection(),
|
|
812
|
+
buildProjectContextSection(fingerprint),
|
|
813
|
+
buildDisciplineSection(),
|
|
814
|
+
buildSidebarTodosSection(),
|
|
815
|
+
buildDelegationSection(disabled),
|
|
816
|
+
buildDelegationNarrationSection(disabled),
|
|
817
|
+
buildCustomAgentDelegationSection(customAgents, disabled),
|
|
818
|
+
buildPlanWorkflowSection(disabled),
|
|
819
|
+
buildReviewWorkflowSection(disabled),
|
|
820
|
+
buildStyleSection()
|
|
821
|
+
].filter((s) => s.length > 0);
|
|
822
|
+
return sections.join(`
|
|
823
|
+
|
|
824
|
+
`);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/agents/loom/default.ts
|
|
828
|
+
var LOOM_DEFAULTS = {
|
|
829
|
+
temperature: 0.1,
|
|
830
|
+
description: "Loom (Main Orchestrator)",
|
|
831
|
+
prompt: composeLoomPrompt()
|
|
615
832
|
};
|
|
616
833
|
|
|
617
834
|
// src/agents/loom/index.ts
|
|
835
|
+
function createLoomAgentWithOptions(model, disabledAgents, fingerprint, customAgents) {
|
|
836
|
+
if ((!disabledAgents || disabledAgents.size === 0) && !fingerprint && (!customAgents || customAgents.length === 0)) {
|
|
837
|
+
return { ...LOOM_DEFAULTS, model, mode: "primary" };
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
...LOOM_DEFAULTS,
|
|
841
|
+
prompt: composeLoomPrompt({ disabledAgents, fingerprint, customAgents }),
|
|
842
|
+
model,
|
|
843
|
+
mode: "primary"
|
|
844
|
+
};
|
|
845
|
+
}
|
|
618
846
|
var createLoomAgent = (model) => ({
|
|
619
847
|
...LOOM_DEFAULTS,
|
|
620
848
|
model,
|
|
@@ -622,21 +850,17 @@ var createLoomAgent = (model) => ({
|
|
|
622
850
|
});
|
|
623
851
|
createLoomAgent.mode = "primary";
|
|
624
852
|
|
|
625
|
-
// src/agents/tapestry/
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
description: "Tapestry (Execution Orchestrator)",
|
|
629
|
-
tools: {
|
|
630
|
-
call_weave_agent: false
|
|
631
|
-
},
|
|
632
|
-
prompt: `<Role>
|
|
853
|
+
// src/agents/tapestry/prompt-composer.ts
|
|
854
|
+
function buildTapestryRoleSection() {
|
|
855
|
+
return `<Role>
|
|
633
856
|
Tapestry — execution orchestrator for Weave.
|
|
634
857
|
You manage todo-list driven execution of multi-step plans.
|
|
635
858
|
Break plans into atomic tasks, track progress rigorously, execute sequentially.
|
|
636
859
|
You do NOT spawn subagents — you execute directly.
|
|
637
|
-
</Role
|
|
638
|
-
|
|
639
|
-
|
|
860
|
+
</Role>`;
|
|
861
|
+
}
|
|
862
|
+
function buildTapestryDisciplineSection() {
|
|
863
|
+
return `<Discipline>
|
|
640
864
|
TODO OBSESSION (NON-NEGOTIABLE):
|
|
641
865
|
- Load existing todos first — never re-plan if a plan exists
|
|
642
866
|
- Mark in_progress before starting EACH task (ONE at a time)
|
|
@@ -644,9 +868,10 @@ TODO OBSESSION (NON-NEGOTIABLE):
|
|
|
644
868
|
- NEVER skip steps, NEVER batch completions
|
|
645
869
|
|
|
646
870
|
Execution without todos = lost work.
|
|
647
|
-
</Discipline
|
|
648
|
-
|
|
649
|
-
|
|
871
|
+
</Discipline>`;
|
|
872
|
+
}
|
|
873
|
+
function buildTapestrySidebarTodosSection() {
|
|
874
|
+
return `<SidebarTodos>
|
|
650
875
|
The user sees a Todo sidebar (~35 char width). Use todowrite to keep it useful:
|
|
651
876
|
|
|
652
877
|
WHEN STARTING A PLAN:
|
|
@@ -674,9 +899,12 @@ FORMAT RULES:
|
|
|
674
899
|
- Summary todo always present during execution
|
|
675
900
|
- Max 5 visible todos (1 summary + 1 in_progress + 2-3 pending)
|
|
676
901
|
- in_progress = yellow highlight — use for CURRENT task only
|
|
677
|
-
</SidebarTodos
|
|
678
|
-
|
|
679
|
-
|
|
902
|
+
</SidebarTodos>`;
|
|
903
|
+
}
|
|
904
|
+
function buildTapestryPlanExecutionSection(disabled = new Set) {
|
|
905
|
+
const hasWeft = isAgentEnabled("weft", disabled);
|
|
906
|
+
const verifySuffix = hasWeft ? " If uncertain about quality, note that Loom should invoke Weft for formal review." : "";
|
|
907
|
+
return `<PlanExecution>
|
|
680
908
|
When activated by /start-work with a plan file:
|
|
681
909
|
|
|
682
910
|
1. READ the plan file first — understand the full scope
|
|
@@ -684,16 +912,17 @@ When activated by /start-work with a plan file:
|
|
|
684
912
|
3. For each task:
|
|
685
913
|
a. Read the task description, files, and acceptance criteria
|
|
686
914
|
b. Execute the work (write code, run commands, create files)
|
|
687
|
-
c. Verify: Follow the <Verification> protocol below — ALL checks must pass before marking complete
|
|
915
|
+
c. Verify: Follow the <Verification> protocol below — ALL checks must pass before marking complete.${verifySuffix}
|
|
688
916
|
d. Mark complete: use Edit tool to change \`- [ ]\` to \`- [x]\` in the plan file
|
|
689
917
|
e. Report: "Completed task N/M: [title]"
|
|
690
918
|
4. CONTINUE to the next unchecked task
|
|
691
919
|
5. When ALL checkboxes are checked, follow the <PostExecutionReview> protocol below before reporting final summary.
|
|
692
920
|
|
|
693
921
|
NEVER stop mid-plan unless explicitly told to or completely blocked.
|
|
694
|
-
</PlanExecution
|
|
695
|
-
|
|
696
|
-
|
|
922
|
+
</PlanExecution>`;
|
|
923
|
+
}
|
|
924
|
+
function buildTapestryVerificationSection() {
|
|
925
|
+
return `<Verification>
|
|
697
926
|
After completing work for each task — BEFORE marking \`- [ ]\` → \`- [x]\`:
|
|
698
927
|
|
|
699
928
|
1. **Inspect changes**:
|
|
@@ -711,39 +940,100 @@ After completing work for each task — BEFORE marking \`- [ ]\` → \`- [x]\`:
|
|
|
711
940
|
- Before starting the NEXT task, read the learnings file for context from previous tasks
|
|
712
941
|
|
|
713
942
|
**Gate**: Only mark complete when ALL checks pass. If ANY check fails, fix first.
|
|
714
|
-
</Verification
|
|
943
|
+
</Verification>`;
|
|
944
|
+
}
|
|
945
|
+
function buildTapestryPostExecutionReviewSection(disabled) {
|
|
946
|
+
const hasWeft = isAgentEnabled("weft", disabled);
|
|
947
|
+
const hasWarp = isAgentEnabled("warp", disabled);
|
|
948
|
+
if (!hasWeft && !hasWarp) {
|
|
949
|
+
return `<PostExecutionReview>
|
|
950
|
+
After ALL plan tasks are checked off:
|
|
715
951
|
|
|
716
|
-
|
|
952
|
+
1. Identify all changed files:
|
|
953
|
+
- If a **Start SHA** was provided in the session context, run \`git diff --name-only <start-sha>..HEAD\` to get the complete list of changed files (this captures all changes including intermediate commits)
|
|
954
|
+
- If no Start SHA is available (non-git workspace), use the plan's \`**Files**:\` fields as the review scope
|
|
955
|
+
2. Report the summary of all changes to the user.
|
|
956
|
+
</PostExecutionReview>`;
|
|
957
|
+
}
|
|
958
|
+
const reviewerLines = [];
|
|
959
|
+
if (hasWeft) {
|
|
960
|
+
reviewerLines.push(` - Weft: subagent_type "weft" — reviews code quality`);
|
|
961
|
+
}
|
|
962
|
+
if (hasWarp) {
|
|
963
|
+
reviewerLines.push(` - Warp: subagent_type "warp" — audits security (self-triages; fast-exits with APPROVE if no security-relevant changes)`);
|
|
964
|
+
}
|
|
965
|
+
const reviewerNames = [hasWeft && "Weft", hasWarp && "Warp"].filter(Boolean).join(" and ");
|
|
966
|
+
return `<PostExecutionReview>
|
|
717
967
|
After ALL plan tasks are checked off, run this mandatory review gate:
|
|
718
968
|
|
|
719
969
|
1. Identify all changed files:
|
|
720
970
|
- If a **Start SHA** was provided in the session context, run \`git diff --name-only <start-sha>..HEAD\` to get the complete list of changed files (this captures all changes including intermediate commits)
|
|
721
971
|
- If no Start SHA is available (non-git workspace), use the plan's \`**Files**:\` fields as the review scope
|
|
722
|
-
2. Delegate to
|
|
723
|
-
|
|
724
|
-
|
|
972
|
+
2. Delegate to ${reviewerNames} in parallel using the Task tool:
|
|
973
|
+
${reviewerLines.join(`
|
|
974
|
+
`)}
|
|
725
975
|
- Include the list of changed files in your prompt to each reviewer
|
|
726
976
|
3. Report the review results to the user:
|
|
727
|
-
- Summarize
|
|
977
|
+
- Summarize ${reviewerNames}'s findings (APPROVE or REJECT with details)
|
|
728
978
|
- If either reviewer REJECTS, present the blocking issues to the user for decision — do NOT attempt to fix them yourself
|
|
729
979
|
- Tapestry follows the plan; review findings require user approval before any further changes
|
|
730
|
-
</PostExecutionReview
|
|
731
|
-
|
|
732
|
-
|
|
980
|
+
</PostExecutionReview>`;
|
|
981
|
+
}
|
|
982
|
+
function buildTapestryExecutionSection() {
|
|
983
|
+
return `<Execution>
|
|
733
984
|
- Work through tasks top to bottom
|
|
734
985
|
- Verify each step before marking complete
|
|
735
986
|
- If blocked: document reason, move to next unblocked task
|
|
736
987
|
- Report completion with evidence (test output, file paths, commands run)
|
|
737
|
-
</Execution
|
|
738
|
-
|
|
739
|
-
|
|
988
|
+
</Execution>`;
|
|
989
|
+
}
|
|
990
|
+
function buildTapestryStyleSection() {
|
|
991
|
+
return `<Style>
|
|
740
992
|
- Terse status updates only
|
|
741
993
|
- No meta-commentary
|
|
742
994
|
- Dense > verbose
|
|
743
|
-
</Style
|
|
995
|
+
</Style>`;
|
|
996
|
+
}
|
|
997
|
+
function composeTapestryPrompt(options = {}) {
|
|
998
|
+
const disabled = options.disabledAgents ?? new Set;
|
|
999
|
+
const sections = [
|
|
1000
|
+
buildTapestryRoleSection(),
|
|
1001
|
+
buildTapestryDisciplineSection(),
|
|
1002
|
+
buildTapestrySidebarTodosSection(),
|
|
1003
|
+
buildTapestryPlanExecutionSection(disabled),
|
|
1004
|
+
buildTapestryVerificationSection(),
|
|
1005
|
+
buildTapestryPostExecutionReviewSection(disabled),
|
|
1006
|
+
buildTapestryExecutionSection(),
|
|
1007
|
+
buildTapestryStyleSection()
|
|
1008
|
+
];
|
|
1009
|
+
return sections.join(`
|
|
1010
|
+
|
|
1011
|
+
`);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/agents/tapestry/default.ts
|
|
1015
|
+
var TAPESTRY_DEFAULTS = {
|
|
1016
|
+
temperature: 0.1,
|
|
1017
|
+
description: "Tapestry (Execution Orchestrator)",
|
|
1018
|
+
tools: {
|
|
1019
|
+
call_weave_agent: false
|
|
1020
|
+
},
|
|
1021
|
+
prompt: composeTapestryPrompt()
|
|
744
1022
|
};
|
|
745
1023
|
|
|
746
1024
|
// src/agents/tapestry/index.ts
|
|
1025
|
+
function createTapestryAgentWithOptions(model, disabledAgents) {
|
|
1026
|
+
if (!disabledAgents || disabledAgents.size === 0) {
|
|
1027
|
+
return { ...TAPESTRY_DEFAULTS, tools: { ...TAPESTRY_DEFAULTS.tools }, model, mode: "primary" };
|
|
1028
|
+
}
|
|
1029
|
+
return {
|
|
1030
|
+
...TAPESTRY_DEFAULTS,
|
|
1031
|
+
tools: { ...TAPESTRY_DEFAULTS.tools },
|
|
1032
|
+
prompt: composeTapestryPrompt({ disabledAgents }),
|
|
1033
|
+
model,
|
|
1034
|
+
mode: "primary"
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
747
1037
|
var createTapestryAgent = (model) => ({
|
|
748
1038
|
...TAPESTRY_DEFAULTS,
|
|
749
1039
|
tools: { ...TAPESTRY_DEFAULTS.tools },
|
|
@@ -1295,7 +1585,7 @@ var AGENT_MODEL_REQUIREMENTS = {
|
|
|
1295
1585
|
}
|
|
1296
1586
|
};
|
|
1297
1587
|
function resolveAgentModel(agentName, options) {
|
|
1298
|
-
const { availableModels, agentMode, uiSelectedModel, categoryModel, overrideModel, systemDefaultModel } = options;
|
|
1588
|
+
const { availableModels, agentMode, uiSelectedModel, categoryModel, overrideModel, systemDefaultModel, customFallbackChain } = options;
|
|
1299
1589
|
const requirement = AGENT_MODEL_REQUIREMENTS[agentName];
|
|
1300
1590
|
if (overrideModel)
|
|
1301
1591
|
return overrideModel;
|
|
@@ -1304,21 +1594,27 @@ function resolveAgentModel(agentName, options) {
|
|
|
1304
1594
|
}
|
|
1305
1595
|
if (categoryModel && availableModels.has(categoryModel))
|
|
1306
1596
|
return categoryModel;
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1597
|
+
const fallbackChain = requirement?.fallbackChain ?? customFallbackChain;
|
|
1598
|
+
if (fallbackChain) {
|
|
1599
|
+
for (const entry of fallbackChain) {
|
|
1600
|
+
for (const provider of entry.providers) {
|
|
1601
|
+
const qualified = `${provider}/${entry.model}`;
|
|
1602
|
+
if (availableModels.has(qualified))
|
|
1603
|
+
return qualified;
|
|
1604
|
+
if (availableModels.has(entry.model))
|
|
1605
|
+
return entry.model;
|
|
1606
|
+
}
|
|
1314
1607
|
}
|
|
1315
1608
|
}
|
|
1316
1609
|
if (systemDefaultModel)
|
|
1317
1610
|
return systemDefaultModel;
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1611
|
+
if (fallbackChain && fallbackChain.length > 0) {
|
|
1612
|
+
const first = fallbackChain[0];
|
|
1613
|
+
if (first.providers.length > 0) {
|
|
1614
|
+
return `${first.providers[0]}/${first.model}`;
|
|
1615
|
+
}
|
|
1321
1616
|
}
|
|
1617
|
+
console.warn(`[weave] No model resolved for agent "${agentName}" — falling back to default github-copilot/claude-opus-4.6`);
|
|
1322
1618
|
return "github-copilot/claude-opus-4.6";
|
|
1323
1619
|
}
|
|
1324
1620
|
|
|
@@ -1328,6 +1624,41 @@ function isFactory(source) {
|
|
|
1328
1624
|
}
|
|
1329
1625
|
|
|
1330
1626
|
// src/agents/agent-builder.ts
|
|
1627
|
+
var AGENT_NAME_VARIANTS = {
|
|
1628
|
+
thread: ["thread", "Thread"],
|
|
1629
|
+
spindle: ["spindle", "Spindle"],
|
|
1630
|
+
weft: ["weft", "Weft"],
|
|
1631
|
+
warp: ["warp", "Warp"],
|
|
1632
|
+
pattern: ["pattern", "Pattern"],
|
|
1633
|
+
shuttle: ["shuttle", "Shuttle"],
|
|
1634
|
+
loom: ["loom", "Loom"],
|
|
1635
|
+
tapestry: ["tapestry", "Tapestry"]
|
|
1636
|
+
};
|
|
1637
|
+
function registerAgentNameVariants(name, variants) {
|
|
1638
|
+
if (AGENT_NAME_VARIANTS[name])
|
|
1639
|
+
return;
|
|
1640
|
+
const titleCase = name.charAt(0).toUpperCase() + name.slice(1);
|
|
1641
|
+
AGENT_NAME_VARIANTS[name] = variants ?? [name, titleCase];
|
|
1642
|
+
}
|
|
1643
|
+
function stripDisabledAgentReferences(prompt, disabled) {
|
|
1644
|
+
if (disabled.size === 0)
|
|
1645
|
+
return prompt;
|
|
1646
|
+
const disabledVariants = [];
|
|
1647
|
+
for (const name of disabled) {
|
|
1648
|
+
const variants = AGENT_NAME_VARIANTS[name];
|
|
1649
|
+
if (variants) {
|
|
1650
|
+
disabledVariants.push(...variants);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
if (disabledVariants.length === 0)
|
|
1654
|
+
return prompt;
|
|
1655
|
+
const pattern = new RegExp(`\\b(${disabledVariants.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`);
|
|
1656
|
+
const lines = prompt.split(`
|
|
1657
|
+
`);
|
|
1658
|
+
const filtered = lines.filter((line) => !pattern.test(line));
|
|
1659
|
+
return filtered.join(`
|
|
1660
|
+
`);
|
|
1661
|
+
}
|
|
1331
1662
|
function buildAgent(source, model, options) {
|
|
1332
1663
|
const base = isFactory(source) ? source(model) : { ...source };
|
|
1333
1664
|
if (base.category && options?.categories) {
|
|
@@ -1352,6 +1683,9 @@ function buildAgent(source, model, options) {
|
|
|
1352
1683
|
` + base.prompt : "");
|
|
1353
1684
|
}
|
|
1354
1685
|
}
|
|
1686
|
+
if (options?.disabledAgents && options.disabledAgents.size > 0 && base.prompt) {
|
|
1687
|
+
base.prompt = stripDisabledAgentReferences(base.prompt, options.disabledAgents);
|
|
1688
|
+
}
|
|
1355
1689
|
return base;
|
|
1356
1690
|
}
|
|
1357
1691
|
|
|
@@ -1366,6 +1700,10 @@ var AGENT_FACTORIES = {
|
|
|
1366
1700
|
weft: createWeftAgent,
|
|
1367
1701
|
warp: createWarpAgent
|
|
1368
1702
|
};
|
|
1703
|
+
var CUSTOM_AGENT_METADATA = {};
|
|
1704
|
+
function registerCustomAgentMetadata(name, metadata) {
|
|
1705
|
+
CUSTOM_AGENT_METADATA[name] = metadata;
|
|
1706
|
+
}
|
|
1369
1707
|
function createBuiltinAgents(options = {}) {
|
|
1370
1708
|
const {
|
|
1371
1709
|
disabledAgents = [],
|
|
@@ -1375,7 +1713,9 @@ function createBuiltinAgents(options = {}) {
|
|
|
1375
1713
|
systemDefaultModel,
|
|
1376
1714
|
availableModels = new Set,
|
|
1377
1715
|
disabledSkills,
|
|
1378
|
-
resolveSkills
|
|
1716
|
+
resolveSkills,
|
|
1717
|
+
fingerprint,
|
|
1718
|
+
customAgentMetadata
|
|
1379
1719
|
} = options;
|
|
1380
1720
|
const disabledSet = new Set(disabledAgents);
|
|
1381
1721
|
const result = {};
|
|
@@ -1391,11 +1731,19 @@ function createBuiltinAgents(options = {}) {
|
|
|
1391
1731
|
systemDefaultModel,
|
|
1392
1732
|
overrideModel
|
|
1393
1733
|
});
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1734
|
+
let built;
|
|
1735
|
+
if (name === "loom") {
|
|
1736
|
+
built = createLoomAgentWithOptions(resolvedModel, disabledSet, fingerprint, customAgentMetadata);
|
|
1737
|
+
} else if (name === "tapestry") {
|
|
1738
|
+
built = createTapestryAgentWithOptions(resolvedModel, disabledSet);
|
|
1739
|
+
} else {
|
|
1740
|
+
built = buildAgent(factory, resolvedModel, {
|
|
1741
|
+
categories,
|
|
1742
|
+
disabledSkills,
|
|
1743
|
+
resolveSkills,
|
|
1744
|
+
disabledAgents: disabledSet
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1399
1747
|
if (override) {
|
|
1400
1748
|
if (override.skills?.length && resolveSkills) {
|
|
1401
1749
|
const skillContent = resolveSkills(override.skills, disabledSkills);
|
|
@@ -1419,14 +1767,152 @@ function createBuiltinAgents(options = {}) {
|
|
|
1419
1767
|
return result;
|
|
1420
1768
|
}
|
|
1421
1769
|
|
|
1770
|
+
// src/agents/prompt-loader.ts
|
|
1771
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
1772
|
+
import { resolve, isAbsolute, normalize } from "path";
|
|
1773
|
+
function loadPromptFile(promptFilePath, basePath) {
|
|
1774
|
+
if (isAbsolute(promptFilePath)) {
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
const base = resolve(basePath ?? process.cwd());
|
|
1778
|
+
const resolvedPath = normalize(resolve(base, promptFilePath));
|
|
1779
|
+
if (!resolvedPath.startsWith(base + "/") && resolvedPath !== base) {
|
|
1780
|
+
return null;
|
|
1781
|
+
}
|
|
1782
|
+
if (!existsSync2(resolvedPath)) {
|
|
1783
|
+
return null;
|
|
1784
|
+
}
|
|
1785
|
+
return readFileSync2(resolvedPath, "utf-8").trim();
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// src/agents/custom-agent-factory.ts
|
|
1789
|
+
var KNOWN_TOOL_NAMES = new Set([
|
|
1790
|
+
"write",
|
|
1791
|
+
"edit",
|
|
1792
|
+
"bash",
|
|
1793
|
+
"glob",
|
|
1794
|
+
"grep",
|
|
1795
|
+
"read",
|
|
1796
|
+
"task",
|
|
1797
|
+
"call_weave_agent",
|
|
1798
|
+
"webfetch",
|
|
1799
|
+
"todowrite",
|
|
1800
|
+
"skill"
|
|
1801
|
+
]);
|
|
1802
|
+
var AGENT_NAME_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
1803
|
+
function parseFallbackModels(models) {
|
|
1804
|
+
return models.map((m) => {
|
|
1805
|
+
if (m.includes("/")) {
|
|
1806
|
+
const [provider, model] = m.split("/", 2);
|
|
1807
|
+
return { providers: [provider], model };
|
|
1808
|
+
}
|
|
1809
|
+
return { providers: ["github-copilot"], model: m };
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
function buildCustomAgent(name, config, options = {}) {
|
|
1813
|
+
if (!AGENT_NAME_PATTERN.test(name)) {
|
|
1814
|
+
throw new Error(`Invalid custom agent name "${name}": must be lowercase alphanumeric, starting with a letter, using only hyphens and underscores`);
|
|
1815
|
+
}
|
|
1816
|
+
const { resolveSkills, disabledSkills, availableModels = new Set, systemDefaultModel, uiSelectedModel, configDir } = options;
|
|
1817
|
+
let prompt = config.prompt ?? "";
|
|
1818
|
+
if (config.prompt_file) {
|
|
1819
|
+
const fileContent = loadPromptFile(config.prompt_file, configDir);
|
|
1820
|
+
if (fileContent) {
|
|
1821
|
+
prompt = fileContent;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
if (config.skills?.length && resolveSkills) {
|
|
1825
|
+
const skillContent = resolveSkills(config.skills, disabledSkills);
|
|
1826
|
+
if (skillContent) {
|
|
1827
|
+
prompt = skillContent + (prompt ? `
|
|
1828
|
+
|
|
1829
|
+
` + prompt : "");
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
const mode = config.mode ?? "subagent";
|
|
1833
|
+
const customFallbackChain = config.fallback_models?.length ? parseFallbackModels(config.fallback_models) : undefined;
|
|
1834
|
+
const model = resolveAgentModel(name, {
|
|
1835
|
+
availableModels,
|
|
1836
|
+
agentMode: mode,
|
|
1837
|
+
overrideModel: config.model,
|
|
1838
|
+
systemDefaultModel,
|
|
1839
|
+
uiSelectedModel,
|
|
1840
|
+
customFallbackChain
|
|
1841
|
+
});
|
|
1842
|
+
const displayName = config.display_name ?? name;
|
|
1843
|
+
registerAgentDisplayName(name, displayName);
|
|
1844
|
+
registerAgentNameVariants(name, displayName !== name ? [name, displayName] : undefined);
|
|
1845
|
+
const agentConfig = {
|
|
1846
|
+
model,
|
|
1847
|
+
prompt: prompt || undefined,
|
|
1848
|
+
description: config.description ?? displayName,
|
|
1849
|
+
mode
|
|
1850
|
+
};
|
|
1851
|
+
if (config.temperature !== undefined)
|
|
1852
|
+
agentConfig.temperature = config.temperature;
|
|
1853
|
+
if (config.top_p !== undefined)
|
|
1854
|
+
agentConfig.top_p = config.top_p;
|
|
1855
|
+
if (config.maxTokens !== undefined)
|
|
1856
|
+
agentConfig.maxTokens = config.maxTokens;
|
|
1857
|
+
if (config.tools) {
|
|
1858
|
+
const unknownTools = Object.keys(config.tools).filter((t) => !KNOWN_TOOL_NAMES.has(t));
|
|
1859
|
+
if (unknownTools.length > 0) {
|
|
1860
|
+
throw new Error(`Custom agent "${name}" specifies unknown tool(s): ${unknownTools.join(", ")}. ` + `Known tools: ${[...KNOWN_TOOL_NAMES].join(", ")}`);
|
|
1861
|
+
}
|
|
1862
|
+
agentConfig.tools = config.tools;
|
|
1863
|
+
}
|
|
1864
|
+
return agentConfig;
|
|
1865
|
+
}
|
|
1866
|
+
function buildCustomAgentMetadata(name, config) {
|
|
1867
|
+
return {
|
|
1868
|
+
category: config.category ?? "utility",
|
|
1869
|
+
cost: config.cost ?? "CHEAP",
|
|
1870
|
+
triggers: config.triggers ?? [
|
|
1871
|
+
{ domain: "Custom", trigger: `Tasks delegated to ${config.display_name ?? name}` }
|
|
1872
|
+
]
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1422
1876
|
// src/create-managers.ts
|
|
1423
1877
|
function createManagers(options) {
|
|
1424
|
-
const { pluginConfig, resolveSkills } = options;
|
|
1878
|
+
const { pluginConfig, resolveSkills, fingerprint, configDir } = options;
|
|
1879
|
+
const customAgentMetadata = [];
|
|
1880
|
+
if (pluginConfig.custom_agents) {
|
|
1881
|
+
const disabledSet = new Set(pluginConfig.disabled_agents ?? []);
|
|
1882
|
+
for (const [name, customConfig] of Object.entries(pluginConfig.custom_agents)) {
|
|
1883
|
+
if (disabledSet.has(name))
|
|
1884
|
+
continue;
|
|
1885
|
+
const metadata = buildCustomAgentMetadata(name, customConfig);
|
|
1886
|
+
customAgentMetadata.push({
|
|
1887
|
+
name,
|
|
1888
|
+
description: customConfig.description ?? customConfig.display_name ?? name,
|
|
1889
|
+
metadata
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1425
1893
|
const agents = createBuiltinAgents({
|
|
1426
1894
|
disabledAgents: pluginConfig.disabled_agents,
|
|
1427
1895
|
agentOverrides: pluginConfig.agents,
|
|
1428
|
-
resolveSkills
|
|
1896
|
+
resolveSkills,
|
|
1897
|
+
fingerprint,
|
|
1898
|
+
customAgentMetadata
|
|
1429
1899
|
});
|
|
1900
|
+
if (pluginConfig.custom_agents) {
|
|
1901
|
+
const disabledSet = new Set(pluginConfig.disabled_agents ?? []);
|
|
1902
|
+
for (const [name, customConfig] of Object.entries(pluginConfig.custom_agents)) {
|
|
1903
|
+
if (disabledSet.has(name))
|
|
1904
|
+
continue;
|
|
1905
|
+
if (agents[name] !== undefined)
|
|
1906
|
+
continue;
|
|
1907
|
+
agents[name] = buildCustomAgent(name, customConfig, {
|
|
1908
|
+
resolveSkills,
|
|
1909
|
+
disabledSkills: pluginConfig.disabled_skills ? new Set(pluginConfig.disabled_skills) : undefined,
|
|
1910
|
+
configDir
|
|
1911
|
+
});
|
|
1912
|
+
const metadata = buildCustomAgentMetadata(name, customConfig);
|
|
1913
|
+
registerCustomAgentMetadata(name, metadata);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1430
1916
|
const configHandler = new ConfigHandler({ pluginConfig });
|
|
1431
1917
|
const backgroundManager = new BackgroundManager({
|
|
1432
1918
|
maxConcurrent: pluginConfig.background?.defaultConcurrency ?? 5
|
|
@@ -1873,7 +2359,7 @@ var WORK_STATE_FILE = "state.json";
|
|
|
1873
2359
|
var WORK_STATE_PATH = `${WEAVE_DIR}/${WORK_STATE_FILE}`;
|
|
1874
2360
|
var PLANS_DIR = `${WEAVE_DIR}/plans`;
|
|
1875
2361
|
// src/features/work-state/storage.ts
|
|
1876
|
-
import { existsSync as
|
|
2362
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync, unlinkSync, mkdirSync, readdirSync as readdirSync2, statSync } from "fs";
|
|
1877
2363
|
import { join as join6, basename } from "path";
|
|
1878
2364
|
import { execSync } from "child_process";
|
|
1879
2365
|
var UNCHECKED_RE = /^[-*]\s*\[\s*\]/gm;
|
|
@@ -1881,9 +2367,9 @@ var CHECKED_RE = /^[-*]\s*\[[xX]\]/gm;
|
|
|
1881
2367
|
function readWorkState(directory) {
|
|
1882
2368
|
const filePath = join6(directory, WEAVE_DIR, WORK_STATE_FILE);
|
|
1883
2369
|
try {
|
|
1884
|
-
if (!
|
|
2370
|
+
if (!existsSync6(filePath))
|
|
1885
2371
|
return null;
|
|
1886
|
-
const raw =
|
|
2372
|
+
const raw = readFileSync5(filePath, "utf-8");
|
|
1887
2373
|
const parsed = JSON.parse(raw);
|
|
1888
2374
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
1889
2375
|
return null;
|
|
@@ -1900,7 +2386,7 @@ function readWorkState(directory) {
|
|
|
1900
2386
|
function writeWorkState(directory, state) {
|
|
1901
2387
|
try {
|
|
1902
2388
|
const dir = join6(directory, WEAVE_DIR);
|
|
1903
|
-
if (!
|
|
2389
|
+
if (!existsSync6(dir)) {
|
|
1904
2390
|
mkdirSync(dir, { recursive: true });
|
|
1905
2391
|
}
|
|
1906
2392
|
writeFileSync(join6(dir, WORK_STATE_FILE), JSON.stringify(state, null, 2), "utf-8");
|
|
@@ -1912,7 +2398,7 @@ function writeWorkState(directory, state) {
|
|
|
1912
2398
|
function clearWorkState(directory) {
|
|
1913
2399
|
const filePath = join6(directory, WEAVE_DIR, WORK_STATE_FILE);
|
|
1914
2400
|
try {
|
|
1915
|
-
if (
|
|
2401
|
+
if (existsSync6(filePath)) {
|
|
1916
2402
|
unlinkSync(filePath);
|
|
1917
2403
|
}
|
|
1918
2404
|
return true;
|
|
@@ -1956,7 +2442,7 @@ function getHeadSha(directory) {
|
|
|
1956
2442
|
function findPlans(directory) {
|
|
1957
2443
|
const plansDir = join6(directory, PLANS_DIR);
|
|
1958
2444
|
try {
|
|
1959
|
-
if (!
|
|
2445
|
+
if (!existsSync6(plansDir))
|
|
1960
2446
|
return [];
|
|
1961
2447
|
const files = readdirSync2(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
1962
2448
|
const fullPath = join6(plansDir, f);
|
|
@@ -1969,11 +2455,11 @@ function findPlans(directory) {
|
|
|
1969
2455
|
}
|
|
1970
2456
|
}
|
|
1971
2457
|
function getPlanProgress(planPath) {
|
|
1972
|
-
if (!
|
|
2458
|
+
if (!existsSync6(planPath)) {
|
|
1973
2459
|
return { total: 0, completed: 0, isComplete: true };
|
|
1974
2460
|
}
|
|
1975
2461
|
try {
|
|
1976
|
-
const content =
|
|
2462
|
+
const content = readFileSync5(planPath, "utf-8");
|
|
1977
2463
|
const unchecked = content.match(UNCHECKED_RE) || [];
|
|
1978
2464
|
const checked = content.match(CHECKED_RE) || [];
|
|
1979
2465
|
const total = unchecked.length + checked.length;
|
|
@@ -1990,14 +2476,28 @@ function getPlanProgress(planPath) {
|
|
|
1990
2476
|
function getPlanName(planPath) {
|
|
1991
2477
|
return basename(planPath, ".md");
|
|
1992
2478
|
}
|
|
2479
|
+
function pauseWork(directory) {
|
|
2480
|
+
const state = readWorkState(directory);
|
|
2481
|
+
if (!state)
|
|
2482
|
+
return false;
|
|
2483
|
+
state.paused = true;
|
|
2484
|
+
return writeWorkState(directory, state);
|
|
2485
|
+
}
|
|
2486
|
+
function resumeWork(directory) {
|
|
2487
|
+
const state = readWorkState(directory);
|
|
2488
|
+
if (!state)
|
|
2489
|
+
return false;
|
|
2490
|
+
state.paused = false;
|
|
2491
|
+
return writeWorkState(directory, state);
|
|
2492
|
+
}
|
|
1993
2493
|
// src/features/work-state/validation.ts
|
|
1994
|
-
import { readFileSync as
|
|
1995
|
-
import { resolve as
|
|
2494
|
+
import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
|
|
2495
|
+
import { resolve as resolve3, sep } from "path";
|
|
1996
2496
|
function validatePlan(planPath, projectDir) {
|
|
1997
2497
|
const errors = [];
|
|
1998
2498
|
const warnings = [];
|
|
1999
|
-
const resolvedPlanPath =
|
|
2000
|
-
const allowedDir =
|
|
2499
|
+
const resolvedPlanPath = resolve3(planPath);
|
|
2500
|
+
const allowedDir = resolve3(projectDir, PLANS_DIR);
|
|
2001
2501
|
if (!resolvedPlanPath.startsWith(allowedDir + sep) && resolvedPlanPath !== allowedDir) {
|
|
2002
2502
|
errors.push({
|
|
2003
2503
|
severity: "error",
|
|
@@ -2006,7 +2506,7 @@ function validatePlan(planPath, projectDir) {
|
|
|
2006
2506
|
});
|
|
2007
2507
|
return { valid: false, errors, warnings };
|
|
2008
2508
|
}
|
|
2009
|
-
if (!
|
|
2509
|
+
if (!existsSync7(resolvedPlanPath)) {
|
|
2010
2510
|
errors.push({
|
|
2011
2511
|
severity: "error",
|
|
2012
2512
|
category: "structure",
|
|
@@ -2014,7 +2514,7 @@ function validatePlan(planPath, projectDir) {
|
|
|
2014
2514
|
});
|
|
2015
2515
|
return { valid: false, errors, warnings };
|
|
2016
2516
|
}
|
|
2017
|
-
const content =
|
|
2517
|
+
const content = readFileSync6(resolvedPlanPath, "utf-8");
|
|
2018
2518
|
validateStructure(content, errors, warnings);
|
|
2019
2519
|
validateCheckboxes(content, errors, warnings);
|
|
2020
2520
|
validateFileReferences(content, projectDir, warnings);
|
|
@@ -2176,8 +2676,8 @@ function validateFileReferences(content, projectDir, warnings) {
|
|
|
2176
2676
|
});
|
|
2177
2677
|
continue;
|
|
2178
2678
|
}
|
|
2179
|
-
const resolvedProject =
|
|
2180
|
-
const absolutePath =
|
|
2679
|
+
const resolvedProject = resolve3(projectDir);
|
|
2680
|
+
const absolutePath = resolve3(projectDir, filePath);
|
|
2181
2681
|
if (!absolutePath.startsWith(resolvedProject + sep) && absolutePath !== resolvedProject) {
|
|
2182
2682
|
warnings.push({
|
|
2183
2683
|
severity: "warning",
|
|
@@ -2186,7 +2686,7 @@ function validateFileReferences(content, projectDir, warnings) {
|
|
|
2186
2686
|
});
|
|
2187
2687
|
continue;
|
|
2188
2688
|
}
|
|
2189
|
-
if (!
|
|
2689
|
+
if (!existsSync7(absolutePath)) {
|
|
2190
2690
|
warnings.push({
|
|
2191
2691
|
severity: "warning",
|
|
2192
2692
|
category: "file-references",
|
|
@@ -2299,6 +2799,7 @@ Tell the user to fix the plan file and run /start-work again.`
|
|
|
2299
2799
|
};
|
|
2300
2800
|
}
|
|
2301
2801
|
appendSessionId(directory, sessionId);
|
|
2802
|
+
resumeWork(directory);
|
|
2302
2803
|
const resumeContext = buildResumeContext(existingState.active_plan, existingState.plan_name, progress, existingState.start_sha);
|
|
2303
2804
|
if (validation.warnings.length > 0) {
|
|
2304
2805
|
return {
|
|
@@ -2505,19 +3006,45 @@ Keep each todo under 35 chars. ${remaining} task${remaining !== 1 ? "s" : ""} re
|
|
|
2505
3006
|
}
|
|
2506
3007
|
|
|
2507
3008
|
// src/hooks/work-continuation.ts
|
|
3009
|
+
var CONTINUATION_MARKER = "<!-- weave:continuation -->";
|
|
3010
|
+
var MAX_STALE_CONTINUATIONS = 3;
|
|
2508
3011
|
function checkContinuation(input) {
|
|
2509
3012
|
const { directory } = input;
|
|
2510
3013
|
const state = readWorkState(directory);
|
|
2511
3014
|
if (!state) {
|
|
2512
3015
|
return { continuationPrompt: null };
|
|
2513
3016
|
}
|
|
3017
|
+
if (state.paused) {
|
|
3018
|
+
return { continuationPrompt: null };
|
|
3019
|
+
}
|
|
3020
|
+
if (state.session_ids.length > 0 && !state.session_ids.includes(input.sessionId)) {
|
|
3021
|
+
return { continuationPrompt: null };
|
|
3022
|
+
}
|
|
2514
3023
|
const progress = getPlanProgress(state.active_plan);
|
|
2515
3024
|
if (progress.isComplete) {
|
|
2516
3025
|
return { continuationPrompt: null };
|
|
2517
3026
|
}
|
|
3027
|
+
if (state.continuation_completed_snapshot === undefined) {
|
|
3028
|
+
state.continuation_completed_snapshot = progress.completed;
|
|
3029
|
+
state.stale_continuation_count = 0;
|
|
3030
|
+
writeWorkState(directory, state);
|
|
3031
|
+
} else if (progress.completed > state.continuation_completed_snapshot) {
|
|
3032
|
+
state.continuation_completed_snapshot = progress.completed;
|
|
3033
|
+
state.stale_continuation_count = 0;
|
|
3034
|
+
writeWorkState(directory, state);
|
|
3035
|
+
} else {
|
|
3036
|
+
state.stale_continuation_count = (state.stale_continuation_count ?? 0) + 1;
|
|
3037
|
+
if (state.stale_continuation_count >= MAX_STALE_CONTINUATIONS) {
|
|
3038
|
+
state.paused = true;
|
|
3039
|
+
writeWorkState(directory, state);
|
|
3040
|
+
return { continuationPrompt: null };
|
|
3041
|
+
}
|
|
3042
|
+
writeWorkState(directory, state);
|
|
3043
|
+
}
|
|
2518
3044
|
const remaining = progress.total - progress.completed;
|
|
2519
3045
|
return {
|
|
2520
|
-
continuationPrompt:
|
|
3046
|
+
continuationPrompt: `${CONTINUATION_MARKER}
|
|
3047
|
+
You have an active work plan with incomplete tasks. Continue working.
|
|
2521
3048
|
|
|
2522
3049
|
**Plan**: ${state.plan_name}
|
|
2523
3050
|
**File**: ${state.active_plan}
|
|
@@ -2559,7 +3086,7 @@ Only mark complete when ALL checks pass.`
|
|
|
2559
3086
|
|
|
2560
3087
|
// src/hooks/create-hooks.ts
|
|
2561
3088
|
function createHooks(args) {
|
|
2562
|
-
const { pluginConfig, isHookEnabled, directory } = args;
|
|
3089
|
+
const { pluginConfig, isHookEnabled, directory, analyticsEnabled = false } = args;
|
|
2563
3090
|
const writeGuardState = createWriteGuardState();
|
|
2564
3091
|
const writeGuard = createWriteGuard(writeGuardState);
|
|
2565
3092
|
const contextWindowThresholds = {
|
|
@@ -2576,7 +3103,8 @@ function createHooks(args) {
|
|
|
2576
3103
|
patternMdOnly: isHookEnabled("pattern-md-only") ? checkPatternWrite : null,
|
|
2577
3104
|
startWork: isHookEnabled("start-work") ? (promptText, sessionId) => handleStartWork({ promptText, sessionId, directory }) : null,
|
|
2578
3105
|
workContinuation: isHookEnabled("work-continuation") ? (sessionId) => checkContinuation({ sessionId, directory }) : null,
|
|
2579
|
-
verificationReminder: isHookEnabled("verification-reminder") ? buildVerificationReminder : null
|
|
3106
|
+
verificationReminder: isHookEnabled("verification-reminder") ? buildVerificationReminder : null,
|
|
3107
|
+
analyticsEnabled
|
|
2580
3108
|
};
|
|
2581
3109
|
}
|
|
2582
3110
|
// src/hooks/session-token-state.ts
|
|
@@ -2605,8 +3133,7 @@ function clearSession2(sessionId) {
|
|
|
2605
3133
|
}
|
|
2606
3134
|
// src/plugin/plugin-interface.ts
|
|
2607
3135
|
function createPluginInterface(args) {
|
|
2608
|
-
const { pluginConfig, hooks, tools, configHandler, agents, client } = args;
|
|
2609
|
-
let pendingInterrupt = false;
|
|
3136
|
+
const { pluginConfig, hooks, tools, configHandler, agents, client, directory = "", tracker } = args;
|
|
2610
3137
|
return {
|
|
2611
3138
|
tool: tools,
|
|
2612
3139
|
config: async (config) => {
|
|
@@ -2660,6 +3187,20 @@ ${result.contextInjection}`;
|
|
|
2660
3187
|
}
|
|
2661
3188
|
}
|
|
2662
3189
|
}
|
|
3190
|
+
if (directory) {
|
|
3191
|
+
const parts = _output.parts;
|
|
3192
|
+
const promptText = parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
3193
|
+
`).trim() ?? "";
|
|
3194
|
+
const isStartWork = promptText.includes("<session-context>");
|
|
3195
|
+
const isContinuation = promptText.includes(CONTINUATION_MARKER);
|
|
3196
|
+
if (!isStartWork && !isContinuation) {
|
|
3197
|
+
const state = readWorkState(directory);
|
|
3198
|
+
if (state && !state.paused) {
|
|
3199
|
+
pauseWork(directory);
|
|
3200
|
+
log("[work-continuation] Auto-paused: user message received during active plan", { sessionId: sessionID });
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
2663
3204
|
},
|
|
2664
3205
|
"chat.params": async (_input, _output) => {
|
|
2665
3206
|
const input = _input;
|
|
@@ -2686,6 +3227,13 @@ ${result.contextInjection}`;
|
|
|
2686
3227
|
if (event.type === "session.deleted") {
|
|
2687
3228
|
const evt = event;
|
|
2688
3229
|
clearSession2(evt.properties.info.id);
|
|
3230
|
+
if (tracker && hooks.analyticsEnabled) {
|
|
3231
|
+
try {
|
|
3232
|
+
tracker.endSession(evt.properties.info.id);
|
|
3233
|
+
} catch (err) {
|
|
3234
|
+
log("[analytics] Failed to end session (non-fatal)", { error: String(err) });
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
2689
3237
|
}
|
|
2690
3238
|
if (event.type === "message.updated" && hooks.checkContextWindow) {
|
|
2691
3239
|
const evt = event;
|
|
@@ -2715,41 +3263,36 @@ ${result.contextInjection}`;
|
|
|
2715
3263
|
if (event.type === "tui.command.execute") {
|
|
2716
3264
|
const evt = event;
|
|
2717
3265
|
if (evt.properties?.command === "session.interrupt") {
|
|
2718
|
-
|
|
2719
|
-
log("[work-continuation] User interrupt detected —
|
|
3266
|
+
pauseWork(directory);
|
|
3267
|
+
log("[work-continuation] User interrupt detected — work paused");
|
|
2720
3268
|
}
|
|
2721
3269
|
}
|
|
2722
3270
|
if (hooks.workContinuation && event.type === "session.idle") {
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
});
|
|
2739
|
-
log("[work-continuation] Injected continuation prompt", { sessionId });
|
|
2740
|
-
} catch (err) {
|
|
2741
|
-
log("[work-continuation] Failed to inject continuation", { sessionId, error: String(err) });
|
|
2742
|
-
}
|
|
2743
|
-
} else if (result.continuationPrompt) {
|
|
2744
|
-
log("[work-continuation] continuationPrompt available but no client", { sessionId });
|
|
3271
|
+
const evt = event;
|
|
3272
|
+
const sessionId = evt.properties?.sessionID ?? "";
|
|
3273
|
+
if (sessionId) {
|
|
3274
|
+
const result = hooks.workContinuation(sessionId);
|
|
3275
|
+
if (result.continuationPrompt && client) {
|
|
3276
|
+
try {
|
|
3277
|
+
await client.session.promptAsync({
|
|
3278
|
+
path: { id: sessionId },
|
|
3279
|
+
body: {
|
|
3280
|
+
parts: [{ type: "text", text: result.continuationPrompt }]
|
|
3281
|
+
}
|
|
3282
|
+
});
|
|
3283
|
+
log("[work-continuation] Injected continuation prompt", { sessionId });
|
|
3284
|
+
} catch (err) {
|
|
3285
|
+
log("[work-continuation] Failed to inject continuation", { sessionId, error: String(err) });
|
|
2745
3286
|
}
|
|
3287
|
+
} else if (result.continuationPrompt) {
|
|
3288
|
+
log("[work-continuation] continuationPrompt available but no client", { sessionId });
|
|
2746
3289
|
}
|
|
2747
3290
|
}
|
|
2748
3291
|
}
|
|
2749
3292
|
},
|
|
2750
3293
|
"tool.execute.before": async (input, _output) => {
|
|
2751
|
-
const
|
|
2752
|
-
const filePath =
|
|
3294
|
+
const toolArgs = _output.args;
|
|
3295
|
+
const filePath = toolArgs?.file_path ?? toolArgs?.path ?? "";
|
|
2753
3296
|
if (filePath && hooks.shouldInjectRules && hooks.getRulesForFile) {
|
|
2754
3297
|
if (hooks.shouldInjectRules(input.tool)) {
|
|
2755
3298
|
hooks.getRulesForFile(filePath);
|
|
@@ -2769,8 +3312,8 @@ ${result.contextInjection}`;
|
|
|
2769
3312
|
}
|
|
2770
3313
|
}
|
|
2771
3314
|
}
|
|
2772
|
-
if (input.tool === "task" &&
|
|
2773
|
-
const agentArg =
|
|
3315
|
+
if (input.tool === "task" && toolArgs) {
|
|
3316
|
+
const agentArg = toolArgs.subagent_type ?? toolArgs.description ?? "unknown";
|
|
2774
3317
|
logDelegation({
|
|
2775
3318
|
phase: "start",
|
|
2776
3319
|
agent: agentArg,
|
|
@@ -2778,6 +3321,10 @@ ${result.contextInjection}`;
|
|
|
2778
3321
|
toolCallId: input.callID
|
|
2779
3322
|
});
|
|
2780
3323
|
}
|
|
3324
|
+
if (tracker && hooks.analyticsEnabled) {
|
|
3325
|
+
const agentArg = input.tool === "task" && toolArgs ? toolArgs.subagent_type ?? toolArgs.description ?? "unknown" : undefined;
|
|
3326
|
+
tracker.trackToolStart(input.sessionID, input.tool, input.callID, agentArg);
|
|
3327
|
+
}
|
|
2781
3328
|
},
|
|
2782
3329
|
"tool.execute.after": async (input, _output) => {
|
|
2783
3330
|
if (input.tool === "task") {
|
|
@@ -2790,25 +3337,417 @@ ${result.contextInjection}`;
|
|
|
2790
3337
|
toolCallId: input.callID
|
|
2791
3338
|
});
|
|
2792
3339
|
}
|
|
3340
|
+
if (tracker && hooks.analyticsEnabled) {
|
|
3341
|
+
const inputArgs = input.args;
|
|
3342
|
+
const agentArg = input.tool === "task" && inputArgs ? inputArgs.subagent_type ?? inputArgs.description ?? "unknown" : undefined;
|
|
3343
|
+
tracker.trackToolEnd(input.sessionID, input.tool, input.callID, agentArg);
|
|
3344
|
+
}
|
|
2793
3345
|
}
|
|
2794
3346
|
};
|
|
2795
3347
|
}
|
|
2796
3348
|
|
|
3349
|
+
// src/features/analytics/types.ts
|
|
3350
|
+
var ANALYTICS_DIR = ".weave/analytics";
|
|
3351
|
+
var SESSION_SUMMARIES_FILE = "session-summaries.jsonl";
|
|
3352
|
+
var FINGERPRINT_FILE = "fingerprint.json";
|
|
3353
|
+
// src/features/analytics/storage.ts
|
|
3354
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
3355
|
+
import { join as join7 } from "path";
|
|
3356
|
+
var MAX_SESSION_ENTRIES = 1000;
|
|
3357
|
+
function ensureAnalyticsDir(directory) {
|
|
3358
|
+
const dir = join7(directory, ANALYTICS_DIR);
|
|
3359
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
3360
|
+
return dir;
|
|
3361
|
+
}
|
|
3362
|
+
function appendSessionSummary(directory, summary) {
|
|
3363
|
+
try {
|
|
3364
|
+
const dir = ensureAnalyticsDir(directory);
|
|
3365
|
+
const filePath = join7(dir, SESSION_SUMMARIES_FILE);
|
|
3366
|
+
const line = JSON.stringify(summary) + `
|
|
3367
|
+
`;
|
|
3368
|
+
appendFileSync2(filePath, line, { encoding: "utf-8", mode: 384 });
|
|
3369
|
+
try {
|
|
3370
|
+
const content = readFileSync7(filePath, "utf-8");
|
|
3371
|
+
const lines = content.split(`
|
|
3372
|
+
`).filter((l) => l.trim().length > 0);
|
|
3373
|
+
if (lines.length > MAX_SESSION_ENTRIES) {
|
|
3374
|
+
const trimmed = lines.slice(-MAX_SESSION_ENTRIES).join(`
|
|
3375
|
+
`) + `
|
|
3376
|
+
`;
|
|
3377
|
+
writeFileSync2(filePath, trimmed, { encoding: "utf-8", mode: 384 });
|
|
3378
|
+
}
|
|
3379
|
+
} catch {}
|
|
3380
|
+
return true;
|
|
3381
|
+
} catch {
|
|
3382
|
+
return false;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
function writeFingerprint(directory, fingerprint) {
|
|
3386
|
+
try {
|
|
3387
|
+
const dir = ensureAnalyticsDir(directory);
|
|
3388
|
+
const filePath = join7(dir, FINGERPRINT_FILE);
|
|
3389
|
+
writeFileSync2(filePath, JSON.stringify(fingerprint, null, 2), { encoding: "utf-8", mode: 384 });
|
|
3390
|
+
return true;
|
|
3391
|
+
} catch {
|
|
3392
|
+
return false;
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
function readFingerprint(directory) {
|
|
3396
|
+
const filePath = join7(directory, ANALYTICS_DIR, FINGERPRINT_FILE);
|
|
3397
|
+
try {
|
|
3398
|
+
if (!existsSync8(filePath))
|
|
3399
|
+
return null;
|
|
3400
|
+
const content = readFileSync7(filePath, "utf-8");
|
|
3401
|
+
const parsed = JSON.parse(content);
|
|
3402
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.stack))
|
|
3403
|
+
return null;
|
|
3404
|
+
return parsed;
|
|
3405
|
+
} catch {
|
|
3406
|
+
return null;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
// src/features/analytics/fingerprint.ts
|
|
3410
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync3 } from "fs";
|
|
3411
|
+
import { join as join8 } from "path";
|
|
3412
|
+
import { arch } from "os";
|
|
3413
|
+
var STACK_MARKERS = [
|
|
3414
|
+
{
|
|
3415
|
+
name: "typescript",
|
|
3416
|
+
files: ["tsconfig.json", "tsconfig.build.json"],
|
|
3417
|
+
confidence: "high",
|
|
3418
|
+
evidence: (f) => `${f} exists`
|
|
3419
|
+
},
|
|
3420
|
+
{
|
|
3421
|
+
name: "bun",
|
|
3422
|
+
files: ["bun.lockb", "bunfig.toml"],
|
|
3423
|
+
confidence: "high",
|
|
3424
|
+
evidence: (f) => `${f} exists`
|
|
3425
|
+
},
|
|
3426
|
+
{
|
|
3427
|
+
name: "node",
|
|
3428
|
+
files: ["package.json"],
|
|
3429
|
+
confidence: "high",
|
|
3430
|
+
evidence: (f) => `${f} exists`
|
|
3431
|
+
},
|
|
3432
|
+
{
|
|
3433
|
+
name: "npm",
|
|
3434
|
+
files: ["package-lock.json"],
|
|
3435
|
+
confidence: "high",
|
|
3436
|
+
evidence: (f) => `${f} exists`
|
|
3437
|
+
},
|
|
3438
|
+
{
|
|
3439
|
+
name: "yarn",
|
|
3440
|
+
files: ["yarn.lock"],
|
|
3441
|
+
confidence: "high",
|
|
3442
|
+
evidence: (f) => `${f} exists`
|
|
3443
|
+
},
|
|
3444
|
+
{
|
|
3445
|
+
name: "pnpm",
|
|
3446
|
+
files: ["pnpm-lock.yaml"],
|
|
3447
|
+
confidence: "high",
|
|
3448
|
+
evidence: (f) => `${f} exists`
|
|
3449
|
+
},
|
|
3450
|
+
{
|
|
3451
|
+
name: "react",
|
|
3452
|
+
files: [],
|
|
3453
|
+
confidence: "medium",
|
|
3454
|
+
evidence: () => "react in package.json dependencies"
|
|
3455
|
+
},
|
|
3456
|
+
{
|
|
3457
|
+
name: "next",
|
|
3458
|
+
files: ["next.config.js", "next.config.ts", "next.config.mjs"],
|
|
3459
|
+
confidence: "high",
|
|
3460
|
+
evidence: (f) => `${f} exists`
|
|
3461
|
+
},
|
|
3462
|
+
{
|
|
3463
|
+
name: "python",
|
|
3464
|
+
files: ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"],
|
|
3465
|
+
confidence: "high",
|
|
3466
|
+
evidence: (f) => `${f} exists`
|
|
3467
|
+
},
|
|
3468
|
+
{
|
|
3469
|
+
name: "go",
|
|
3470
|
+
files: ["go.mod"],
|
|
3471
|
+
confidence: "high",
|
|
3472
|
+
evidence: (f) => `${f} exists`
|
|
3473
|
+
},
|
|
3474
|
+
{
|
|
3475
|
+
name: "rust",
|
|
3476
|
+
files: ["Cargo.toml"],
|
|
3477
|
+
confidence: "high",
|
|
3478
|
+
evidence: (f) => `${f} exists`
|
|
3479
|
+
},
|
|
3480
|
+
{
|
|
3481
|
+
name: "dotnet",
|
|
3482
|
+
files: ["global.json", "Directory.Build.props", "Directory.Packages.props"],
|
|
3483
|
+
confidence: "high",
|
|
3484
|
+
evidence: (f) => `${f} exists`
|
|
3485
|
+
},
|
|
3486
|
+
{
|
|
3487
|
+
name: "docker",
|
|
3488
|
+
files: ["Dockerfile", "docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"],
|
|
3489
|
+
confidence: "high",
|
|
3490
|
+
evidence: (f) => `${f} exists`
|
|
3491
|
+
}
|
|
3492
|
+
];
|
|
3493
|
+
var MONOREPO_MARKERS = [
|
|
3494
|
+
"lerna.json",
|
|
3495
|
+
"nx.json",
|
|
3496
|
+
"turbo.json",
|
|
3497
|
+
"pnpm-workspace.yaml",
|
|
3498
|
+
"rush.json"
|
|
3499
|
+
];
|
|
3500
|
+
function detectStack(directory) {
|
|
3501
|
+
const detected = [];
|
|
3502
|
+
for (const marker of STACK_MARKERS) {
|
|
3503
|
+
for (const file of marker.files) {
|
|
3504
|
+
if (existsSync9(join8(directory, file))) {
|
|
3505
|
+
detected.push({
|
|
3506
|
+
name: marker.name,
|
|
3507
|
+
confidence: marker.confidence,
|
|
3508
|
+
evidence: marker.evidence(file)
|
|
3509
|
+
});
|
|
3510
|
+
break;
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
try {
|
|
3515
|
+
const pkgPath = join8(directory, "package.json");
|
|
3516
|
+
if (existsSync9(pkgPath)) {
|
|
3517
|
+
const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
|
|
3518
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3519
|
+
if (deps.react) {
|
|
3520
|
+
detected.push({
|
|
3521
|
+
name: "react",
|
|
3522
|
+
confidence: "medium",
|
|
3523
|
+
evidence: "react in package.json dependencies"
|
|
3524
|
+
});
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
} catch {}
|
|
3528
|
+
if (!detected.some((d) => d.name === "dotnet")) {
|
|
3529
|
+
try {
|
|
3530
|
+
const entries = readdirSync3(directory);
|
|
3531
|
+
const dotnetFile = entries.find((e) => e.endsWith(".csproj") || e.endsWith(".fsproj") || e.endsWith(".sln"));
|
|
3532
|
+
if (dotnetFile) {
|
|
3533
|
+
detected.push({
|
|
3534
|
+
name: "dotnet",
|
|
3535
|
+
confidence: "high",
|
|
3536
|
+
evidence: `${dotnetFile} found`
|
|
3537
|
+
});
|
|
3538
|
+
}
|
|
3539
|
+
} catch {}
|
|
3540
|
+
}
|
|
3541
|
+
const seen = new Set;
|
|
3542
|
+
return detected.filter((entry) => {
|
|
3543
|
+
if (seen.has(entry.name))
|
|
3544
|
+
return false;
|
|
3545
|
+
seen.add(entry.name);
|
|
3546
|
+
return true;
|
|
3547
|
+
});
|
|
3548
|
+
}
|
|
3549
|
+
function detectPackageManager(directory) {
|
|
3550
|
+
if (existsSync9(join8(directory, "bun.lockb")))
|
|
3551
|
+
return "bun";
|
|
3552
|
+
if (existsSync9(join8(directory, "pnpm-lock.yaml")))
|
|
3553
|
+
return "pnpm";
|
|
3554
|
+
if (existsSync9(join8(directory, "yarn.lock")))
|
|
3555
|
+
return "yarn";
|
|
3556
|
+
if (existsSync9(join8(directory, "package-lock.json")))
|
|
3557
|
+
return "npm";
|
|
3558
|
+
if (existsSync9(join8(directory, "package.json")))
|
|
3559
|
+
return "npm";
|
|
3560
|
+
return;
|
|
3561
|
+
}
|
|
3562
|
+
function detectMonorepo(directory) {
|
|
3563
|
+
for (const marker of MONOREPO_MARKERS) {
|
|
3564
|
+
if (existsSync9(join8(directory, marker)))
|
|
3565
|
+
return true;
|
|
3566
|
+
}
|
|
3567
|
+
try {
|
|
3568
|
+
const pkgPath = join8(directory, "package.json");
|
|
3569
|
+
if (existsSync9(pkgPath)) {
|
|
3570
|
+
const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
|
|
3571
|
+
if (pkg.workspaces)
|
|
3572
|
+
return true;
|
|
3573
|
+
}
|
|
3574
|
+
} catch {}
|
|
3575
|
+
return false;
|
|
3576
|
+
}
|
|
3577
|
+
function detectPrimaryLanguage(stack) {
|
|
3578
|
+
const languages = ["typescript", "python", "go", "rust", "dotnet"];
|
|
3579
|
+
for (const lang of languages) {
|
|
3580
|
+
if (stack.some((s) => s.name === lang))
|
|
3581
|
+
return lang;
|
|
3582
|
+
}
|
|
3583
|
+
if (stack.some((s) => s.name === "node"))
|
|
3584
|
+
return "javascript";
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
function generateFingerprint(directory) {
|
|
3588
|
+
const stack = detectStack(directory);
|
|
3589
|
+
return {
|
|
3590
|
+
generatedAt: new Date().toISOString(),
|
|
3591
|
+
stack,
|
|
3592
|
+
isMonorepo: detectMonorepo(directory),
|
|
3593
|
+
packageManager: detectPackageManager(directory),
|
|
3594
|
+
primaryLanguage: detectPrimaryLanguage(stack),
|
|
3595
|
+
os: process.platform,
|
|
3596
|
+
arch: arch()
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3599
|
+
function fingerprintProject(directory) {
|
|
3600
|
+
try {
|
|
3601
|
+
const fingerprint = generateFingerprint(directory);
|
|
3602
|
+
writeFingerprint(directory, fingerprint);
|
|
3603
|
+
log("[analytics] Project fingerprinted", {
|
|
3604
|
+
stack: fingerprint.stack.map((s) => s.name),
|
|
3605
|
+
primaryLanguage: fingerprint.primaryLanguage,
|
|
3606
|
+
packageManager: fingerprint.packageManager
|
|
3607
|
+
});
|
|
3608
|
+
return fingerprint;
|
|
3609
|
+
} catch (err) {
|
|
3610
|
+
log("[analytics] Fingerprinting failed (non-fatal)", { error: String(err) });
|
|
3611
|
+
return null;
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
function getOrCreateFingerprint(directory) {
|
|
3615
|
+
try {
|
|
3616
|
+
const existing = readFingerprint(directory);
|
|
3617
|
+
if (existing)
|
|
3618
|
+
return existing;
|
|
3619
|
+
return fingerprintProject(directory);
|
|
3620
|
+
} catch (err) {
|
|
3621
|
+
log("[analytics] getOrCreateFingerprint failed (non-fatal)", { error: String(err) });
|
|
3622
|
+
return null;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
// src/features/analytics/session-tracker.ts
|
|
3626
|
+
class SessionTracker {
|
|
3627
|
+
sessions = new Map;
|
|
3628
|
+
directory;
|
|
3629
|
+
constructor(directory) {
|
|
3630
|
+
this.directory = directory;
|
|
3631
|
+
}
|
|
3632
|
+
startSession(sessionId) {
|
|
3633
|
+
const existing = this.sessions.get(sessionId);
|
|
3634
|
+
if (existing)
|
|
3635
|
+
return existing;
|
|
3636
|
+
const session = {
|
|
3637
|
+
sessionId,
|
|
3638
|
+
startedAt: new Date().toISOString(),
|
|
3639
|
+
toolCounts: {},
|
|
3640
|
+
delegations: [],
|
|
3641
|
+
inFlight: {}
|
|
3642
|
+
};
|
|
3643
|
+
this.sessions.set(sessionId, session);
|
|
3644
|
+
return session;
|
|
3645
|
+
}
|
|
3646
|
+
trackToolStart(sessionId, toolName, callId, agent) {
|
|
3647
|
+
const session = this.startSession(sessionId);
|
|
3648
|
+
session.toolCounts[toolName] = (session.toolCounts[toolName] ?? 0) + 1;
|
|
3649
|
+
const inFlight = {
|
|
3650
|
+
tool: toolName,
|
|
3651
|
+
startedAt: Date.now()
|
|
3652
|
+
};
|
|
3653
|
+
if (agent) {
|
|
3654
|
+
inFlight.agent = agent;
|
|
3655
|
+
}
|
|
3656
|
+
session.inFlight[callId] = inFlight;
|
|
3657
|
+
}
|
|
3658
|
+
trackToolEnd(sessionId, toolName, callId, agent) {
|
|
3659
|
+
const session = this.sessions.get(sessionId);
|
|
3660
|
+
if (!session)
|
|
3661
|
+
return;
|
|
3662
|
+
const inFlight = session.inFlight[callId];
|
|
3663
|
+
delete session.inFlight[callId];
|
|
3664
|
+
if (toolName === "task") {
|
|
3665
|
+
const delegation = {
|
|
3666
|
+
agent: agent ?? inFlight?.agent ?? "unknown",
|
|
3667
|
+
toolCallId: callId
|
|
3668
|
+
};
|
|
3669
|
+
if (inFlight) {
|
|
3670
|
+
delegation.durationMs = Date.now() - inFlight.startedAt;
|
|
3671
|
+
}
|
|
3672
|
+
session.delegations.push(delegation);
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
endSession(sessionId) {
|
|
3676
|
+
const session = this.sessions.get(sessionId);
|
|
3677
|
+
if (!session)
|
|
3678
|
+
return null;
|
|
3679
|
+
const now = new Date;
|
|
3680
|
+
const startedAt = new Date(session.startedAt);
|
|
3681
|
+
const durationMs = now.getTime() - startedAt.getTime();
|
|
3682
|
+
const toolUsage = Object.entries(session.toolCounts).map(([tool, count]) => ({ tool, count }));
|
|
3683
|
+
const totalToolCalls = toolUsage.reduce((sum, entry) => sum + entry.count, 0);
|
|
3684
|
+
const summary = {
|
|
3685
|
+
sessionId,
|
|
3686
|
+
startedAt: session.startedAt,
|
|
3687
|
+
endedAt: now.toISOString(),
|
|
3688
|
+
durationMs,
|
|
3689
|
+
toolUsage,
|
|
3690
|
+
delegations: session.delegations,
|
|
3691
|
+
totalToolCalls,
|
|
3692
|
+
totalDelegations: session.delegations.length
|
|
3693
|
+
};
|
|
3694
|
+
try {
|
|
3695
|
+
appendSessionSummary(this.directory, summary);
|
|
3696
|
+
log("[analytics] Session summary persisted", {
|
|
3697
|
+
sessionId,
|
|
3698
|
+
totalToolCalls,
|
|
3699
|
+
totalDelegations: session.delegations.length
|
|
3700
|
+
});
|
|
3701
|
+
} catch (err) {
|
|
3702
|
+
log("[analytics] Failed to persist session summary (non-fatal)", {
|
|
3703
|
+
sessionId,
|
|
3704
|
+
error: String(err)
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
this.sessions.delete(sessionId);
|
|
3708
|
+
return summary;
|
|
3709
|
+
}
|
|
3710
|
+
isTracking(sessionId) {
|
|
3711
|
+
return this.sessions.has(sessionId);
|
|
3712
|
+
}
|
|
3713
|
+
getSession(sessionId) {
|
|
3714
|
+
return this.sessions.get(sessionId);
|
|
3715
|
+
}
|
|
3716
|
+
get activeSessionCount() {
|
|
3717
|
+
return this.sessions.size;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
function createSessionTracker(directory) {
|
|
3721
|
+
return new SessionTracker(directory);
|
|
3722
|
+
}
|
|
3723
|
+
// src/features/analytics/index.ts
|
|
3724
|
+
function createAnalytics(directory, fingerprint) {
|
|
3725
|
+
const tracker = createSessionTracker(directory);
|
|
3726
|
+
const resolvedFingerprint = fingerprint ?? getOrCreateFingerprint(directory);
|
|
3727
|
+
return { tracker, fingerprint: resolvedFingerprint };
|
|
3728
|
+
}
|
|
3729
|
+
|
|
2797
3730
|
// src/index.ts
|
|
2798
3731
|
var WeavePlugin = async (ctx) => {
|
|
2799
3732
|
const pluginConfig = loadWeaveConfig(ctx.directory, ctx);
|
|
2800
3733
|
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
|
2801
3734
|
const isHookEnabled = (name) => !disabledHooks.has(name);
|
|
3735
|
+
const analyticsEnabled = pluginConfig.analytics?.enabled === true;
|
|
3736
|
+
const fingerprint = analyticsEnabled ? getOrCreateFingerprint(ctx.directory) : null;
|
|
3737
|
+
const configDir = join9(ctx.directory, ".opencode");
|
|
2802
3738
|
const toolsResult = await createTools({ ctx, pluginConfig });
|
|
2803
|
-
const managers = createManagers({ ctx, pluginConfig, resolveSkills: toolsResult.resolveSkillsFn });
|
|
2804
|
-
const hooks = createHooks({ pluginConfig, isHookEnabled, directory: ctx.directory });
|
|
3739
|
+
const managers = createManagers({ ctx, pluginConfig, resolveSkills: toolsResult.resolveSkillsFn, fingerprint, configDir });
|
|
3740
|
+
const hooks = createHooks({ pluginConfig, isHookEnabled, directory: ctx.directory, analyticsEnabled });
|
|
3741
|
+
const analytics = analyticsEnabled ? createAnalytics(ctx.directory, fingerprint) : null;
|
|
2805
3742
|
return createPluginInterface({
|
|
2806
3743
|
pluginConfig,
|
|
2807
3744
|
hooks,
|
|
2808
3745
|
tools: toolsResult.tools,
|
|
2809
3746
|
configHandler: managers.configHandler,
|
|
2810
3747
|
agents: managers.agents,
|
|
2811
|
-
client: ctx.client
|
|
3748
|
+
client: ctx.client,
|
|
3749
|
+
directory: ctx.directory,
|
|
3750
|
+
tracker: analytics?.tracker
|
|
2812
3751
|
});
|
|
2813
3752
|
};
|
|
2814
3753
|
var src_default = WeavePlugin;
|