mirror of
https://github.com/dtzp555-max/memory-continuity.git
synced 2026-07-19 09:42:42 +00:00
v0.2: rewrite skill with discipline protocol, doctor, and archive
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 dtzp555-max
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# memory-continuity
|
||||||
|
|
||||||
|
OpenClaw skill for **short-term working continuity** — so your agent can pick up
|
||||||
|
exactly where it left off after a gateway crash, `/new`, model fallback, or
|
||||||
|
context compaction.
|
||||||
|
|
||||||
|
## What problem does this solve?
|
||||||
|
|
||||||
|
When your gateway dies mid-conversation, or you hit `/new` to start fresh, or
|
||||||
|
the model falls back to a different provider — your agent loses its working
|
||||||
|
context. Long-term memory can tell it *what it knows*, but not *what it was
|
||||||
|
doing*. This skill fills that gap.
|
||||||
|
|
||||||
|
**One-line summary:** Long-term memory = what you know. This skill = what you
|
||||||
|
are doing right now.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Go to your OpenClaw workspace skills directory
|
||||||
|
cd ~/.openclaw/workspace/skills/
|
||||||
|
|
||||||
|
# Clone
|
||||||
|
git clone https://github.com/dtzp555-max/memory-continuity.git
|
||||||
|
|
||||||
|
# That's it. No npm install, no API keys, no database.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test it
|
||||||
|
|
||||||
|
1. Start a conversation with your agent about a multi-step task
|
||||||
|
2. Chat for a few turns, make some decisions
|
||||||
|
3. Check: does `memory/CURRENT_STATE.md` exist in your workspace? Does it
|
||||||
|
reflect what you were doing?
|
||||||
|
4. Type `/new` to start a fresh session
|
||||||
|
5. The agent should read `CURRENT_STATE.md` and ask:
|
||||||
|
*"Last session we were working on X. Want to continue?"*
|
||||||
|
|
||||||
|
If step 5 works, the skill is doing its job.
|
||||||
|
|
||||||
|
### Run the doctor
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/continuity_doctor.py --workspace ~/.openclaw/workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Sample output:
|
||||||
|
```
|
||||||
|
Continuity Doctor — scanning: /Users/you/.openclaw/workspace
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
[OK] memory/CURRENT_STATE.md exists
|
||||||
|
[OK] CURRENT_STATE.md is fresh (0.3h old)
|
||||||
|
[OK] Template compliance: all sections present
|
||||||
|
[WARNING] Unsurfaced Results section is not empty — review needed
|
||||||
|
[INFO] Found 3 session archive(s), latest: 2026-03-12_14-30.md
|
||||||
|
|
||||||
|
Overall status: WARNING
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
The skill installs a behavioral protocol via `SKILL.md`. When loaded, the agent
|
||||||
|
follows these rules:
|
||||||
|
|
||||||
|
1. **Session start:** Read `memory/CURRENT_STATE.md`, brief the user, wait for
|
||||||
|
confirmation
|
||||||
|
2. **During work:** Overwrite the state file at key moments (decisions,
|
||||||
|
completed steps, errors, before long tool calls, every ~10 turns)
|
||||||
|
3. **Session end / `/new`:** Final state save + archive a timestamped snapshot
|
||||||
|
|
||||||
|
The state file uses a fixed template:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Current State
|
||||||
|
> Last updated: 2026-03-12T14:30:00Z
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Build the user authentication module
|
||||||
|
|
||||||
|
## Current Step
|
||||||
|
Completed JWT token generation, starting refresh endpoint
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
- Using RS256 for token signing (user approved)
|
||||||
|
- Token expiry: 15 minutes access, 7 days refresh
|
||||||
|
|
||||||
|
## Next Action
|
||||||
|
Implement POST /auth/refresh endpoint
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
None
|
||||||
|
|
||||||
|
## Unsurfaced Results
|
||||||
|
None
|
||||||
|
```
|
||||||
|
|
||||||
|
The entire file is designed to be read in 15 seconds. It is overwritten (not
|
||||||
|
appended) on every update, keeping it permanently short.
|
||||||
|
|
||||||
|
## Architecture position
|
||||||
|
|
||||||
|
This skill occupies a specific niche. Here is how it relates to other tools:
|
||||||
|
|
||||||
|
| Layer | Tool | What it stores |
|
||||||
|
|---|---|---|
|
||||||
|
| Working state | **memory-continuity** (this skill) | What you are doing *right now* |
|
||||||
|
| Stable facts | OpenClaw native markdown memory | Preferences, decisions, knowledge |
|
||||||
|
| Retrieval | memory-lancedb-pro / similar | Searchable long-term history |
|
||||||
|
|
||||||
|
These layers are complementary. This skill has **zero dependency** on any
|
||||||
|
database or external memory plugin. It works with plain markdown files that
|
||||||
|
live in your workspace and can be backed up with `git` or `cp`.
|
||||||
|
|
||||||
|
## File structure
|
||||||
|
|
||||||
|
```
|
||||||
|
memory-continuity/
|
||||||
|
├── SKILL.md # Core skill (loaded by OpenClaw)
|
||||||
|
├── README.md # This file
|
||||||
|
├── LICENSE # MIT
|
||||||
|
├── references/
|
||||||
|
│ ├── template.md # CURRENT_STATE.md blank template
|
||||||
|
│ └── doctor-spec.md # Doctor check specifications
|
||||||
|
└── scripts/
|
||||||
|
└── continuity_doctor.py # Diagnostic tool (reports only, no auto-repair)
|
||||||
|
```
|
||||||
|
|
||||||
|
At runtime, the skill creates these files in your workspace:
|
||||||
|
|
||||||
|
```
|
||||||
|
$WORKSPACE/
|
||||||
|
└── memory/
|
||||||
|
├── CURRENT_STATE.md # Live workbench (overwritten each update)
|
||||||
|
└── session_archive/ # Timestamped snapshots from past sessions
|
||||||
|
├── 2026-03-12_14-30.md
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design principles
|
||||||
|
|
||||||
|
1. **Zero dependencies.** No database, no API, no npm packages. Just files.
|
||||||
|
2. **Backup = copy.** The entire state is in `memory/`. Back it up however you
|
||||||
|
back up your workspace.
|
||||||
|
3. **Overwrite, don't append.** CURRENT_STATE.md is a workbench, not a journal.
|
||||||
|
It stays short because it is replaced on every update.
|
||||||
|
4. **Diagnose, don't auto-repair.** The doctor script flags problems for you to
|
||||||
|
fix. Automated repair of state files is too risky at this stage.
|
||||||
|
5. **Complement, don't compete.** This skill does not replace long-term memory.
|
||||||
|
It solves a different problem (crash recovery vs knowledge retrieval).
|
||||||
|
|
||||||
|
## Current status
|
||||||
|
|
||||||
|
**v0.2 — Draft / early version**
|
||||||
|
|
||||||
|
What works:
|
||||||
|
- SKILL.md protocol with discipline rules
|
||||||
|
- CURRENT_STATE.md template
|
||||||
|
- Continuity doctor diagnostic script
|
||||||
|
- Session archive on `/new`
|
||||||
|
|
||||||
|
Planned:
|
||||||
|
- Plugin version with `command:new` and `before_agent_start` hooks
|
||||||
|
(for guaranteed save/restore without relying on agent self-discipline)
|
||||||
|
- More real-world validation across different gateway configurations
|
||||||
|
- Sub-agent continuity handover protocol
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
---
|
||||||
|
name: memory-continuity
|
||||||
|
description: >
|
||||||
|
Short-term working continuity for OpenClaw agents. Preserves in-flight state
|
||||||
|
across gateway restarts, /new, model fallback, and context compaction.
|
||||||
|
Use this skill whenever an agent needs to survive session breaks without
|
||||||
|
losing what it was doing. Triggers on: session start, /new, context recovery,
|
||||||
|
"where were we", "continue", resuming work, or any situation where recent
|
||||||
|
working state may have been lost. This is NOT a long-term memory system —
|
||||||
|
it is a crash-recovery workbench.
|
||||||
|
---
|
||||||
|
|
||||||
|
# memory-continuity
|
||||||
|
|
||||||
|
Lightweight continuity layer that keeps a single overwrite-oriented state file
|
||||||
|
(`memory/CURRENT_STATE.md`) so any agent can pick up exactly where it left off
|
||||||
|
after a restart, `/new`, gateway crash, model fallback, or context compaction.
|
||||||
|
|
||||||
|
## Why this exists
|
||||||
|
|
||||||
|
Long-term memory (vector DB, markdown journals) stores *what you know*.
|
||||||
|
This skill stores *what you are doing right now*. They solve different problems.
|
||||||
|
|
||||||
|
When a gateway crashes mid-task, no amount of long-term memory recall can tell
|
||||||
|
the next session: "you were halfway through step 3, the user approved option B,
|
||||||
|
and the blocker was X." That is what CURRENT_STATE.md does.
|
||||||
|
|
||||||
|
## File layout
|
||||||
|
|
||||||
|
```
|
||||||
|
$WORKSPACE/
|
||||||
|
├── memory/
|
||||||
|
│ ├── CURRENT_STATE.md # THE workbench (overwritten, never appended)
|
||||||
|
│ └── session_archive/ # compressed snapshots from past sessions
|
||||||
|
│ ├── 2026-03-12_14-30.md
|
||||||
|
│ └── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MANDATORY PROTOCOL
|
||||||
|
|
||||||
|
### 1. On every session start
|
||||||
|
|
||||||
|
```
|
||||||
|
IF memory/CURRENT_STATE.md exists:
|
||||||
|
READ it
|
||||||
|
Tell user: "Last session we were working on [Objective]. We reached [Current Step]. Want to continue?"
|
||||||
|
WAIT for user confirmation before proceeding
|
||||||
|
ELSE:
|
||||||
|
Create memory/CURRENT_STATE.md with empty template (see below)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. When to update CURRENT_STATE.md (the discipline rules)
|
||||||
|
|
||||||
|
Update the file by **overwriting** it (not appending) at these moments:
|
||||||
|
|
||||||
|
| Trigger | Why |
|
||||||
|
|---|---|
|
||||||
|
| User confirms a decision | Decisions are the hardest thing to reconstruct |
|
||||||
|
| A task step completes | Marks progress so next session knows where to start |
|
||||||
|
| An error or blocker appears | Prevents the next session from hitting the same wall |
|
||||||
|
| Before any tool call that might take long | If gateway dies during the call, state is already saved |
|
||||||
|
| User says "let's stop here" or similar | Explicit save point |
|
||||||
|
| Every ~10 turns of substantive conversation | Periodic checkpoint against silent context loss |
|
||||||
|
|
||||||
|
**The update must be quick.** Write only what changed. The entire file should
|
||||||
|
stay under 40 lines. If you find yourself writing more, you are journaling,
|
||||||
|
not checkpointing. Stop and compress.
|
||||||
|
|
||||||
|
### 3. On `/new` or session end
|
||||||
|
|
||||||
|
Before the session closes:
|
||||||
|
|
||||||
|
1. Do a final overwrite of `memory/CURRENT_STATE.md` with latest state
|
||||||
|
2. Copy a timestamped snapshot to `memory/session_archive/YYYY-MM-DD_HH-MM.md`
|
||||||
|
3. The snapshot is a frozen record; CURRENT_STATE.md is the live workbench
|
||||||
|
|
||||||
|
If gateway crashes before you can do this, that is OK — the last mid-session
|
||||||
|
checkpoint in CURRENT_STATE.md is your recovery point. It will not be perfect,
|
||||||
|
but it will be vastly better than starting from zero.
|
||||||
|
|
||||||
|
### 4. Result surfacing rule
|
||||||
|
|
||||||
|
If you are a sub-agent or execution agent:
|
||||||
|
- Before exiting, write your key results into CURRENT_STATE.md under `## Unsurfaced Results`
|
||||||
|
- The main agent MUST check this section on startup and relay findings to the user
|
||||||
|
|
||||||
|
This prevents the #1 silent failure: sub-agent did the work, but nobody saw it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CURRENT_STATE.md Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Current State
|
||||||
|
> Last updated: [ISO timestamp]
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
[One sentence: what are we trying to accomplish]
|
||||||
|
|
||||||
|
## Current Step
|
||||||
|
[What step are we on, what was the last thing completed]
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
- [Decision 1: what was decided and why, max 3 items]
|
||||||
|
|
||||||
|
## Next Action
|
||||||
|
[Exactly what should happen next]
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
[What is preventing progress, or "None"]
|
||||||
|
|
||||||
|
## Unsurfaced Results
|
||||||
|
[Results from sub-agents or tools not yet shown to user, or "None"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rules for this template:**
|
||||||
|
- Every field is mandatory. Write "None" rather than omitting a section.
|
||||||
|
- `Objective` and `Next Action` are the two most critical fields. If you can
|
||||||
|
only save two things before a crash, save these.
|
||||||
|
- `Key Decisions` caps at 3 items. Older decisions belong in long-term memory,
|
||||||
|
not here.
|
||||||
|
- The entire file should be readable in 15 seconds. If it takes longer, trim it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Continuity Doctor (optional)
|
||||||
|
|
||||||
|
Run `scripts/continuity_doctor.py` to check workspace health:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/continuity_doctor.py --workspace /path/to/workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
It checks:
|
||||||
|
- Does `memory/CURRENT_STATE.md` exist?
|
||||||
|
- Is it stale (not updated in the last session)?
|
||||||
|
- Does `Objective` match any active tasks file?
|
||||||
|
- Are there `Unsurfaced Results` that were never addressed?
|
||||||
|
- Are there session archives without a corresponding state update?
|
||||||
|
|
||||||
|
The doctor **reports only** — it does not auto-repair. You decide what to fix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this skill is NOT
|
||||||
|
|
||||||
|
- Not a long-term memory system (use OpenClaw's native markdown memory or LanceDB for that)
|
||||||
|
- Not a conversation log or journal (CURRENT_STATE.md is overwritten, not appended)
|
||||||
|
- Not a task manager (use OpenSpec or tasks.md for project planning)
|
||||||
|
- Not dependent on any external database (works with plain files only)
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
- Works with any OpenClaw agent (main or sub-agent)
|
||||||
|
- No external dependencies (no npm install, no API keys, no database)
|
||||||
|
- Backup: `git add memory/` or `cp -r memory/ /backup/` — that is the entire disaster recovery plan
|
||||||
|
- Can coexist with memory-lancedb-pro, hippocampus, or any other memory skill
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Continuity Doctor — Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The continuity doctor is a diagnostic tool that checks whether the
|
||||||
|
memory-continuity protocol is being followed correctly in a workspace.
|
||||||
|
It reports problems but does **not** auto-fix them.
|
||||||
|
|
||||||
|
## Design philosophy
|
||||||
|
|
||||||
|
- **Diagnose, don't repair.** Automated repair of state files is dangerous
|
||||||
|
because incorrect "fixes" can overwrite valid state. The doctor flags
|
||||||
|
issues for human or agent review.
|
||||||
|
- **Fast and offline.** No API calls, no database queries. Reads files only.
|
||||||
|
- **Exit codes matter.** 0 = healthy, 1 = warnings found, 2 = critical issues.
|
||||||
|
|
||||||
|
## Checks performed
|
||||||
|
|
||||||
|
### 1. Existence check
|
||||||
|
- Does `memory/CURRENT_STATE.md` exist?
|
||||||
|
- Severity: CRITICAL if missing (no recovery possible)
|
||||||
|
|
||||||
|
### 2. Staleness check
|
||||||
|
- When was `CURRENT_STATE.md` last modified?
|
||||||
|
- If older than the most recent session transcript, it is stale.
|
||||||
|
- Severity: WARNING
|
||||||
|
|
||||||
|
### 3. Template compliance
|
||||||
|
- Does the file contain all mandatory sections?
|
||||||
|
(Objective, Current Step, Key Decisions, Next Action, Blockers, Unsurfaced Results)
|
||||||
|
- Are any sections still showing placeholder text like `[One sentence: ...]`?
|
||||||
|
- Severity: WARNING for missing sections, INFO for placeholder text
|
||||||
|
|
||||||
|
### 4. Unsurfaced results
|
||||||
|
- Is the `Unsurfaced Results` section non-empty?
|
||||||
|
- If yes, someone needs to review those results.
|
||||||
|
- Severity: WARNING
|
||||||
|
|
||||||
|
### 5. Archive consistency
|
||||||
|
- Are there files in `memory/session_archive/` ?
|
||||||
|
- Does the newest archive have a different Objective than CURRENT_STATE.md?
|
||||||
|
(This is expected if the user switched tasks, but worth flagging.)
|
||||||
|
- Severity: INFO
|
||||||
|
|
||||||
|
### 6. Conflict detection (optional, if tasks file exists)
|
||||||
|
- If a `tasks.md` or `openspec/` directory exists, does the Objective in
|
||||||
|
CURRENT_STATE.md align with any active task?
|
||||||
|
- Severity: INFO (alignment is nice-to-have, not mandatory)
|
||||||
|
|
||||||
|
## Output format
|
||||||
|
|
||||||
|
```
|
||||||
|
[CRITICAL] memory/CURRENT_STATE.md does not exist
|
||||||
|
[WARNING] CURRENT_STATE.md is stale (last modified 2h ago, session ran 30m ago)
|
||||||
|
[WARNING] Unsurfaced Results section is not empty — review needed
|
||||||
|
[INFO] Archive objective differs from current objective (task switch?)
|
||||||
|
[OK] Template compliance: all sections present
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future extensions (not yet implemented)
|
||||||
|
|
||||||
|
- Multi-workspace scan (check all sub-agent workspaces)
|
||||||
|
- JSON output mode for programmatic consumption
|
||||||
|
- Integration with OpenClaw cron for scheduled health checks
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Current State
|
||||||
|
> Last updated: [ISO timestamp]
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
[One sentence: what are we trying to accomplish]
|
||||||
|
|
||||||
|
## Current Step
|
||||||
|
[What step are we on, what was the last thing completed]
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
- None yet
|
||||||
|
|
||||||
|
## Next Action
|
||||||
|
[Exactly what should happen next]
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
None
|
||||||
|
|
||||||
|
## Unsurfaced Results
|
||||||
|
None
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
continuity_doctor.py — Diagnostic tool for memory-continuity skill.
|
||||||
|
|
||||||
|
Checks workspace health and reports issues. Does NOT auto-repair.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 continuity_doctor.py --workspace /path/to/workspace
|
||||||
|
python3 continuity_doctor.py --workspace ~/.openclaw/workspace/main
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Severity levels
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class Severity:
|
||||||
|
OK = "OK"
|
||||||
|
INFO = "INFO"
|
||||||
|
WARNING = "WARNING"
|
||||||
|
CRITICAL = "CRITICAL"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Collector
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class DiagnosticReport:
|
||||||
|
def __init__(self):
|
||||||
|
self.entries: list[tuple[str, str]] = []
|
||||||
|
self._worst = Severity.OK
|
||||||
|
|
||||||
|
def add(self, severity: str, message: str):
|
||||||
|
self.entries.append((severity, message))
|
||||||
|
rank = {Severity.OK: 0, Severity.INFO: 1, Severity.WARNING: 2, Severity.CRITICAL: 3}
|
||||||
|
if rank.get(severity, 0) > rank.get(self._worst, 0):
|
||||||
|
self._worst = severity
|
||||||
|
|
||||||
|
def print_report(self):
|
||||||
|
for severity, message in self.entries:
|
||||||
|
tag = f"[{severity}]".ljust(12)
|
||||||
|
print(f"{tag}{message}")
|
||||||
|
print()
|
||||||
|
print(f"Overall status: {self._worst}")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exit_code(self) -> int:
|
||||||
|
if self._worst == Severity.CRITICAL:
|
||||||
|
return 2
|
||||||
|
if self._worst == Severity.WARNING:
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Required sections in CURRENT_STATE.md
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
REQUIRED_SECTIONS = [
|
||||||
|
"Objective",
|
||||||
|
"Current Step",
|
||||||
|
"Key Decisions",
|
||||||
|
"Next Action",
|
||||||
|
"Blockers",
|
||||||
|
"Unsurfaced Results",
|
||||||
|
]
|
||||||
|
|
||||||
|
PLACEHOLDER_PATTERNS = [
|
||||||
|
r"\[.*?\]", # anything in square brackets like [One sentence: ...]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Checks
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def check_existence(workspace: Path, report: DiagnosticReport) -> Path | None:
|
||||||
|
"""Check that memory/CURRENT_STATE.md exists."""
|
||||||
|
state_file = workspace / "memory" / "CURRENT_STATE.md"
|
||||||
|
if not state_file.exists():
|
||||||
|
report.add(Severity.CRITICAL, "memory/CURRENT_STATE.md does not exist")
|
||||||
|
return None
|
||||||
|
report.add(Severity.OK, "memory/CURRENT_STATE.md exists")
|
||||||
|
return state_file
|
||||||
|
|
||||||
|
|
||||||
|
def check_staleness(state_file: Path, workspace: Path, report: DiagnosticReport):
|
||||||
|
"""Check if the state file is older than the most recent activity."""
|
||||||
|
mtime = datetime.fromtimestamp(state_file.stat().st_mtime, tz=timezone.utc)
|
||||||
|
age_seconds = (datetime.now(timezone.utc) - mtime).total_seconds()
|
||||||
|
age_hours = age_seconds / 3600
|
||||||
|
|
||||||
|
if age_hours > 24:
|
||||||
|
report.add(
|
||||||
|
Severity.WARNING,
|
||||||
|
f"CURRENT_STATE.md is stale (last modified {age_hours:.1f}h ago)",
|
||||||
|
)
|
||||||
|
elif age_hours > 4:
|
||||||
|
report.add(
|
||||||
|
Severity.INFO,
|
||||||
|
f"CURRENT_STATE.md last modified {age_hours:.1f}h ago",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
report.add(Severity.OK, f"CURRENT_STATE.md is fresh ({age_hours:.1f}h old)")
|
||||||
|
|
||||||
|
|
||||||
|
def check_template_compliance(state_file: Path, report: DiagnosticReport) -> dict:
|
||||||
|
"""Check all required sections are present and not placeholder-only."""
|
||||||
|
content = state_file.read_text(encoding="utf-8")
|
||||||
|
sections_found: dict[str, str] = {}
|
||||||
|
|
||||||
|
for section in REQUIRED_SECTIONS:
|
||||||
|
# Match ## Section or ## Section\n
|
||||||
|
pattern = rf"##\s+{re.escape(section)}\s*\n(.*?)(?=\n##\s|\Z)"
|
||||||
|
match = re.search(pattern, content, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
report.add(Severity.WARNING, f"Missing section: ## {section}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
body = match.group(1).strip()
|
||||||
|
sections_found[section] = body
|
||||||
|
|
||||||
|
# Check for placeholder text
|
||||||
|
if body and all(re.fullmatch(p, body) for p in PLACEHOLDER_PATTERNS):
|
||||||
|
report.add(Severity.INFO, f"Section '{section}' still contains placeholder text")
|
||||||
|
|
||||||
|
if len(sections_found) == len(REQUIRED_SECTIONS):
|
||||||
|
report.add(Severity.OK, "Template compliance: all sections present")
|
||||||
|
|
||||||
|
return sections_found
|
||||||
|
|
||||||
|
|
||||||
|
def check_unsurfaced_results(sections: dict, report: DiagnosticReport):
|
||||||
|
"""Check if there are unsurfaced results that need attention."""
|
||||||
|
results = sections.get("Unsurfaced Results", "").strip().lower()
|
||||||
|
if results and results != "none":
|
||||||
|
report.add(
|
||||||
|
Severity.WARNING,
|
||||||
|
"Unsurfaced Results section is not empty — review needed",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
report.add(Severity.OK, "No unsurfaced results pending")
|
||||||
|
|
||||||
|
|
||||||
|
def check_archive(workspace: Path, sections: dict, report: DiagnosticReport):
|
||||||
|
"""Check session archive consistency."""
|
||||||
|
archive_dir = workspace / "memory" / "session_archive"
|
||||||
|
|
||||||
|
if not archive_dir.exists() or not list(archive_dir.glob("*.md")):
|
||||||
|
report.add(Severity.INFO, "No session archives found (first session?)")
|
||||||
|
return
|
||||||
|
|
||||||
|
archives = sorted(archive_dir.glob("*.md"))
|
||||||
|
latest_archive = archives[-1]
|
||||||
|
report.add(Severity.OK, f"Found {len(archives)} session archive(s), latest: {latest_archive.name}")
|
||||||
|
|
||||||
|
# Compare objectives
|
||||||
|
current_objective = sections.get("Objective", "").strip()
|
||||||
|
archive_content = latest_archive.read_text(encoding="utf-8")
|
||||||
|
obj_match = re.search(r"##\s+Objective\s*\n(.*?)(?=\n##\s|\Z)", archive_content, re.DOTALL)
|
||||||
|
if obj_match:
|
||||||
|
archive_objective = obj_match.group(1).strip()
|
||||||
|
if archive_objective != current_objective and current_objective:
|
||||||
|
report.add(
|
||||||
|
Severity.INFO,
|
||||||
|
"Archive objective differs from current objective (task switch?)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_tasks_alignment(workspace: Path, sections: dict, report: DiagnosticReport):
|
||||||
|
"""Optional: check if objective aligns with tasks.md if it exists."""
|
||||||
|
tasks_file = workspace / "tasks.md"
|
||||||
|
if not tasks_file.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
objective = sections.get("Objective", "").strip().lower()
|
||||||
|
if not objective:
|
||||||
|
return
|
||||||
|
|
||||||
|
tasks_content = tasks_file.read_text(encoding="utf-8").lower()
|
||||||
|
# Very rough heuristic: check if any significant word from objective appears in tasks
|
||||||
|
words = [w for w in objective.split() if len(w) > 4]
|
||||||
|
matches = sum(1 for w in words if w in tasks_content)
|
||||||
|
if words and matches == 0:
|
||||||
|
report.add(
|
||||||
|
Severity.INFO,
|
||||||
|
"Objective does not seem to match any content in tasks.md",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def run_doctor(workspace_path: str) -> int:
|
||||||
|
workspace = Path(workspace_path).expanduser().resolve()
|
||||||
|
report = DiagnosticReport()
|
||||||
|
|
||||||
|
print(f"Continuity Doctor — scanning: {workspace}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if not workspace.exists():
|
||||||
|
report.add(Severity.CRITICAL, f"Workspace does not exist: {workspace}")
|
||||||
|
report.print_report()
|
||||||
|
return report.exit_code
|
||||||
|
|
||||||
|
# Run all checks
|
||||||
|
state_file = check_existence(workspace, report)
|
||||||
|
|
||||||
|
if state_file:
|
||||||
|
check_staleness(state_file, workspace, report)
|
||||||
|
sections = check_template_compliance(state_file, report)
|
||||||
|
check_unsurfaced_results(sections, report)
|
||||||
|
check_archive(workspace, sections, report)
|
||||||
|
check_tasks_alignment(workspace, sections, report)
|
||||||
|
|
||||||
|
print()
|
||||||
|
report.print_report()
|
||||||
|
return report.exit_code
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Continuity Doctor — diagnose memory-continuity health"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--workspace",
|
||||||
|
required=True,
|
||||||
|
help="Path to the OpenClaw workspace to check",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
sys.exit(run_doctor(args.workspace))
|
||||||
Reference in New Issue
Block a user