@evo-dev/evodev 0.0.1-alpha.2 → 0.0.1-alpha.20

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "evodev",
3
3
  "description": "EvoDev hook, skill, and Teams MCP integration for disciplined AI coding workflows.",
4
- "version": "0.0.1-alpha.2",
4
+ "version": "0.0.1-alpha.20",
5
5
  "author": {
6
6
  "name": "EvoDev"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evodev",
3
- "version": "0.0.1-alpha.2",
3
+ "version": "0.0.1-alpha.20",
4
4
  "description": "EvoDev hook, skill, and Teams MCP integration for disciplined AI coding workflows.",
5
5
  "author": {
6
6
  "name": "EvoDev"
@@ -54,6 +54,7 @@ export interface CodexPluginOptions {
54
54
  pluginSelector?: string;
55
55
  pluginMarketplaceSource?: string;
56
56
  pluginMarketplaceFallbackSource?: string;
57
+ pluginVersion?: string;
57
58
  paths?: CodexPaths;
58
59
  syncFileSystem?: CodexSyncFileSystem;
59
60
  }
@@ -195,6 +196,7 @@ async function installCodexPlugin(
195
196
  | "pluginSelector"
196
197
  | "pluginMarketplaceSource"
197
198
  | "pluginMarketplaceFallbackSource"
199
+ | "pluginVersion"
198
200
  | "paths"
199
201
  >,
200
202
  ): Promise<PluginInstallResult> {
@@ -202,8 +204,46 @@ async function installCodexPlugin(
202
204
  const selector = options.pluginSelector ?? "evodev@evodev";
203
205
  const args = ["plugin", "add", selector];
204
206
  const runner = options.commandRunner ?? new NodeCodexCommandRunner();
207
+ const installedPlugin =
208
+ input.force === true || options.pluginVersion === undefined
209
+ ? null
210
+ : await readInstalledCodexPlugin(runner, selector);
211
+ let needsRepair = false;
212
+
213
+ if (
214
+ installedPlugin !== null &&
215
+ installedPlugin.version === options.pluginVersion &&
216
+ installedPlugin.enabled
217
+ ) {
218
+ try {
219
+ const hydration = await hydrateCodexWorkspaceCoreDependency({
220
+ paths: options.paths,
221
+ selector,
222
+ });
223
+ if (hydration.warnings.length === 0 && hydration.errors.length === 0) {
224
+ return createPluginInstallResult(
225
+ input.targetPlugin,
226
+ "already-installed",
227
+ command,
228
+ args,
229
+ `Codex plugin ${selector} already matches EvoDev ${options.pluginVersion}; skipped marketplace refresh.`,
230
+ );
231
+ }
232
+ } catch {
233
+ // Fall through to the normal repair path, which returns a structured install failure.
234
+ }
235
+ needsRepair = true;
236
+ }
237
+
238
+ const effectiveInput =
239
+ input.force === true ||
240
+ needsRepair ||
241
+ (installedPlugin !== null &&
242
+ (installedPlugin.version !== options.pluginVersion || !installedPlugin.enabled))
243
+ ? { ...input, force: true }
244
+ : input;
205
245
  const primaryResult = await installCodexPluginOnce({
206
- input,
246
+ input: effectiveInput,
207
247
  command,
208
248
  selector,
209
249
  args,
@@ -224,7 +264,7 @@ async function installCodexPlugin(
224
264
  }
225
265
 
226
266
  const fallbackResult = await installCodexPluginOnce({
227
- input,
267
+ input: effectiveInput,
228
268
  command,
229
269
  selector,
230
270
  args,
@@ -243,6 +283,33 @@ async function installCodexPlugin(
243
283
  };
244
284
  }
245
285
 
286
+ interface InstalledCodexPlugin {
287
+ version: string | null;
288
+ enabled: boolean;
289
+ }
290
+
291
+ async function readInstalledCodexPlugin(
292
+ runner: CodexCommandRunner,
293
+ selector: string,
294
+ ): Promise<InstalledCodexPlugin | null> {
295
+ try {
296
+ const result = await runner.run("codex", ["plugin", "list", "--json"]);
297
+ if (result.exitCode !== 0 || result.stdout === undefined) return null;
298
+ const parsed = JSON.parse(result.stdout) as unknown;
299
+ const plugins = isRecord(parsed) && Array.isArray(parsed.installed) ? parsed.installed : [];
300
+ const plugin = plugins.find(
301
+ (candidate) => isRecord(candidate) && candidate.pluginId === selector,
302
+ );
303
+ if (!isRecord(plugin)) return null;
304
+ return {
305
+ version: typeof plugin.version === "string" ? plugin.version : null,
306
+ enabled: plugin.enabled !== false,
307
+ };
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
312
+
246
313
  async function installCodexPluginOnce(input: {
247
314
  input: PluginInstallInput;
248
315
  command: string;
@@ -158,7 +158,7 @@ export function planClaudeHookInstallDryRun(
158
158
  action: "merge-with-backup",
159
159
  targetPath: paths.settingsPath,
160
160
  reason:
161
- "would add or replace EvoDev-managed user-level hook entries without touching user-owned hooks",
161
+ "would reconcile EvoDev-managed user-level hook entries with Claude plugin ownership without touching user-owned hooks",
162
162
  },
163
163
  ],
164
164
  warnings: ["Project .claude, .codex, CLAUDE.md, and AGENTS.md are not targeted."],
@@ -191,12 +191,34 @@ export async function installClaudeHooks(input: {
191
191
  paths?: ClaudePaths;
192
192
  now?: string;
193
193
  pluginSelector?: string;
194
+ pluginManaged?: boolean;
194
195
  }): Promise<HookInstallResult> {
195
196
  const paths = input.paths ?? resolveClaudePaths({ homeDir: input.homeDir });
196
197
  const existing = await readTextIfExists(paths.settingsPath);
198
+ const pluginSelector = input.pluginSelector ?? DEFAULT_CLAUDE_PLUGIN_SELECTOR;
199
+ if (input.pluginManaged === true || isClaudePluginEnabled(existing, pluginSelector)) {
200
+ const next = removeClaudeManagedUserHooks(existing);
201
+ if (next === null) {
202
+ return {
203
+ target: "claude",
204
+ targetPath: paths.settingsPath,
205
+ backupPath: null,
206
+ changed: false,
207
+ warnings: [],
208
+ };
209
+ }
210
+ return writeMergedConfig({
211
+ target: "claude",
212
+ path: paths.settingsPath,
213
+ existing,
214
+ next,
215
+ now: input.now,
216
+ warnings: [],
217
+ });
218
+ }
197
219
  const runtimeCommand = await resolveClaudeUserHookRuntimeCommand({
198
220
  paths,
199
- pluginSelector: input.pluginSelector ?? DEFAULT_CLAUDE_PLUGIN_SELECTOR,
221
+ pluginSelector,
200
222
  });
201
223
  const next = mergeClaudeSettingsHooks(existing, createClaudeHookConfig(runtimeCommand).hooks);
202
224
  return writeMergedConfig({
@@ -497,6 +519,37 @@ function mergeClaudeSettingsHooks(
497
519
  return `${JSON.stringify({ ...settings, hooks: nextHooks }, null, 2)}\n`;
498
520
  }
499
521
 
522
+ function isClaudePluginEnabled(existing: string | null, pluginSelector: string): boolean {
523
+ if (existing === null) return false;
524
+ const settings = parseJsonConfig(existing, "Claude settings");
525
+ const enabledPlugins = isRecord(settings.enabledPlugins) ? settings.enabledPlugins : {};
526
+ return enabledPlugins[pluginSelector] === true;
527
+ }
528
+
529
+ function removeClaudeManagedUserHooks(existing: string | null): string | null {
530
+ if (existing === null) return null;
531
+ const settings = parseJsonConfig(existing, "Claude settings");
532
+ if (!isRecord(settings.hooks)) return existing;
533
+
534
+ const nextHooks: Record<string, unknown> = {};
535
+ let changed = false;
536
+ for (const [eventName, groups] of Object.entries(settings.hooks)) {
537
+ if (!Array.isArray(groups)) {
538
+ nextHooks[eventName] = groups;
539
+ continue;
540
+ }
541
+ const retained = groups.filter((group) => !isEvoDevHookGroup(group, "claude"));
542
+ changed ||= retained.length !== groups.length;
543
+ if (retained.length > 0 || groups.length === 0) nextHooks[eventName] = retained;
544
+ }
545
+ if (!changed) return existing;
546
+
547
+ const nextSettings = { ...settings };
548
+ if (Object.keys(nextHooks).length === 0) nextSettings.hooks = undefined;
549
+ else nextSettings.hooks = nextHooks;
550
+ return `${JSON.stringify(nextSettings, null, 2)}\n`;
551
+ }
552
+
500
553
  function parseJsonConfig(existing: string | null, label: string): Record<string, unknown> {
501
554
  if (existing === null || existing.trim() === "") return {};
502
555
  const parsed = JSON.parse(existing) as unknown;
@@ -56,6 +56,7 @@ export interface ClaudePluginOptions {
56
56
  pluginSelector?: string;
57
57
  pluginMarketplaceSource?: string;
58
58
  pluginScope?: "user";
59
+ pluginVersion?: string;
59
60
  }
60
61
 
61
62
  export function createClaudePlugin(options: ClaudePluginOptions = {}): CodeAgentPlugin {
@@ -186,7 +187,12 @@ async function installClaudeCodePlugin(
186
187
  input: PluginInstallInput,
187
188
  options: Pick<
188
189
  ClaudePluginOptions,
189
- "commandRunner" | "paths" | "pluginSelector" | "pluginMarketplaceSource" | "pluginScope"
190
+ | "commandRunner"
191
+ | "paths"
192
+ | "pluginSelector"
193
+ | "pluginMarketplaceSource"
194
+ | "pluginScope"
195
+ | "pluginVersion"
190
196
  >,
191
197
  ): Promise<PluginInstallResult> {
192
198
  const command = "claude";
@@ -195,6 +201,42 @@ async function installClaudeCodePlugin(
195
201
  const args = ["plugin", "install", selector, "--scope", scope];
196
202
  const runner = options.commandRunner ?? new NodeClaudeCommandRunner();
197
203
  const warnings: string[] = [];
204
+ const installedPlugin =
205
+ input.force === true || options.pluginVersion === undefined
206
+ ? null
207
+ : await readInstalledClaudePlugin(runner, selector);
208
+ let needsRepair = false;
209
+
210
+ if (
211
+ installedPlugin !== null &&
212
+ installedPlugin.version === options.pluginVersion &&
213
+ installedPlugin.enabled
214
+ ) {
215
+ try {
216
+ const hydration = await hydrateClaudeWorkspaceCoreDependency({
217
+ paths: options.paths,
218
+ selector,
219
+ });
220
+ if (hydration.warnings.length === 0 && hydration.errors.length === 0) {
221
+ return createPluginInstallResult(
222
+ input.targetPlugin,
223
+ "already-installed",
224
+ command,
225
+ args,
226
+ `Claude Code plugin ${selector} already matches EvoDev ${options.pluginVersion}; skipped marketplace refresh.`,
227
+ );
228
+ }
229
+ } catch {
230
+ // Fall through to the normal repair path, which returns a structured install failure.
231
+ }
232
+ needsRepair = true;
233
+ }
234
+
235
+ const forceRefresh =
236
+ input.force === true ||
237
+ needsRepair ||
238
+ (installedPlugin !== null &&
239
+ (installedPlugin.version !== options.pluginVersion || !installedPlugin.enabled));
198
240
 
199
241
  try {
200
242
  if (options.pluginMarketplaceSource !== undefined) {
@@ -244,7 +286,7 @@ async function installClaudeCodePlugin(
244
286
  }
245
287
  }
246
288
 
247
- if (input.force === true) {
289
+ if (forceRefresh) {
248
290
  const uninstallArgs = [
249
291
  "plugin",
250
292
  "uninstall",
@@ -293,7 +335,7 @@ async function installClaudeCodePlugin(
293
335
  result.stdout,
294
336
  [
295
337
  ...warnings,
296
- input.force === true
338
+ forceRefresh
297
339
  ? "Claude Code reported the plugin is already installed after forced refresh; check the local plugin cache if changes are not visible."
298
340
  : "Claude Code reported the plugin is already installed; if local plugin files changed without a version bump, Claude may keep the existing cached copy. Bump the plugin version or uninstall and reinstall to refresh the cache.",
299
341
  ],
@@ -330,6 +372,35 @@ async function installClaudeCodePlugin(
330
372
  }
331
373
  }
332
374
 
375
+ interface InstalledClaudePlugin {
376
+ version: string | null;
377
+ enabled: boolean;
378
+ }
379
+
380
+ async function readInstalledClaudePlugin(
381
+ runner: ClaudeCommandRunner,
382
+ selector: string,
383
+ ): Promise<InstalledClaudePlugin | null> {
384
+ try {
385
+ const result = await runner.run("claude", ["plugin", "list", "--json"]);
386
+ if (result.exitCode !== 0 || result.stdout === undefined) return null;
387
+ const parsed = JSON.parse(result.stdout) as unknown;
388
+ const plugins = Array.isArray(parsed)
389
+ ? parsed
390
+ : isRecord(parsed) && Array.isArray(parsed.plugins)
391
+ ? parsed.plugins
392
+ : [];
393
+ const plugin = plugins.find((candidate) => isRecord(candidate) && candidate.id === selector);
394
+ if (!isRecord(plugin)) return null;
395
+ return {
396
+ version: typeof plugin.version === "string" ? plugin.version : null,
397
+ enabled: plugin.enabled !== false,
398
+ };
399
+ } catch {
400
+ return null;
401
+ }
402
+ }
403
+
333
404
  function isAlreadyInstalledMessage(message: string | undefined): boolean {
334
405
  return message !== undefined && /already\s+installed/i.test(message);
335
406
  }
@@ -655,6 +726,10 @@ function isNotDirectoryError(error: unknown): boolean {
655
726
  );
656
727
  }
657
728
 
729
+ function isRecord(value: unknown): value is Record<string, unknown> {
730
+ return typeof value === "object" && value !== null && !Array.isArray(value);
731
+ }
732
+
658
733
  function describeError(error: unknown): string {
659
734
  if (error instanceof Error) {
660
735
  return error.message;
@@ -79,6 +79,7 @@ export async function runHookRuntimeCli(options: HookRuntimeCliOptions = {}): Pr
79
79
  receivedAt: event.time.receivedAt,
80
80
  environment,
81
81
  runtimeInjectionEnabled: settings.memory.runtimeInjection,
82
+ sessionMemoryPolicy: settings.memory.sessionMemory,
82
83
  });
83
84
  const formatted = runtimeApi.core.formatHookRuntimeOutput(result);
84
85
  await appendExecutionEventSafely({
@@ -183,6 +184,12 @@ async function appendExecutionEventSafely(input: {
183
184
  environment: input.environment,
184
185
  payload: input.payload,
185
186
  });
187
+ const cwd =
188
+ input.payload !== null && typeof input.payload.cwd === "string" ? input.payload.cwd : null;
189
+ const workspace =
190
+ team === null && cwd !== null
191
+ ? await input.core.resolveProjectWorkspaceFromCwd({ homeDir: input.homeDir, cwd })
192
+ : null;
186
193
  const traceRefId = await recordTraceRefSafely(input);
187
194
  await input.core.appendEvoDevExecutionEvent(
188
195
  input.homeDir,
@@ -191,7 +198,7 @@ async function appendExecutionEventSafely(input: {
191
198
  target: input.target,
192
199
  eventType: input.event?.type ?? runtimePhaseEventType(input.phase),
193
200
  sessionKey,
194
- projectKey: team?.projectKey ?? null,
201
+ projectKey: team?.projectKey ?? workspace?.projectKey ?? null,
195
202
  runId: team?.runId ?? null,
196
203
  roleId: team?.roleId ?? null,
197
204
  taskId: input.event?.scope.taskId ?? null,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evo-dev/plugin",
3
- "version": "0.0.1-alpha.2",
3
+ "version": "0.0.1-alpha.20",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./hooks/index.ts"
@@ -23,7 +23,7 @@ It is not a trace summarizer, automatic memory writer, OKF file writer, or team
23
23
  ## Do Not Use This Skill For
24
24
 
25
25
  - Writing `CLAUDE.md`, `AGENTS.md`, `.claude/`, `.codex/`, `.evodev/`, source files, or project assets without explicit project opt-in.
26
- - Storing raw prompts, transcripts, source dumps, raw command output, secrets, tokens, internal links, or private URLs.
26
+ - Storing credentials or wholesale prompts, transcripts, source files, source dumps, or raw command output. Distilled private facts and necessary short excerpts may remain in user-local knowledge.
27
27
  - Treating unreviewed observations, model reflection, or trace logs as accepted memory.
28
28
  - Writing final OKF concept documents directly; output a transient plan for the organizer.
29
29
  - Starting subagents or teams automatically.
@@ -62,14 +62,14 @@ Classify every input before extraction:
62
62
  ## Distillation Pipeline
63
63
 
64
64
  1. **Scope**: identify repo, task, role audience, workflow audience, path scope, evidence ids, and user-local OKF target scope.
65
- 2. **Minimize**: remove raw prompts, raw logs, source dumps, secrets, personal data, internal links, and one-off noise.
65
+ 2. **Minimize**: remove credentials, wholesale prompts/logs/source/command output, and one-off noise. Preserve private facts or short excerpts only when needed to keep the knowledge accurate and reusable.
66
66
  3. **Analyze execution structure**: when event evidence is present, identify task slices, owner roles, dependencies, key tool calls, skill invocations, subagent lifecycle events, verification gates, and merge outcomes.
67
67
  4. **Extract atomic candidates**: one claim per candidate. Allowed `kind` values are `rule`, `decision`, `pattern`, `anti-pattern`, `warning`, `checklist`, `concept`, `workflow-improvement`, `task-split-improvement`, `tool-use-improvement`, `skill-improvement`, `repo-asset-suggestion`, `role-agent-suggestion`, `team-suggestion`, `eval-set`, and `open-question`.
68
68
  5. **Separate fact from inference**: mark whether the candidate is directly evidenced or inferred from evidence.
69
69
  6. **Classify**: add `okfType`, `targetPath`, `stableKey`, `roleTags`, `repoTags`, `workflowTags`, `pathScopes`, `domainTags`, `stability`, `sensitivity`, and `targetStore`.
70
70
  7. **Score**: estimate evidence strength, reuse value, actionability, stability, novelty, privacy risk, and duplication risk.
71
71
  8. **Pair improvements with evals**: every proposed skill, role-agent, team, workflow, routing, tool-use, or subagent behavior change should include an `evoEvalSets` entry, unless the plan explains why eval coverage is not applicable.
72
- 9. **Privacy gate**: defer candidates that require raw private content to remain meaningful.
72
+ 9. **Privacy gate**: reject credentials and unbounded raw copying. Private facts, internal links, local paths, emails, and short excerpts may remain in local-private knowledge when necessary.
73
73
  10. **Route**: emit `no_write`, `create`, `update`, `skip`, or `needs-human` for each candidate.
74
74
  11. **Plan OKF organization**: provide canonical concept targets and repo/role/workflow overlay updates. Do not emit final OKF files.
75
75
  12. **Recovery notes**: explain conflicts, stale information, required repo fact checks, and why human intervention is needed when applicable.
@@ -197,6 +197,8 @@ Return executable JSON with `schemaVersion: 1` and `kind: "knowledge-distillatio
197
197
 
198
198
  Required candidate fields are `id`, `decision`, `kind`, `okfType`, `targetStore`, `targetPath`, `stableKey`, `confidence`, `title`, `description`, `claim`, `basis`, `metadataOnlyEvidence`, `howToApply`, `antiCriteria`, `roleTags`, `repoTags`, `workflowTags`, `pathScopes`, `relatedConceptLinks`, `overlayUpdates`, `scores`, `decisionReason`, `evidenceRefs`, `reviewState`, `bodySections`, and `privacyCheck`.
199
199
 
200
+ All raw-content and credential privacy flags must remain `false`. `internalLinksStored` is instead a disclosure flag: set it to `true` when the durable candidate retains an HTTP(S) link. EvoDev recomputes this field locally before persistence.
201
+
200
202
  Valid decisions are `auto-accept`, `create`, `update`, `needs-human`, `skip`, and canonical `no_write`. `no-write` may be normalized by the runtime but new output should emit `no_write`. Active writes (`auto-accept`, `create`, `update`) must target `okf`, use reviewState `auto-accepted` or `accepted`, use metadata-only evidence, include evidence refs, have a safe relative `.md` target path outside reserved `index.md` and `log.md`, and include verification or `verificationNotApplicableReason`.
201
203
 
202
204
  Behavior-changing active writes, including skill, role-agent, team, workflow, routing, tool-use, task-split, and subagent changes, must include `evalSetRefs` that point to provided `evoEvalSets`. Eval sets must be metadata-only and use privacy flags `usesRawPrompt: false`, `usesSourceDump: false`, and `usesRawCommandOutput: false`.