omegon 0.8.2 → 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.
- package/extensions/cleave/index.ts +114 -56
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
432
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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:
|
|
464
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omegon",
|
|
3
|
-
"version": "0.8.
|
|
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",
|