@opencode_weave/weave 0.6.2 → 0.6.4

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/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/index.ts
2
+ import { join as join10 } 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/loom/default.ts
471
- var LOOM_DEFAULTS = {
472
- temperature: 0.1,
473
- description: "Loom (Main Orchestrator)",
474
- prompt: `<Role>
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
- <Discipline>
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
- <SidebarTodos>
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
- <Delegation>
516
- - Use thread for fast codebase exploration (read-only, cheap)
517
- - Use spindle for external docs and research (read-only)
518
- - Use pattern for detailed planning before complex implementations
519
- - Use /start-work to hand off to Tapestry for todo-list driven execution of multi-step plans
520
- - Use shuttle for category-specific specialized work
521
- - Use Weft for reviewing completed work or validating plans before execution
522
- - 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.
523
- - Delegate aggressively to keep your context lean
524
- </Delegation>
525
-
526
- <DelegationNarration>
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
- <PlanWorkflow>
555
- For complex tasks that benefit from structured planning before execution:
556
-
557
- 1. PLAN: Delegate to Pattern to produce a plan saved to \`.weave/plans/{name}.md\`
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
- 2. REVIEW: Delegate to Weft to validate the plan before execution
561
- - TRIGGER: Plan touches 3+ files OR has 5+ tasks — Weft review is mandatory
562
- - SKIP ONLY IF: User explicitly says "skip review"
563
- - Weft reads the plan, verifies file references, checks executability
564
- - If Weft rejects, send issues back to Pattern for revision
565
- - MANDATORY: If the plan touches security-relevant areas (crypto, auth, certificates, tokens, signatures, or input validation) → also run Warp on the plan
566
- 3. EXECUTE: Tell the user to run \`/start-work\` to begin execution
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
- 4. RESUME: If work was interrupted, \`/start-work\` resumes from the last unchecked task
570
-
571
- Note: Tapestry runs Weft and Warp reviews directly after completing all tasks — Loom does not need to gate this.
572
-
573
- When to use this workflow vs. direct execution:
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
- </PlanWorkflow>
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
- <ReviewWorkflow>
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
- **Ad-Hoc Review (non-plan work):**
586
- - Delegate to Weft to review the changes
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
- </ReviewWorkflow>
608
-
609
- <Style>
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/default.ts
626
- var TAPESTRY_DEFAULTS = {
627
- temperature: 0.1,
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
- <Discipline>
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
- <SidebarTodos>
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
- <PlanExecution>
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. If uncertain about quality, note that Loom should invoke Weft for formal review.
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
- <Verification>
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
- <PostExecutionReview>
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 Weft (quality review) AND Warp (security audit) in parallel using the Task tool:
723
- - Weft: subagent_type "weft" — reviews code quality
724
- - Warp: subagent_type "warp" — audits security (self-triages; fast-exits with APPROVE if no security-relevant changes)
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 Weft's and Warp's findings (APPROVE or REJECT with details)
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
- <Execution>
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
- <Style>
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
- for (const entry of requirement.fallbackChain) {
1308
- for (const provider of entry.providers) {
1309
- const qualified = `${provider}/${entry.model}`;
1310
- if (availableModels.has(qualified))
1311
- return qualified;
1312
- if (availableModels.has(entry.model))
1313
- return entry.model;
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
- const first = requirement.fallbackChain[0];
1319
- if (first && first.providers.length > 0) {
1320
- return `${first.providers[0]}/${first.model}`;
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
- const built = buildAgent(factory, resolvedModel, {
1395
- categories,
1396
- disabledSkills,
1397
- resolveSkills
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 existsSync5, readFileSync as readFileSync4, writeFileSync, unlinkSync, mkdirSync, readdirSync as readdirSync2, statSync } from "fs";
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 (!existsSync5(filePath))
2370
+ if (!existsSync6(filePath))
1885
2371
  return null;
1886
- const raw = readFileSync4(filePath, "utf-8");
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 (!existsSync5(dir)) {
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 (existsSync5(filePath)) {
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 (!existsSync5(plansDir))
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 (!existsSync5(planPath)) {
2458
+ if (!existsSync6(planPath)) {
1973
2459
  return { total: 0, completed: 0, isComplete: true };
1974
2460
  }
1975
2461
  try {
1976
- const content = readFileSync4(planPath, "utf-8");
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;
@@ -2005,13 +2491,13 @@ function resumeWork(directory) {
2005
2491
  return writeWorkState(directory, state);
2006
2492
  }
2007
2493
  // src/features/work-state/validation.ts
2008
- import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
2009
- import { resolve as resolve2, sep } from "path";
2494
+ import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
2495
+ import { resolve as resolve3, sep } from "path";
2010
2496
  function validatePlan(planPath, projectDir) {
2011
2497
  const errors = [];
2012
2498
  const warnings = [];
2013
- const resolvedPlanPath = resolve2(planPath);
2014
- const allowedDir = resolve2(projectDir, PLANS_DIR);
2499
+ const resolvedPlanPath = resolve3(planPath);
2500
+ const allowedDir = resolve3(projectDir, PLANS_DIR);
2015
2501
  if (!resolvedPlanPath.startsWith(allowedDir + sep) && resolvedPlanPath !== allowedDir) {
2016
2502
  errors.push({
2017
2503
  severity: "error",
@@ -2020,7 +2506,7 @@ function validatePlan(planPath, projectDir) {
2020
2506
  });
2021
2507
  return { valid: false, errors, warnings };
2022
2508
  }
2023
- if (!existsSync6(resolvedPlanPath)) {
2509
+ if (!existsSync7(resolvedPlanPath)) {
2024
2510
  errors.push({
2025
2511
  severity: "error",
2026
2512
  category: "structure",
@@ -2028,7 +2514,7 @@ function validatePlan(planPath, projectDir) {
2028
2514
  });
2029
2515
  return { valid: false, errors, warnings };
2030
2516
  }
2031
- const content = readFileSync5(resolvedPlanPath, "utf-8");
2517
+ const content = readFileSync6(resolvedPlanPath, "utf-8");
2032
2518
  validateStructure(content, errors, warnings);
2033
2519
  validateCheckboxes(content, errors, warnings);
2034
2520
  validateFileReferences(content, projectDir, warnings);
@@ -2190,8 +2676,8 @@ function validateFileReferences(content, projectDir, warnings) {
2190
2676
  });
2191
2677
  continue;
2192
2678
  }
2193
- const resolvedProject = resolve2(projectDir);
2194
- const absolutePath = resolve2(projectDir, filePath);
2679
+ const resolvedProject = resolve3(projectDir);
2680
+ const absolutePath = resolve3(projectDir, filePath);
2195
2681
  if (!absolutePath.startsWith(resolvedProject + sep) && absolutePath !== resolvedProject) {
2196
2682
  warnings.push({
2197
2683
  severity: "warning",
@@ -2200,7 +2686,7 @@ function validateFileReferences(content, projectDir, warnings) {
2200
2686
  });
2201
2687
  continue;
2202
2688
  }
2203
- if (!existsSync6(absolutePath)) {
2689
+ if (!existsSync7(absolutePath)) {
2204
2690
  warnings.push({
2205
2691
  severity: "warning",
2206
2692
  category: "file-references",
@@ -2520,6 +3006,8 @@ Keep each todo under 35 chars. ${remaining} task${remaining !== 1 ? "s" : ""} re
2520
3006
  }
2521
3007
 
2522
3008
  // src/hooks/work-continuation.ts
3009
+ var CONTINUATION_MARKER = "<!-- weave:continuation -->";
3010
+ var MAX_STALE_CONTINUATIONS = 3;
2523
3011
  function checkContinuation(input) {
2524
3012
  const { directory } = input;
2525
3013
  const state = readWorkState(directory);
@@ -2529,13 +3017,34 @@ function checkContinuation(input) {
2529
3017
  if (state.paused) {
2530
3018
  return { continuationPrompt: null };
2531
3019
  }
3020
+ if (state.session_ids.length > 0 && !state.session_ids.includes(input.sessionId)) {
3021
+ return { continuationPrompt: null };
3022
+ }
2532
3023
  const progress = getPlanProgress(state.active_plan);
2533
3024
  if (progress.isComplete) {
2534
3025
  return { continuationPrompt: null };
2535
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
+ }
2536
3044
  const remaining = progress.total - progress.completed;
2537
3045
  return {
2538
- continuationPrompt: `You have an active work plan with incomplete tasks. Continue working.
3046
+ continuationPrompt: `${CONTINUATION_MARKER}
3047
+ You have an active work plan with incomplete tasks. Continue working.
2539
3048
 
2540
3049
  **Plan**: ${state.plan_name}
2541
3050
  **File**: ${state.active_plan}
@@ -2577,7 +3086,7 @@ Only mark complete when ALL checks pass.`
2577
3086
 
2578
3087
  // src/hooks/create-hooks.ts
2579
3088
  function createHooks(args) {
2580
- const { pluginConfig, isHookEnabled, directory } = args;
3089
+ const { pluginConfig, isHookEnabled, directory, analyticsEnabled = false } = args;
2581
3090
  const writeGuardState = createWriteGuardState();
2582
3091
  const writeGuard = createWriteGuard(writeGuardState);
2583
3092
  const contextWindowThresholds = {
@@ -2594,7 +3103,8 @@ function createHooks(args) {
2594
3103
  patternMdOnly: isHookEnabled("pattern-md-only") ? checkPatternWrite : null,
2595
3104
  startWork: isHookEnabled("start-work") ? (promptText, sessionId) => handleStartWork({ promptText, sessionId, directory }) : null,
2596
3105
  workContinuation: isHookEnabled("work-continuation") ? (sessionId) => checkContinuation({ sessionId, directory }) : null,
2597
- verificationReminder: isHookEnabled("verification-reminder") ? buildVerificationReminder : null
3106
+ verificationReminder: isHookEnabled("verification-reminder") ? buildVerificationReminder : null,
3107
+ analyticsEnabled
2598
3108
  };
2599
3109
  }
2600
3110
  // src/hooks/session-token-state.ts
@@ -2623,7 +3133,7 @@ function clearSession2(sessionId) {
2623
3133
  }
2624
3134
  // src/plugin/plugin-interface.ts
2625
3135
  function createPluginInterface(args) {
2626
- const { pluginConfig, hooks, tools, configHandler, agents, client, directory = "" } = args;
3136
+ const { pluginConfig, hooks, tools, configHandler, agents, client, directory = "", tracker } = args;
2627
3137
  return {
2628
3138
  tool: tools,
2629
3139
  config: async (config) => {
@@ -2677,6 +3187,20 @@ ${result.contextInjection}`;
2677
3187
  }
2678
3188
  }
2679
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
+ }
2680
3204
  },
2681
3205
  "chat.params": async (_input, _output) => {
2682
3206
  const input = _input;
@@ -2703,6 +3227,13 @@ ${result.contextInjection}`;
2703
3227
  if (event.type === "session.deleted") {
2704
3228
  const evt = event;
2705
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
+ }
2706
3237
  }
2707
3238
  if (event.type === "message.updated" && hooks.checkContextWindow) {
2708
3239
  const evt = event;
@@ -2760,8 +3291,8 @@ ${result.contextInjection}`;
2760
3291
  }
2761
3292
  },
2762
3293
  "tool.execute.before": async (input, _output) => {
2763
- const args2 = _output.args;
2764
- const filePath = args2?.file_path ?? args2?.path ?? "";
3294
+ const toolArgs = _output.args;
3295
+ const filePath = toolArgs?.file_path ?? toolArgs?.path ?? "";
2765
3296
  if (filePath && hooks.shouldInjectRules && hooks.getRulesForFile) {
2766
3297
  if (hooks.shouldInjectRules(input.tool)) {
2767
3298
  hooks.getRulesForFile(filePath);
@@ -2781,8 +3312,8 @@ ${result.contextInjection}`;
2781
3312
  }
2782
3313
  }
2783
3314
  }
2784
- if (input.tool === "task" && args2) {
2785
- const agentArg = args2.subagent_type ?? args2.description ?? "unknown";
3315
+ if (input.tool === "task" && toolArgs) {
3316
+ const agentArg = toolArgs.subagent_type ?? toolArgs.description ?? "unknown";
2786
3317
  logDelegation({
2787
3318
  phase: "start",
2788
3319
  agent: agentArg,
@@ -2790,6 +3321,10 @@ ${result.contextInjection}`;
2790
3321
  toolCallId: input.callID
2791
3322
  });
2792
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
+ }
2793
3328
  },
2794
3329
  "tool.execute.after": async (input, _output) => {
2795
3330
  if (input.tool === "task") {
@@ -2802,18 +3337,444 @@ ${result.contextInjection}`;
2802
3337
  toolCallId: input.callID
2803
3338
  });
2804
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
+ }
2805
3345
  }
2806
3346
  };
2807
3347
  }
