omegon 0.8.1 → 0.8.3

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.
@@ -194,21 +194,22 @@ export const PATTERNS: Record<string, PatternDefinition> = {
194
194
  modifiersDefault: ["breaking_changes"],
195
195
  splitStrategy: ["New versioned endpoint", "Deprecation + dual-support", "Client migration"],
196
196
  },
197
- simple_refactor: {
198
- name: "Simple Refactor",
199
- description: "Code cleanup, renaming, or structural changes without functional modifications",
197
+ refactor: {
198
+ name: "Refactor",
199
+ description: "Code cleanup, renaming, restructuring, or replacing implementations while preserving behavior",
200
200
  keywords: [
201
201
  "refactor", "rename", "reorganize", "cleanup", "extract", "inline", "move",
202
- "restructure", "mechanical", "no functional",
202
+ "restructure", "mechanical", "no functional", "replace", "rewrite",
203
+ "swap", "substitute", "modernize",
203
204
  ],
204
- requiredAny: ["refactor", "rename", "cleanup", "extract", "reorganize"],
205
+ requiredAny: ["refactor", "rename", "cleanup", "extract", "reorganize", "replace", "rewrite", "swap", "substitute"],
205
206
  expectedComponents: {
206
- operation: ["rename", "extract", "inline", "move", "reorganize"],
207
- scope: ["function", "class", "file", "module", "component", "method"],
207
+ operation: ["rename", "extract", "inline", "move", "reorganize", "replace", "rewrite", "swap", "substitute"],
208
+ scope: ["function", "class", "file", "module", "component", "method", "implementation", "approach", "library", "framework", "pattern"],
208
209
  },
209
210
  systemsBase: 1,
210
211
  modifiersDefault: [],
211
- splitStrategy: ["By module/scope", "Update tests"],
212
+ splitStrategy: ["Implement replacement / perform refactor", "Update call sites + tests", "Remove old implementation"],
212
213
  },
213
214
  bug_fix: {
214
215
  name: "Bug Fix",
@@ -282,21 +283,27 @@ export const PATTERNS: Record<string, PatternDefinition> = {
282
283
  modifiersDefault: ["state_coordination"],
283
284
  splitStrategy: ["Core data model", "Rendering/UI layer", "Event handling + state management", "Application shell"],
284
285
  },
285
- refactor: {
286
- name: "Refactor",
287
- description: "Replace or rewrite implementation while preserving behavior",
286
+ infrastructure_tooling: {
287
+ name: "Infrastructure & Tooling",
288
+ description: "Extension development, CLI tooling, internal infrastructure, build systems, or agentic framework work",
288
289
  keywords: [
289
- "refactor", "replace", "rewrite", "improve", "clean", "restructure", "reorganize",
290
- "swap", "substitute", "modernize",
290
+ "extension", "plugin", "tool", "command", "cli", "config", "configuration",
291
+ "pipeline", "build", "deploy", "ci", "cd", "workflow", "script", "hook",
292
+ "provider", "routing", "dispatch", "handler", "registry", "loader",
293
+ "prompt", "agent", "inference",
294
+ "dashboard", "diagnostic", "telemetry", "logging",
295
+ "skill", "template", "scaffold", "generate", "emit",
291
296
  ],
292
- requiredAny: ["replace", "rewrite", "swap", "substitute", "refactor"],
297
+ requiredAny: ["extension", "plugin", "provider", "routing",
298
+ "dispatch", "registry", "agent", "skill"],
293
299
  expectedComponents: {
294
- operation: ["replace", "rewrite", "swap", "substitute"],
295
- target: ["implementation", "approach", "library", "framework", "pattern"],
300
+ core: ["extension", "plugin", "tool", "command", "handler", "provider", "registry"],
301
+ integration: ["config", "pipeline", "workflow", "routing", "dispatch", "loader"],
302
+ surface: ["cli", "dashboard", "status", "prompt", "template", "diagnostic"],
296
303
  },
297
- systemsBase: 1.0,
304
+ systemsBase: 1,
298
305
  modifiersDefault: [],
299
- splitStrategy: ["Implement replacement", "Update call sites", "Remove old implementation"],
306
+ splitStrategy: ["Core implementation", "Integration + wiring", "Tests + documentation"],
300
307
  },
301
308
  };
302
309
 
@@ -462,11 +469,18 @@ export function detectModifiers(directive: string): string[] {
462
469
  }
463
470
 
464
471
  /**
465
- * Calculate complexity: (1 + systems) × (1 + 0.5 × modifiers).
472
+ * Calculate complexity: systems × (1 + 0.5 × modifiers).
473
+ *
474
+ * Previous formula was (1 + systems) which gave a floor of 2.0 even for
475
+ * single-system, zero-modifier directives — making the heuristic path
476
+ * always exceed the default threshold of 2.0. Using bare `systems`
477
+ * allows trivial directives (1 system, 0 modifiers) to score 1.0,
478
+ * so they correctly get `needs_assessment` instead of `cleave`.
479
+ *
466
480
  * Capped at 100.0.
467
481
  */
468
482
  export function calculateComplexity(systems: number, modifiers: string[]): number {
469
- const raw = (1 + systems) * (1 + 0.5 * modifiers.length);
483
+ const raw = systems * (1 + 0.5 * modifiers.length);
470
484
  return Math.round(Math.min(raw, 100.0) * 10) / 10;
471
485
  }
472
486
 
@@ -536,7 +550,7 @@ export function assessDirective(
536
550
  reasoning:
537
551
  `Pattern '${match.name}' matched with ${(match.confidence * 100).toFixed(0)}% confidence. ` +
538
552
  `Systems: ${systemsForDisplay}, Modifiers: ${allModifiers.length}. ` +
539
- `Formula: (1 + ${systemsForCalc}) × (1 + 0.5 × ${allModifiers.length}) = ${complexity}. ` +
553
+ `Formula: ${systemsForCalc} × (1 + 0.5 × ${allModifiers.length}) = ${complexity}. ` +
540
554
  `Effective (validate=${validate}): ${effComplexity}`,
541
555
  skipInterrogation: false,
542
556
  };
@@ -545,7 +559,7 @@ export function assessDirective(
545
559
  if (
546
560
  match.confidence >= 0.90 &&
547
561
  effComplexity <= threshold &&
548
- match.name === "Simple Refactor"
562
+ match.name === "Refactor"
549
563
  ) {
550
564
  result.skipInterrogation = true;
551
565
  }
@@ -347,6 +347,50 @@ async function checkpointRelatedChanges(
347
347
  }
348
348
  }
349
349
 
350
+ /**
351
+ * Maximum number of interactive preflight attempts before bailing.
352
+ * Prevents infinite loops when the agent repeatedly picks actions
353
+ * that don't resolve the dirty tree.
354
+ */
355
+ const MAX_PREFLIGHT_ATTEMPTS = 3;
356
+
357
+ /**
358
+ * Verify the tree is clean after an action. If only volatile files remain,
359
+ * auto-stash them. Returns true if the tree is resolved, false if dirty
360
+ * files remain.
361
+ */
362
+ async function verifyCleanAfterAction(
363
+ pi: ExtensionAPI,
364
+ repoPath: string,
365
+ changeName: string | null,
366
+ openspecContext: OpenSpecContext | null,
367
+ onUpdate?: AgentToolUpdateCallback<Record<string, unknown>>,
368
+ ): Promise<{ clean: boolean; classification: WorkspaceDirtyTreeClassification | null }> {
369
+ const postStatus = await pi.exec("git", ["status", "--porcelain"], {
370
+ cwd: repoPath,
371
+ timeout: 5_000,
372
+ });
373
+ const postState = inspectGitState(postStatus.stdout);
374
+ if (postState.entries.length === 0) return { clean: true, classification: null };
375
+
376
+ const postClassification = classifyPreflightDirtyPaths(
377
+ postState.entries.map((e) => e.path),
378
+ { changeName, openspecContext },
379
+ );
380
+
381
+ // If only volatile files remain, auto-stash and treat as clean.
382
+ if (postState.nonVolatile.length === 0 && postClassification.volatile.length > 0) {
383
+ await stashPaths(pi, repoPath, "cleave-preflight-volatile", postClassification.volatile);
384
+ onUpdate?.({
385
+ content: [{ type: "text", text: "Remaining volatile artifacts stashed automatically — tree is clean." }],
386
+ details: { phase: "preflight", autoResolved: "volatile_only_stash" },
387
+ });
388
+ return { clean: true, classification: null };
389
+ }
390
+
391
+ return { clean: false, classification: postClassification };
392
+ }
393
+
350
394
  export async function runDirtyTreePreflight(pi: ExtensionAPI, options: DirtyTreePreflightOptions): Promise<"continue" | "skip_cleave" | "cancelled"> {
351
395
  const status = await pi.exec("git", ["status", "--porcelain"], {
352
396
  cwd: options.repoPath,
@@ -369,12 +413,11 @@ export async function runDirtyTreePreflight(pi: ExtensionAPI, options: DirtyTree
369
413
  changeName,
370
414
  openspecContext,
371
415
  });
372
- // Compute initial checkpoint plan for the summary display only.
373
- // The plan is rebuilt from currentClassification inside the loop on each attempt (C4).
374
416
  const initialCheckpointPlan = buildCheckpointPlan(classification, { changeName, openspecContext });
375
417
  const summary = formatDirtyTreeSummary(classification, initialCheckpointPlan.message);
376
418
  options.onUpdate?.({ content: [{ type: "text", text: summary }], details: { phase: "preflight" } });
377
419
 
420
+ // Auto-resolve: volatile-only dirty tree — stash and continue without prompting.
378
421
  if (gitState.nonVolatile.length === 0) {
379
422
  if (classification.volatile.length > 0) {
380
423
  await stashPaths(pi, options.repoPath, "cleave-preflight-volatile", classification.volatile);
@@ -392,86 +435,88 @@ export async function runDirtyTreePreflight(pi: ExtensionAPI, options: DirtyTree
392
435
  throw new Error(summary + "\n\nInteractive input is unavailable, so cleave cannot resolve the dirty tree automatically.");
393
436
  }
394
437
 
395
- // Mutable classification — refreshed after each checkpoint attempt (C1/W1).
438
+ // Mutable classification — refreshed after each action attempt.
396
439
  let currentClassification = classification;
440
+ let attempts = 0;
441
+
442
+ while (attempts < MAX_PREFLIGHT_ATTEMPTS) {
443
+ attempts++;
397
444
 
398
- while (true) {
399
445
  let answer: string | undefined;
400
446
  if (hasSelect) {
401
- // Preferred path: show a modal select with labelled options and descriptions.
402
447
  const selected = await options.ui!.select!(
403
448
  "Dirty tree detected — choose a preflight action to proceed:",
404
449
  [...PREFLIGHT_ACTION_OPTIONS],
405
450
  );
406
451
  answer = parsePreflightAction(selected);
407
452
  } else {
408
- // Fallback: raw text input (headless / test environments).
409
453
  answer = normalizePreflightInput(
410
454
  await options.ui!.input!("Dirty tree action [checkpoint|stash-unrelated|stash-volatile|proceed-without-cleave|cancel]:"),
411
455
  )?.toLowerCase();
412
456
  }
457
+
413
458
  try {
414
459
  switch (answer) {
415
460
  case "checkpoint": {
416
- // Rebuild the checkpoint plan from the current (possibly refreshed) classification (C4).
417
461
  const currentCheckpointPlan = buildCheckpointPlan(currentClassification, { changeName, openspecContext });
418
- const committedFiles = new Set(currentClassification.checkpointFiles);
419
462
  await checkpointRelatedChanges(pi, options.repoPath, currentClassification, currentCheckpointPlan.message, options.ui);
420
- // Re-verify cleanliness after the checkpoint commit.
421
- const postCheckpointStatus = await pi.exec("git", ["status", "--porcelain"], {
422
- cwd: options.repoPath,
423
- timeout: 5_000,
424
- });
425
- const postState = inspectGitState(postCheckpointStatus.stdout);
426
- if (postState.entries.length === 0) {
427
- // Tree is clean — checkpoint fully resolved the dirty tree.
428
- return "continue";
429
- }
430
463
 
431
- // Re-derive classification from the post-checkpoint state (C1).
432
- currentClassification = classifyPreflightDirtyPaths(
433
- postState.entries.map((e) => e.path),
434
- { changeName, openspecContext },
464
+ const { clean, classification: postClassification } = await verifyCleanAfterAction(
465
+ pi, options.repoPath, changeName, openspecContext, options.onUpdate,
435
466
  );
436
-
437
- // C2: If only volatile files remain, auto-stash and continue.
438
- if (postState.nonVolatile.length === 0 && currentClassification.volatile.length > 0) {
439
- await stashPaths(pi, options.repoPath, "cleave-preflight-volatile", currentClassification.volatile);
440
- options.onUpdate?.({
441
- content: [{ type: "text", text: "Checkpoint succeeded. Remaining volatile artifacts stashed automatically — cleave continuing." }],
442
- details: { phase: "preflight", autoResolved: "volatile_only_stash" },
443
- });
444
- return "continue";
445
- }
446
-
447
- // Remaining dirty files — emit precise diagnosis (W1: distinguish committed-but-still-dirty vs excluded-from-scope).
448
- const remainingPaths = postState.entries.map((e) => e.path);
449
- const diagnosisLines = [
450
- "Checkpoint committed successfully, but dirty files remain — cleave cannot continue yet:",
451
- ...currentClassification.related.map((f) =>
452
- committedFiles.has(f.path)
453
- ? ` • ${f.path} [was committed but remains dirty — file may have been modified after staging or only partially staged]`
454
- : ` • ${f.path} [related but excluded from checkpoint scope — confidence too low to commit automatically]`
455
- ),
456
- ...currentClassification.unrelated.map((f) => ` • ${f.path} [unrelated: ${f.reason}]`),
457
- ...currentClassification.unknown.map((f) => ` • ${f.path} [unknown — not in change scope, was not checkpointed]`),
458
- ...currentClassification.volatile.map((f) => ` • ${f.path} [volatile artifact — will be auto-stashed]`),
459
- "",
460
- "Choose another preflight action to resolve the remaining files.",
467
+ if (clean) return "continue";
468
+
469
+ // Dirty files remain after checkpoint update classification and show diagnosis.
470
+ currentClassification = postClassification!;
471
+ const remainingPaths = [
472
+ ...currentClassification.related,
473
+ ...currentClassification.unrelated,
474
+ ...currentClassification.unknown,
475
+ ...currentClassification.volatile,
461
476
  ];
462
477
  options.onUpdate?.({
463
- content: [{ type: "text", text: diagnosisLines.join("\n") }],
464
- details: { phase: "preflight", postCheckpointDirty: remainingPaths },
478
+ content: [{ type: "text", text:
479
+ "Checkpoint committed, but dirty files remain:\n" +
480
+ remainingPaths.map((f) => ` • ${f.path} [${f.reason}]`).join("\n") +
481
+ "\n\nChoose another action to resolve.",
482
+ }],
483
+ details: { phase: "preflight", postCheckpointDirty: remainingPaths.map((f) => f.path) },
465
484
  });
466
485
  break;
467
486
  }
468
- case "stash-unrelated":
469
- // C1: Use currentClassification (refreshed after checkpoint) not the stale original.
470
- await stashPaths(pi, options.repoPath, "cleave-preflight-unrelated", [...currentClassification.unrelated, ...currentClassification.unknown]);
471
- return "continue";
472
- case "stash-volatile":
487
+ case "stash-unrelated": {
488
+ const toStash = [...currentClassification.unrelated, ...currentClassification.unknown];
489
+ if (toStash.length === 0) {
490
+ options.onUpdate?.({
491
+ content: [{ type: "text", text: "No unrelated or unknown files to stash. Choose a different action." }],
492
+ details: { phase: "preflight" },
493
+ });
494
+ break;
495
+ }
496
+ await stashPaths(pi, options.repoPath, "cleave-preflight-unrelated", toStash);
497
+ const { clean, classification: postClassification } = await verifyCleanAfterAction(
498
+ pi, options.repoPath, changeName, openspecContext, options.onUpdate,
499
+ );
500
+ if (clean) return "continue";
501
+ currentClassification = postClassification!;
502
+ break;
503
+ }
504
+ case "stash-volatile": {
505
+ if (currentClassification.volatile.length === 0) {
506
+ options.onUpdate?.({
507
+ content: [{ type: "text", text: "No volatile files to stash. Choose a different action." }],
508
+ details: { phase: "preflight" },
509
+ });
510
+ break;
511
+ }
473
512
  await stashPaths(pi, options.repoPath, "cleave-preflight-volatile", currentClassification.volatile);
474
- return "continue";
513
+ const { clean, classification: postClassification } = await verifyCleanAfterAction(
514
+ pi, options.repoPath, changeName, openspecContext, options.onUpdate,
515
+ );
516
+ if (clean) return "continue";
517
+ currentClassification = postClassification!;
518
+ break;
519
+ }
475
520
  case "proceed-without-cleave":
476
521
  return "skip_cleave";
477
522
  case "cancel":
@@ -479,7 +524,7 @@ export async function runDirtyTreePreflight(pi: ExtensionAPI, options: DirtyTree
479
524
  return "cancelled";
480
525
  default:
481
526
  options.onUpdate?.({
482
- content: [{ type: "text", text: "Invalid preflight action. Choose checkpoint, stash-unrelated, stash-volatile, proceed-without-cleave, or cancel." }],
527
+ content: [{ type: "text", text: "Invalid action. Choose checkpoint, stash-unrelated, stash-volatile, proceed-without-cleave, or cancel." }],
483
528
  details: { phase: "preflight" },
484
529
  });
485
530
  }
@@ -491,6 +536,19 @@ export async function runDirtyTreePreflight(pi: ExtensionAPI, options: DirtyTree
491
536
  });
492
537
  }
493
538
  }
539
+
540
+ // Exhausted attempts — report remaining dirty files and bail.
541
+ const remaining = [
542
+ ...currentClassification.related,
543
+ ...currentClassification.unrelated,
544
+ ...currentClassification.unknown,
545
+ ...currentClassification.volatile,
546
+ ];
547
+ throw new Error(
548
+ `Dirty tree not resolved after ${MAX_PREFLIGHT_ATTEMPTS} attempts. Remaining files:\n` +
549
+ remaining.map((f) => ` • ${f.path}`).join("\n") +
550
+ "\n\nResolve manually (git commit/stash/checkout) and retry /cleave.",
551
+ );
494
552
  }
495
553
 
496
554
  interface AssessExecutionContext {
@@ -1586,7 +1644,7 @@ export default function cleaveExtension(pi: ExtensionAPI) {
1586
1644
  "Every non-trivial code change must include tests in co-located *.test.ts files. Untested code is incomplete — do not commit without tests for new functions and changed behavior.",
1587
1645
  "Call cleave_assess before starting any multi-system or cross-cutting task to determine if decomposition is needed",
1588
1646
  "If decision is 'execute', proceed directly. If 'cleave', use /cleave to decompose. If 'needs_assessment', proceed directly — it means no pattern matched but the task is likely simple enough for in-session execution.",
1589
- "Complexity formula: (1 + systems) × (1 + 0.5 × modifiers). Threshold default: 2.0.",
1647
+ "Complexity formula: systems × (1 + 0.5 × modifiers). Threshold default: 2.0.",
1590
1648
  "The /assess command provides code assessment: `/assess cleave` (adversarial review + auto-fix), `/assess diff [ref]` (review only), `/assess spec [change]` (validate against OpenSpec scenarios), `/assess design [node-id]` (evaluate design-tree node readiness before set_status(decided)).",
1591
1649
  "When the repo has openspec/ with active changes, suggest `/assess spec` after implementation and before `/opsx:archive`.",
1592
1650
  "Run `/assess design <node-id>` before calling design_tree_update with set_status(decided) to verify acceptance criteria are satisfied.",
@@ -427,9 +427,13 @@ export function computeAssessmentSnapshot(repoPath: string, changeName: string):
427
427
  const gitHead = safeReadGit(repoPath, ["rev-parse", "HEAD"]);
428
428
  const dirty = detectGitDirty(repoPath, snapshotPaths);
429
429
 
430
+ // Fingerprint represents implementation file content — NOT git HEAD.
431
+ // Including gitHead would create a chicken-and-egg: committing the
432
+ // assessment.json (which lives in-repo) changes HEAD, which invalidates
433
+ // the fingerprint, making the assessment permanently stale.
434
+ // Git HEAD is stored separately in the snapshot for informational use.
430
435
  const fingerprintSeed = JSON.stringify({
431
436
  changeName,
432
- gitHead,
433
437
  dirty,
434
438
  files,
435
439
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omegon",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "Omegon — an opinionated distribution of pi (by Mario Zechner) with extensions for lifecycle management, memory, orchestration, and visualization",
5
5
  "bin": {
6
6
  "omegon": "bin/omegon.mjs",
@@ -58,7 +58,6 @@
58
58
  "./extensions/cleave",
59
59
  "./extensions/openspec",
60
60
  "./extensions/defaults.ts",
61
- "./extensions/distill.ts",
62
61
  "./extensions/render",
63
62
  "./extensions/local-inference",
64
63
  "./extensions/mcp-bridge",
@@ -91,15 +91,21 @@ This status is injected into the agent context (not just displayed).
91
91
  ## Complexity Formula
92
92
 
93
93
  ```
94
- complexity = (1 + systems) × (1 + 0.5 × modifiers)
94
+ complexity = systems × (1 + 0.5 × modifiers)
95
95
  effective = complexity + 1 (when validation enabled)
96
96
  ```
97
97
 
98
- ## Patterns (9)
98
+ The formula uses bare `systems` (not `1 + systems`) so that single-system,
99
+ zero-modifier directives score 1.0 (effective 2.0) — at the default threshold
100
+ of 2.0, they get `needs_assessment` rather than being falsely recommended
101
+ for decomposition.
102
+
103
+ ## Patterns (12)
99
104
 
100
105
  Full-Stack CRUD, Authentication System, External Service Integration,
101
106
  Database Migration, Performance Optimization, Breaking API Change,
102
- Simple Refactor, Bug Fix, Refactor.
107
+ Refactor, Bug Fix, Greenfield Project, Multi-Module Library,
108
+ Application Bootstrap, Infrastructure & Tooling.
103
109
 
104
110
  ## Adversarial Review Loop
105
111