lossless-openclaw-orchestrator 1.2.0-beta.2 → 1.2.0-beta.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.
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { mkdirSync, writeFileSync } from "node:fs";
4
4
  import { basename, dirname, isAbsolute } from "node:path";
5
+ import { PREPARED_CARD_STATES } from "../../core/src/index.js";
5
6
  export const DEFAULT_REQUIRED_TOOL_CALLS = [
6
7
  "loo_doctor",
7
8
  "loo_search_sessions",
@@ -40,6 +41,7 @@ export const DEFAULT_REQUIRED_TOOL_CALLS = [
40
41
  "loo_prepared_cards",
41
42
  "loo_prepared_inbox"
42
43
  ];
44
+ const PREPARED_CARD_STATE_SET = new Set(PREPARED_CARD_STATES);
43
45
  const AUTONOMY_TICK_SUMMARY_KEYS = [
44
46
  "totalLanes",
45
47
  "returnedSteps",
@@ -1214,7 +1216,7 @@ function summarizeInvocation(toolName, call, requestArgs = {}) {
1214
1216
  const state = stringPath(card, ["state"]);
1215
1217
  return !cardRef?.startsWith("prepared_card:")
1216
1218
  || !targetRef?.startsWith("codex_thread:")
1217
- || !["ready", "stale", "partial", "unknown"].includes(state ?? "");
1219
+ || !PREPARED_CARD_STATE_SET.has(state ?? "");
1218
1220
  }))
1219
1221
  blockers.push("prepared_cards_public_refs_invalid");
1220
1222
  }
@@ -14,6 +14,21 @@ function getDatabaseSync() {
14
14
  }
15
15
  return cachedDatabaseSync;
16
16
  }
17
+ export const PREPARED_CARD_STATES = [
18
+ "ready",
19
+ "stale",
20
+ "partial",
21
+ "unknown",
22
+ "completed",
23
+ "blocked_missing_info",
24
+ "waiting_approval",
25
+ "watching_external_check",
26
+ "needs_resume",
27
+ "dirty_worktree_handoff",
28
+ "stale_or_partial",
29
+ "ready_for_review",
30
+ "unknown_lifecycle"
31
+ ];
17
32
  const COLLABORATION_COCKPIT_INTERNAL_CARD_LIMIT = 5000;
