@zhijiewang/openharness 2.3.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -19,6 +19,8 @@ import { readOhConfig } from "./harness/config.js";
19
19
  import { emitHook } from "./harness/hooks.js";
20
20
  import { detectProject, projectContextToPrompt } from "./harness/onboarding.js";
21
21
  import { createRulesFile, loadRules, loadRulesAsPrompt } from "./harness/rules.js";
22
+ import { loadActiveMemories, memoriesToPrompt, userProfileToPrompt } from "./harness/memory.js";
23
+ import { discoverSkills, skillsToPrompt } from "./harness/plugins.js";
22
24
  import { listSessions } from "./harness/session.js";
23
25
  import { connectedMcpServers, disconnectMcpClients, getMcpInstructions, loadMcpTools } from "./mcp/loader.js";
24
26
  import { getAllTools } from "./tools.js";
@@ -78,6 +80,20 @@ function buildSystemPrompt(model) {
78
80
  const rulesPrompt = loadRulesAsPrompt();
79
81
  if (rulesPrompt)
80
82
  parts.push(rulesPrompt);
83
+ // User profile (highest priority personal context)
84
+ const userProfile = userProfileToPrompt();
85
+ if (userProfile)
86
+ parts.push(userProfile);
87
+ // Remembered context from past sessions
88
+ const memories = loadActiveMemories();
89
+ const memoriesPrompt = memoriesToPrompt(memories);
90
+ if (memoriesPrompt)
91
+ parts.push(memoriesPrompt);
92
+ // Available skills (Level 0 — names + descriptions only)
93
+ const skills = discoverSkills();
94
+ const skillsPrompt = skillsToPrompt(skills);
95
+ if (skillsPrompt)
96
+ parts.push(skillsPrompt);
81
97
  // MCP server instructions (sandboxed — treat as untrusted)
82
98
  const mcpInstructions = getMcpInstructions();
83
99
  if (mcpInstructions.length > 0) {
@@ -21,7 +21,7 @@ const DEFAULT_MAX_TURNS = 50;
21
21
  export async function* query(userMessage, config, existingMessages = []) {
22
22
  const maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
23
23
  const toolContext = {
24
- workingDir: process.cwd(),
24
+ workingDir: config.workingDir ?? process.cwd(),
25
25
  abortSignal: config.abortSignal,
26
26
  provider: config.provider,
27
27
  model: config.model,
@@ -44,9 +44,19 @@ export async function* query(userMessage, config, existingMessages = []) {
44
44
  toolPrompts += `\n\n[${deferredCount} additional tools available — use ToolSearch to discover them]`;
45
45
  }
46
46
  }
47
- const fullSystemPrompt = toolPrompts
47
+ let fullSystemPrompt = toolPrompts
48
48
  ? `${config.systemPrompt}\n\n# Available Tools\n\n${toolPrompts}`
49
49
  : config.systemPrompt;
50
+ // Auto-trigger skills matching user message
51
+ try {
52
+ const { findTriggeredSkills } = await import("../harness/plugins.js");
53
+ const triggered = findTriggeredSkills(userMessage);
54
+ if (triggered.length > 0) {
55
+ const hints = triggered.map((s) => `- **${s.name}**: ${s.description}`).join("\n");
56
+ fullSystemPrompt += `\n\n# Suggested Skills\nThese skills match your request. Use Skill tool to load them:\n${hints}`;
57
+ }
58
+ }
59
+ catch { /* skills optional */ }
50
60
  const state = {
51
61
  messages: [...existingMessages, createUserMessage(userMessage)],
52
62
  turn: 0,
@@ -16,6 +16,8 @@ export type QueryConfig = {
16
16
  maxCost?: number;
17
17
  model?: string;
18
18
  abortSignal?: AbortSignal;
19
+ /** Working directory for tool execution (defaults to process.cwd()) */
20
+ workingDir?: string;
19
21
  };
20
22
  export type TransitionReason = "next_turn" | "retry_network" | "retry_prompt_too_long" | "retry_max_output_tokens";
21
23
  export type QueryLoopState = {
package/dist/repl.js CHANGED
@@ -674,17 +674,28 @@ export async function startREPL(config) {
674
674
  if (extracted.length > 0) {
675
675
  console.log(`[learn] Extracted ${extracted.length} skill(s) from this session.`);
676
676
  }
677
- // User profile update
677
+ // User profile update with LLM consolidation
678
678
  if (messages.length >= 6) {
679
679
  const detected = await detectMemories(config.provider, messages, currentModel);
680
680
  const profileUpdates = detected.filter((d) => d.type === "user");
681
681
  if (profileUpdates.length > 0) {
682
682
  const currentProfile = loadUserProfile();
683
683
  const newObservations = profileUpdates.map((d) => d.content).join("\n");
684
- const merged = currentProfile
685
- ? `${currentProfile}\n\n## Recent Observations\n${newObservations}`
686
- : newObservations;
687
- updateUserProfile(merged);
684
+ if (currentProfile) {
685
+ // LLM-assisted merge: consolidate instead of blind append
686
+ const { createUserMessage: makeMsg } = await import("./types/message.js");
687
+ try {
688
+ const consolidated = await config.provider.complete([makeMsg(`Merge this user profile with new observations into a single cohesive profile. Keep the most important and recent information. Remove duplicates. Stay under 2000 characters. Return ONLY the merged profile text.\n\nCurrent profile:\n${currentProfile}\n\nNew observations:\n${newObservations}`)], "You are a profile curator. Return ONLY the merged profile, no commentary.", undefined, currentModel);
689
+ updateUserProfile(consolidated.content);
690
+ }
691
+ catch {
692
+ // Fallback to simple append if LLM fails
693
+ updateUserProfile(`${currentProfile}\n\n${newObservations}`);
694
+ }
695
+ }
696
+ else {
697
+ updateUserProfile(newObservations);
698
+ }
688
699
  }
689
700
  }
690
701
  }
@@ -113,6 +113,23 @@ ${candidate.verification}
113
113
  writeFileSync(filePath, content, "utf-8");
114
114
  return filePath;
115
115
  }
116
+ /** Quick LLM quality check — is this skill worth keeping? */
117
+ async function isSkillWorthy(provider, candidate, model) {
118
+ try {
119
+ const prompt = `Is this extracted skill worth saving for future reuse? Answer YES or NO (one word only).
120
+
121
+ Name: ${candidate.name}
122
+ Description: ${candidate.description}
123
+ Procedure: ${candidate.procedure}
124
+
125
+ Criteria: Is it reusable (not a one-off)? Is the procedure clear and complete? Would it save time in future sessions?`;
126
+ const response = await provider.complete([createUserMessage(prompt)], "Answer YES or NO only.", undefined, model);
127
+ return response.content.trim().toUpperCase().startsWith("YES");
128
+ }
129
+ catch {
130
+ return true; // On error, allow the skill through
131
+ }
132
+ }
116
133
  /**
117
134
  * Orchestrate the full extraction pipeline:
118
135
  * 1. Check if extraction is warranted
@@ -132,9 +149,12 @@ export async function runExtraction(provider, messages, sessionId, model) {
132
149
  const written = [];
133
150
  for (const candidate of candidates) {
134
151
  const similar = findSimilarSkill(candidate.name, candidate.description, existingSkills);
135
- // Skip if a very similar skill already exists (avoid duplicates)
136
152
  if (similar)
137
153
  continue;
154
+ // Quality gate: quick LLM check before persisting
155
+ const worthy = await isSkillWorthy(provider, candidate, model);
156
+ if (!worthy)
157
+ continue;
138
158
  const filePath = persistSkill(candidate, sessionId);
139
159
  written.push(filePath);
140
160
  }
@@ -87,6 +87,7 @@ export const AgentTool = {
87
87
  model: agentModel,
88
88
  maxTurns: 20,
89
89
  abortSignal: context.abortSignal,
90
+ workingDir: agentWorkingDir,
90
91
  };
91
92
  const agentId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
92
93
  emitHook("subagentStart", { agentId, toolName: input.subagent_type ?? "general" });
@@ -97,30 +98,13 @@ export const AgentTool = {
97
98
  bus.registerBackgroundAgent(bgId, input.subagent_type ?? "general");
98
99
  const runAgent = async () => {
99
100
  let finalText = "";
100
- const originalCwd = process.cwd();
101
101
  try {
102
- if (worktreePath) {
103
- try {
104
- process.chdir(agentWorkingDir);
105
- }
106
- catch {
107
- /* ignore */
108
- }
109
- }
110
102
  for await (const event of query(input.prompt, config)) {
111
103
  if (event.type === "text_delta")
112
104
  finalText += event.content;
113
105
  }
114
106
  }
115
107
  finally {
116
- if (worktreePath) {
117
- try {
118
- process.chdir(originalCwd);
119
- }
120
- catch {
121
- /* ignore */
122
- }
123
- }
124
108
  // Clean up worktree only if no changes were made
125
109
  if (worktreePath) {
126
110
  const hasChanges = hasWorktreeChanges(worktreePath);
@@ -152,16 +136,6 @@ export const AgentTool = {
152
136
  const outputChunks = [];
153
137
  let finalText = "";
154
138
  try {
155
- // Override process.cwd for the sub-agent by setting workingDir in tool context
156
- const originalCwd = process.cwd();
157
- if (worktreePath) {
158
- try {
159
- process.chdir(agentWorkingDir);
160
- }
161
- catch {
162
- /* ignore */
163
- }
164
- }
165
139
  try {
166
140
  for await (const event of query(input.prompt, config)) {
167
141
  if (event.type === "text_delta") {
@@ -184,15 +158,7 @@ export const AgentTool = {
184
158
  }
185
159
  }
186
160
  finally {
187
- // Restore original working directory
188
- if (worktreePath) {
189
- try {
190
- process.chdir(originalCwd);
191
- }
192
- catch {
193
- /* ignore */
194
- }
195
- }
161
+ /* workingDir passed via config — no process.chdir cleanup needed */
196
162
  }
197
163
  }
198
164
  catch (err) {
@@ -7,13 +7,13 @@ declare const createSchema: z.ZodObject<{
7
7
  prompt: z.ZodString;
8
8
  }, "strip", z.ZodTypeAny, {
9
9
  action: "create";
10
- prompt: string;
11
10
  name: string;
11
+ prompt: string;
12
12
  schedule: string;
13
13
  }, {
14
14
  action: "create";
15
- prompt: string;
16
15
  name: string;
16
+ prompt: string;
17
17
  schedule: string;
18
18
  }>;
19
19
  declare const deleteSchema: z.ZodObject<{
@@ -12,18 +12,18 @@ declare const inputSchema: z.ZodObject<{
12
12
  action: "search" | "list" | "save";
13
13
  content?: string | undefined;
14
14
  type?: "user" | "convention" | "preference" | "project" | "debugging" | "feedback" | "reference" | undefined;
15
- query?: string | undefined;
16
- description?: string | undefined;
17
- name?: string | undefined;
18
15
  global?: boolean | undefined;
16
+ name?: string | undefined;
17
+ description?: string | undefined;
18
+ query?: string | undefined;
19
19
  }, {
20
20
  action: "search" | "list" | "save";
21
21
  content?: string | undefined;
22
22
  type?: "user" | "convention" | "preference" | "project" | "debugging" | "feedback" | "reference" | undefined;
23
- query?: string | undefined;
24
- description?: string | undefined;
25
- name?: string | undefined;
26
23
  global?: boolean | undefined;
24
+ name?: string | undefined;
25
+ description?: string | undefined;
26
+ query?: string | undefined;
27
27
  }>;
28
28
  export declare const MemoryTool: Tool<typeof inputSchema>;
29
29
  export {};
@@ -8,29 +8,29 @@ declare const inputSchema: z.ZodObject<{
8
8
  dependsOn: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
9
9
  }, "strip", z.ZodTypeAny, {
10
10
  tool: string;
11
- id: string;
12
11
  args: Record<string, unknown>;
12
+ id: string;
13
13
  dependsOn?: string[] | undefined;
14
14
  }, {
15
15
  tool: string;
16
- id: string;
17
16
  args: Record<string, unknown>;
17
+ id: string;
18
18
  dependsOn?: string[] | undefined;
19
19
  }>, "many">;
20
20
  description: z.ZodOptional<z.ZodString>;
21
21
  }, "strip", z.ZodTypeAny, {
22
22
  steps: {
23
23
  tool: string;
24
- id: string;
25
24
  args: Record<string, unknown>;
25
+ id: string;
26
26
  dependsOn?: string[] | undefined;
27
27
  }[];
28
28
  description?: string | undefined;
29
29
  }, {
30
30
  steps: {
31
31
  tool: string;
32
- id: string;
33
32
  args: Record<string, unknown>;
33
+ id: string;
34
34
  dependsOn?: string[] | undefined;
35
35
  }[];
36
36
  description?: string | undefined;
@@ -13,8 +13,8 @@ declare const inputSchema: z.ZodObject<{
13
13
  }, "strip", z.ZodTypeAny, {
14
14
  taskId: number;
15
15
  status?: "completed" | "pending" | "cancelled" | "in_progress" | "deleted" | undefined;
16
- metadata?: Record<string, unknown> | undefined;
17
16
  description?: string | undefined;
17
+ metadata?: Record<string, unknown> | undefined;
18
18
  subject?: string | undefined;
19
19
  activeForm?: string | undefined;
20
20
  owner?: string | undefined;
@@ -23,8 +23,8 @@ declare const inputSchema: z.ZodObject<{
23
23
  }, {
24
24
  taskId: number;
25
25
  status?: "completed" | "pending" | "cancelled" | "in_progress" | "deleted" | undefined;
26
- metadata?: Record<string, unknown> | undefined;
27
26
  description?: string | undefined;
27
+ metadata?: Record<string, unknown> | undefined;
28
28
  subject?: string | undefined;
29
29
  activeForm?: string | undefined;
30
30
  owner?: string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhijiewang/openharness",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Open-source terminal coding agent. Works with any LLM.",
5
5
  "type": "module",
6
6
  "bin": {