2808
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 readFileSync9, readdirSync as readdirSync3 } from "fs";
3411
+ import { join as join9 } from "path";
3412
+ import { arch } from "os";
3413
+
3414
+ // src/shared/version.ts
3415
+ import { readFileSync as readFileSync8 } from "fs";
3416
+ import { fileURLToPath } from "url";
3417
+ import { dirname as dirname2, join as join8 } from "path";
3418
+ var cachedVersion;
3419
+ function getWeaveVersion() {
3420
+ if (cachedVersion !== undefined)
3421
+ return cachedVersion;
3422
+ try {
3423
+ const thisDir = dirname2(fileURLToPath(import.meta.url));
3424
+ for (const rel of ["../../package.json", "../package.json"]) {
3425
+ try {
3426
+ const pkg = JSON.parse(readFileSync8(join8(thisDir, rel), "utf-8"));
3427
+ if (pkg.name === "@opencode_weave/weave" && typeof pkg.version === "string") {
3428
+ const version = pkg.version;
3429
+ cachedVersion = version;
3430
+ return version;
3431
+ }
3432
+ } catch {}
3433
+ }
3434
+ } catch {}
3435
+ cachedVersion = "0.0.0";
3436
+ return cachedVersion;
3437
+ }
3438
+
3439
+ // src/features/analytics/fingerprint.ts
3440
+ var STACK_MARKERS = [
3441
+ {
3442
+ name: "typescript",
3443
+ files: ["tsconfig.json", "tsconfig.build.json"],
3444
+ confidence: "high",
3445
+ evidence: (f) => `${f} exists`
3446
+ },
3447
+ {
3448
+ name: "bun",
3449
+ files: ["bun.lockb", "bunfig.toml"],
3450
+ confidence: "high",
3451
+ evidence: (f) => `${f} exists`
3452
+ },
3453
+ {
3454
+ name: "node",
3455
+ files: ["package.json"],
3456
+ confidence: "high",
3457
+ evidence: (f) => `${f} exists`
3458
+ },
3459
+ {
3460
+ name: "npm",
3461
+ files: ["package-lock.json"],
3462
+ confidence: "high",
3463
+ evidence: (f) => `${f} exists`
3464
+ },
3465
+ {
3466
+ name: "yarn",
3467
+ files: ["yarn.lock"],
3468
+ confidence: "high",
3469
+ evidence: (f) => `${f} exists`
3470
+ },
3471
+ {
3472
+ name: "pnpm",
3473
+ files: ["pnpm-lock.yaml"],
3474
+ confidence: "high",
3475
+ evidence: (f) => `${f} exists`
3476
+ },
3477
+ {
3478
+ name: "react",
3479
+ files: [],
3480
+ confidence: "medium",
3481
+ evidence: () => "react in package.json dependencies"
3482
+ },
3483
+ {
3484
+ name: "next",
3485
+ files: ["next.config.js", "next.config.ts", "next.config.mjs"],
3486
+ confidence: "high",
3487
+ evidence: (f) => `${f} exists`
3488
+ },
3489
+ {
3490
+ name: "python",
3491
+ files: ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"],
3492
+ confidence: "high",
3493
+ evidence: (f) => `${f} exists`
3494
+ },
3495
+ {
3496
+ name: "go",
3497
+ files: ["go.mod"],
3498
+ confidence: "high",
3499
+ evidence: (f) => `${f} exists`
3500
+ },
3501
+ {
3502
+ name: "rust",
3503
+ files: ["Cargo.toml"],
3504
+ confidence: "high",
3505
+ evidence: (f) => `${f} exists`
3506
+ },
3507
+ {
3508
+ name: "dotnet",
3509
+ files: ["global.json", "Directory.Build.props", "Directory.Packages.props"],
3510
+ confidence: "high",
3511
+ evidence: (f) => `${f} exists`
3512
+ },
3513
+ {
3514
+ name: "docker",
3515
+ files: ["Dockerfile", "docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"],
3516
+ confidence: "high",
3517
+ evidence: (f) => `${f} exists`
3518
+ }
3519
+ ];
3520
+ var MONOREPO_MARKERS = [
3521
+ "lerna.json",
3522
+ "nx.json",
3523
+ "turbo.json",
3524
+ "pnpm-workspace.yaml",
3525
+ "rush.json"
3526
+ ];
3527
+ function detectStack(directory) {
3528
+ const detected = [];
3529
+ for (const marker of STACK_MARKERS) {
3530
+ for (const file of marker.files) {
3531
+ if (existsSync9(join9(directory, file))) {
3532
+ detected.push({
3533
+ name: marker.name,
3534
+ confidence: marker.confidence,
3535
+ evidence: marker.evidence(file)
3536
+ });
3537
+ break;
3538
+ }
3539
+ }
3540
+ }
3541
+ try {
3542
+ const pkgPath = join9(directory, "package.json");
3543
+ if (existsSync9(pkgPath)) {
3544
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
3545
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3546
+ if (deps.react) {
3547
+ detected.push({
3548
+ name: "react",
3549
+ confidence: "medium",
3550
+ evidence: "react in package.json dependencies"
3551
+ });
3552
+ }
3553
+ }
3554
+ } catch {}
3555
+ if (!detected.some((d) => d.name === "dotnet")) {
3556
+ try {
3557
+ const entries = readdirSync3(directory);
3558
+ const dotnetFile = entries.find((e) => e.endsWith(".csproj") || e.endsWith(".fsproj") || e.endsWith(".sln"));
3559
+ if (dotnetFile) {
3560
+ detected.push({
3561
+ name: "dotnet",
3562
+ confidence: "high",
3563
+ evidence: `${dotnetFile} found`
3564
+ });
3565
+ }
3566
+ } catch {}
3567
+ }
3568
+ const seen = new Set;
3569
+ return detected.filter((entry) => {
3570
+ if (seen.has(entry.name))
3571
+ return false;
3572
+ seen.add(entry.name);
3573
+ return true;
3574
+ });
3575
+ }
3576
+ function detectPackageManager(directory) {
3577
+ if (existsSync9(join9(directory, "bun.lockb")))
3578
+ return "bun";
3579
+ if (existsSync9(join9(directory, "pnpm-lock.yaml")))
3580
+ return "pnpm";
3581
+ if (existsSync9(join9(directory, "yarn.lock")))
3582
+ return "yarn";
3583
+ if (existsSync9(join9(directory, "package-lock.json")))
3584
+ return "npm";
3585
+ if (existsSync9(join9(directory, "package.json")))
3586
+ return "npm";
3587
+ return;
3588
+ }
3589
+ function detectMonorepo(directory) {
3590
+ for (const marker of MONOREPO_MARKERS) {
3591
+ if (existsSync9(join9(directory, marker)))
3592
+ return true;
3593
+ }
3594
+ try {
3595
+ const pkgPath = join9(directory, "package.json");
3596
+ if (existsSync9(pkgPath)) {
3597
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
3598
+ if (pkg.workspaces)
3599
+ return true;
3600
+ }
3601
+ } catch {}
3602
+ return false;
3603
+ }
3604
+ function detectPrimaryLanguage(stack) {
3605
+ const languages = ["typescript", "python", "go", "rust", "dotnet"];
3606
+ for (const lang of languages) {
3607
+ if (stack.some((s) => s.name === lang))
3608
+ return lang;
3609
+ }
3610
+ if (stack.some((s) => s.name === "node"))
3611
+ return "javascript";
3612
+ return;
3613
+ }
3614
+ function generateFingerprint(directory) {
3615
+ const stack = detectStack(directory);
3616
+ return {
3617
+ generatedAt: new Date().toISOString(),
3618
+ stack,
3619
+ isMonorepo: detectMonorepo(directory),
3620
+ packageManager: detectPackageManager(directory),
3621
+ primaryLanguage: detectPrimaryLanguage(stack),
3622
+ os: process.platform,
3623
+ arch: arch(),
3624
+ weaveVersion: getWeaveVersion()
3625
+ };
3626
+ }
3627
+ function fingerprintProject(directory) {
3628
+ try {
3629
+ const fingerprint = generateFingerprint(directory);
3630
+ writeFingerprint(directory, fingerprint);
3631
+ log("[analytics] Project fingerprinted", {
3632
+ stack: fingerprint.stack.map((s) => s.name),
3633
+ primaryLanguage: fingerprint.primaryLanguage,
3634
+ packageManager: fingerprint.packageManager
3635
+ });
3636
+ return fingerprint;
3637
+ } catch (err) {
3638
+ log("[analytics] Fingerprinting failed (non-fatal)", { error: String(err) });
3639
+ return null;
3640
+ }
3641
+ }
3642
+ function getOrCreateFingerprint(directory) {
3643
+ try {
3644
+ const existing = readFingerprint(directory);
3645
+ if (existing) {
3646
+ const currentVersion = getWeaveVersion();
3647
+ if (existing.weaveVersion === currentVersion) {
3648
+ return existing;
3649
+ }
3650
+ log("[analytics] Fingerprint version mismatch — regenerating", {
3651
+ cached: existing.weaveVersion ?? "none",
3652
+ current: currentVersion
3653
+ });
3654
+ }
3655
+ return fingerprintProject(directory);
3656
+ } catch (err) {
3657
+ log("[analytics] getOrCreateFingerprint failed (non-fatal)", { error: String(err) });
3658
+ return null;
3659
+ }
3660
+ }
3661
+ // src/features/analytics/session-tracker.ts
3662
+ class SessionTracker {
3663
+ sessions = new Map;
3664
+ directory;
3665
+ constructor(directory) {
3666
+ this.directory = directory;
3667
+ }
3668
+ startSession(sessionId) {
3669
+ const existing = this.sessions.get(sessionId);
3670
+ if (existing)
3671
+ return existing;
3672
+ const session = {
3673
+ sessionId,
3674
+ startedAt: new Date().toISOString(),
3675
+ toolCounts: {},
3676
+ delegations: [],
3677
+ inFlight: {}
3678
+ };
3679
+ this.sessions.set(sessionId, session);
3680
+ return session;
3681
+ }
3682
+ trackToolStart(sessionId, toolName, callId, agent) {
3683
+ const session = this.startSession(sessionId);
3684
+ session.toolCounts[toolName] = (session.toolCounts[toolName] ?? 0) + 1;
3685
+ const inFlight = {
3686
+ tool: toolName,
3687
+ startedAt: Date.now()
3688
+ };
3689
+ if (agent) {
3690
+ inFlight.agent = agent;
3691
+ }
3692
+ session.inFlight[callId] = inFlight;
3693
+ }
3694
+ trackToolEnd(sessionId, toolName, callId, agent) {
3695
+ const session = this.sessions.get(sessionId);
3696
+ if (!session)
3697
+ return;
3698
+ const inFlight = session.inFlight[callId];
3699
+ delete session.inFlight[callId];
3700
+ if (toolName === "task") {
3701
+ const delegation = {
3702
+ agent: agent ?? inFlight?.agent ?? "unknown",
3703
+ toolCallId: callId
3704
+ };
3705
+ if (inFlight) {
3706
+ delegation.durationMs = Date.now() - inFlight.startedAt;
3707
+ }
3708
+ session.delegations.push(delegation);
3709
+ }
3710
+ }
3711
+ endSession(sessionId) {
3712
+ const session = this.sessions.get(sessionId);
3713
+ if (!session)
3714
+ return null;
3715
+ const now = new Date;
3716
+ const startedAt = new Date(session.startedAt);
3717
+ const durationMs = now.getTime() - startedAt.getTime();
3718
+ const toolUsage = Object.entries(session.toolCounts).map(([tool, count]) => ({ tool, count }));
3719
+ const totalToolCalls = toolUsage.reduce((sum, entry) => sum + entry.count, 0);
3720
+ const summary = {
3721
+ sessionId,
3722
+ startedAt: session.startedAt,
3723
+ endedAt: now.toISOString(),
3724
+ durationMs,
3725
+ toolUsage,
3726
+ delegations: session.delegations,
3727
+ totalToolCalls,
3728
+ totalDelegations: session.delegations.length
3729
+ };
3730
+ try {
3731
+ appendSessionSummary(this.directory, summary);
3732
+ log("[analytics] Session summary persisted", {
3733
+ sessionId,
3734
+ totalToolCalls,
3735
+ totalDelegations: session.delegations.length
3736
+ });
3737
+ } catch (err) {
3738
+ log("[analytics] Failed to persist session summary (non-fatal)", {
3739
+ sessionId,
3740
+ error: String(err)
3741
+ });
3742
+ }
3743
+ this.sessions.delete(sessionId);
3744
+ return summary;
3745
+ }
3746
+ isTracking(sessionId) {
3747
+ return this.sessions.has(sessionId);
3748
+ }
3749
+ getSession(sessionId) {
3750
+ return this.sessions.get(sessionId);
3751
+ }
3752
+ get activeSessionCount() {
3753
+ return this.sessions.size;
3754
+ }
3755
+ }
3756
+ function createSessionTracker(directory) {
3757
+ return new SessionTracker(directory);
3758
+ }
3759
+ // src/features/analytics/index.ts
3760
+ function createAnalytics(directory, fingerprint) {
3761
+ const tracker = createSessionTracker(directory);
3762
+ const resolvedFingerprint = fingerprint ?? getOrCreateFingerprint(directory);
3763
+ return { tracker, fingerprint: resolvedFingerprint };
3764
+ }
3765
+
2809
3766
  // src/index.ts