18
33
  const OPERATING_SOURCE_KINDS = [
19
34
  "lco",
@@ -1581,6 +1596,7 @@ export function getPreparedCards(db, options = {}) {
1581
1596
  let stale = 0;
1582
1597
  let partial = 0;
1583
1598
  let unknown = 0;
1599
+ let completed = 0;
1584
1600
  let lowConfidence = 0;
1585
1601
  for (const row of rows) {
1586
1602
  const card = publicPreparedCardFromRow(row);
@@ -1588,12 +1604,14 @@ export function getPreparedCards(db, options = {}) {
1588
1604
  filteredUnsafeRows += 1;
1589
1605
  continue;
1590
1606
  }
1591
- if (card.stale)
1607
+ if (card.stale || card.state === "stale")
1592
1608
  stale += 1;
1593
- if (card.state === "partial")
1609
+ if (preparedCardCountsAsPartialSummary(card.state))
1594
1610
  partial += 1;
1595
- if (card.state === "unknown")
1611
+ if (card.state === "unknown" || card.state === "unknown_lifecycle")
1596
1612
  unknown += 1;
1613
+ if (card.state === "completed")
1614
+ completed += 1;
1597
1615
  if (card.confidence < 0.5)
1598
1616
  lowConfidence += 1;
1599
1617
  if (validCards.length < limit)
@@ -1631,6 +1649,7 @@ export function getPreparedCards(db, options = {}) {
1631
1649
  stale,
1632
1650
  partial,
1633
1651
  unknown,
1652
+ completed,
1634
1653
  lowConfidence
1635
1654
  },
1636
1655
  cards: validCards,
@@ -1929,9 +1948,19 @@ function buildPreparedCardDraft(db, threadId, leaves, filteredUnsafeRows) {
1929
1948
  const watcherObservationsStatus = watcherObservationCoverageForTarget(db, targetRef);
1930
1949
  const freshnessAt = latestIso([...leaves.map((leaf) => leaf.freshnessAt), session?.updatedAt ?? null]);
1931
1950
  const stale = leaves.some((leaf) => leaf.stale);
1951
+ const evidenceState = stale
1952
+ ? "stale_or_partial"
1953
+ : summaryLeavesStatus === "unknown"
1954
+ ? "unknown_lifecycle"
1955
+ : summaryLeavesStatus === "partial" || sessionMetadataStatus === "partial"
1956
+ ? "stale_or_partial"
1957
+ : "ready";
1958
+ const lifecycle = preparedLifecycleFromMetadata(metadata, evidenceState);
1959
+ const state = lifecycle.state;
1932
1960
  const reasonCodes = unique([
1933
1961
  leaves.length > 0 ? "summary_leaves_ready" : "summary_leaves_missing",
1934
1962
  "metadata_only",
1963
+ ...lifecycle.reasonCodes,
1935
1964
  watcherObservationsStatus === "not_configured" ? "watcher_not_configured" : "watcher_observations_available",
1936
1965
  stale ? "stale_cache" : "",
1937
1966
  summaryLeavesStatus === "partial" ? "authority_partial" : "",
@@ -1941,17 +1970,10 @@ function buildPreparedCardDraft(db, threadId, leaves, filteredUnsafeRows) {
1941
1970
  watcherObservationsStatus === "unknown" ? "watcher_observations_unknown" : "",
1942
1971
  filteredUnsafeRows > 0 ? "filtered_unsafe_rows" : ""
1943
1972
  ].filter(Boolean));
1944
- const state = stale
1945
- ? "stale"
1946
- : summaryLeavesStatus === "unknown"
1947
- ? "unknown"
1948
- : summaryLeavesStatus === "partial" || sessionMetadataStatus === "partial"
1949
- ? "partial"
1950
- : "ready";
1951
1973
  const averageLeafConfidence = leaves.length
1952
1974
  ? leaves.reduce((sum, leaf) => sum + leaf.confidence, 0) / leaves.length
1953
1975
  : 0.3;
1954
- const confidence = preparedCardConfidence(averageLeafConfidence, state);
1976
+ const confidence = preparedCardConfidence(averageLeafConfidence, state, evidenceState);
1955
1977
  const sourceRangeRefsFull = unique(leaves.flatMap((leaf) => leaf.sourceRangeRefs));
1956
1978
  const sourceRangeRefs = sourceRangeRefsFull.slice(0, PREPARED_CARD_SOURCE_RANGE_REF_LIMIT);
1957
1979
  const sourceRangeRefsOmitted = Math.max(0, sourceRangeRefsFull.length - sourceRangeRefs.length)
@@ -1965,7 +1987,7 @@ function buildPreparedCardDraft(db, threadId, leaves, filteredUnsafeRows) {
1965
1987
  role: "title"
1966
1988
  });
1967
1989
  const title = looksSensitiveRefLike(cleanedTitle.text) ? publicSafeText(safeThreadId(threadId), 160) : cleanedTitle.text;
1968
- const summaryText = publicSafeText(`Prepared state: ${leaves.length} summary leaf${leaves.length === 1 ? "" : "s"} across ${Math.max(0, leafRangeCount)} prepared source range${leafRangeCount === 1 ? "" : "s"}.`, 320);
1990
+ const summaryText = publicSafeText(`Prepared state: ${leaves.length} summary leaf${leaves.length === 1 ? "" : "s"} across ${Math.max(0, leafRangeCount)} prepared source range${leafRangeCount === 1 ? "" : "s"}. Lifecycle: ${preparedCardStateLabel(state)}.`, 320);
1969
1991
  const nextAction = preparedCardNextAction(state);
1970
1992
  const authorityCoverage = {
1971
1993
  summaryLeaves: {
@@ -1985,6 +2007,10 @@ function buildPreparedCardDraft(db, threadId, leaves, filteredUnsafeRows) {
1985
2007
  leafRefs: leaves.map((leaf) => leaf.leafRef),
1986
2008
  inputHashes: leaves.map((leaf) => leaf.inputHash),
1987
2009
  outputHashes: leaves.map((leaf) => leaf.outputHash),
2010
+ metadataSignalHash: lifecycle.metadataSignalHash,
2011
+ lifecycleState: state,
2012
+ lifecycleReasonCodes: lifecycle.reasonCodes,
2013
+ evidenceState,
1988
2014
  freshnessAt,
1989
2015
  stale,
1990
2016
  summaryLeavesStatus,
@@ -2020,7 +2046,7 @@ function buildPreparedCardDraft(db, threadId, leaves, filteredUnsafeRows) {
2020
2046
  function preparedInboxItemFromCard(card) {
2021
2047
  const reasonCodes = unique([
2022
2048
  ...card.reasonCodes,
2023
- card.state === "ready" ? "prepared_card_ready" : "needs_attention"
2049
+ card.state === "ready" ? "prepared_card_ready" : card.state === "completed" ? "prepared_card_completed" : "needs_attention"
2024
2050
  ]);
2025
2051
  const urgencyScore = preparedInboxUrgencyScore(card, reasonCodes);
2026
2052
  return {
@@ -2289,7 +2315,7 @@ function getPreparedTargetCoverage(db, threadId) {
2289
2315
  preparedSourceEvents: preparedSourceEvents.count > 0 ? "ok" : "not_configured",
2290
2316
  preparedSourceRanges: preparedSourceRanges.publicCount > 0 ? "ok" : preparedSourceRanges.rawCount > 0 ? "partial" : "not_configured",
2291
2317
  summaryLeaves: summaryLeaves.publicCount > 0 ? "ok" : summaryLeaves.rawCount > 0 ? "partial" : "not_configured",
2292
- preparedCards: publicCard ? publicCard.state === "ready" && !publicCard.stale ? "ok" : "partial" : preparedCards > 0 ? "partial" : "not_configured",
2318
+ preparedCards: publicCard ? preparedTargetCardCoverage(publicCard) : preparedCards > 0 ? "partial" : "not_configured",
2293
2319
  preparedInboxItems: publicCard && preparedInboxItems.publicCount > 0 ? "ok" : preparedInboxItems.rawCount > 0 ? "partial" : "not_configured",
2294
2320
  watcherObservations: watcherObservationCoverageForTarget(db, targetRef)
2295
2321
  };
@@ -2314,8 +2340,7 @@ function getPreparedTargetCoverage(db, threadId) {
2314
2340
  const partialDerivedCache = requiredCoverage.some((state) => state === "partial");
2315
2341
  const allDerivedLayersMissing = requiredCoverage.every((state) => state === "not_configured");
2316
2342
  const stale = Boolean(session && (missingDerivedCache
2317
- || publicCard?.stale
2318
- || publicCard?.state !== "ready"
2343
+ || (publicCard ? preparedTargetCardCoverage(publicCard) !== "ok" : false)
2319
2344
  || summaryLeaves.staleCount > 0
2320
2345
  || (sourceUpdatedAt && preparedFreshnessAt && sourceUpdatedAt > preparedFreshnessAt)));
2321
2346
  const anyDerivedRows = preparedSourceEvents.count + preparedSourceRanges.rawCount + summaryLeaves.rawCount + preparedCards + preparedInboxItems.rawCount > 0;
@@ -2353,6 +2378,18 @@ function getPreparedTargetCoverage(db, threadId) {
2353
2378
  nextAction: preparedTargetNextAction(status)
2354
2379
  };
2355
2380
  }
2381
+ function preparedTargetCardCoverage(card) {
2382
+ if (card.stale)
2383
+ return "partial";
2384
+ return preparedCardStateHasFreshTargetCoverage(card.state) ? "ok" : "partial";
2385
+ }
2386
+ function preparedCardStateHasFreshTargetCoverage(state) {
2387
+ return state !== "stale"
2388
+ && state !== "partial"
2389
+ && state !== "stale_or_partial"
2390
+ && state !== "unknown"
2391
+ && state !== "unknown_lifecycle";
2392
+ }
2356
2393
  function countPreparedTargetRows(db, sql, ...params) {
2357
2394
  return Number(db.prepare(sql).get(...params)?.count ?? 0);
2358
2395
  }
@@ -2545,7 +2582,7 @@ function preparedCoverageState(value) {
2545
2582
  return value === "ok" || value === "partial" || value === "not_configured" || value === "unknown" ? value : "unknown";
2546
2583
  }
2547
2584
  function isPreparedCardState(value) {
2548
- return value === "ready" || value === "stale" || value === "partial" || value === "unknown";
2585
+ return PREPARED_CARD_STATES.includes(value);
2549
2586
  }
2550
2587
  function isPublicPreparedSourceRef(value) {
2551
2588
  if (value.startsWith("codex_thread:"))
@@ -2569,7 +2606,125 @@ function isPublicCodexSubagentResultRef(value) {
2569
2606
  }
2570
2607
  return /^[A-Za-z0-9._:-]{1,160}$/.test(decodedId) && !looksSensitiveRefLike(decodedId);
2571
2608
  }
2572
- function preparedCardConfidence(averageLeafConfidence, state) {
2609
+ function preparedLifecycleFromMetadata(metadata, evidenceState) {
2610
+ const signals = {
2611
+ status: normalizedMetadataValue(metadata.status),
2612
+ blocker: normalizedMetadataValue(metadata.blocker),
2613
+ nextAction: normalizedMetadataValue(metadata.nextAction),
2614
+ closeoutState: normalizedMetadataValue(metadata.closeoutState),
2615
+ planCompletionState: normalizedMetadataValue(metadata.planCompletionState)
2616
+ };
2617
+ const metadataSignalHash = stableId(JSON.stringify(signals));
2618
+ const nonBlockerText = [signals.status, signals.nextAction, signals.closeoutState, signals.planCompletionState].filter(Boolean).join(" ");
2619
+ const text = [nonBlockerText, signals.blocker].filter(Boolean).join(" ");
2620
+ const sourceReasonCodes = Object.entries(signals)
2621
+ .filter(([, value]) => value.length > 0)
2622
+ .map(([field]) => `lifecycle_signal:${field.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`)}`);
2623
+ const completedByStatus = lifecycleCompletionLike(signals.status);
2624
+ const completedByCloseoutAndPlan = lifecycleCompletionLike(signals.closeoutState)
2625
+ && lifecycleCompletionLike(signals.planCompletionState);
2626
+ const completed = completedByStatus || completedByCloseoutAndPlan;
2627
+ const dirtyHandoff = /\b(?:dirty[-_ ]?worktree|uncommitted|worktree[-_ ]?handoff|dirty[-_ ]?handoff|handoff[-_ ]?dirty|cleanup[-_ ]?required)\b/.test(text);
2628
+ const waitingApproval = /\b(?:waiting[-_ ]?(?:for[-_ ]?)?approval|needs[-_ ]?approval|requires[-_ ]?approval|approval[-_ ]?(?:required|needed|pending)|pending[-_ ]?approval|approval[-_ ]?gate|do[-_ ]?not[-_ ]?execute[-_ ]?without[-_ ]?explicit[-_ ]?approval)\b/.test(text);
2629
+ const needsResume = /\b(?:needs[-_ ]?(?:to[-_ ]?)?resume|resume[-_ ]?(?:session|thread|work|run|lane|needed|required|requested|request)|continue[-_ ]?(?:session|thread|lane)|nudge[-_ ]?(?:session|thread|agent|lane)|rejoin[-_ ]?(?:session|thread|lane))\b/.test(text);
2630
+ const watchingExternalCheck = /\b(?:(?:watch|watching|monitor|monitoring)[-_ ]+(?:ci|checks?|codeql|coderabbit|deploy(?:[-_ ]?check)?|external[-_ ]?(?:check|review)|review[-_ ]?check)|(?:waiting[-_ ]?(?:on|for)|pending)[-_ ]+(?:ci|checks?|codeql|coderabbit|deploy|external[-_ ]?check|review[-_ ]?check)|(?:ci|checks?|codeql|coderabbit|deploy[-_ ]?check|review[-_ ]?check)[-_ ]+(?:pending|queued|running|failing|failed|blocked|red|not[-_ ]?green|in[-_ ]?progress))\b/.test(text);
2631
+ const readyForReview = /\b(?:ready[-_ ]?for[-_ ]?review|review[-_ ]?ready|ready[-_ ]?to[-_ ]?review|pr[-_ ]?ready|needs[-_ ]?review)\b/.test(text);
2632
+ const negatedBlocker = /\b(?:not[-_ ]?blocked|no[-_ ]?blocker|unblocked|without[-_ ]?blocker|blocker[-_ ]?(?:none|na|n[-_ ]?a))\b/.test(text);
2633
+ const missingInfoSignal = /\b(?:missing[-_ ]?(?:info|input|context)|needs[-_ ]?(?:info|input|context)|waiting[-_ ]?(?:on|for)[-_ ]?(?:user|operator|human|input|context)|cannot[-_ ]?proceed)\b/.test(nonBlockerText);
2634
+ const blockedSignal = !negatedBlocker && /\b(?:blocked|blocker)\b/.test(nonBlockerText);
2635
+ const blockedMissingInfo = !waitingApproval
2636
+ && !dirtyHandoff
2637
+ && !watchingExternalCheck
2638
+ && !negatedBlocker
2639
+ && (hasRealBlocker(metadata.blocker) || missingInfoSignal || blockedSignal);
2640
+ const matchedLifecycleStates = [
2641
+ completed ? "completed" : null,
2642
+ dirtyHandoff ? "dirty_worktree_handoff" : null,
2643
+ waitingApproval ? "waiting_approval" : null,
2644
+ blockedMissingInfo ? "blocked_missing_info" : null,
2645
+ needsResume ? "needs_resume" : null,
2646
+ watchingExternalCheck ? "watching_external_check" : null,
2647
+ readyForReview ? "ready_for_review" : null
2648
+ ].filter((state) => Boolean(state));
2649
+ const evidenceReasonCodes = evidenceState !== "ready" ? [`lifecycle:${evidenceState}`] : [];
2650
+ const conflict = completed && matchedLifecycleStates.some((state) => state !== "completed");
2651
+ if (conflict) {
2652
+ return {
2653
+ state: "unknown_lifecycle",
2654
+ reasonCodes: unique([
2655
+ "semantic_lifecycle",
2656
+ "lifecycle:unknown_lifecycle",
2657
+ "lifecycle_conflict",
2658
+ ...matchedLifecycleStates.map((state) => `lifecycle:${state}`),
2659
+ ...evidenceReasonCodes,
2660
+ ...sourceReasonCodes
2661
+ ]),
2662
+ metadataSignalHash
2663
+ };
2664
+ }
2665
+ const semanticState = dirtyHandoff
2666
+ ? "dirty_worktree_handoff"
2667
+ : waitingApproval
2668
+ ? "waiting_approval"
2669
+ : blockedMissingInfo
2670
+ ? "blocked_missing_info"
2671
+ : needsResume
2672
+ ? "needs_resume"
2673
+ : watchingExternalCheck
2674
+ ? "watching_external_check"
2675
+ : readyForReview
2676
+ ? "ready_for_review"
2677
+ : completed
2678
+ ? "completed"
2679
+ : null;
2680
+ if (semanticState === "completed" && evidenceState !== "ready") {
2681
+ return {
2682
+ state: evidenceState,
2683
+ reasonCodes: unique(["semantic_lifecycle", "lifecycle:completed", `lifecycle:${evidenceState}`, ...sourceReasonCodes]),
2684
+ metadataSignalHash
2685
+ };
2686
+ }
2687
+ if (semanticState) {
2688
+ return {
2689
+ state: semanticState,
2690
+ reasonCodes: unique(["semantic_lifecycle", `lifecycle:${semanticState}`, ...evidenceReasonCodes, ...sourceReasonCodes]),
2691
+ metadataSignalHash
2692
+ };
2693
+ }
2694
+ if (evidenceState !== "ready") {
2695
+ return {
2696
+ state: evidenceState,
2697
+ reasonCodes: unique(["semantic_lifecycle", `lifecycle:${evidenceState}`, "lifecycle_signal_missing", ...sourceReasonCodes]),
2698
+ metadataSignalHash
2699
+ };
2700
+ }
2701
+ return {
2702
+ state: "ready",
2703
+ reasonCodes: unique(["semantic_lifecycle", "lifecycle:unknown_lifecycle", "lifecycle_signal_missing"]),
2704
+ metadataSignalHash
2705
+ };
2706
+ }
2707
+ function lifecycleCompletionLike(value) {
2708
+ return ["complete", "completed", "done", "closed", "merged", "success", "successful", "succeeded", "passed"].includes(value);
2709
+ }
2710
+ function preparedCardStateLabel(state) {
2711
+ return {
2712
+ ready: "evidence ready, lifecycle unknown",
2713
+ stale: "stale evidence",
2714
+ partial: "partial evidence",
2715
+ unknown: "unknown evidence",
2716
+ completed: "completed",
2717
+ blocked_missing_info: "blocked missing info",
2718
+ waiting_approval: "waiting approval",
2719
+ watching_external_check: "watching external check",
2720
+ needs_resume: "needs resume",
2721
+ dirty_worktree_handoff: "dirty worktree handoff",
2722
+ stale_or_partial: "stale or partial",
2723
+ ready_for_review: "ready for review",
2724
+ unknown_lifecycle: "unknown lifecycle"
2725
+ }[state] ?? "unknown lifecycle";
2726
+ }
2727
+ function preparedCardConfidence(averageLeafConfidence, state, evidenceState = state) {
2573
2728
  let confidence = Number.isFinite(averageLeafConfidence) ? averageLeafConfidence : 0.3;
2574
2729
  if (state === "partial")
2575
2730
  confidence = Math.min(confidence, 0.49);
@@ -2577,6 +2732,14 @@ function preparedCardConfidence(averageLeafConfidence, state) {
2577
2732
  confidence = Math.min(confidence, 0.49);
2578
2733
  if (state === "unknown")
2579
2734
  confidence = Math.min(confidence, 0.44);
2735
+ if (state === "stale_or_partial")
2736
+ confidence = Math.min(confidence, 0.49);
2737
+ if (state === "unknown_lifecycle")
2738
+ confidence = Math.min(confidence, 0.44);
2739
+ if (evidenceState === "partial" || evidenceState === "stale" || evidenceState === "stale_or_partial")
2740
+ confidence = Math.min(confidence, 0.49);
2741
+ if (evidenceState === "unknown" || evidenceState === "unknown_lifecycle")
2742
+ confidence = Math.min(confidence, 0.44);
2580
2743
  return Math.max(0.2, Math.min(0.99, Number(confidence.toFixed(2))));
2581
2744
  }
2582
2745
  function preparedCardNextAction(state) {
@@ -2586,14 +2749,41 @@ function preparedCardNextAction(state) {
2586
2749
  return "Refresh the local prepared-state cache before treating this card as current.";
2587
2750
  if (state === "partial")
2588
2751
  return "Inspect source coverage and summary leaves before relying on this prepared card.";
2752
+ if (state === "completed")
2753
+ return "No action is needed unless bounded evidence contradicts the completed lifecycle state.";
2754
+ if (state === "blocked_missing_info")
2755
+ return "Inspect bounded evidence and resolve the missing input or blocker before resuming.";
2756
+ if (state === "waiting_approval")
2757
+ return "Inspect the bounded approval context; do not execute without explicit approval.";
2758
+ if (state === "watching_external_check")
2759
+ return "Refresh external check or watcher evidence before deciding the next action.";
2760
+ if (state === "needs_resume")
2761
+ return "Inspect bounded evidence and resume only through the approval-gated control path.";
2762
+ if (state === "dirty_worktree_handoff")
2763
+ return "Inspect handoff evidence and clean or transfer the dirty worktree deliberately.";
2764
+ if (state === "stale_or_partial")
2765
+ return "Refresh or inspect partial prepared-state evidence before relying on this lifecycle card.";
2766
+ if (state === "ready_for_review")
2767
+ return "Review bounded evidence and source refs before marking the lane complete.";
2768
+ if (state === "unknown_lifecycle")
2769
+ return "Inspect summary leaves and session metadata; lifecycle signals are missing or conflicting.";
2589
2770
  return "Inspect summary leaves and source coverage; missing or conflicting authority prevents a ready claim.";
2590
2771
  }
2591
2772
  function preparedInboxUrgencyScore(card, reasonCodes) {
2592
2773
  const stateScore = {
2593
2774
  unknown: 88,
2775
+ unknown_lifecycle: 60,
2594
2776
  stale: 82,
2777
+ stale_or_partial: 74,
2595
2778
  partial: 74,
2596
- ready: 42
2779
+ blocked_missing_info: 94,
2780
+ waiting_approval: 92,
2781
+ dirty_worktree_handoff: 89,
2782
+ watching_external_check: 78,
2783
+ needs_resume: 76,
2784
+ ready_for_review: 68,
2785
+ ready: 42,
2786
+ completed: 18
2597
2787
  };
2598
2788
  const codeScore = reasonCodes.reduce((score, code) => score + ({
2599
2789
  authority_unknown: 8,
@@ -2601,9 +2791,26 @@ function preparedInboxUrgencyScore(card, reasonCodes) {
2601
2791
  stale_cache: 8,
2602
2792
  low_confidence: 6,
2603
2793
  filtered_unsafe_rows: 5,
2604
- needs_attention: 4
2794
+ needs_attention: 4,
2795
+ "lifecycle_conflict": 10,
2796
+ "lifecycle:blocked_missing_info": 8,
2797
+ "lifecycle:waiting_approval": 7,
2798
+ "lifecycle:dirty_worktree_handoff": 7,
2799
+ "lifecycle:watching_external_check": 5,
2800
+ "lifecycle:needs_resume": 5
2605
2801
  }[code] ?? 0), 0);
2606
- return Math.max(0, Math.min(100, Number((stateScore[card.state] + codeScore + Math.round((1 - card.confidence) * 10)).toFixed(2))));
2802
+ const baseStateScore = stateScore[card.state] ?? 50;
2803
+ return Math.max(0, Math.min(100, Number((baseStateScore + codeScore + Math.round((1 - card.confidence) * 10)).toFixed(2))));
2804
+ }
2805
+ function preparedCardCountsAsPartialSummary(state) {
2806
+ return state === "partial"
2807
+ || state === "stale_or_partial"
2808
+ || state === "blocked_missing_info"
2809
+ || state === "waiting_approval"
2810
+ || state === "watching_external_check"
2811
+ || state === "needs_resume"
2812
+ || state === "dirty_worktree_handoff"
2813
+ || state === "ready_for_review";
2607
2814
  }
2608
2815
  function preparedCardReadActions() {
2609
2816
  return {
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import { basename } from "node:path";
3
- import { configuredLcmPeerDbPaths, createCloseoutEnvelopeReport, createAttentionInbox, createBusinessPulse, createCodexActiveThreadState, createCodexAutonomyTick, createCodexCollaborationNextSteps, createCodexCollaborationCockpit, createCodexRuntimeDesktopVisibilityStatus, describeSession, describeRecallRef, defaultCodexRoots, createIndexedSessionSanitizerReport, createIndexedSessionSanitizerRepairPlan, expandSession, expandQuery, expandSummaryLeaves, getPreparedCards, getPreparedInbox, getPreparedStateStatus, getWatcherEvents, createPlanStatePinsReport, createGithubOperatingItemsReport, createProjectDigest, createResumeRequestPacket, createWatcherStatusReport, createCodexDesktopCoherenceReport, createVisibleCodexSessionMap, getCockpitInbox, getCodexFinalMessages, getCodexPlans, getCodexSessionManagementMap, getCodexThreadMap, getCodexTouchedFiles, getCodexToolCalls, getRecentSessions, getSummaryLeaves, grepRecall, indexCodexSessions, probeLcmPeerDbs, probeCodexSqliteStores, searchSessions } from "../../core/src/index.js";
3
+ import { configuredLcmPeerDbPaths, createCloseoutEnvelopeReport, createAttentionInbox, createBusinessPulse, createCodexActiveThreadState, createCodexAutonomyTick, createCodexCollaborationNextSteps, createCodexCollaborationCockpit, createCodexRuntimeDesktopVisibilityStatus, describeSession, describeRecallRef, defaultCodexRoots, createIndexedSessionSanitizerReport, createIndexedSessionSanitizerRepairPlan, expandSession, expandQuery, expandSummaryLeaves, getPreparedCards, getPreparedInbox, getPreparedStateStatus, PREPARED_CARD_STATES, getWatcherEvents, createPlanStatePinsReport, createGithubOperatingItemsReport, createProjectDigest, createResumeRequestPacket, createWatcherStatusReport, createCodexDesktopCoherenceReport, createVisibleCodexSessionMap, getCockpitInbox, getCodexFinalMessages, getCodexPlans, getCodexSessionManagementMap, getCodexThreadMap, getCodexTouchedFiles, getCodexToolCalls, getRecentSessions, getSummaryLeaves, grepRecall, indexCodexSessions, probeLcmPeerDbs, probeCodexSqliteStores, searchSessions } from "../../core/src/index.js";
4
4
  import { LOO_COMMAND_POLICY, createCodexAppServerStatusReport, createCodexAppServerThreadsReport, codexTransportStatus, createCodexControl, createCodexDesktopCollaborationProof, createCodexDesktopFallbackReport, createDesktopGuiProofReport, createDesktopLiveProofHarness, createDesktopProofAction, desktopActDryRun, desktopFallbackDiagnostics, desktopSee, isDesktopBackend, redactValue } from "../../adapters/src/index.js";
5
5
  export const LOO_TOOL_TIERS = ["public_facade", "workflow_detail", "proof_debug", "internal_low_level"];
6
6
  export const LOO_TOOL_SURFACE = {
@@ -274,7 +274,7 @@ export function createLooTools(options) {
274
274
  })),
275
275
  tool("loo_prepared_cards", "List public-safe prepared Codex state cards over summary leaves.", {
276
276
  thread_id: { type: "string" },
277
- state: { type: "string", enum: ["ready", "stale", "partial", "unknown"] },
277
+ state: { type: "string", enum: [...PREPARED_CARD_STATES] },
278
278
  limit: { type: "integer", minimum: 1, maximum: 500 }
279
279
  }, (input) => getPreparedCards(options.db, {
280
280
  threadId: optionalString(input.thread_id),
@@ -1366,9 +1366,9 @@ function optionalSummaryLeafKind(value) {
1366
1366
  function optionalPreparedCardState(value) {
1367
1367
  if (value === undefined)
1368
1368
  return undefined;
1369
- if (value === "ready" || value === "stale" || value === "partial" || value === "unknown")
1369
+ if (typeof value === "string" && PREPARED_CARD_STATES.includes(value))
1370
1370
  return value;
1371
- throw new Error("state must be ready, stale, partial, or unknown");
1371
+ throw new Error(`state must be one of ${PREPARED_CARD_STATES.join(", ")}`);
1372
1372
  }
1373
1373
  function optionalRecentScope(value) {
1374
1374
  if (value === undefined)
@@ -0,0 +1,95 @@
1
+ # Release Notes 1.2.0-beta.3
2
+
3
+ `1.2.0-beta.3` is a scoped prerelease checkpoint for Codex-first local orchestration
4
+ and the LCO 1.2 prepared-state sprint. It publishes semantic
5
+ prepared-card lifecycle routing and the completed-card target coverage follow-up
6
+ merged after `1.2.0-beta.2`.
7
+
8
+ This release is intentionally on the npm `beta` channel. It does not promote
9
+ `latest` and it does not claim 1.2 GA.
10
+
11
+ ## What Changed
12
+
13
+ - Added semantic lifecycle states for prepared cards and prepared inbox routing:
14
+ `completed`, `blocked_missing_info`, `waiting_approval`,
15
+ `watching_external_check`, `needs_resume`, `dirty_worktree_handoff`,
16
+ `ready_for_review`, `stale_or_partial`, and `unknown_lifecycle`.
17
+ - Exposed the lifecycle state enum through the shared core registry, MCP tool
18
+ schema, OpenClaw plugin manifests, and OpenClaw gateway smoke validation.
19
+ - Added deterministic lifecycle reason codes, lifecycle-aware next actions, and
20
+ urgency ranking for advisory prepared cards.
21
+ - Added a completed-card summary counter so finished lanes remain visible in
22
+ prepared-card summaries instead of disappearing from stale/partial/unknown
23
+ counters.
24
+ - Tightened lifecycle classification so generic words such as `resume`,
25
+ `monitor`, and `ci` do not become lifecycle states without operator-action
26
+ context.
27
+ - Fixed completed prepared-card target coverage so a fresh public completed card
28
+ counts as target coverage `ok` and does not make `loo_prepared_state_status`
29
+ report stale/partial coverage for a fully materialized completed lane.
30
+ - Preserved stale, partial, unknown, unsafe-row, and stale-freshness downgrades
31
+ for incomplete or unsafe prepared-state evidence.
32
+ - Carries forward the #160 desktop proof-action release boundary:
33
+ `loo_desktop_proof_action` / `loo desktop proof-action` validates the CUA Driver TextEdit scratch proof gate and does not prove generic GUI mutation.
34
+ The proof action still requires exact backend, target app, target window, action hash, approval ref, permission state, scratch file path, and `execute: true` before the backend can run.
35
+ Generic gateway invocation without exact proof args fails closed. The desktop proof action keeps the same proof boundary as beta.35.
36
+ - Carries forward strict OpenClaw gateway result handling: plugin output with
37
+ `output.details.ok: false` is reported as
38
+ `openclaw_tool_result_not_ok:<tool>` rather than successful tool proof.
39
+
40
+ ## Current Claim Scope
41
+
42
+ Allowed prerelease claim:
43
+
44
+ > Local prepared Codex state and summary-leaf recall for OpenClaw/Eva, including
45
+ > semantic prepared-card lifecycle routing, prepared inbox prioritization,
46
+ > bounded summary expansion, visible Codex sidebar inventory, and
47
+ > approval-gated start-thread proof packets without raw transcript reads.
48
+
49
+ Prepared state remains an advisory local derived cache, not source authority
50
+ for PR, CI, release, runtime, customer, or business truth.
51
+
52
+ This release remains focused on local Codex sessions.
53
+
54
+ ## Release Gate Notes
55
+
56
+ - Parent 1.2 tracker: #405.
57
+ - Cockpit tracker: #448.
58
+ - Release checkpoint issue: #473.
59
+ - Included implementation PRs: #452 and #472.
60
+ - Baseline stable release: `v1.1.4`.
61
+ - Prior beta release: `v1.2.0-beta.2`.
62
+ - Candidate package: `lossless-openclaw-orchestrator@1.2.0-beta.3`.
63
+ - Expected npm dist-tag: `beta`.
64
+ - Expected git tag: `v1.2.0-beta.3`.
65
+ - Expected GitHub Release:
66
+ `https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/releases/tag/v1.2.0-beta.3`.
67
+ - Required scoped prerelease gates: focused prepared-card/OpenClaw tool-smoke
68
+ tests, build/typecheck, `npm pack --dry-run`, release bundle/status checks,
69
+ GitHub CI, CodeQL, current-head review threads clear, npm publish to `beta`,
70
+ GitHub prerelease, and post-publish finalization status.
71
+ - Bundle/status/finalization checks do not publish to npm and do not create a GitHub Release.
72
+ - Working-app status example:
73
+ `loo release status --claim-scope codex-working-app-proof --runtime-proof-dir <path> --approved-live-control-evidence <path> --npm-publish-approval-evidence <path> --github-release-approval-evidence <path> --candidate-sha <sha> --github-ci-evidence <path> --codeql-evidence <path> --evidence-dir <path> --strict`
74
+ - Reduced-scope status example:
75
+ `loo release status --claim-scope codex-read-search-expand-dry-run --npm-publish-approval-evidence <path> --github-release-approval-evidence <path> --candidate-sha <sha> --github-ci-evidence <path> --codeql-evidence <path> --evidence-dir <path> --strict`
76
+ - `approved_live_control_smoke_missing` remains the expected blocker when a
77
+ working-app or live-control claim is attempted without approved live-control
78
+ smoke evidence for the exact candidate SHA.
79
+
80
+ ## Explicit Non-Claims
81
+
82
+ No new live Codex control smoke is run by this release.
83
+ It does not run generic GUI mutation and does not run Codex GUI mutation.
84
+ No automatic gateway authorization.
85
+ No broad gateway scope approval. No prompt typing. No clicking. No arbitrary app control.
86
+ No screenshots or videos are part of the public release evidence.
87
+ Claude Code remains an adapter stub, not an adapter-equivalence claim.
88
+ No true Codex compaction-summary capture.
89
+ No model compaction proof.
90
+ No raw transcript upload and no OpenClaw LCM merge.
91
+ No Notion, support-control, Stripe, or Company Brain P1 adapter proof.
92
+ No cloud sync.
93
+ No unattended desktop takeover.
94
+ No npm `latest` promotion.
95
+ No release-grade enterprise security or customer-ready security claim.
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "scenario_version": "1.0",
3
3
  "id": "prepared-cards-inbox-v1",
4
- "title": "Prepared-state OpenClaw gateway dogfood",
4
+ "title": "Prepared-state lifecycle OpenClaw gateway dogfood",
5
5
  "claim_scope": "codex-read-search-expand-dry-run",
6
- "user_task": "As Eva, inspect prepared Codex session cards, choose the highest-priority attention item, and expand only bounded summary-leaf evidence before deciding the next safe action.",
6
+ "user_task": "As Eva, inspect prepared Codex session cards, distinguish evidence readiness from semantic lifecycle state, choose the highest-priority attention item, and expand only bounded summary-leaf evidence before deciding the next safe action.",
7
7
  "surface": "openclaw_gateway",
8
8
  "allowed_tools": [
9
9
  "loo_prepared_state_status",
@@ -39,7 +39,9 @@
39
39
  "source coverage",
40
40
  "authority coverage",
41
41
  "freshness and confidence",
42
- "stale partial unknown and low-confidence reason codes",
42
+ "semantic lifecycle state and lifecycle reason codes",
43
+ "completed blocked_missing_info waiting_approval watching_external_check needs_resume dirty_worktree_handoff ready_for_review stale_or_partial and unknown_lifecycle classification",
44
+ "stale partial unknown and low-confidence evidence-readiness reason codes",
43
45
  "execute=false attention queue actions",
44
46
  "bounded summary expansion references",
45
47
  "OpenClaw gateway tool-smoke",
@@ -69,6 +71,8 @@
69
71
  "requires_execute_false_actions": true,
70
72
  "requires_openclaw_gateway_tool_smoke": true,
71
73
  "requires_agent_recommendation": true,
74
+ "requires_semantic_lifecycle_reason_codes": true,
75
+ "requires_lifecycle_urgency_ranking": true,
72
76
  "requires_stale_partial_unknown_reason_codes": true,
73
77
  "max_raw_transcript_spans": 0,
74
78
  "max_raw_tool_payloads": 0,
@@ -76,6 +80,6 @@
76
80
  "max_gui_actions": 0,
77
81
  "max_screenshots_in_public_evidence": 0
78
82
  },
79
- "proof_boundary": "Dry-run fixture and OpenClaw gateway dogfood scenario for deterministic prepared-card and attention-inbox materialization over public-safe summary leaves. It proves advisory local prepared Codex state handoff and targeted prepared-state coverage for OpenClaw/Eva only when paired with a public-safe `loo openclaw tool-smoke` report that invokes prepared-state tools for a requested thread id, emits an agent recommendation, and records `rawTranscriptRead=false`. It does not prove model compaction, true Codex compaction-summary capture, live control, GUI mutation, unattended autonomy, release readiness, or 1.2 GA readiness.",
80
- "next_action": "Attach issue-specific OpenClaw gateway dogfood evidence for #413/#451, then continue #414 and #415 behind their explicit canary/protocol boundaries."
83
+ "proof_boundary": "Dry-run fixture and OpenClaw gateway dogfood scenario for deterministic prepared-card and attention-inbox materialization over public-safe summary leaves plus session metadata lifecycle signals. It proves advisory local prepared Codex lifecycle routing and targeted prepared-state coverage for OpenClaw/Eva only when paired with focused lifecycle fixtures, a public-safe `loo openclaw tool-smoke` report that invokes prepared-state tools for a requested thread id, emits an agent recommendation, and records `rawTranscriptRead=false`. It does not prove model compaction, true Codex compaction-summary capture, live control, GUI mutation, unattended autonomy, release readiness, or 1.2 GA readiness.",
84
+ "next_action": "Attach issue-specific semantic lifecycle evidence for #446, then continue remaining #405/#448 children behind their explicit canary/protocol boundaries."
81
85
  }
@@ -2,7 +2,7 @@
2
2
  "id": "lossless-openclaw-orchestrator",
3
3
  "name": "Lossless OpenClaw Orchestrator",
4
4
  "description": "Control and collaborate with local Codex sessions through OpenClaw using local indexing, bounded recall, and approval-gated controls.",
5
- "version": "1.2.0-beta.2",
5
+ "version": "1.2.0-beta.3",
6
6
  "kind": "tool",
7
7
  "tools": {
8
8
  "prefix": "loo_"
@@ -477,7 +477,16 @@
477
477
  "ready",
478
478
  "stale",
479
479
  "partial",
480
- "unknown"
480
+ "unknown",
481
+ "completed",
482
+ "blocked_missing_info",
483
+ "waiting_approval",
484
+ "watching_external_check",
485
+ "needs_resume",
486
+ "dirty_worktree_handoff",
487
+ "stale_or_partial",
488
+ "ready_for_review",
489
+ "unknown_lifecycle"
481
490
  ]
482
491
  },
483
492
  "limit": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lossless-openclaw-orchestrator",
3
- "version": "1.2.0-beta.2",
3
+ "version": "1.2.0-beta.3",
4
4
  "description": "Index, search, and control local Codex sessions through OpenClaw with approval-gated safety; Claude Code remains an experimental adapter stub.",
5
5
  "type": "module",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { mkdirSync, writeFileSync } from "node:fs";
4
4
  import { basename, dirname, isAbsolute } from "node:path";
5
+ import { PREPARED_CARD_STATES } from "../../core/src/index.js";
5
6
 
6
7
  export const DEFAULT_REQUIRED_TOOL_CALLS = [
7
8
  "loo_doctor",
@@ -42,6 +43,8 @@ export const DEFAULT_REQUIRED_TOOL_CALLS = [
42
43
  "loo_prepared_inbox"
43
44
  ];
44
45
 
46
+ const PREPARED_CARD_STATE_SET = new Set<string>(PREPARED_CARD_STATES);
47
+
45
48
  const AUTONOMY_TICK_SUMMARY_KEYS = [
46
49
  "totalLanes",
47
50
  "returnedSteps",
@@ -1332,7 +1335,7 @@ function summarizeInvocation(
1332
1335
  const state = stringPath(card, ["state"]);
1333
1336
  return !cardRef?.startsWith("prepared_card:")
1334
1337
  || !targetRef?.startsWith("codex_thread:")
1335
- || !["ready", "stale", "partial", "unknown"].includes(state ?? "");
1338
+ || !PREPARED_CARD_STATE_SET.has(state ?? "");
1336
1339
  })) blockers.push("prepared_cards_public_refs_invalid");
1337
1340
  }
1338
1341
  if (toolName === "loo_prepared_inbox") {
@@ -262,7 +262,22 @@ export type SummaryLeafMaterializationOptions = {
262
262
  limit?: number;
263
263
  };
264
264
 
265
- export type PreparedCardState = "ready" | "stale" | "partial" | "unknown";
265
+ export const PREPARED_CARD_STATES = [
266
+ "ready",
267
+ "stale",
268
+ "partial",
269
+ "unknown",
270
+ "completed",
271
+ "blocked_missing_info",
272
+ "waiting_approval",
273
+ "watching_external_check",
274
+ "needs_resume",
275
+ "dirty_worktree_handoff",
276
+ "stale_or_partial",
277
+ "ready_for_review",
278
+ "unknown_lifecycle"
279
+ ] as const;
280
+ export type PreparedCardState = (typeof PREPARED_CARD_STATES)[number];
266
281
  export type PreparedCardKind = "codex_session";
267
282
  export type PreparedStateCoverage = "ok" | "partial" | "not_configured" | "unknown";
268
283
 
@@ -393,6 +408,7 @@ export type PreparedCardsReport = {
393
408
  stale: number;
394
409
  partial: number;
395
410
  unknown: number;
411
+ completed: number;
396
412
  lowConfidence: number;
397
413
  };
398
414
  cards: PreparedCard[];
@@ -3891,6 +3907,7 @@ export function getPreparedCards(db: LooDatabase, options: PreparedCardsOptions
3891
3907
  let stale = 0;
3892
3908
  let partial = 0;
3893
3909
  let unknown = 0;
3910
+ let completed = 0;
3894
3911
  let lowConfidence = 0;
3895
3912
  for (const row of rows) {
3896
3913
  const card = publicPreparedCardFromRow(row);
@@ -3898,9 +3915,10 @@ export function getPreparedCards(db: LooDatabase, options: PreparedCardsOptions
3898
3915
  filteredUnsafeRows += 1;
3899
3916
  continue;
3900
3917
  }
3901
- if (card.stale) stale += 1;
3902
- if (card.state === "partial") partial += 1;
3903
- if (card.state === "unknown") unknown += 1;
3918
+ if (card.stale || card.state === "stale") stale += 1;
3919
+ if (preparedCardCountsAsPartialSummary(card.state)) partial += 1;
3920
+ if (card.state === "unknown" || card.state === "unknown_lifecycle") unknown += 1;
3921
+ if (card.state === "completed") completed += 1;
3904
3922
  if (card.confidence < 0.5) lowConfidence += 1;
3905
3923
  if (validCards.length < limit) validCards.push(card);
3906
3924
  }
@@ -3936,6 +3954,7 @@ export function getPreparedCards(db: LooDatabase, options: PreparedCardsOptions
3936
3954
  stale,
3937
3955
  partial,
3938
3956
  unknown,
3957
+ completed,
3939
3958
  lowConfidence
3940
3959
  },
3941
3960
  cards: validCards,
@@ -4268,9 +4287,19 @@ function buildPreparedCardDraft(db: LooDatabase, threadId: string, leaves: Summa
4268
4287
  const watcherObservationsStatus = watcherObservationCoverageForTarget(db, targetRef);
4269
4288
  const freshnessAt = latestIso([...leaves.map((leaf) => leaf.freshnessAt), session?.updatedAt ?? null]);
4270
4289
  const stale = leaves.some((leaf) => leaf.stale);
4290
+ const evidenceState: PreparedCardState = stale
4291
+ ? "stale_or_partial"
4292
+ : summaryLeavesStatus === "unknown"
4293
+ ? "unknown_lifecycle"
4294
+ : summaryLeavesStatus === "partial" || sessionMetadataStatus === "partial"
4295
+ ? "stale_or_partial"
4296
+ : "ready";
4297
+ const lifecycle = preparedLifecycleFromMetadata(metadata, evidenceState);
4298
+ const state = lifecycle.state;
4271
4299
  const reasonCodes = unique([
4272
4300
  leaves.length > 0 ? "summary_leaves_ready" : "summary_leaves_missing",
4273
4301
  "metadata_only",
4302
+ ...lifecycle.reasonCodes,
4274
4303
  watcherObservationsStatus === "not_configured" ? "watcher_not_configured" : "watcher_observations_available",
4275
4304
  stale ? "stale_cache" : "",
4276
4305
  summaryLeavesStatus === "partial" ? "authority_partial" : "",
@@ -4280,17 +4309,10 @@ function buildPreparedCardDraft(db: LooDatabase, threadId: string, leaves: Summa
4280
4309
  watcherObservationsStatus === "unknown" ? "watcher_observations_unknown" : "",
4281
4310
  filteredUnsafeRows > 0 ? "filtered_unsafe_rows" : ""
4282
4311
  ].filter(Boolean));
4283
- const state: PreparedCardState = stale
4284
- ? "stale"
4285
- : summaryLeavesStatus === "unknown"
4286
- ? "unknown"
4287
- : summaryLeavesStatus === "partial" || sessionMetadataStatus === "partial"
4288
- ? "partial"
4289
- : "ready";
4290
4312
  const averageLeafConfidence = leaves.length
4291
4313
  ? leaves.reduce((sum, leaf) => sum + leaf.confidence, 0) / leaves.length
4292
4314
  : 0.3;
4293
- const confidence = preparedCardConfidence(averageLeafConfidence, state);
4315
+ const confidence = preparedCardConfidence(averageLeafConfidence, state, evidenceState);
4294
4316
  const sourceRangeRefsFull = unique(leaves.flatMap((leaf) => leaf.sourceRangeRefs));
4295
4317
  const sourceRangeRefs = sourceRangeRefsFull.slice(0, PREPARED_CARD_SOURCE_RANGE_REF_LIMIT);
4296
4318
  const sourceRangeRefsOmitted = Math.max(0, sourceRangeRefsFull.length - sourceRangeRefs.length)
@@ -4305,7 +4327,7 @@ function buildPreparedCardDraft(db: LooDatabase, threadId: string, leaves: Summa
4305
4327
  });
4306
4328
  const title = looksSensitiveRefLike(cleanedTitle.text) ? publicSafeText(safeThreadId(threadId), 160) : cleanedTitle.text;
4307
4329
  const summaryText = publicSafeText(
4308
- `Prepared state: ${leaves.length} summary leaf${leaves.length === 1 ? "" : "s"} across ${Math.max(0, leafRangeCount)} prepared source range${leafRangeCount === 1 ? "" : "s"}.`,
4330
+ `Prepared state: ${leaves.length} summary leaf${leaves.length === 1 ? "" : "s"} across ${Math.max(0, leafRangeCount)} prepared source range${leafRangeCount === 1 ? "" : "s"}. Lifecycle: ${preparedCardStateLabel(state)}.`,
4309
4331
  320
4310
4332
  );
4311
4333
  const nextAction = preparedCardNextAction(state);
@@ -4327,6 +4349,10 @@ function buildPreparedCardDraft(db: LooDatabase, threadId: string, leaves: Summa
4327
4349
  leafRefs: leaves.map((leaf) => leaf.leafRef),
4328
4350
  inputHashes: leaves.map((leaf) => leaf.inputHash),
4329
4351
  outputHashes: leaves.map((leaf) => leaf.outputHash),
4352
+ metadataSignalHash: lifecycle.metadataSignalHash,
4353
+ lifecycleState: state,
4354
+ lifecycleReasonCodes: lifecycle.reasonCodes,
4355
+ evidenceState,
4330
4356
  freshnessAt,
4331
4357
  stale,
4332
4358
  summaryLeavesStatus,
@@ -4363,7 +4389,7 @@ function buildPreparedCardDraft(db: LooDatabase, threadId: string, leaves: Summa
4363
4389
  function preparedInboxItemFromCard(card: PreparedCard): PreparedInboxItem {
4364
4390
  const reasonCodes = unique([
4365
4391
  ...card.reasonCodes,
4366
- card.state === "ready" ? "prepared_card_ready" : "needs_attention"
4392
+ card.state === "ready" ? "prepared_card_ready" : card.state === "completed" ? "prepared_card_completed" : "needs_attention"
4367
4393
  ]);
4368
4394
  const urgencyScore = preparedInboxUrgencyScore(card, reasonCodes);
4369
4395
  return {
@@ -4642,7 +4668,7 @@ function getPreparedTargetCoverage(db: LooDatabase, threadId?: string): Prepared
4642
4668
  preparedSourceEvents: preparedSourceEvents.count > 0 ? "ok" : "not_configured",
4643
4669
  preparedSourceRanges: preparedSourceRanges.publicCount > 0 ? "ok" : preparedSourceRanges.rawCount > 0 ? "partial" : "not_configured",
4644
4670
  summaryLeaves: summaryLeaves.publicCount > 0 ? "ok" : summaryLeaves.rawCount > 0 ? "partial" : "not_configured",
4645
- preparedCards: publicCard ? publicCard.state === "ready" && !publicCard.stale ? "ok" : "partial" : preparedCards > 0 ? "partial" : "not_configured",
4671
+ preparedCards: publicCard ? preparedTargetCardCoverage(publicCard) : preparedCards > 0 ? "partial" : "not_configured",
4646
4672
  preparedInboxItems: publicCard && preparedInboxItems.publicCount > 0 ? "ok" : preparedInboxItems.rawCount > 0 ? "partial" : "not_configured",
4647
4673
  watcherObservations: watcherObservationCoverageForTarget(db, targetRef)
4648
4674
  } satisfies PreparedTargetCoverage["sourceCoverage"];
@@ -4668,8 +4694,7 @@ function getPreparedTargetCoverage(db: LooDatabase, threadId?: string): Prepared
4668
4694
  const allDerivedLayersMissing = requiredCoverage.every((state) => state === "not_configured");
4669
4695
  const stale = Boolean(session && (
4670
4696
  missingDerivedCache
4671
- || publicCard?.stale
4672
- || publicCard?.state !== "ready"
4697
+ || (publicCard ? preparedTargetCardCoverage(publicCard) !== "ok" : false)
4673
4698
  || summaryLeaves.staleCount > 0
4674
4699
  || (sourceUpdatedAt && preparedFreshnessAt && sourceUpdatedAt > preparedFreshnessAt)
4675
4700
  ));
@@ -4709,6 +4734,19 @@ function getPreparedTargetCoverage(db: LooDatabase, threadId?: string): Prepared
4709
4734
  };
4710
4735
  }
4711
4736
 
4737
+ function preparedTargetCardCoverage(card: PreparedCard): PreparedStateCoverage {
4738
+ if (card.stale) return "partial";
4739
+ return preparedCardStateHasFreshTargetCoverage(card.state) ? "ok" : "partial";
4740
+ }
4741
+
4742
+ function preparedCardStateHasFreshTargetCoverage(state: PreparedCardState): boolean {
4743
+ return state !== "stale"
4744
+ && state !== "partial"
4745
+ && state !== "stale_or_partial"
4746
+ && state !== "unknown"
4747
+ && state !== "unknown_lifecycle";
4748
+ }
4749
+
4712
4750
  function countPreparedTargetRows(db: LooDatabase, sql: string, ...params: Array<string | number>): number {
4713
4751
  return Number((db.prepare(sql).get(...params) as { count: number } | undefined)?.count ?? 0);
4714
4752
  }
@@ -4911,7 +4949,7 @@ function preparedCoverageState(value: unknown): PreparedStateCoverage {
4911
4949
  }
4912
4950
 
4913
4951
  function isPreparedCardState(value: string): value is PreparedCardState {
4914
- return value === "ready" || value === "stale" || value === "partial" || value === "unknown";
4952
+ return (PREPARED_CARD_STATES as readonly string[]).includes(value);
4915
4953
  }
4916
4954
 
4917
4955
  function isPublicPreparedSourceRef(value: string): boolean {
@@ -4933,11 +4971,139 @@ function isPublicCodexSubagentResultRef(value: string): boolean {
4933
4971
  return /^[A-Za-z0-9._:-]{1,160}$/.test(decodedId) && !looksSensitiveRefLike(decodedId);
4934
4972
  }
4935
4973
 
4936
- function preparedCardConfidence(averageLeafConfidence: number, state: PreparedCardState): number {
4974
+ function preparedLifecycleFromMetadata(
4975
+ metadata: SessionMetadata,
4976
+ evidenceState: PreparedCardState
4977
+ ): { state: PreparedCardState; reasonCodes: string[]; metadataSignalHash: string } {
4978
+ const signals = {
4979
+ status: normalizedMetadataValue(metadata.status),
4980
+ blocker: normalizedMetadataValue(metadata.blocker),
4981
+ nextAction: normalizedMetadataValue(metadata.nextAction),
4982
+ closeoutState: normalizedMetadataValue(metadata.closeoutState),
4983
+ planCompletionState: normalizedMetadataValue(metadata.planCompletionState)
4984
+ };
4985
+ const metadataSignalHash = stableId(JSON.stringify(signals));
4986
+ const nonBlockerText = [signals.status, signals.nextAction, signals.closeoutState, signals.planCompletionState].filter(Boolean).join(" ");
4987
+ const text = [nonBlockerText, signals.blocker].filter(Boolean).join(" ");
4988
+ const sourceReasonCodes = Object.entries(signals)
4989
+ .filter(([, value]) => value.length > 0)
4990
+ .map(([field]) => `lifecycle_signal:${field.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`)}`);
4991
+ const completedByStatus = lifecycleCompletionLike(signals.status);
4992
+ const completedByCloseoutAndPlan = lifecycleCompletionLike(signals.closeoutState)
4993
+ && lifecycleCompletionLike(signals.planCompletionState);
4994
+ const completed = completedByStatus || completedByCloseoutAndPlan;
4995
+ const dirtyHandoff = /\b(?:dirty[-_ ]?worktree|uncommitted|worktree[-_ ]?handoff|dirty[-_ ]?handoff|handoff[-_ ]?dirty|cleanup[-_ ]?required)\b/.test(text);
4996
+ const waitingApproval = /\b(?:waiting[-_ ]?(?:for[-_ ]?)?approval|needs[-_ ]?approval|requires[-_ ]?approval|approval[-_ ]?(?:required|needed|pending)|pending[-_ ]?approval|approval[-_ ]?gate|do[-_ ]?not[-_ ]?execute[-_ ]?without[-_ ]?explicit[-_ ]?approval)\b/.test(text);
4997
+ const needsResume = /\b(?:needs[-_ ]?(?:to[-_ ]?)?resume|resume[-_ ]?(?:session|thread|work|run|lane|needed|required|requested|request)|continue[-_ ]?(?:session|thread|lane)|nudge[-_ ]?(?:session|thread|agent|lane)|rejoin[-_ ]?(?:session|thread|lane))\b/.test(text);
4998
+ const watchingExternalCheck = /\b(?:(?:watch|watching|monitor|monitoring)[-_ ]+(?:ci|checks?|codeql|coderabbit|deploy(?:[-_ ]?check)?|external[-_ ]?(?:check|review)|review[-_ ]?check)|(?:waiting[-_ ]?(?:on|for)|pending)[-_ ]+(?:ci|checks?|codeql|coderabbit|deploy|external[-_ ]?check|review[-_ ]?check)|(?:ci|checks?|codeql|coderabbit|deploy[-_ ]?check|review[-_ ]?check)[-_ ]+(?:pending|queued|running|failing|failed|blocked|red|not[-_ ]?green|in[-_ ]?progress))\b/.test(text);
4999
+ const readyForReview = /\b(?:ready[-_ ]?for[-_ ]?review|review[-_ ]?ready|ready[-_ ]?to[-_ ]?review|pr[-_ ]?ready|needs[-_ ]?review)\b/.test(text);
5000
+ const negatedBlocker = /\b(?:not[-_ ]?blocked|no[-_ ]?blocker|unblocked|without[-_ ]?blocker|blocker[-_ ]?(?:none|na|n[-_ ]?a))\b/.test(text);
5001
+ const missingInfoSignal = /\b(?:missing[-_ ]?(?:info|input|context)|needs[-_ ]?(?:info|input|context)|waiting[-_ ]?(?:on|for)[-_ ]?(?:user|operator|human|input|context)|cannot[-_ ]?proceed)\b/.test(nonBlockerText);
5002
+ const blockedSignal = !negatedBlocker && /\b(?:blocked|blocker)\b/.test(nonBlockerText);
5003
+ const blockedMissingInfo = !waitingApproval
5004
+ && !dirtyHandoff
5005
+ && !watchingExternalCheck
5006
+ && !negatedBlocker
5007
+ && (hasRealBlocker(metadata.blocker) || missingInfoSignal || blockedSignal);
5008
+ const matchedLifecycleStates: PreparedCardState[] = [
5009
+ completed ? "completed" : null,
5010
+ dirtyHandoff ? "dirty_worktree_handoff" : null,
5011
+ waitingApproval ? "waiting_approval" : null,
5012
+ blockedMissingInfo ? "blocked_missing_info" : null,
5013
+ needsResume ? "needs_resume" : null,
5014
+ watchingExternalCheck ? "watching_external_check" : null,
5015
+ readyForReview ? "ready_for_review" : null
5016
+ ].filter((state): state is PreparedCardState => Boolean(state));
5017
+ const evidenceReasonCodes = evidenceState !== "ready" ? [`lifecycle:${evidenceState}`] : [];
5018
+ const conflict = completed && matchedLifecycleStates.some((state) => state !== "completed");
5019
+ if (conflict) {
5020
+ return {
5021
+ state: "unknown_lifecycle",
5022
+ reasonCodes: unique([
5023
+ "semantic_lifecycle",
5024
+ "lifecycle:unknown_lifecycle",
5025
+ "lifecycle_conflict",
5026
+ ...matchedLifecycleStates.map((state) => `lifecycle:${state}`),
5027
+ ...evidenceReasonCodes,
5028
+ ...sourceReasonCodes
5029
+ ]),
5030
+ metadataSignalHash
5031
+ };
5032
+ }
5033
+ const semanticState: PreparedCardState | null = dirtyHandoff
5034
+ ? "dirty_worktree_handoff"
5035
+ : waitingApproval
5036
+ ? "waiting_approval"
5037
+ : blockedMissingInfo
5038
+ ? "blocked_missing_info"
5039
+ : needsResume
5040
+ ? "needs_resume"
5041
+ : watchingExternalCheck
5042
+ ? "watching_external_check"
5043
+ : readyForReview
5044
+ ? "ready_for_review"
5045
+ : completed
5046
+ ? "completed"
5047
+ : null;
5048
+ if (semanticState === "completed" && evidenceState !== "ready") {
5049
+ return {
5050
+ state: evidenceState,
5051
+ reasonCodes: unique(["semantic_lifecycle", "lifecycle:completed", `lifecycle:${evidenceState}`, ...sourceReasonCodes]),
5052
+ metadataSignalHash
5053
+ };
5054
+ }
5055
+ if (semanticState) {
5056
+ return {
5057
+ state: semanticState,
5058
+ reasonCodes: unique(["semantic_lifecycle", `lifecycle:${semanticState}`, ...evidenceReasonCodes, ...sourceReasonCodes]),
5059
+ metadataSignalHash
5060
+ };
5061
+ }
5062
+ if (evidenceState !== "ready") {
5063
+ return {
5064
+ state: evidenceState,
5065
+ reasonCodes: unique(["semantic_lifecycle", `lifecycle:${evidenceState}`, "lifecycle_signal_missing", ...sourceReasonCodes]),
5066
+ metadataSignalHash
5067
+ };
5068
+ }
5069
+ return {
5070
+ state: "ready",
5071
+ reasonCodes: unique(["semantic_lifecycle", "lifecycle:unknown_lifecycle", "lifecycle_signal_missing"]),
5072
+ metadataSignalHash
5073
+ };
5074
+ }
5075
+
5076
+ function lifecycleCompletionLike(value: string): boolean {
5077
+ return ["complete", "completed", "done", "closed", "merged", "success", "successful", "succeeded", "passed"].includes(value);
5078
+ }
5079
+
5080
+ function preparedCardStateLabel(state: PreparedCardState): string {
5081
+ return ({
5082
+ ready: "evidence ready, lifecycle unknown",
5083
+ stale: "stale evidence",
5084
+ partial: "partial evidence",
5085
+ unknown: "unknown evidence",
5086
+ completed: "completed",
5087
+ blocked_missing_info: "blocked missing info",
5088
+ waiting_approval: "waiting approval",
5089
+ watching_external_check: "watching external check",
5090
+ needs_resume: "needs resume",
5091
+ dirty_worktree_handoff: "dirty worktree handoff",
5092
+ stale_or_partial: "stale or partial",
5093
+ ready_for_review: "ready for review",
5094
+ unknown_lifecycle: "unknown lifecycle"
5095
+ } as Record<PreparedCardState, string>)[state] ?? "unknown lifecycle";
5096
+ }
5097
+
5098
+ function preparedCardConfidence(averageLeafConfidence: number, state: PreparedCardState, evidenceState: PreparedCardState = state): number {
4937
5099
  let confidence = Number.isFinite(averageLeafConfidence) ? averageLeafConfidence : 0.3;
4938
5100
  if (state === "partial") confidence = Math.min(confidence, 0.49);
4939
5101
  if (state === "stale") confidence = Math.min(confidence, 0.49);
4940
5102
  if (state === "unknown") confidence = Math.min(confidence, 0.44);
5103
+ if (state === "stale_or_partial") confidence = Math.min(confidence, 0.49);
5104
+ if (state === "unknown_lifecycle") confidence = Math.min(confidence, 0.44);
5105
+ if (evidenceState === "partial" || evidenceState === "stale" || evidenceState === "stale_or_partial") confidence = Math.min(confidence, 0.49);
5106
+ if (evidenceState === "unknown" || evidenceState === "unknown_lifecycle") confidence = Math.min(confidence, 0.44);
4941
5107
  return Math.max(0.2, Math.min(0.99, Number(confidence.toFixed(2))));
4942
5108
  }
4943
5109
 
@@ -4945,15 +5111,33 @@ function preparedCardNextAction(state: PreparedCardState): string {
4945
5111
  if (state === "ready") return "Use loo_summary_expand for bounded evidence before deciding whether broader session expansion is needed.";
4946
5112
  if (state === "stale") return "Refresh the local prepared-state cache before treating this card as current.";
4947
5113
  if (state === "partial") return "Inspect source coverage and summary leaves before relying on this prepared card.";
5114
+ if (state === "completed") return "No action is needed unless bounded evidence contradicts the completed lifecycle state.";
5115
+ if (state === "blocked_missing_info") return "Inspect bounded evidence and resolve the missing input or blocker before resuming.";
5116
+ if (state === "waiting_approval") return "Inspect the bounded approval context; do not execute without explicit approval.";
5117
+ if (state === "watching_external_check") return "Refresh external check or watcher evidence before deciding the next action.";
5118
+ if (state === "needs_resume") return "Inspect bounded evidence and resume only through the approval-gated control path.";
5119
+ if (state === "dirty_worktree_handoff") return "Inspect handoff evidence and clean or transfer the dirty worktree deliberately.";
5120
+ if (state === "stale_or_partial") return "Refresh or inspect partial prepared-state evidence before relying on this lifecycle card.";
5121
+ if (state === "ready_for_review") return "Review bounded evidence and source refs before marking the lane complete.";
5122
+ if (state === "unknown_lifecycle") return "Inspect summary leaves and session metadata; lifecycle signals are missing or conflicting.";
4948
5123
  return "Inspect summary leaves and source coverage; missing or conflicting authority prevents a ready claim.";
4949
5124
  }
4950
5125
 
4951
5126
  function preparedInboxUrgencyScore(card: PreparedCard, reasonCodes: string[]): number {
4952
5127
  const stateScore = {
4953
5128
  unknown: 88,
5129
+ unknown_lifecycle: 60,
4954
5130
  stale: 82,
5131
+ stale_or_partial: 74,
4955
5132
  partial: 74,
4956
- ready: 42
5133
+ blocked_missing_info: 94,
5134
+ waiting_approval: 92,
5135
+ dirty_worktree_handoff: 89,
5136
+ watching_external_check: 78,
5137
+ needs_resume: 76,
5138
+ ready_for_review: 68,
5139
+ ready: 42,
5140
+ completed: 18
4957
5141
  } as const;
4958
5142
  const codeScore = reasonCodes.reduce((score, code) => score + ({
4959
5143
  authority_unknown: 8,
@@ -4961,9 +5145,27 @@ function preparedInboxUrgencyScore(card: PreparedCard, reasonCodes: string[]): n
4961
5145
  stale_cache: 8,
4962
5146
  low_confidence: 6,
4963
5147
  filtered_unsafe_rows: 5,
4964
- needs_attention: 4
5148
+ needs_attention: 4,
5149
+ "lifecycle_conflict": 10,
5150
+ "lifecycle:blocked_missing_info": 8,
5151
+ "lifecycle:waiting_approval": 7,
5152
+ "lifecycle:dirty_worktree_handoff": 7,
5153
+ "lifecycle:watching_external_check": 5,
5154
+ "lifecycle:needs_resume": 5
4965
5155
  }[code] ?? 0), 0);
4966
- return Math.max(0, Math.min(100, Number((stateScore[card.state] + codeScore + Math.round((1 - card.confidence) * 10)).toFixed(2))));
5156
+ const baseStateScore = stateScore[card.state] ?? 50;
5157
+ return Math.max(0, Math.min(100, Number((baseStateScore + codeScore + Math.round((1 - card.confidence) * 10)).toFixed(2))));
5158
+ }
5159
+
5160
+ function preparedCardCountsAsPartialSummary(state: PreparedCardState): boolean {
5161
+ return state === "partial"
5162
+ || state === "stale_or_partial"
5163
+ || state === "blocked_missing_info"
5164
+ || state === "waiting_approval"
5165
+ || state === "watching_external_check"
5166
+ || state === "needs_resume"
5167
+ || state === "dirty_worktree_handoff"
5168
+ || state === "ready_for_review";
4967
5169
  }
4968
5170
 
4969
5171
  function preparedCardReadActions(): PreparedCardsReport["actionsPerformed"] {
@@ -22,6 +22,7 @@ import {
22
22
  getPreparedCards,
23
23
  getPreparedInbox,
24
24
  getPreparedStateStatus,
25
+ PREPARED_CARD_STATES,
25
26
  getWatcherEvents,
26
27
  createPlanStatePinsReport,
27
28
  createGithubOperatingItemsReport,
@@ -406,7 +407,7 @@ export function createLooTools(options: { db: LooDatabase; audit: AuditStore; co
406
407
  })),
407
408
  tool("loo_prepared_cards", "List public-safe prepared Codex state cards over summary leaves.", {
408
409
  thread_id: { type: "string" },
409
- state: { type: "string", enum: ["ready", "stale", "partial", "unknown"] },
410
+ state: { type: "string", enum: [...PREPARED_CARD_STATES] },
410
411
  limit: { type: "integer", minimum: 1, maximum: 500 }
411
412
  }, (input) => getPreparedCards(options.db, {
412
413
  threadId: optionalString(input.thread_id),
@@ -1544,8 +1545,8 @@ function optionalSummaryLeafKind(value: unknown): SummaryLeafKind | undefined {
1544
1545
 
1545
1546
  function optionalPreparedCardState(value: unknown): PreparedCardState | undefined {
1546
1547
  if (value === undefined) return undefined;
1547
- if (value === "ready" || value === "stale" || value === "partial" || value === "unknown") return value;
1548
- throw new Error("state must be ready, stale, partial, or unknown");
1548
+ if (typeof value === "string" && (PREPARED_CARD_STATES as readonly string[]).includes(value)) return value as PreparedCardState;
1549
+ throw new Error(`state must be one of ${PREPARED_CARD_STATES.join(", ")}`);
1549
1550
  }
1550
1551
 
1551
1552
  function optionalRecentScope(value: unknown): "active" | "recent" | "all" | undefined {
@@ -2,7 +2,7 @@
2
2
  "id": "lossless-openclaw-orchestrator",
3
3
  "name": "Lossless OpenClaw Orchestrator",
4
4
  "description": "Control and collaborate with local Codex sessions through OpenClaw using local indexing, bounded recall, and approval-gated controls.",
5
- "version": "1.2.0-beta.2",
5
+ "version": "1.2.0-beta.3",
6
6
  "kind": "tool",
7
7
  "tools": {
8
8
  "prefix": "loo_"
@@ -477,7 +477,16 @@
477
477
  "ready",
478
478
  "stale",
479
479
  "partial",
480
- "unknown"
480
+ "unknown",
481
+ "completed",
482
+ "blocked_missing_info",
483
+ "waiting_approval",
484
+ "watching_external_check",
485
+ "needs_resume",
486
+ "dirty_worktree_handoff",
487
+ "stale_or_partial",
488
+ "ready_for_review",
489
+ "unknown_lifecycle"
481
490
  ]
482
491
  },
483
492
  "limit": {