pi-continuous-learning 0.3.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 (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -0
  3. package/dist/active-instincts.d.ts +4 -0
  4. package/dist/active-instincts.d.ts.map +1 -0
  5. package/dist/active-instincts.js +11 -0
  6. package/dist/active-instincts.js.map +1 -0
  7. package/dist/agents-md.d.ts +12 -0
  8. package/dist/agents-md.d.ts.map +1 -0
  9. package/dist/agents-md.js +23 -0
  10. package/dist/agents-md.js.map +1 -0
  11. package/dist/cli/analyze-prompt.d.ts +2 -0
  12. package/dist/cli/analyze-prompt.d.ts.map +1 -0
  13. package/dist/cli/analyze-prompt.js +72 -0
  14. package/dist/cli/analyze-prompt.js.map +1 -0
  15. package/dist/cli/analyze.d.ts +3 -0
  16. package/dist/cli/analyze.d.ts.map +1 -0
  17. package/dist/cli/analyze.js +214 -0
  18. package/dist/cli/analyze.js.map +1 -0
  19. package/dist/confidence.d.ts +25 -0
  20. package/dist/confidence.d.ts.map +1 -0
  21. package/dist/confidence.js +77 -0
  22. package/dist/confidence.js.map +1 -0
  23. package/dist/config.d.ts +19 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +89 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/error-logger.d.ts +34 -0
  28. package/dist/error-logger.d.ts.map +1 -0
  29. package/dist/error-logger.js +102 -0
  30. package/dist/error-logger.js.map +1 -0
  31. package/dist/index.d.ts +10 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +118 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/instinct-decay.d.ts +39 -0
  36. package/dist/instinct-decay.d.ts.map +1 -0
  37. package/dist/instinct-decay.js +93 -0
  38. package/dist/instinct-decay.js.map +1 -0
  39. package/dist/instinct-evolve.d.ts +5 -0
  40. package/dist/instinct-evolve.d.ts.map +1 -0
  41. package/dist/instinct-evolve.js +24 -0
  42. package/dist/instinct-evolve.js.map +1 -0
  43. package/dist/instinct-export.d.ts +40 -0
  44. package/dist/instinct-export.d.ts.map +1 -0
  45. package/dist/instinct-export.js +94 -0
  46. package/dist/instinct-export.js.map +1 -0
  47. package/dist/instinct-import.d.ts +50 -0
  48. package/dist/instinct-import.d.ts.map +1 -0
  49. package/dist/instinct-import.js +168 -0
  50. package/dist/instinct-import.js.map +1 -0
  51. package/dist/instinct-injector.d.ts +39 -0
  52. package/dist/instinct-injector.d.ts.map +1 -0
  53. package/dist/instinct-injector.js +89 -0
  54. package/dist/instinct-injector.js.map +1 -0
  55. package/dist/instinct-loader.d.ts +37 -0
  56. package/dist/instinct-loader.d.ts.map +1 -0
  57. package/dist/instinct-loader.js +96 -0
  58. package/dist/instinct-loader.js.map +1 -0
  59. package/dist/instinct-parser.d.ts +28 -0
  60. package/dist/instinct-parser.d.ts.map +1 -0
  61. package/dist/instinct-parser.js +143 -0
  62. package/dist/instinct-parser.js.map +1 -0
  63. package/dist/instinct-projects.d.ts +32 -0
  64. package/dist/instinct-projects.d.ts.map +1 -0
  65. package/dist/instinct-projects.js +96 -0
  66. package/dist/instinct-projects.js.map +1 -0
  67. package/dist/instinct-promote.d.ts +51 -0
  68. package/dist/instinct-promote.d.ts.map +1 -0
  69. package/dist/instinct-promote.js +169 -0
  70. package/dist/instinct-promote.js.map +1 -0
  71. package/dist/instinct-status.d.ts +39 -0
  72. package/dist/instinct-status.d.ts.map +1 -0
  73. package/dist/instinct-status.js +108 -0
  74. package/dist/instinct-status.js.map +1 -0
  75. package/dist/instinct-store.d.ts +30 -0
  76. package/dist/instinct-store.d.ts.map +1 -0
  77. package/dist/instinct-store.js +118 -0
  78. package/dist/instinct-store.js.map +1 -0
  79. package/dist/instinct-tools.d.ts +161 -0
  80. package/dist/instinct-tools.d.ts.map +1 -0
  81. package/dist/instinct-tools.js +240 -0
  82. package/dist/instinct-tools.js.map +1 -0
  83. package/dist/observations.d.ts +22 -0
  84. package/dist/observations.d.ts.map +1 -0
  85. package/dist/observations.js +62 -0
  86. package/dist/observations.js.map +1 -0
  87. package/dist/observer-guard.d.ts +3 -0
  88. package/dist/observer-guard.d.ts.map +1 -0
  89. package/dist/observer-guard.js +13 -0
  90. package/dist/observer-guard.js.map +1 -0
  91. package/dist/project.d.ts +16 -0
  92. package/dist/project.d.ts.map +1 -0
  93. package/dist/project.js +59 -0
  94. package/dist/project.js.map +1 -0
  95. package/dist/prompt-observer.d.ts +25 -0
  96. package/dist/prompt-observer.d.ts.map +1 -0
  97. package/dist/prompt-observer.js +63 -0
  98. package/dist/prompt-observer.js.map +1 -0
  99. package/dist/prompts/analyzer-user.d.ts +38 -0
  100. package/dist/prompts/analyzer-user.d.ts.map +1 -0
  101. package/dist/prompts/analyzer-user.js +105 -0
  102. package/dist/prompts/analyzer-user.js.map +1 -0
  103. package/dist/prompts/evolve-prompt.d.ts +3 -0
  104. package/dist/prompts/evolve-prompt.d.ts.map +1 -0
  105. package/dist/prompts/evolve-prompt.js +51 -0
  106. package/dist/prompts/evolve-prompt.js.map +1 -0
  107. package/dist/scrubber.d.ts +9 -0
  108. package/dist/scrubber.d.ts.map +1 -0
  109. package/dist/scrubber.js +40 -0
  110. package/dist/scrubber.js.map +1 -0
  111. package/dist/storage.d.ts +22 -0
  112. package/dist/storage.d.ts.map +1 -0
  113. package/dist/storage.js +71 -0
  114. package/dist/storage.js.map +1 -0
  115. package/dist/tool-observer.d.ts +32 -0
  116. package/dist/tool-observer.d.ts.map +1 -0
  117. package/dist/tool-observer.js +77 -0
  118. package/dist/tool-observer.js.map +1 -0
  119. package/dist/types.d.ts +64 -0
  120. package/dist/types.d.ts.map +1 -0
  121. package/dist/types.js +6 -0
  122. package/dist/types.js.map +1 -0
  123. package/package.json +66 -0
  124. package/src/active-instincts.ts +13 -0
  125. package/src/agents-md.ts +23 -0
  126. package/src/cli/analyze-prompt.ts +71 -0
  127. package/src/cli/analyze.ts +286 -0
  128. package/src/confidence.ts +103 -0
  129. package/src/config.ts +111 -0
  130. package/src/error-logger.ts +130 -0
  131. package/src/index.ts +144 -0
  132. package/src/instinct-decay.ts +117 -0
  133. package/src/instinct-evolve.ts +44 -0
  134. package/src/instinct-export.ts +138 -0
  135. package/src/instinct-import.ts +260 -0
  136. package/src/instinct-injector.ts +128 -0
  137. package/src/instinct-loader.ts +146 -0
  138. package/src/instinct-parser.ts +171 -0
  139. package/src/instinct-projects.ts +119 -0
  140. package/src/instinct-promote.ts +231 -0
  141. package/src/instinct-status.ts +135 -0
  142. package/src/instinct-store.ts +149 -0
  143. package/src/instinct-tools.ts +340 -0
  144. package/src/observations.ts +82 -0
  145. package/src/observer-guard.ts +14 -0
  146. package/src/project.ts +70 -0
  147. package/src/prompt-observer.ts +92 -0
  148. package/src/prompts/analyzer-user.ts +156 -0
  149. package/src/prompts/evolve-prompt.ts +71 -0
  150. package/src/scrubber.ts +42 -0
  151. package/src/storage.ts +91 -0
  152. package/src/tool-observer.ts +114 -0
  153. package/src/types.ts +90 -0
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Instinct file parsing and serialization.
3
+ * Instinct files use YAML frontmatter + a markdown body for the action text.
4
+ *
5
+ * Format:
6
+ * ---
7
+ * id: some-kebab-id
8
+ * title: Human readable title
9
+ * trigger: when condition is met
10
+ * confidence: 0.7
11
+ * ...other metadata...
12
+ * ---
13
+ *
14
+ * Action text describing what to do.
15
+ */
16
+
17
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
18
+ import type { Instinct } from "./types.js";
19
+
20
+ // Constants
21
+ const KEBAB_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
22
+ const MIN_CONFIDENCE = 0.1;
23
+ const MAX_CONFIDENCE = 0.9;
24
+
25
+ const REQUIRED_FIELDS = [
26
+ "id",
27
+ "title",
28
+ "trigger",
29
+ "confidence",
30
+ "domain",
31
+ "source",
32
+ "scope",
33
+ "created_at",
34
+ "updated_at",
35
+ "observation_count",
36
+ "confirmed_count",
37
+ "contradicted_count",
38
+ "inactive_count",
39
+ ] as const;
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Helpers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ function clampConfidence(value: number): number {
46
+ return Math.min(MAX_CONFIDENCE, Math.max(MIN_CONFIDENCE, value));
47
+ }
48
+
49
+ function assertKebabCase(id: string): void {
50
+ if (!KEBAB_RE.test(id)) {
51
+ throw new Error(
52
+ `Invalid instinct ID "${id}": must be kebab-case (lowercase letters, numbers, hyphens only).`
53
+ );
54
+ }
55
+ }
56
+
57
+ function splitFrontmatter(content: string): {
58
+ frontmatterStr: string;
59
+ body: string;
60
+ } {
61
+ // Expect content starting with ---\n<yaml>\n---
62
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
63
+ if (!match) {
64
+ throw new Error(
65
+ "Invalid instinct file: content must begin with YAML frontmatter delimiters (---)."
66
+ );
67
+ }
68
+ return { frontmatterStr: match[1] ?? "", body: (match[2] ?? "").trim() };
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Public API
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /**
76
+ * Parse an instinct markdown file (YAML frontmatter + body) into an Instinct.
77
+ * Throws if required fields are missing or the ID is not kebab-case.
78
+ * Clamps confidence to 0.1–0.9 rather than throwing.
79
+ */
80
+ export function parseInstinct(content: string): Instinct {
81
+ const { frontmatterStr, body } = splitFrontmatter(content);
82
+
83
+ const fm = parseYaml(frontmatterStr) as Record<string, unknown>;
84
+
85
+ if (!fm || typeof fm !== "object") {
86
+ throw new Error("Invalid instinct file: frontmatter is not a valid YAML object.");
87
+ }
88
+
89
+ for (const field of REQUIRED_FIELDS) {
90
+ if (fm[field] === null || fm[field] === undefined) {
91
+ throw new Error(`Invalid instinct file: missing required field "${field}".`);
92
+ }
93
+ }
94
+
95
+ const id = String(fm["id"]);
96
+ assertKebabCase(id);
97
+
98
+ const confidence = clampConfidence(Number(fm["confidence"]));
99
+
100
+ const instinct: Instinct = {
101
+ id,
102
+ title: String(fm["title"]),
103
+ trigger: String(fm["trigger"]),
104
+ action: body,
105
+ confidence,
106
+ domain: String(fm["domain"]),
107
+ source: fm["source"] as Instinct["source"],
108
+ scope: fm["scope"] as Instinct["scope"],
109
+ created_at: String(fm["created_at"]),
110
+ updated_at: String(fm["updated_at"]),
111
+ observation_count: Number(fm["observation_count"]),
112
+ confirmed_count: Number(fm["confirmed_count"]),
113
+ contradicted_count: Number(fm["contradicted_count"]),
114
+ inactive_count: Number(fm["inactive_count"]),
115
+ };
116
+
117
+ if (fm["project_id"] !== undefined && fm["project_id"] !== null) {
118
+ instinct.project_id = String(fm["project_id"]);
119
+ }
120
+ if (fm["project_name"] !== undefined && fm["project_name"] !== null) {
121
+ instinct.project_name = String(fm["project_name"]);
122
+ }
123
+ if (Array.isArray(fm["evidence"])) {
124
+ instinct.evidence = (fm["evidence"] as unknown[]).map(String);
125
+ }
126
+ if (fm["flagged_for_removal"] !== undefined && fm["flagged_for_removal"] !== null) {
127
+ instinct.flagged_for_removal = Boolean(fm["flagged_for_removal"]);
128
+ }
129
+
130
+ return instinct;
131
+ }
132
+
133
+ /**
134
+ * Serialize an Instinct into a YAML-frontmatter markdown string.
135
+ * Confidence is clamped to 0.1–0.9 before writing.
136
+ */
137
+ export function serializeInstinct(instinct: Instinct): string {
138
+ const confidence = clampConfidence(instinct.confidence);
139
+
140
+ const frontmatter: Record<string, unknown> = {
141
+ id: instinct.id,
142
+ title: instinct.title,
143
+ trigger: instinct.trigger,
144
+ confidence,
145
+ domain: instinct.domain,
146
+ source: instinct.source,
147
+ scope: instinct.scope,
148
+ created_at: instinct.created_at,
149
+ updated_at: instinct.updated_at,
150
+ observation_count: instinct.observation_count,
151
+ confirmed_count: instinct.confirmed_count,
152
+ contradicted_count: instinct.contradicted_count,
153
+ inactive_count: instinct.inactive_count,
154
+ };
155
+
156
+ if (instinct.project_id !== undefined) {
157
+ frontmatter["project_id"] = instinct.project_id;
158
+ }
159
+ if (instinct.project_name !== undefined) {
160
+ frontmatter["project_name"] = instinct.project_name;
161
+ }
162
+ if (instinct.evidence !== undefined) {
163
+ frontmatter["evidence"] = instinct.evidence;
164
+ }
165
+ if (instinct.flagged_for_removal !== undefined) {
166
+ frontmatter["flagged_for_removal"] = instinct.flagged_for_removal;
167
+ }
168
+
169
+ const yamlStr = stringifyYaml(frontmatter);
170
+ return `---\n${yamlStr}---\n\n${instinct.action}\n`;
171
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * /instinct-projects command for pi-continuous-learning.
3
+ * Displays all known projects with their instinct counts and last seen dates.
4
+ */
5
+
6
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
7
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
8
+ import type { ProjectEntry } from "./types.js";
9
+ import { getProjectsRegistryPath, getProjectInstinctsDir, getBaseDir } from "./storage.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Constants
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export const COMMAND_NAME = "instinct-projects";
16
+ const NO_PROJECTS_MSG = "No projects found.";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Data loading helpers
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Reads the projects.json registry. Returns an empty record on missing/invalid file.
24
+ */
25
+ export function readProjectsRegistry(baseDir?: string): Record<string, ProjectEntry> {
26
+ const registryPath = getProjectsRegistryPath(baseDir ?? getBaseDir());
27
+ if (!existsSync(registryPath)) {
28
+ return {};
29
+ }
30
+ try {
31
+ return JSON.parse(readFileSync(registryPath, "utf-8")) as Record<string, ProjectEntry>;
32
+ } catch {
33
+ return {};
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Counts .md files in a project's personal instincts directory.
39
+ * Returns 0 if the directory does not exist.
40
+ */
41
+ export function countProjectInstincts(projectId: string, baseDir?: string): number {
42
+ const dir = getProjectInstinctsDir(projectId, "personal", baseDir ?? getBaseDir());
43
+ if (!existsSync(dir)) {
44
+ return 0;
45
+ }
46
+ try {
47
+ const entries = readdirSync(dir);
48
+ return entries.filter((f) => f.endsWith(".md")).length;
49
+ } catch {
50
+ return 0;
51
+ }
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Formatting helpers
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /**
59
+ * Formats an ISO 8601 date string as a human-readable local date.
60
+ * Falls back to the raw string on parse failure.
61
+ */
62
+ export function formatDate(isoDate: string): string {
63
+ try {
64
+ return new Date(isoDate).toLocaleDateString();
65
+ } catch {
66
+ return isoDate;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Formats the full projects output string from a registry.
72
+ * Returns a fallback message when no projects exist.
73
+ */
74
+ export function formatProjectsOutput(
75
+ registry: Record<string, ProjectEntry>,
76
+ baseDir?: string
77
+ ): string {
78
+ const entries = Object.values(registry);
79
+ if (entries.length === 0) {
80
+ return NO_PROJECTS_MSG;
81
+ }
82
+
83
+ const sorted = [...entries].sort((a, b) =>
84
+ b.last_seen.localeCompare(a.last_seen)
85
+ );
86
+
87
+ const lines: string[] = ["=== Known Projects ===", ""];
88
+
89
+ for (const project of sorted) {
90
+ const count = countProjectInstincts(project.id, baseDir);
91
+ const lastSeen = formatDate(project.last_seen);
92
+ lines.push(`${project.name} (${project.id})`);
93
+ lines.push(` Instincts: ${count} | Last seen: ${lastSeen}`);
94
+ lines.push("");
95
+ }
96
+
97
+ const total = entries.length;
98
+ lines.push(`Total: ${total} project${total !== 1 ? "s" : ""}`);
99
+
100
+ return lines.join("\n");
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Command handler
105
+ // ---------------------------------------------------------------------------
106
+
107
+ /**
108
+ * Command handler for /instinct-projects.
109
+ * Reads the project registry, counts instincts per project, and displays results.
110
+ */
111
+ export async function handleInstinctProjects(
112
+ _args: string,
113
+ ctx: ExtensionCommandContext,
114
+ baseDir?: string
115
+ ): Promise<void> {
116
+ const registry = readProjectsRegistry(baseDir);
117
+ const output = formatProjectsOutput(registry, baseDir);
118
+ ctx.ui.notify(output, "info");
119
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * /instinct-promote command for pi-continuous-learning.
3
+ * Promotes project-scoped instincts to global scope.
4
+ *
5
+ * With an ID argument: promotes that specific project instinct to global.
6
+ * Without an ID: auto-promotes all qualifying instincts
7
+ * (confidence >= 0.8, present in 2+ projects).
8
+ */
9
+
10
+ import { mkdirSync } from "node:fs";
11
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
12
+ import type { Instinct } from "./types.js";
13
+ import {
14
+ loadProjectInstincts,
15
+ loadGlobalInstincts,
16
+ saveInstinct,
17
+ listInstincts,
18
+ } from "./instinct-store.js";
19
+ import {
20
+ getGlobalInstinctsDir,
21
+ getProjectInstinctsDir,
22
+ getProjectsRegistryPath,
23
+ getBaseDir,
24
+ } from "./storage.js";
25
+ import { readFileSync, existsSync } from "node:fs";
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Constants
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export const COMMAND_NAME = "instinct-promote";
32
+
33
+ /** Minimum confidence to qualify for auto-promotion. */
34
+ export const AUTO_PROMOTE_MIN_CONFIDENCE = 0.8;
35
+
36
+ /** Minimum number of distinct projects an instinct must appear in for auto-promotion. */
37
+ export const AUTO_PROMOTE_MIN_PROJECTS = 2;
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Pure helpers
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Builds a promoted copy of the instinct with scope set to global and
45
+ * project-specific fields removed.
46
+ * Does NOT mutate the original.
47
+ */
48
+ export function toGlobalInstinct(instinct: Instinct): Instinct {
49
+ const promoted: Instinct = {
50
+ ...instinct,
51
+ scope: "global",
52
+ updated_at: new Date().toISOString(),
53
+ };
54
+ // Remove project-specific fields
55
+ delete (promoted as Partial<Instinct>).project_id;
56
+ delete (promoted as Partial<Instinct>).project_name;
57
+ return promoted;
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Project registry reading
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /**
65
+ * Returns all known project IDs from the projects.json registry.
66
+ */
67
+ export function getKnownProjectIds(baseDir: string): string[] {
68
+ const registryPath = getProjectsRegistryPath(baseDir);
69
+ if (!existsSync(registryPath)) return [];
70
+ try {
71
+ const raw = readFileSync(registryPath, "utf-8");
72
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
73
+ return Object.keys(parsed);
74
+ } catch {
75
+ return [];
76
+ }
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Manual promotion
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /**
84
+ * Promotes a single project instinct to global personal/ by ID.
85
+ * Returns the promoted instinct, or null if not found.
86
+ */
87
+ export function promoteById(
88
+ id: string,
89
+ projectId: string,
90
+ baseDir: string
91
+ ): Instinct | null {
92
+ const projectInstincts = loadProjectInstincts(projectId, baseDir);
93
+ const found = projectInstincts.find((i) => i.id === id);
94
+ if (!found) return null;
95
+
96
+ const globalDir = getGlobalInstinctsDir("personal", baseDir);
97
+ mkdirSync(globalDir, { recursive: true });
98
+
99
+ const promoted = toGlobalInstinct(found);
100
+ saveInstinct(promoted, globalDir);
101
+ return promoted;
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Auto-promotion
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * Finds instinct IDs that appear in at least minProjects distinct projects.
110
+ * Returns a map of id -> array of matching instincts across projects.
111
+ */
112
+ export function findCrossProjectInstincts(
113
+ projectIds: string[],
114
+ baseDir: string
115
+ ): Map<string, Instinct[]> {
116
+ const byId = new Map<string, Instinct[]>();
117
+
118
+ for (const projectId of projectIds) {
119
+ const personalDir = getProjectInstinctsDir(projectId, "personal", baseDir);
120
+ const instincts = listInstincts(personalDir);
121
+ for (const instinct of instincts) {
122
+ const existing = byId.get(instinct.id) ?? [];
123
+ byId.set(instinct.id, [...existing, instinct]);
124
+ }
125
+ }
126
+
127
+ return byId;
128
+ }
129
+
130
+ /**
131
+ * Auto-promotes all qualifying instincts:
132
+ * - confidence >= AUTO_PROMOTE_MIN_CONFIDENCE
133
+ * - present in >= AUTO_PROMOTE_MIN_PROJECTS distinct projects
134
+ * Already-global instincts (same ID in global personal/) are skipped.
135
+ *
136
+ * Returns the list of promoted instincts.
137
+ */
138
+ export function autoPromoteInstincts(baseDir: string): Instinct[] {
139
+ const projectIds = getKnownProjectIds(baseDir);
140
+ if (projectIds.length < AUTO_PROMOTE_MIN_PROJECTS) return [];
141
+
142
+ const crossProject = findCrossProjectInstincts(projectIds, baseDir);
143
+
144
+ const existingGlobal = loadGlobalInstincts(baseDir);
145
+ const existingGlobalIds = new Set(existingGlobal.map((i) => i.id));
146
+
147
+ const globalDir = getGlobalInstinctsDir("personal", baseDir);
148
+ mkdirSync(globalDir, { recursive: true });
149
+
150
+ const promoted: Instinct[] = [];
151
+
152
+ for (const [id, instances] of crossProject) {
153
+ if (instances.length < AUTO_PROMOTE_MIN_PROJECTS) continue;
154
+
155
+ // Use the highest-confidence instance as the canonical one
156
+ const sorted = [...instances].sort((a, b) => b.confidence - a.confidence);
157
+ const best = sorted[0];
158
+ if (!best) continue;
159
+
160
+ if (best.confidence < AUTO_PROMOTE_MIN_CONFIDENCE) continue;
161
+ if (existingGlobalIds.has(id)) continue;
162
+
163
+ const globalInstinct = toGlobalInstinct(best);
164
+ saveInstinct(globalInstinct, globalDir);
165
+ promoted.push(globalInstinct);
166
+ }
167
+
168
+ return promoted;
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // handleInstinctPromote
173
+ // ---------------------------------------------------------------------------
174
+
175
+ /**
176
+ * Command handler for /instinct-promote.
177
+ * With an ID arg: promotes that specific instinct.
178
+ * Without an ID: auto-promotes qualifying instincts.
179
+ */
180
+ export async function handleInstinctPromote(
181
+ args: string,
182
+ ctx: ExtensionCommandContext,
183
+ projectId?: string | null,
184
+ baseDir?: string
185
+ ): Promise<void> {
186
+ const effectiveBase = baseDir ?? getBaseDir();
187
+ const id = args.trim();
188
+
189
+ if (id.length > 0) {
190
+ // Manual promotion by ID
191
+ if (projectId == null) {
192
+ ctx.ui.notify(
193
+ "Cannot promote by ID: no active project detected.",
194
+ "error"
195
+ );
196
+ return;
197
+ }
198
+
199
+ const promoted = promoteById(id, projectId, effectiveBase);
200
+ if (!promoted) {
201
+ ctx.ui.notify(
202
+ `Instinct "${id}" not found in project instincts.`,
203
+ "error"
204
+ );
205
+ return;
206
+ }
207
+
208
+ ctx.ui.notify(
209
+ `Promoted instinct "${promoted.id}" ("${promoted.title}") to global scope.`,
210
+ "info"
211
+ );
212
+ return;
213
+ }
214
+
215
+ // Auto-promotion
216
+ const promoted = autoPromoteInstincts(effectiveBase);
217
+
218
+ if (promoted.length === 0) {
219
+ ctx.ui.notify(
220
+ `No instincts qualify for auto-promotion (confidence >= ${AUTO_PROMOTE_MIN_CONFIDENCE}, present in ${AUTO_PROMOTE_MIN_PROJECTS}+ projects).`,
221
+ "info"
222
+ );
223
+ return;
224
+ }
225
+
226
+ const lines = [
227
+ `Auto-promoted ${promoted.length} instinct${promoted.length !== 1 ? "s" : ""} to global scope:`,
228
+ ...promoted.map((i) => ` - ${i.id} (${i.confidence.toFixed(2)}): ${i.title}`),
229
+ ];
230
+ ctx.ui.notify(lines.join("\n"), "info");
231
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * /instinct-status command for pi-continuous-learning.
3
+ * Displays all instincts grouped by domain with confidence scores,
4
+ * trend arrows, and feedback ratios.
5
+ */
6
+
7
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
8
+ import type { Instinct } from "./types.js";
9
+ import { loadProjectInstincts, loadGlobalInstincts } from "./instinct-store.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Constants
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const TREND_UP = "↑";
16
+ const TREND_DOWN = "↓";
17
+ const TREND_STABLE = "→";
18
+ const FLAG_REMOVAL = "⚠ FLAGGED FOR REMOVAL";
19
+ const COMMAND_NAME = "instinct-status";
20
+ const NO_INSTINCTS_MSG = "No instincts found.";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Trend and formatting helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Returns a trend arrow based on confirmed vs contradicted counts.
28
+ * ↑ when confirmed > contradicted, ↓ when contradicted > confirmed, → when equal.
29
+ */
30
+ export function getTrendArrow(instinct: Instinct): string {
31
+ if (instinct.confirmed_count > instinct.contradicted_count) return TREND_UP;
32
+ if (instinct.contradicted_count > instinct.confirmed_count) return TREND_DOWN;
33
+ return TREND_STABLE;
34
+ }
35
+
36
+ /**
37
+ * Formats a single instinct line for display.
38
+ * Format: [confidence] title trend feedback_ratio [⚠ FLAGGED FOR REMOVAL]
39
+ */
40
+ export function formatInstinct(instinct: Instinct): string {
41
+ const confidence = `[${instinct.confidence.toFixed(2)}]`;
42
+ const trend = getTrendArrow(instinct);
43
+ const feedbackRatio =
44
+ `✓${instinct.confirmed_count} ✗${instinct.contradicted_count} ○${instinct.inactive_count}`;
45
+
46
+ const parts = [` ${confidence} ${instinct.title} ${trend} (${feedbackRatio})`];
47
+ if (instinct.flagged_for_removal) {
48
+ parts.push(` ${FLAG_REMOVAL}`);
49
+ }
50
+ return parts.join("\n");
51
+ }
52
+
53
+ /**
54
+ * Groups instincts by domain. Returns a sorted record (sorted by domain name).
55
+ */
56
+ export function groupByDomain(instincts: Instinct[]): Record<string, Instinct[]> {
57
+ const groups: Record<string, Instinct[]> = {};
58
+ for (const instinct of instincts) {
59
+ const domain = instinct.domain || "uncategorized";
60
+ if (!groups[domain]) {
61
+ groups[domain] = [];
62
+ }
63
+ groups[domain].push(instinct);
64
+ }
65
+ return groups;
66
+ }
67
+
68
+ /**
69
+ * Formats the full status output string from a list of instincts.
70
+ * Returns a header message when no instincts exist.
71
+ */
72
+ export function formatStatusOutput(instincts: Instinct[]): string {
73
+ if (instincts.length === 0) return NO_INSTINCTS_MSG;
74
+
75
+ const groups = groupByDomain(instincts);
76
+ const sortedDomains = Object.keys(groups).sort();
77
+
78
+ const lines: string[] = ["=== Instinct Status ===", ""];
79
+
80
+ for (const domain of sortedDomains) {
81
+ const domainInstincts = groups[domain];
82
+ if (!domainInstincts || domainInstincts.length === 0) continue;
83
+
84
+ lines.push(`## ${domain}`);
85
+ for (const instinct of domainInstincts) {
86
+ lines.push(formatInstinct(instinct));
87
+ }
88
+ lines.push("");
89
+ }
90
+
91
+ const total = instincts.length;
92
+ const flagged = instincts.filter((i) => i.flagged_for_removal).length;
93
+ lines.push(`Total: ${total} instinct${total !== 1 ? "s" : ""} (${flagged} flagged for removal)`);
94
+
95
+ return lines.join("\n");
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // loadAllInstincts
100
+ // ---------------------------------------------------------------------------
101
+
102
+ /**
103
+ * Loads all instincts from disk (project + global), including flagged ones.
104
+ * Does NOT apply confidence filtering - status command shows everything.
105
+ */
106
+ export function loadAllInstincts(
107
+ projectId?: string | null,
108
+ baseDir?: string
109
+ ): Instinct[] {
110
+ const projectInstincts =
111
+ projectId != null ? loadProjectInstincts(projectId, baseDir) : [];
112
+ const globalInstincts = loadGlobalInstincts(baseDir);
113
+ return [...projectInstincts, ...globalInstincts];
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // handleInstinctStatus
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /**
121
+ * Command handler for /instinct-status.
122
+ * Loads all instincts, formats them grouped by domain, and notifies the user.
123
+ */
124
+ export async function handleInstinctStatus(
125
+ _args: string,
126
+ ctx: ExtensionCommandContext,
127
+ projectId?: string | null,
128
+ baseDir?: string
129
+ ): Promise<void> {
130
+ const instincts = loadAllInstincts(projectId, baseDir);
131
+ const output = formatStatusOutput(instincts);
132
+ ctx.ui.notify(output, "info");
133
+ }
134
+
135
+ export { COMMAND_NAME };