2810
3767
  var WeavePlugin = async (ctx) => {
2811
3768
  const pluginConfig = loadWeaveConfig(ctx.directory, ctx);
2812
3769
  const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
2813
3770
  const isHookEnabled = (name) => !disabledHooks.has(name);
3771
+ const analyticsEnabled = pluginConfig.analytics?.enabled === true;
3772
+ const fingerprint = analyticsEnabled ? getOrCreateFingerprint(ctx.directory) : null;
3773
+ const configDir = join10(ctx.directory, ".opencode");
2814
3774
  const toolsResult = await createTools({ ctx, pluginConfig });
2815
- const managers = createManagers({ ctx, pluginConfig, resolveSkills: toolsResult.resolveSkillsFn });
2816
- const hooks = createHooks({ pluginConfig, isHookEnabled, directory: ctx.directory });
3775
+ const managers = createManagers({ ctx, pluginConfig, resolveSkills: toolsResult.resolveSkillsFn, fingerprint, configDir });
3776
+ const hooks = createHooks({ pluginConfig, isHookEnabled, directory: ctx.directory, analyticsEnabled });
3777
+ const analytics = analyticsEnabled ? createAnalytics(ctx.directory, fingerprint) : null;
2817
3778
  return createPluginInterface({
2818
3779
  pluginConfig,
2819
3780
  hooks,
@@ -2821,7 +3782,8 @@ var WeavePlugin = async (ctx) => {
2821
3782
  configHandler: managers.configHandler,
2822
3783
  agents: managers.agents,
2823
3784
  client: ctx.client,
2824
- directory: ctx.directory
3785
+ directory: ctx.directory,
3786
+ tracker: analytics?.tracker
2825
3787
  });
2826
3788
  };
2827
3789
  var src_default = WeavePlugin;