thumbgate 0.9.13 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +6 -3
  5. package/adapters/README.md +1 -1
  6. package/adapters/chatgpt/openapi.yaml +105 -0
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/codex/config.toml +2 -2
  9. package/adapters/forge/forge.yaml +28 -0
  10. package/adapters/mcp/server-stdio.js +32 -1
  11. package/adapters/opencode/opencode.json +1 -1
  12. package/bin/cli.js +53 -3
  13. package/config/mcp-allowlists.json +10 -0
  14. package/openapi/openapi.yaml +105 -0
  15. package/package.json +4 -4
  16. package/plugins/amp-skill/INSTALL.md +3 -4
  17. package/plugins/amp-skill/SKILL.md +0 -1
  18. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  19. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  20. package/plugins/claude-skill/INSTALL.md +1 -2
  21. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  22. package/plugins/codex-profile/.mcp.json +1 -1
  23. package/plugins/codex-profile/INSTALL.md +1 -1
  24. package/plugins/codex-profile/README.md +1 -1
  25. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  26. package/plugins/opencode-profile/INSTALL.md +1 -1
  27. package/public/blog.html +1 -0
  28. package/public/dashboard.html +1 -1
  29. package/public/guide.html +1 -1
  30. package/public/index.html +29 -5
  31. package/public/learn/agent-harness-pattern.html +1 -1
  32. package/public/learn/ai-agent-persistent-memory.html +1 -1
  33. package/public/learn/mcp-pre-action-gates-explained.html +1 -1
  34. package/public/learn/stop-ai-agent-force-push.html +1 -1
  35. package/public/learn/vibe-coding-safety-net.html +1 -1
  36. package/public/learn.html +62 -1
  37. package/public/lessons.html +1 -1
  38. package/public/pro.html +1 -1
  39. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  40. package/scripts/agent-security-hardening.js +4 -4
  41. package/scripts/async-job-runner.js +84 -24
  42. package/scripts/auto-wire-hooks.js +59 -1
  43. package/scripts/context-manager.js +330 -0
  44. package/scripts/dashboard.js +1 -1
  45. package/scripts/distribution-surfaces.js +12 -0
  46. package/scripts/ensure-repo-bootstrap.js +15 -14
  47. package/scripts/feedback-history-distiller.js +7 -1
  48. package/scripts/feedback-loop.js +10 -4
  49. package/scripts/feedback-paths.js +142 -10
  50. package/scripts/feedback-root-consolidator.js +18 -4
  51. package/scripts/gates-engine.js +96 -10
  52. package/scripts/hook-auto-capture.sh +1 -1
  53. package/scripts/hosted-job-launcher.js +260 -0
  54. package/scripts/managed-dpo-export.js +91 -0
  55. package/scripts/obsidian-export.js +0 -1
  56. package/scripts/operational-integrity.js +50 -7
  57. package/scripts/post-everywhere.js +10 -0
  58. package/scripts/prove-lancedb.js +62 -4
  59. package/scripts/publish-decision.js +16 -0
  60. package/scripts/self-healing-check.js +6 -1
  61. package/scripts/seo-gsd.js +217 -4
  62. package/scripts/social-analytics/load-env.js +33 -2
  63. package/scripts/social-analytics/store.js +200 -2
  64. package/scripts/statusline-cache-path.js +9 -6
  65. package/scripts/sync-version.js +18 -11
  66. package/scripts/tool-registry.js +37 -0
  67. package/scripts/train_from_feedback.py +0 -4
  68. package/scripts/workflow-sentinel.js +793 -0
  69. package/src/api/server.js +297 -38
  70. /package/scripts/{rlhf_session_start.sh → thumbgate_session_start.sh} +0 -0
@@ -132,11 +132,54 @@ function serializeJobForState(job) {
132
132
  skill: job.skill || null,
133
133
  partnerProfile: job.partnerProfile || null,
134
134
  autoImprove: job.autoImprove !== false,
135
+ verificationMode: job.verificationMode === 'none' ? 'none' : 'standard',
136
+ recordFeedback: job.recordFeedback !== false,
135
137
  jobFilePath: job.jobFilePath || null,
136
138
  stages,
137
139
  };
