session-glue 0.1.0__py3-none-any.whl
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.
- session_glue/__init__.py +11 -0
- session_glue/__main__.py +8 -0
- session_glue/cli.py +275 -0
- session_glue/installer.py +92 -0
- session_glue/reader.py +69 -0
- session_glue/schema.py +404 -0
- session_glue/validator.py +100 -0
- session_glue/writer.py +181 -0
- session_glue-0.1.0.dist-info/METADATA +219 -0
- session_glue-0.1.0.dist-info/RECORD +13 -0
- session_glue-0.1.0.dist-info/WHEEL +4 -0
- session_glue-0.1.0.dist-info/entry_points.txt +3 -0
- session_glue-0.1.0.dist-info/licenses/LICENSE +21 -0
session_glue/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Session Glue.
|
|
2
|
+
|
|
3
|
+
A zero-daemon, repo-local session-continuity protocol and CLI for coding agents.
|
|
4
|
+
|
|
5
|
+
Cut a bloated coding-agent session at a clean boundary, then glue the useful
|
|
6
|
+
context onto the next session.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
__all__ = ["__version__"]
|
session_glue/__main__.py
ADDED
session_glue/cli.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Command-line entry point for Session Glue.
|
|
2
|
+
|
|
3
|
+
This module wires up the ``glue`` (and fallback ``session-glue``) console
|
|
4
|
+
scripts. It implements ``glue create``, ``glue validate``, ``glue status``,
|
|
5
|
+
``glue resume-prompt``, and ``glue install --dry-run`` (see
|
|
6
|
+
:mod:`session_glue.writer`, :mod:`session_glue.validator`,
|
|
7
|
+
:mod:`session_glue.reader`, and :mod:`session_glue.installer`). The CLI is built
|
|
8
|
+
on :mod:`argparse` from the standard library so the package has no required
|
|
9
|
+
runtime dependencies.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from collections.abc import Sequence
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from . import __version__, installer, reader, validator, writer
|
|
20
|
+
from .schema import Handoff, HandoffParseError, parse_frontmatter
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
24
|
+
"""Construct the top-level argument parser for the ``glue`` CLI."""
|
|
25
|
+
parser = argparse.ArgumentParser(
|
|
26
|
+
prog="glue",
|
|
27
|
+
description=(
|
|
28
|
+
"Session Glue — cut a bloated coding-agent session at a clean "
|
|
29
|
+
"boundary and glue the useful context onto a clean new session."
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--version",
|
|
34
|
+
action="version",
|
|
35
|
+
version=f"session-glue {__version__}",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
subparsers = parser.add_subparsers(dest="command", metavar="<command>")
|
|
39
|
+
|
|
40
|
+
create = subparsers.add_parser(
|
|
41
|
+
"create",
|
|
42
|
+
help="Archive a handoff into .agent-history/ and update pointers.",
|
|
43
|
+
description=(
|
|
44
|
+
"Write an agent-composed handoff (frontmatter + body) into "
|
|
45
|
+
".agent-history/: archive a timestamped session file, refresh "
|
|
46
|
+
"LATEST.md, RESUME_PROMPT.txt, and INDEX.yaml."
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
create.add_argument(
|
|
50
|
+
"--input",
|
|
51
|
+
"-i",
|
|
52
|
+
default="-",
|
|
53
|
+
metavar="PATH",
|
|
54
|
+
help="Handoff markdown file (frontmatter + body). Use '-' for stdin (default).",
|
|
55
|
+
)
|
|
56
|
+
create.add_argument(
|
|
57
|
+
"--repo-root",
|
|
58
|
+
default=".",
|
|
59
|
+
metavar="PATH",
|
|
60
|
+
help="Repository root that holds .agent-history/ (default: current directory).",
|
|
61
|
+
)
|
|
62
|
+
create.set_defaults(func=_cmd_create)
|
|
63
|
+
|
|
64
|
+
validate = subparsers.add_parser(
|
|
65
|
+
"validate",
|
|
66
|
+
help="Validate .agent-history/ handoff artifacts.",
|
|
67
|
+
description=(
|
|
68
|
+
"Validate LATEST.md frontmatter, the next_todo_items[0] lint, "
|
|
69
|
+
"RESUME_PROMPT.txt presence, and INDEX.yaml consistency. Exits "
|
|
70
|
+
"non-zero if any problem is found."
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
validate.add_argument(
|
|
74
|
+
"--repo-root",
|
|
75
|
+
default=".",
|
|
76
|
+
metavar="PATH",
|
|
77
|
+
help="Repository root that holds .agent-history/ (default: current directory).",
|
|
78
|
+
)
|
|
79
|
+
validate.add_argument(
|
|
80
|
+
"--sessions",
|
|
81
|
+
action="store_true",
|
|
82
|
+
help="Also validate archived session files under sessions/.",
|
|
83
|
+
)
|
|
84
|
+
validate.set_defaults(func=_cmd_validate)
|
|
85
|
+
|
|
86
|
+
status = subparsers.add_parser(
|
|
87
|
+
"status",
|
|
88
|
+
help="Show compact .agent-history/ status.",
|
|
89
|
+
description=(
|
|
90
|
+
"Print compact metadata from INDEX.yaml (latest session/file, "
|
|
91
|
+
"branch, head commit, first next action) and a cheap validation "
|
|
92
|
+
"summary. Does not print the full session narrative."
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
status.add_argument(
|
|
96
|
+
"--repo-root",
|
|
97
|
+
default=".",
|
|
98
|
+
metavar="PATH",
|
|
99
|
+
help="Repository root that holds .agent-history/ (default: current directory).",
|
|
100
|
+
)
|
|
101
|
+
status.set_defaults(func=_cmd_status)
|
|
102
|
+
|
|
103
|
+
resume_prompt = subparsers.add_parser(
|
|
104
|
+
"resume-prompt",
|
|
105
|
+
help="Print .agent-history/RESUME_PROMPT.txt exactly.",
|
|
106
|
+
description="Print the exact contents of .agent-history/RESUME_PROMPT.txt.",
|
|
107
|
+
)
|
|
108
|
+
resume_prompt.add_argument(
|
|
109
|
+
"--repo-root",
|
|
110
|
+
default=".",
|
|
111
|
+
metavar="PATH",
|
|
112
|
+
help="Repository root that holds .agent-history/ (default: current directory).",
|
|
113
|
+
)
|
|
114
|
+
resume_prompt.set_defaults(func=_cmd_resume_prompt)
|
|
115
|
+
|
|
116
|
+
install = subparsers.add_parser(
|
|
117
|
+
"install",
|
|
118
|
+
help="Show the managed instruction block for a coding agent (dry-run only).",
|
|
119
|
+
description=(
|
|
120
|
+
"Dry-run only: print the target instruction file and the managed "
|
|
121
|
+
"Session Glue block that would be added for a coding agent. Never "
|
|
122
|
+
"modifies user-home files; real installation is not implemented "
|
|
123
|
+
"(operator-gated)."
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
install.add_argument(
|
|
127
|
+
"agent",
|
|
128
|
+
choices=(*installer.AGENT_ORDER, "all"),
|
|
129
|
+
help="Coding agent to show the install block for (or 'all').",
|
|
130
|
+
)
|
|
131
|
+
install.add_argument(
|
|
132
|
+
"--dry-run",
|
|
133
|
+
action="store_true",
|
|
134
|
+
help="Required: print the target path and proposed block without writing.",
|
|
135
|
+
)
|
|
136
|
+
install.set_defaults(func=_cmd_install)
|
|
137
|
+
|
|
138
|
+
return parser
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _read_input(source: str) -> str:
|
|
142
|
+
"""Read handoff text from a file path or stdin (``-``)."""
|
|
143
|
+
if source == "-":
|
|
144
|
+
return sys.stdin.read()
|
|
145
|
+
return Path(source).read_text(encoding="utf-8")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _cmd_create(args: argparse.Namespace) -> int:
|
|
149
|
+
"""Implement ``glue create``."""
|
|
150
|
+
try:
|
|
151
|
+
text = _read_input(args.input)
|
|
152
|
+
except OSError as exc:
|
|
153
|
+
print(f"glue create: cannot read input: {exc}", file=sys.stderr)
|
|
154
|
+
return 1
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
frontmatter, body = parse_frontmatter(text)
|
|
158
|
+
except HandoffParseError as exc:
|
|
159
|
+
print(f"glue create: invalid handoff: {exc}", file=sys.stderr)
|
|
160
|
+
return 2
|
|
161
|
+
|
|
162
|
+
handoff = Handoff.from_frontmatter(frontmatter, body)
|
|
163
|
+
errors = handoff.validate()
|
|
164
|
+
if errors:
|
|
165
|
+
print("glue create: handoff failed validation:", file=sys.stderr)
|
|
166
|
+
for error in errors:
|
|
167
|
+
print(f" - {error}", file=sys.stderr)
|
|
168
|
+
return 2
|
|
169
|
+
|
|
170
|
+
repo_root = Path(args.repo_root)
|
|
171
|
+
if not repo_root.is_dir():
|
|
172
|
+
print(f"glue create: repo root is not a directory: {repo_root}", file=sys.stderr)
|
|
173
|
+
return 1
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
written = writer.create_handoff(
|
|
177
|
+
repo_root=repo_root,
|
|
178
|
+
frontmatter=frontmatter,
|
|
179
|
+
body=body,
|
|
180
|
+
handoff=handoff,
|
|
181
|
+
)
|
|
182
|
+
except writer.HandoffWriteError as exc:
|
|
183
|
+
print(f"glue create: {exc}", file=sys.stderr)
|
|
184
|
+
return 1
|
|
185
|
+
print("Wrote handoff for session " + str(handoff.session_id) + ":")
|
|
186
|
+
for label in ("archive", "latest", "resume_prompt", "index"):
|
|
187
|
+
print(f" {label}: {written[label]}")
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _cmd_validate(args: argparse.Namespace) -> int:
|
|
192
|
+
"""Implement ``glue validate``."""
|
|
193
|
+
problems = validator.validate_history(
|
|
194
|
+
Path(args.repo_root), check_sessions=args.sessions
|
|
195
|
+
)
|
|
196
|
+
if problems:
|
|
197
|
+
print("glue validate: found problems:", file=sys.stderr)
|
|
198
|
+
for problem in problems:
|
|
199
|
+
print(f" - {problem}", file=sys.stderr)
|
|
200
|
+
return 1
|
|
201
|
+
print("glue validate: OK")
|
|
202
|
+
return 0
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _cmd_status(args: argparse.Namespace) -> int:
|
|
206
|
+
"""Implement ``glue status``."""
|
|
207
|
+
status = reader.collect_status(Path(args.repo_root))
|
|
208
|
+
if not status.exists:
|
|
209
|
+
print(f"glue status: no .agent-history/ found at {status.history_dir}")
|
|
210
|
+
return 1
|
|
211
|
+
|
|
212
|
+
print(".agent-history: present")
|
|
213
|
+
index = status.index or {}
|
|
214
|
+
for label, key in reader.STATUS_FIELDS:
|
|
215
|
+
value = index.get(key)
|
|
216
|
+
print(f"{label}: {value if value not in (None, '') else '(unknown)'}")
|
|
217
|
+
|
|
218
|
+
if status.problems:
|
|
219
|
+
print(f"validation: {len(status.problems)} problem(s)")
|
|
220
|
+
for problem in status.problems:
|
|
221
|
+
print(f" - {problem}")
|
|
222
|
+
else:
|
|
223
|
+
print("validation: OK")
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _cmd_resume_prompt(args: argparse.Namespace) -> int:
|
|
228
|
+
"""Implement ``glue resume-prompt``."""
|
|
229
|
+
text = reader.read_resume_prompt(Path(args.repo_root))
|
|
230
|
+
if text is None:
|
|
231
|
+
print("glue resume-prompt: no .agent-history/RESUME_PROMPT.txt found", file=sys.stderr)
|
|
232
|
+
return 1
|
|
233
|
+
# Print the file contents exactly (no added or stripped trailing newline).
|
|
234
|
+
sys.stdout.write(text)
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _cmd_install(args: argparse.Namespace) -> int:
|
|
239
|
+
"""Implement ``glue install <agent> --dry-run`` (dry-run only)."""
|
|
240
|
+
if not args.dry_run:
|
|
241
|
+
print(
|
|
242
|
+
"glue install: only --dry-run is supported; real installation is "
|
|
243
|
+
"operator-gated and not implemented. Re-run with --dry-run.",
|
|
244
|
+
file=sys.stderr,
|
|
245
|
+
)
|
|
246
|
+
return 2
|
|
247
|
+
|
|
248
|
+
block = installer.managed_block()
|
|
249
|
+
for target in installer.resolve_agents(args.agent):
|
|
250
|
+
print(f"# {target.name} (dry-run — no files modified)")
|
|
251
|
+
print(f"target: {target.target}")
|
|
252
|
+
if target.note:
|
|
253
|
+
print(f"note: {target.note}")
|
|
254
|
+
print("proposed block:")
|
|
255
|
+
print(block)
|
|
256
|
+
print()
|
|
257
|
+
return 0
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
261
|
+
"""Program entry point. Returns the process exit code."""
|
|
262
|
+
parser = build_parser()
|
|
263
|
+
args = parser.parse_args(argv)
|
|
264
|
+
|
|
265
|
+
handler = getattr(args, "func", None)
|
|
266
|
+
if handler is None:
|
|
267
|
+
# No subcommand given: show help and exit successfully.
|
|
268
|
+
parser.print_help()
|
|
269
|
+
return 0
|
|
270
|
+
|
|
271
|
+
return handler(args)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
if __name__ == "__main__": # pragma: no cover
|
|
275
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Dry-run installer blocks for ``glue install <agent> --dry-run``.
|
|
2
|
+
|
|
3
|
+
This module only *describes* what Session Glue would add to a coding agent's
|
|
4
|
+
global instruction file — it renders the managed instruction block and the
|
|
5
|
+
target path. It never mutates user-home files, accesses the network or OS
|
|
6
|
+
clipboard, or performs a real install. Non-dry-run installation is intentionally
|
|
7
|
+
not implemented here (operator-gated).
|
|
8
|
+
|
|
9
|
+
The managed block is delimited by stable begin/end markers so a future updater
|
|
10
|
+
can replace it idempotently, and so existing installs can be detected.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
# Stable managed-block markers. These are part of the on-disk contract: keep
|
|
18
|
+
# them byte-for-byte stable so idempotent updates and detection keep working.
|
|
19
|
+
BEGIN_MARKER = "<!-- BEGIN SESSION GLUE (managed) -->"
|
|
20
|
+
END_MARKER = "<!-- END SESSION GLUE (managed) -->"
|
|
21
|
+
|
|
22
|
+
# Agent-agnostic body of the managed block. Tells any file-writing coding agent
|
|
23
|
+
# how to respond to the glue triggers while preserving the v1 constraints.
|
|
24
|
+
_BLOCK_BODY = """\
|
|
25
|
+
## Session Glue
|
|
26
|
+
|
|
27
|
+
When the user runs `/glue`, `/freeze`, `/handoff`, or `/checkpoint`, or asks in
|
|
28
|
+
natural language to glue / freeze / checkpoint / hand off the current session:
|
|
29
|
+
|
|
30
|
+
1. Compose a handoff document: YAML frontmatter (session_id, session_date,
|
|
31
|
+
generated_at, schema_version, project_root, repo_root, current_branch,
|
|
32
|
+
head_commit, agent, status, active_context_files, completed_tasks,
|
|
33
|
+
next_todo_items, known_issues) plus a narrative body covering what changed,
|
|
34
|
+
what is still broken, and what to do next. `next_todo_items[0]` MUST be the
|
|
35
|
+
first productive work item, never a resume mechanic (do not write "read
|
|
36
|
+
LATEST.md", "paste the prompt", "start a new session", etc.).
|
|
37
|
+
2. Run `glue create` to write the handoff into the repository-local
|
|
38
|
+
`.agent-history/` directory (LATEST.md, RESUME_PROMPT.txt, INDEX.yaml, and
|
|
39
|
+
sessions/<id>.md). Optionally run `glue validate` to check it.
|
|
40
|
+
3. Print the contents of `.agent-history/RESUME_PROMPT.txt` in a fenced code
|
|
41
|
+
block so the operator can copy it into a fresh session.
|
|
42
|
+
|
|
43
|
+
Constraints:
|
|
44
|
+
- Write only the repository-local `.agent-history/` directory.
|
|
45
|
+
- Do not access the OS clipboard.
|
|
46
|
+
- Do not run a daemon, watcher, or any external/network service."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class AgentTarget:
|
|
51
|
+
"""Where a given agent's global instruction block lives (display only)."""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
target: str
|
|
55
|
+
note: str = ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Target instruction files per agent. Paths are shown in ``~`` form and are
|
|
59
|
+
# never opened or written during dry-run.
|
|
60
|
+
AGENT_TARGETS: dict[str, AgentTarget] = {
|
|
61
|
+
"codex": AgentTarget("codex", "~/.codex/AGENTS.md"),
|
|
62
|
+
"claude": AgentTarget("claude", "~/.claude/commands/glue.md"),
|
|
63
|
+
"cursor": AgentTarget(
|
|
64
|
+
"cursor",
|
|
65
|
+
"~/.cursor/rules/session-glue.md",
|
|
66
|
+
note="or paste the block into Cursor Settings -> Rules for AI",
|
|
67
|
+
),
|
|
68
|
+
"gemini": AgentTarget("gemini", "~/.gemini/GEMINI.md"),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Order used when installing "all".
|
|
72
|
+
AGENT_ORDER: tuple[str, ...] = ("codex", "claude", "cursor", "gemini")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def managed_block() -> str:
|
|
76
|
+
"""Return the full managed instruction block, including begin/end markers."""
|
|
77
|
+
return f"{BEGIN_MARKER}\n{_BLOCK_BODY}\n{END_MARKER}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def has_managed_block(text: str) -> bool:
|
|
81
|
+
"""Return True if ``text`` already contains a Session Glue managed block."""
|
|
82
|
+
return BEGIN_MARKER in text and END_MARKER in text
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def resolve_agents(agent: str) -> list[AgentTarget]:
|
|
86
|
+
"""Resolve an agent name (or ``all``) to a list of targets.
|
|
87
|
+
|
|
88
|
+
Raises ``KeyError`` for an unknown agent name.
|
|
89
|
+
"""
|
|
90
|
+
if agent == "all":
|
|
91
|
+
return [AGENT_TARGETS[name] for name in AGENT_ORDER]
|
|
92
|
+
return [AGENT_TARGETS[agent]]
|
session_glue/reader.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Read-only helpers for ``glue status`` and ``glue resume-prompt``.
|
|
2
|
+
|
|
3
|
+
These commands orient the operator without recreating context bloat: ``status``
|
|
4
|
+
reports compact metadata from ``INDEX.yaml`` plus a cheap validation summary —
|
|
5
|
+
it deliberately does **not** read or print the full session narrative — and
|
|
6
|
+
``resume-prompt`` returns the exact contents of ``RESUME_PROMPT.txt``. Both are
|
|
7
|
+
strictly read-only: no writes, no network, no subprocess.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .schema import HandoffParseError, parse_mapping
|
|
17
|
+
from .validator import validate_history
|
|
18
|
+
from .writer import AGENT_HISTORY_DIRNAME, INDEX_FILENAME, RESUME_PROMPT_FILENAME
|
|
19
|
+
|
|
20
|
+
# Compact fields surfaced by ``glue status``, in display order, mapped from
|
|
21
|
+
# their ``INDEX.yaml`` keys. The full ``next_todo_items`` narrative is never
|
|
22
|
+
# included (token-economics requirement).
|
|
23
|
+
STATUS_FIELDS: tuple[tuple[str, str], ...] = (
|
|
24
|
+
("latest session", "latest_session"),
|
|
25
|
+
("latest file", "latest_file"),
|
|
26
|
+
("current branch", "current_branch"),
|
|
27
|
+
("head commit", "head_commit"),
|
|
28
|
+
("first next action", "first_next_action"),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Status:
|
|
34
|
+
"""A compact, read-only snapshot of ``.agent-history/`` state."""
|
|
35
|
+
|
|
36
|
+
exists: bool
|
|
37
|
+
history_dir: Path
|
|
38
|
+
index: dict[str, Any] | None = None
|
|
39
|
+
problems: list[str] = field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def collect_status(repo_root: Path, run_validation: bool = True) -> Status:
|
|
43
|
+
"""Gather compact status for ``<repo_root>/.agent-history/``.
|
|
44
|
+
|
|
45
|
+
Reads only ``INDEX.yaml`` (plus a cheap, non-``--sessions`` validation pass
|
|
46
|
+
when ``run_validation`` is true). Never reads archived session narratives.
|
|
47
|
+
"""
|
|
48
|
+
history = Path(repo_root) / AGENT_HISTORY_DIRNAME
|
|
49
|
+
if not history.is_dir():
|
|
50
|
+
return Status(exists=False, history_dir=history)
|
|
51
|
+
|
|
52
|
+
index: dict[str, Any] | None = None
|
|
53
|
+
index_path = history / INDEX_FILENAME
|
|
54
|
+
if index_path.is_file():
|
|
55
|
+
try:
|
|
56
|
+
index = parse_mapping(index_path.read_text(encoding="utf-8"))
|
|
57
|
+
except HandoffParseError:
|
|
58
|
+
index = None
|
|
59
|
+
|
|
60
|
+
problems = validate_history(repo_root) if run_validation else []
|
|
61
|
+
return Status(exists=True, history_dir=history, index=index, problems=problems)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def read_resume_prompt(repo_root: Path) -> str | None:
|
|
65
|
+
"""Return the exact contents of ``RESUME_PROMPT.txt``, or None if missing."""
|
|
66
|
+
path = Path(repo_root) / AGENT_HISTORY_DIRNAME / RESUME_PROMPT_FILENAME
|
|
67
|
+
if not path.is_file():
|
|
68
|
+
return None
|
|
69
|
+
return path.read_text(encoding="utf-8")
|