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.
- package/LICENSE +21 -0
- package/README.md +326 -0
- package/dist/active-instincts.d.ts +4 -0
- package/dist/active-instincts.d.ts.map +1 -0
- package/dist/active-instincts.js +11 -0
- package/dist/active-instincts.js.map +1 -0
- package/dist/agents-md.d.ts +12 -0
- package/dist/agents-md.d.ts.map +1 -0
- package/dist/agents-md.js +23 -0
- package/dist/agents-md.js.map +1 -0
- package/dist/cli/analyze-prompt.d.ts +2 -0
- package/dist/cli/analyze-prompt.d.ts.map +1 -0
- package/dist/cli/analyze-prompt.js +72 -0
- package/dist/cli/analyze-prompt.js.map +1 -0
- package/dist/cli/analyze.d.ts +3 -0
- package/dist/cli/analyze.d.ts.map +1 -0
- package/dist/cli/analyze.js +214 -0
- package/dist/cli/analyze.js.map +1 -0
- package/dist/confidence.d.ts +25 -0
- package/dist/confidence.d.ts.map +1 -0
- package/dist/confidence.js +77 -0
- package/dist/confidence.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +89 -0
- package/dist/config.js.map +1 -0
- package/dist/error-logger.d.ts +34 -0
- package/dist/error-logger.d.ts.map +1 -0
- package/dist/error-logger.js +102 -0
- package/dist/error-logger.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +118 -0
- package/dist/index.js.map +1 -0
- package/dist/instinct-decay.d.ts +39 -0
- package/dist/instinct-decay.d.ts.map +1 -0
- package/dist/instinct-decay.js +93 -0
- package/dist/instinct-decay.js.map +1 -0
- package/dist/instinct-evolve.d.ts +5 -0
- package/dist/instinct-evolve.d.ts.map +1 -0
- package/dist/instinct-evolve.js +24 -0
- package/dist/instinct-evolve.js.map +1 -0
- package/dist/instinct-export.d.ts +40 -0
- package/dist/instinct-export.d.ts.map +1 -0
- package/dist/instinct-export.js +94 -0
- package/dist/instinct-export.js.map +1 -0
- package/dist/instinct-import.d.ts +50 -0
- package/dist/instinct-import.d.ts.map +1 -0
- package/dist/instinct-import.js +168 -0
- package/dist/instinct-import.js.map +1 -0
- package/dist/instinct-injector.d.ts +39 -0
- package/dist/instinct-injector.d.ts.map +1 -0
- package/dist/instinct-injector.js +89 -0
- package/dist/instinct-injector.js.map +1 -0
- package/dist/instinct-loader.d.ts +37 -0
- package/dist/instinct-loader.d.ts.map +1 -0
- package/dist/instinct-loader.js +96 -0
- package/dist/instinct-loader.js.map +1 -0
- package/dist/instinct-parser.d.ts +28 -0
- package/dist/instinct-parser.d.ts.map +1 -0
- package/dist/instinct-parser.js +143 -0
- package/dist/instinct-parser.js.map +1 -0
- package/dist/instinct-projects.d.ts +32 -0
- package/dist/instinct-projects.d.ts.map +1 -0
- package/dist/instinct-projects.js +96 -0
- package/dist/instinct-projects.js.map +1 -0
- package/dist/instinct-promote.d.ts +51 -0
- package/dist/instinct-promote.d.ts.map +1 -0
- package/dist/instinct-promote.js +169 -0
- package/dist/instinct-promote.js.map +1 -0
- package/dist/instinct-status.d.ts +39 -0
- package/dist/instinct-status.d.ts.map +1 -0
- package/dist/instinct-status.js +108 -0
- package/dist/instinct-status.js.map +1 -0
- package/dist/instinct-store.d.ts +30 -0
- package/dist/instinct-store.d.ts.map +1 -0
- package/dist/instinct-store.js +118 -0
- package/dist/instinct-store.js.map +1 -0
- package/dist/instinct-tools.d.ts +161 -0
- package/dist/instinct-tools.d.ts.map +1 -0
- package/dist/instinct-tools.js +240 -0
- package/dist/instinct-tools.js.map +1 -0
- package/dist/observations.d.ts +22 -0
- package/dist/observations.d.ts.map +1 -0
- package/dist/observations.js +62 -0
- package/dist/observations.js.map +1 -0
- package/dist/observer-guard.d.ts +3 -0
- package/dist/observer-guard.d.ts.map +1 -0
- package/dist/observer-guard.js +13 -0
- package/dist/observer-guard.js.map +1 -0
- package/dist/project.d.ts +16 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +59 -0
- package/dist/project.js.map +1 -0
- package/dist/prompt-observer.d.ts +25 -0
- package/dist/prompt-observer.d.ts.map +1 -0
- package/dist/prompt-observer.js +63 -0
- package/dist/prompt-observer.js.map +1 -0
- package/dist/prompts/analyzer-user.d.ts +38 -0
- package/dist/prompts/analyzer-user.d.ts.map +1 -0
- package/dist/prompts/analyzer-user.js +105 -0
- package/dist/prompts/analyzer-user.js.map +1 -0
- package/dist/prompts/evolve-prompt.d.ts +3 -0
- package/dist/prompts/evolve-prompt.d.ts.map +1 -0
- package/dist/prompts/evolve-prompt.js +51 -0
- package/dist/prompts/evolve-prompt.js.map +1 -0
- package/dist/scrubber.d.ts +9 -0
- package/dist/scrubber.d.ts.map +1 -0
- package/dist/scrubber.js +40 -0
- package/dist/scrubber.js.map +1 -0
- package/dist/storage.d.ts +22 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +71 -0
- package/dist/storage.js.map +1 -0
- package/dist/tool-observer.d.ts +32 -0
- package/dist/tool-observer.d.ts.map +1 -0
- package/dist/tool-observer.js +77 -0
- package/dist/tool-observer.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -0
- package/src/active-instincts.ts +13 -0
- package/src/agents-md.ts +23 -0
- package/src/cli/analyze-prompt.ts +71 -0
- package/src/cli/analyze.ts +286 -0
- package/src/confidence.ts +103 -0
- package/src/config.ts +111 -0
- package/src/error-logger.ts +130 -0
- package/src/index.ts +144 -0
- package/src/instinct-decay.ts +117 -0
- package/src/instinct-evolve.ts +44 -0
- package/src/instinct-export.ts +138 -0
- package/src/instinct-import.ts +260 -0
- package/src/instinct-injector.ts +128 -0
- package/src/instinct-loader.ts +146 -0
- package/src/instinct-parser.ts +171 -0
- package/src/instinct-projects.ts +119 -0
- package/src/instinct-promote.ts +231 -0
- package/src/instinct-status.ts +135 -0
- package/src/instinct-store.ts +149 -0
- package/src/instinct-tools.ts +340 -0
- package/src/observations.ts +82 -0
- package/src/observer-guard.ts +14 -0
- package/src/project.ts +70 -0
- package/src/prompt-observer.ts +92 -0
- package/src/prompts/analyzer-user.ts +156 -0
- package/src/prompts/evolve-prompt.ts +71 -0
- package/src/scrubber.ts +42 -0
- package/src/storage.ts +91 -0
- package/src/tool-observer.ts +114 -0
- 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 };
|