@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/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/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;
@@ -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 readFileSync5, existsSync as existsSync6 } from "fs";
1995
- 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";
1996
2496
  function validatePlan(planPath, projectDir) {
1997
2497
  const errors = [];
1998
2498
  const warnings = [];
1999
- const resolvedPlanPath = resolve2(planPath);
2000
- const allowedDir = resolve2(projectDir, PLANS_DIR);
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 (!existsSync6(resolvedPlanPath)) {
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 = readFileSync5(resolvedPlanPath, "utf-8");
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 = resolve2(projectDir);
2180
- const absolutePath = resolve2(projectDir, filePath);
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 (!existsSync6(absolutePath)) {
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: `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.
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
- pendingInterrupt = true;
2719
- log("[work-continuation] User interrupt detected — will suppress next continuation");
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
- if (pendingInterrupt) {
2724
- pendingInterrupt = false;
2725
- log("[work-continuation] Skipping continuation — session was interrupted by user");
2726
- } else {
2727
- const evt = event;
2728
- const sessionId = evt.properties?.sessionID ?? "";
2729
- if (sessionId) {
2730
- const result = hooks.workContinuation(sessionId);
2731
- if (result.continuationPrompt && client) {
2732
- try {
2733
- await client.session.promptAsync({
2734
- path: { id: sessionId },
2735
- body: {
2736
- parts: [{ type: "text", text: result.continuationPrompt }]
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 args2 = _output.args;
2752
- const filePath = args2?.file_path ?? args2?.path ?? "";
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" && args2) {
2773
- const agentArg = args2.subagent_type ?? args2.description ?? "unknown";
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;