138
140
  }
139
141
 
142
+ function queueJob(job) {
143
+ const normalizedJob = {
144
+ ...job,
145
+ id: job.id || generateJobId(),
146
+ tags: Array.isArray(job.tags) ? job.tags : [],
147
+ autoImprove: job.autoImprove !== false,
148
+ verificationMode: job.verificationMode === 'none' ? 'none' : 'standard',
149
+ recordFeedback: job.recordFeedback !== false,
150
+ stages: normalizeStages(job),
151
+ };
152
+ const previousState = readJobState(normalizedJob.id);
153
+ const currentStage = normalizedJob.stages[0] ? normalizedJob.stages[0].name : null;
154
+ return writeJobState({
155
+ jobId: normalizedJob.id,
156
+ status: 'queued',
157
+ createdAt: previousState && previousState.createdAt ? previousState.createdAt : nowIso(),
158
+ startedAt: previousState && previousState.startedAt ? previousState.startedAt : null,
159
+ resumedAt: null,
160
+ updatedAt: nowIso(),
161
+ endedAt: null,
162
+ tags: normalizedJob.tags,
163
+ skill: normalizedJob.skill || null,
164
+ partnerProfile: normalizedJob.partnerProfile || null,
165
+ autoImprove: normalizedJob.autoImprove,
166
+ verificationMode: normalizedJob.verificationMode,
167
+ recordFeedback: normalizedJob.recordFeedback,
168
+ totalStages: normalizedJob.stages.length,
169
+ nextStageIndex: 0,
170
+ currentStage,
171
+ currentContext: '',
172
+ checkpoints: [],
173
+ stageHistory: [],
174
+ jobFilePath: normalizedJob.jobFilePath || null,
175
+ jobSpec: serializeJobForState(normalizedJob),
176
+ lastError: null,
177
+ stopReason: null,
178
+ improvementExperimentId: null,
179
+ verification: null,
180
+ });
181
+ }
182
+
140
183
  function readJobState(jobId) {
141
184
  if (!jobId) return null;
142
185
  return readJson(getJobRuntimePaths(jobId).statePath);
@@ -618,6 +661,8 @@ function executeJob(job, options = {}) {
618
661
  id: job.id || generateJobId(),
619
662
  tags: Array.isArray(job.tags) ? job.tags : [],
620
663
  autoImprove: job.autoImprove !== false,
664
+ verificationMode: job.verificationMode === 'none' ? 'none' : 'standard',
665
+ recordFeedback: job.recordFeedback !== false,
621
666
  stages: normalizeStages(job),
622
667
  };
623
668
  const previousState = options.previousState || readJobState(normalizedJob.id);
@@ -643,6 +688,8 @@ function executeJob(job, options = {}) {
643
688
  skill: normalizedJob.skill || null,
644
689
  partnerProfile: normalizedJob.partnerProfile || null,
645
690
  autoImprove: normalizedJob.autoImprove,
691
+ verificationMode: normalizedJob.verificationMode,
692
+ recordFeedback: normalizedJob.recordFeedback,
646
693
  totalStages: normalizedJob.stages.length,
647
694
  nextStageIndex,
648
695
  currentStage: normalizedJob.stages[nextStageIndex] ? normalizedJob.stages[nextStageIndex].name : 'verification',
@@ -733,7 +780,7 @@ function executeJob(job, options = {}) {
733
780
 
734
781
  clearJobControl(normalizedJob.id);
735
782
 
736
- const feedback = error && error.code === 'JOB_CANCELLED'
783
+ const feedback = (error && error.code === 'JOB_CANCELLED') || normalizedJob.recordFeedback === false
737
784
  ? null
738
785
  : captureFeedback({
739
786
  signal: 'down',
@@ -756,48 +803,60 @@ function executeJob(job, options = {}) {
756
803
  }
757
804
  }
758
805
 
759
- const verification = runVerificationLoop({
760
- context: currentContext,
761
- tags: normalizedJob.tags,
762
- skill: normalizedJob.skill,
763
- partnerProfile: normalizedJob.partnerProfile,
764
- onRetry: normalizedJob.onRetry,
765
- maxRetries: normalizedJob.maxRetries,
766
- });
806
+ const verification = normalizedJob.verificationMode === 'none'
807
+ ? null
808
+ : runVerificationLoop({
809
+ context: currentContext,
810
+ tags: normalizedJob.tags,
811
+ skill: normalizedJob.skill,
812
+ partnerProfile: normalizedJob.partnerProfile,
813
+ onRetry: normalizedJob.onRetry,
814
+ maxRetries: normalizedJob.maxRetries,
815
+ });
767
816
 
768
- const improvementExperiment = verification.accepted
817
+ const improvementExperiment = !verification || verification.accepted
769
818
  ? null
770
819
  : maybeQueueImprovementExperiment(normalizedJob, state, recall, {
771
820
  type: 'verification',
772
821
  verification,
773
822
  });
774
823
 
775
- const feedback = captureFeedback({
776
- signal: verification.accepted ? 'up' : 'down',
777
- context: verification.accepted
778
- ? `Job ${normalizedJob.id} passed verification after ${verification.attempts} attempt(s)`
779
- : `Job ${normalizedJob.id} failed verification after ${verification.attempts} attempt(s): ${(verification.finalVerification.violations || []).map((violation) => violation.pattern).join('; ')}`,
780
- whatWorked: verification.accepted ? 'Verification loop accepted output' : undefined,
781
- whatWentWrong: !verification.accepted ? `Failed ${verification.attempts} verification attempts` : undefined,
782
- whatToChange: !verification.accepted ? 'Improve output to avoid known mistake patterns' : undefined,
783
- tags: [...normalizedJob.tags, 'verification-loop'],
784
- skill: normalizedJob.skill || 'async-job-runner',
785
- });
824
+ const feedback = normalizedJob.recordFeedback === false
825
+ ? null
826
+ : captureFeedback({
827
+ signal: !verification || verification.accepted ? 'up' : 'down',
828
+ context: !verification
829
+ ? `Job ${normalizedJob.id} completed without post-run verification`
830
+ : verification.accepted
831
+ ? `Job ${normalizedJob.id} passed verification after ${verification.attempts} attempt(s)`
832
+ : `Job ${normalizedJob.id} failed verification after ${verification.attempts} attempt(s): ${(verification.finalVerification.violations || []).map((violation) => violation.pattern).join('; ')}`,
833
+ whatWorked: !verification
834
+ ? 'Operational job completed successfully'
835
+ : verification.accepted
836
+ ? 'Verification loop accepted output'
837
+ : undefined,
838
+ whatWentWrong: verification && !verification.accepted ? `Failed ${verification.attempts} verification attempts` : undefined,
839
+ whatToChange: verification && !verification.accepted ? 'Improve output to avoid known mistake patterns' : undefined,
840
+ tags: !verification
841
+ ? [...normalizedJob.tags, 'async-job-runner', 'verification-skipped']
842
+ : [...normalizedJob.tags, 'verification-loop'],
843
+ skill: normalizedJob.skill || 'async-job-runner',
844
+ });
786
845
 
787
846
  const terminalState = writeJobState({
788
847
  ...(readJobState(normalizedJob.id) || state),
789
- status: verification.accepted ? 'completed' : 'failed',
848
+ status: !verification || verification.accepted ? 'completed' : 'failed',
790
849
  updatedAt: nowIso(),
791
850
  endedAt: nowIso(),
792
851
  currentStage: null,
793
852
  nextStageIndex: normalizedJob.stages.length,
794
853
  currentContext,
795
854
  improvementExperimentId: improvementExperiment ? improvementExperiment.id : null,
796
- verification: {
855
+ verification: verification ? {
797
856
  accepted: verification.accepted,
798
857
  attempts: verification.attempts,
799
858
  score: verification.finalVerification ? verification.finalVerification.score : 0,
800
- },
859
+ } : null,
801
860
  });
802
861
 
803
862
  clearJobControl(normalizedJob.id);
@@ -880,6 +939,7 @@ function runBatch(jobs) {
880
939
  module.exports = {
881
940
  recallContext,
882
941
  executeJob,
942
+ queueJob,
883
943
  runBatch,
884
944
  appendJobLog,
885
945
  readJobLog,
@@ -53,6 +53,7 @@ function detectAgent(flagAgent) {
53
53
  if (['claude-code', 'claude'].includes(normalized)) return 'claude-code';
54
54
  if (['codex'].includes(normalized)) return 'codex';
55
55
  if (['gemini'].includes(normalized)) return 'gemini';
56
+ if (['forge', 'forgecode', 'forge-code'].includes(normalized)) return 'forge';
56
57
  return null;
57
58
  }
58
59
 
@@ -61,6 +62,7 @@ function detectAgent(flagAgent) {
61
62
  if (fs.existsSync(path.join(home, '.claude'))) return 'claude-code';
62
63
  if (fs.existsSync(path.join(home, '.codex'))) return 'codex';
63
64
  if (fs.existsSync(path.join(home, '.gemini'))) return 'gemini';
65
+ if (fs.existsSync(path.join(process.cwd(), 'forge.yaml'))) return 'forge';
64
66
  return null;
65
67
  }
66
68
 
@@ -310,13 +312,64 @@ function wireGeminiHooks(options) {
310
312
  return { changed: true, settingsPath, added };
311
313
  }
312
314
 
315
+ // --- ForgeCode wiring ---
316
+
317
+ function forgeConfigPath() {
318
+ return path.join(process.cwd(), 'forge.yaml');
319
+ }
320
+
321
+ function wireForgeHooks(options) {
322
+ const dryRun = options.dryRun || false;
323
+
324
+ const preToolCmd = preToolHookCommand();
325
+ const userPromptCmd = userPromptHookCommand();
326
+
327
+ // ForgeCode uses YAML config (forge.yaml). We write a JSON-based hooks
328
+ // sidecar file (.thumbgate/forge-hooks.json) and append skill entries to
329
+ // forge.yaml if they are not already present.
330
+ const hooksPath = options.settingsPath || path.join(path.dirname(forgeConfigPath()), '.thumbgate', 'forge-hooks.json');
331
+ let existing = loadJsonFile(hooksPath) || {};
332
+ existing.hooks = existing.hooks || {};
333
+
334
+ const added = [];
335
+
336
+ if (!hookAlreadyPresent(existing.hooks.PreToolUse, preToolCmd)) {
337
+ existing.hooks.PreToolUse = existing.hooks.PreToolUse || [];
338
+ existing.hooks.PreToolUse.push({
339
+ matcher: 'Bash',
340
+ hooks: [{ type: 'command', command: preToolCmd }],
341
+ });
342
+ added.push({ lifecycle: 'PreToolUse', command: preToolCmd });
343
+ }
344
+
345
+ if (!hookAlreadyPresent(existing.hooks.UserPromptSubmit, userPromptCmd)) {
346
+ existing.hooks.UserPromptSubmit = existing.hooks.UserPromptSubmit || [];
347
+ existing.hooks.UserPromptSubmit.push({
348
+ hooks: [{ type: 'command', command: userPromptCmd }],
349
+ });
350
+ added.push({ lifecycle: 'UserPromptSubmit', command: userPromptCmd });
351
+ }
352
+
353
+ if (added.length === 0) {
354
+ return { changed: false, settingsPath: hooksPath, added: [] };
355
+ }
356
+
357
+ if (!dryRun) {
358
+ const dir = path.dirname(hooksPath);
359
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
360
+ fs.writeFileSync(hooksPath, JSON.stringify(existing, null, 2) + '\n');
361
+ }
362
+
363
+ return { changed: true, settingsPath: hooksPath, added };
364
+ }
365
+
313
366
  // --- Dispatcher ---
314
367
 
315
368
  function wireHooks(options) {
316
369
  const agent = detectAgent(options.agent);
317
370
  if (!agent) {
318
371
  return {
319
- error: 'Could not detect AI agent. Use --agent=claude-code|codex|gemini',
372
+ error: 'Could not detect AI agent. Use --agent=claude-code|codex|gemini|forge',
320
373
  agent: null,
321
374
  changed: false,
322
375
  };
@@ -333,6 +386,9 @@ function wireHooks(options) {
333
386
  case 'gemini':
334
387
  result = wireGeminiHooks(options);
335
388
  break;
389
+ case 'forge':
390
+ result = wireForgeHooks(options);
391
+ break;
336
392
  default:
337
393
  return { error: `Unsupported agent: ${agent}`, agent, changed: false };
338
394
  }
@@ -364,6 +420,7 @@ module.exports = {
364
420
  wireClaudeHooks,
365
421
  wireCodexHooks,
366
422
  wireGeminiHooks,
423
+ wireForgeHooks,
367
424
  hookAlreadyPresent,
368
425
  loadJsonFile,
369
426
  parseFlags,
@@ -372,6 +429,7 @@ module.exports = {
372
429
  codexConfigPath,
373
430
  geminiSettingsPath,
374
431
  syncClaudeStatusLine,
432
+ forgeConfigPath,
375
433
  CLAUDE_HOOKS,
376
434
  preToolHookCommand,
377
435
  userPromptHookCommand,
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Context Manager — Unified Context-Augmented Generation (CAG) Orchestrator
6
+ *
7
+ * Single entry point that assembles a normalized context object from:
8
+ * - Session state (primer / handoff)
9
+ * - User profile (role, preferences, agent type)
10
+ * - Relevant lessons (per-action retrieval)
11
+ * - Prevention rules / pre-tool guards
12
+ * - Context pack (ContextFS retrieval)
13
+ * - Code-graph impact (optional, for coding tasks)
14
+ *
15
+ * Implements tiered graceful degradation:
16
+ * Tier 1 (full) — session + lessons + rules + context pack + code-graph
17
+ * Tier 2 (warm) — lessons + rules + context pack (no session)
18
+ * Tier 3 (cold) — prevention rules + global defaults only
19
+ *
20
+ * Role-aware filtering shapes output by agent type and license tier.
21
+ */
22
+
23
+ const {
24
+ ensureContextFs,
25
+ constructContextPack,
26
+ readSessionHandoff,
27
+ recordProvenance,
28
+ } = require('./contextfs');
29
+ const { retrieveRelevantLessons } = require('./lesson-retrieval');
30
+ const { evaluatePretool } = require('./hybrid-feedback-context');
31
+ const { loadProfile } = require('./user-profile');
32
+ const {
33
+ analyzeCodeGraphImpact,
34
+ formatCodeGraphRecallSection,
35
+ } = require('./codegraph-context');
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Agent capability profiles — shapes what context each agent type receives
39
+ // ---------------------------------------------------------------------------
40
+
41
+ const AGENT_PROFILES = {
42
+ claude: {
43
+ maxLessons: 8,
44
+ includeCodeGraph: true,
45
+ includeStructuredRules: true,
46
+ contextBudget: 10000,
47
+ },
48
+ cursor: {
49
+ maxLessons: 5,
50
+ includeCodeGraph: true,
51
+ includeStructuredRules: true,
52
+ contextBudget: 6000,
53
+ },
54
+ forgecode: {
55
+ maxLessons: 5,
56
+ includeCodeGraph: false,
57
+ includeStructuredRules: true,
58
+ contextBudget: 6000,
59
+ },
60
+ codex: {
61
+ maxLessons: 6,
62
+ includeCodeGraph: true,
63
+ includeStructuredRules: true,
64
+ contextBudget: 8000,
65
+ },
66
+ default: {
67
+ maxLessons: 5,
68
+ includeCodeGraph: false,
69
+ includeStructuredRules: true,
70
+ contextBudget: 6000,
71
+ },
72
+ };
73
+
74
+ function getAgentProfile(agentType) {
75
+ const key = String(agentType || 'default').toLowerCase();
76
+ return AGENT_PROFILES[key] || AGENT_PROFILES.default;
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Tier assembly helpers
81
+ // ---------------------------------------------------------------------------
82
+
83
+ function assembleSession() {
84
+ try {
85
+ return readSessionHandoff();
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ function assembleLessons(query, agentProfile, options = {}) {
92
+ try {
93
+ return retrieveRelevantLessons(
94
+ options.toolName || '',
95
+ query,
96
+ { maxResults: agentProfile.maxLessons, feedbackDir: options.feedbackDir },
97
+ );
98
+ } catch {
99
+ return [];
100
+ }
101
+ }
102
+
103
+ function assembleGuards(toolName, toolInput) {
104
+ try {
105
+ return evaluatePretool(toolName || '', toolInput || {});
106
+ } catch {
107
+ return { mode: 'allow', reason: 'guard-unavailable' };
108
+ }
109
+ }
110
+
111
+ function assembleContextPack(query, agentProfile) {
112
+ try {
113
+ ensureContextFs();
114
+ return constructContextPack({
115
+ query,
116
+ maxItems: Math.min(8, Math.ceil(agentProfile.contextBudget / 1000)),
117
+ maxChars: agentProfile.contextBudget,
118
+ });
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ function assembleCodeGraph(query, repoPath, agentProfile) {
125
+ if (!agentProfile.includeCodeGraph) return null;
126
+ try {
127
+ const impact = analyzeCodeGraphImpact({
128
+ intentId: null,
129
+ context: query,
130
+ repoPath,
131
+ });
132
+ return formatCodeGraphRecallSection(impact) || null;
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ function assembleUserProfile() {
139
+ try {
140
+ const profile = loadProfile();
141
+ if (!profile || !profile.entries || profile.entries.length === 0) return null;
142
+ return {
143
+ entries: profile.entries,
144
+ charCount: profile.charCount || 0,
145
+ };
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Tier classification
153
+ // ---------------------------------------------------------------------------
154
+
155
+ function classifyTier(components) {
156
+ const hasSession = !!components.session;
157
+ const hasLessons = components.lessons && components.lessons.length > 0;
158
+ const hasPack = !!components.contextPack;
159
+
160
+ if (hasSession && (hasLessons || hasPack)) return 'full';
161
+ if (hasLessons || hasPack) return 'warm';
162
+ return 'cold';
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Main orchestrator
167
+ // ---------------------------------------------------------------------------
168
+
169
+ /**
170
+ * Assemble a unified context object for a given query.
171
+ *
172
+ * @param {Object} params
173
+ * @param {string} params.query - Task description / context query
174
+ * @param {string} [params.toolName] - Current tool being invoked (for lesson retrieval)
175
+ * @param {Object} [params.toolInput] - Current tool input (for guard evaluation)
176
+ * @param {string} [params.agentType] - Agent type: claude, cursor, forgecode, codex
177
+ * @param {string} [params.repoPath] - Repo path for code-graph analysis
178
+ * @param {string} [params.feedbackDir] - Override feedback directory
179
+ * @returns {Object} Normalized context object
180
+ */
181
+ function assembleUnifiedContext(params = {}) {
182
+ const {
183
+ query = '',
184
+ toolName,
185
+ toolInput,
186
+ agentType,
187
+ repoPath,
188
+ feedbackDir,
189
+ } = params;
190
+
191
+ const agentProfile = getAgentProfile(agentType);
192
+
193
+ // Assemble all components — each is fault-tolerant
194
+ const session = assembleSession();
195
+ const userProfile = assembleUserProfile();
196
+ const lessons = assembleLessons(query, agentProfile, { toolName, feedbackDir });
197
+ const guards = assembleGuards(toolName, toolInput);
198
+ const contextPack = assembleContextPack(query, agentProfile);
199
+ const codeGraph = assembleCodeGraph(query, repoPath, agentProfile);
200
+
201
+ const components = { session, userProfile, lessons, guards, contextPack, codeGraph };
202
+ const tier = classifyTier(components);
203
+
204
+ const result = {
205
+ tier,
206
+ agentType: agentType || 'default',
207
+ agentProfile: {
208
+ maxLessons: agentProfile.maxLessons,
209
+ contextBudget: agentProfile.contextBudget,
210
+ includeCodeGraph: agentProfile.includeCodeGraph,
211
+ },
212
+ session: session || null,
213
+ userProfile: userProfile || null,
214
+ lessons,
215
+ guards,
216
+ contextPack: contextPack ? {
217
+ packId: contextPack.packId,
218
+ itemCount: Array.isArray(contextPack.items) ? contextPack.items.length : 0,
219
+ items: (contextPack.items || []).slice(0, 5).map((item) => ({
220
+ id: item.id,
221
+ namespace: item.namespace,
222
+ title: item.title,
223
+ tags: item.tags || [],
224
+ score: item.score,
225
+ })),
226
+ visibility: contextPack.visibility || null,
227
+ cached: !!(contextPack.cache && contextPack.cache.hit),
228
+ } : null,
229
+ codeGraph: codeGraph || null,
230
+ assembledAt: new Date().toISOString(),
231
+ };
232
+
233
+ // Record provenance for audit trail
234
+ try {
235
+ recordProvenance({
236
+ type: 'unified_context_assembled',
237
+ tier,
238
+ agentType: result.agentType,
239
+ lessonCount: lessons.length,
240
+ guardDecision: guards.mode || 'allow',
241
+ hasSession: !!session,
242
+ hasUserProfile: !!userProfile,
243
+ hasCodeGraph: !!codeGraph,
244
+ packId: result.contextPack ? result.contextPack.packId : null,
245
+ });
246
+ } catch {
247
+ // Provenance write failure must never break context assembly
248
+ }
249
+
250
+ return result;
251
+ }
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Formatting for MCP tool response
255
+ // ---------------------------------------------------------------------------
256
+
257
+ function formatUnifiedContext(ctx) {
258
+ const lines = [];
259
+
260
+ lines.push(`## Unified Context (Tier: ${ctx.tier})`);
261
+ lines.push(`Agent: ${ctx.agentType} | Assembled: ${ctx.assembledAt}`);
262
+ lines.push('');
263
+
264
+ // Session
265
+ if (ctx.session) {
266
+ lines.push('### Session');
267
+ if (ctx.session.lastTask) lines.push(`Last task: ${ctx.session.lastTask}`);
268
+ if (ctx.session.nextStep) lines.push(`Next step: ${ctx.session.nextStep}`);
269
+ if (ctx.session.blockers && ctx.session.blockers.length > 0) {
270
+ lines.push(`Blockers: ${ctx.session.blockers.join(', ')}`);
271
+ }
272
+ lines.push('');
273
+ }
274
+
275
+ // User profile
276
+ if (ctx.userProfile) {
277
+ lines.push('### User Profile');
278
+ ctx.userProfile.entries.slice(0, 3).forEach((entry) => {
279
+ lines.push(`- ${entry.slice(0, 120)}`);
280
+ });
281
+ lines.push('');
282
+ }
283
+
284
+ // Guards
285
+ if (ctx.guards && ctx.guards.mode !== 'allow') {
286
+ lines.push(`### Guard: ${ctx.guards.mode.toUpperCase()}`);
287
+ lines.push(ctx.guards.reason || 'No reason provided');
288
+ lines.push('');
289
+ }
290
+
291
+ // Lessons
292
+ if (ctx.lessons && ctx.lessons.length > 0) {
293
+ lines.push(`### Lessons (${ctx.lessons.length})`);
294
+ ctx.lessons.forEach((lesson) => {
295
+ const signal = lesson.signal === 'negative' ? '[-]' : '[+]';
296
+ lines.push(`${signal} ${lesson.title || lesson.id} (score: ${lesson.relevanceScore})`);
297
+ if (lesson.rule) {
298
+ lines.push(` Rule: IF ${lesson.rule.condition || '?'} THEN ${lesson.rule.action || '?'}`);
299
+ }
300
+ });
301
+ lines.push('');
302
+ }
303
+
304
+ // Context pack
305
+ if (ctx.contextPack) {
306
+ lines.push(`### Context Pack (${ctx.contextPack.itemCount} items)`);
307
+ ctx.contextPack.items.forEach((item) => {
308
+ lines.push(`- [${item.namespace}] ${item.title} (score: ${item.score})`);
309
+ });
310
+ if (ctx.contextPack.cached) lines.push('(cached)');
311
+ lines.push('');
312
+ }
313
+
314
+ // Code graph
315
+ if (ctx.codeGraph) {
316
+ lines.push('### Code Graph Impact');
317
+ lines.push(ctx.codeGraph);
318
+ lines.push('');
319
+ }
320
+
321
+ return lines.join('\n');
322
+ }
323
+
324
+ module.exports = {
325
+ assembleUnifiedContext,
326
+ formatUnifiedContext,
327
+ getAgentProfile,
328
+ AGENT_PROFILES,
329
+ classifyTier,
330
+ };
@@ -523,7 +523,7 @@ function computeInstrumentationReadiness(analytics, billing) {
523
523
  const cli = telemetry.cli || {};
524
524
 
525
525
  return {
526
- plausibleConfigured: /\/js\/analytics\.js/.test(landingPage),
526
+ plausibleConfigured: /plausible\.io\/js\/script\.js|\/js\/analytics\.js/.test(landingPage),
527
527
  ga4Configured: Boolean(runtimeConfig.gaMeasurementId),
528
528
  googleSearchConsoleConfigured: Boolean(runtimeConfig.googleSiteVerification),
529
529
  softwareApplicationSchemaPresent: /"@type": "SoftwareApplication"/.test(landingPage),
@@ -6,6 +6,7 @@ const path = require('node:path');
6
6
  const ROOT = path.join(__dirname, '..');
7
7
  const PRODUCTHUNT_URL = 'https://www.producthunt.com/products/thumbgate';
8
8
  const CLAUDE_PLUGIN_LATEST_ASSET_NAME = 'thumbgate-claude-desktop.mcpb';
9
+ const CLAUDE_PLUGIN_NEXT_ASSET_NAME = 'thumbgate-claude-desktop-next.mcpb';
9
10
 
10
11
  function readJson(root, relativePath) {
11
12
  return JSON.parse(fs.readFileSync(path.join(root, relativePath), 'utf8'));
@@ -24,6 +25,14 @@ function getClaudePluginVersionedAssetName(version = getPackageVersion(ROOT)) {
24
25
  return `thumbgate-claude-desktop-v${normalized}.mcpb`;
25
26
  }
26
27
 
28
+ function isPrereleaseVersion(version = getPackageVersion(ROOT)) {
29
+ return /^\d+\.\d+\.\d+-[0-9A-Za-z.-]+$/.test(String(version || '').trim());
30
+ }
31
+
32
+ function getClaudePluginChannelAssetName(version = getPackageVersion(ROOT)) {
33
+ return isPrereleaseVersion(version) ? CLAUDE_PLUGIN_NEXT_ASSET_NAME : CLAUDE_PLUGIN_LATEST_ASSET_NAME;
34
+ }
35
+
27
36
  function getClaudePluginLatestDownloadUrl(root = ROOT) {
28
37
  return `${getRepositoryUrl(root)}/releases/latest/download/${CLAUDE_PLUGIN_LATEST_ASSET_NAME}`;
29
38
  }
@@ -35,10 +44,13 @@ function getClaudePluginVersionedDownloadUrl(root = ROOT, version = getPackageVe
35
44
 
36
45
  module.exports = {
37
46
  CLAUDE_PLUGIN_LATEST_ASSET_NAME,
47
+ CLAUDE_PLUGIN_NEXT_ASSET_NAME,
38
48
  PRODUCTHUNT_URL,
49
+ getClaudePluginChannelAssetName,
39
50
  getClaudePluginLatestDownloadUrl,
40
51
  getClaudePluginVersionedAssetName,
41
52
  getClaudePluginVersionedDownloadUrl,
42
53
  getPackageVersion,
43
54
  getRepositoryUrl,
55
+ isPrereleaseVersion,
44
56
  };