raise-cli 2.2.1__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.
- raise_cli/__init__.py +38 -0
- raise_cli/__main__.py +30 -0
- raise_cli/adapters/__init__.py +91 -0
- raise_cli/adapters/declarative/__init__.py +26 -0
- raise_cli/adapters/declarative/adapter.py +267 -0
- raise_cli/adapters/declarative/discovery.py +94 -0
- raise_cli/adapters/declarative/expressions.py +150 -0
- raise_cli/adapters/declarative/reference/__init__.py +1 -0
- raise_cli/adapters/declarative/reference/github.yaml +143 -0
- raise_cli/adapters/declarative/schema.py +98 -0
- raise_cli/adapters/filesystem.py +299 -0
- raise_cli/adapters/mcp_bridge.py +10 -0
- raise_cli/adapters/mcp_confluence.py +246 -0
- raise_cli/adapters/mcp_jira.py +405 -0
- raise_cli/adapters/models.py +205 -0
- raise_cli/adapters/protocols.py +180 -0
- raise_cli/adapters/registry.py +90 -0
- raise_cli/adapters/sync.py +149 -0
- raise_cli/agents/__init__.py +14 -0
- raise_cli/agents/antigravity.yaml +8 -0
- raise_cli/agents/claude.yaml +8 -0
- raise_cli/agents/copilot.yaml +8 -0
- raise_cli/agents/copilot_plugin.py +124 -0
- raise_cli/agents/cursor.yaml +7 -0
- raise_cli/agents/roo.yaml +8 -0
- raise_cli/agents/windsurf.yaml +8 -0
- raise_cli/artifacts/__init__.py +30 -0
- raise_cli/artifacts/models.py +43 -0
- raise_cli/artifacts/reader.py +55 -0
- raise_cli/artifacts/renderer.py +104 -0
- raise_cli/artifacts/story_design.py +69 -0
- raise_cli/artifacts/writer.py +45 -0
- raise_cli/backlog/__init__.py +1 -0
- raise_cli/backlog/sync.py +115 -0
- raise_cli/cli/__init__.py +3 -0
- raise_cli/cli/commands/__init__.py +3 -0
- raise_cli/cli/commands/_resolve.py +153 -0
- raise_cli/cli/commands/adapters.py +362 -0
- raise_cli/cli/commands/artifact.py +137 -0
- raise_cli/cli/commands/backlog.py +333 -0
- raise_cli/cli/commands/base.py +31 -0
- raise_cli/cli/commands/discover.py +551 -0
- raise_cli/cli/commands/docs.py +130 -0
- raise_cli/cli/commands/doctor.py +177 -0
- raise_cli/cli/commands/gate.py +223 -0
- raise_cli/cli/commands/graph.py +1086 -0
- raise_cli/cli/commands/info.py +81 -0
- raise_cli/cli/commands/init.py +746 -0
- raise_cli/cli/commands/journal.py +167 -0
- raise_cli/cli/commands/mcp.py +524 -0
- raise_cli/cli/commands/memory.py +467 -0
- raise_cli/cli/commands/pattern.py +348 -0
- raise_cli/cli/commands/profile.py +59 -0
- raise_cli/cli/commands/publish.py +80 -0
- raise_cli/cli/commands/release.py +338 -0
- raise_cli/cli/commands/session.py +528 -0
- raise_cli/cli/commands/signal.py +410 -0
- raise_cli/cli/commands/skill.py +350 -0
- raise_cli/cli/commands/skill_set.py +145 -0
- raise_cli/cli/error_handler.py +158 -0
- raise_cli/cli/main.py +163 -0
- raise_cli/compat.py +66 -0
- raise_cli/config/__init__.py +41 -0
- raise_cli/config/agent_plugin.py +105 -0
- raise_cli/config/agent_registry.py +233 -0
- raise_cli/config/agents.py +120 -0
- raise_cli/config/ide.py +32 -0
- raise_cli/config/paths.py +379 -0
- raise_cli/config/settings.py +180 -0
- raise_cli/context/__init__.py +42 -0
- raise_cli/context/analyzers/__init__.py +16 -0
- raise_cli/context/analyzers/models.py +36 -0
- raise_cli/context/analyzers/protocol.py +43 -0
- raise_cli/context/analyzers/python.py +292 -0
- raise_cli/context/builder.py +1569 -0
- raise_cli/context/diff.py +213 -0
- raise_cli/context/extractors/__init__.py +13 -0
- raise_cli/context/extractors/skills.py +121 -0
- raise_cli/core/__init__.py +37 -0
- raise_cli/core/files.py +66 -0
- raise_cli/core/text.py +174 -0
- raise_cli/core/tools.py +441 -0
- raise_cli/discovery/__init__.py +50 -0
- raise_cli/discovery/analyzer.py +691 -0
- raise_cli/discovery/drift.py +355 -0
- raise_cli/discovery/scanner.py +1687 -0
- raise_cli/doctor/__init__.py +4 -0
- raise_cli/doctor/checks/__init__.py +1 -0
- raise_cli/doctor/checks/environment.py +110 -0
- raise_cli/doctor/checks/project.py +238 -0
- raise_cli/doctor/fix.py +80 -0
- raise_cli/doctor/models.py +56 -0
- raise_cli/doctor/protocol.py +43 -0
- raise_cli/doctor/registry.py +100 -0
- raise_cli/doctor/report.py +141 -0
- raise_cli/doctor/runner.py +95 -0
- raise_cli/engines/__init__.py +3 -0
- raise_cli/exceptions.py +215 -0
- raise_cli/gates/__init__.py +19 -0
- raise_cli/gates/builtin/__init__.py +1 -0
- raise_cli/gates/builtin/coverage.py +52 -0
- raise_cli/gates/builtin/lint.py +48 -0
- raise_cli/gates/builtin/tests.py +48 -0
- raise_cli/gates/builtin/types.py +48 -0
- raise_cli/gates/models.py +40 -0
- raise_cli/gates/protocol.py +41 -0
- raise_cli/gates/registry.py +141 -0
- raise_cli/governance/__init__.py +11 -0
- raise_cli/governance/extractor.py +412 -0
- raise_cli/governance/models.py +134 -0
- raise_cli/governance/parsers/__init__.py +35 -0
- raise_cli/governance/parsers/_convert.py +38 -0
- raise_cli/governance/parsers/adr.py +274 -0
- raise_cli/governance/parsers/backlog.py +356 -0
- raise_cli/governance/parsers/constitution.py +119 -0
- raise_cli/governance/parsers/epic.py +323 -0
- raise_cli/governance/parsers/glossary.py +316 -0
- raise_cli/governance/parsers/guardrails.py +345 -0
- raise_cli/governance/parsers/prd.py +112 -0
- raise_cli/governance/parsers/roadmap.py +118 -0
- raise_cli/governance/parsers/vision.py +116 -0
- raise_cli/graph/__init__.py +1 -0
- raise_cli/graph/backends/__init__.py +57 -0
- raise_cli/graph/backends/api.py +137 -0
- raise_cli/graph/backends/dual.py +139 -0
- raise_cli/graph/backends/pending.py +84 -0
- raise_cli/handlers/__init__.py +3 -0
- raise_cli/hooks/__init__.py +54 -0
- raise_cli/hooks/builtin/__init__.py +1 -0
- raise_cli/hooks/builtin/backlog.py +216 -0
- raise_cli/hooks/builtin/gate_bridge.py +83 -0
- raise_cli/hooks/builtin/jira_sync.py +127 -0
- raise_cli/hooks/builtin/memory.py +117 -0
- raise_cli/hooks/builtin/telemetry.py +72 -0
- raise_cli/hooks/emitter.py +184 -0
- raise_cli/hooks/events.py +262 -0
- raise_cli/hooks/protocol.py +38 -0
- raise_cli/hooks/registry.py +117 -0
- raise_cli/mcp/__init__.py +33 -0
- raise_cli/mcp/bridge.py +218 -0
- raise_cli/mcp/models.py +43 -0
- raise_cli/mcp/registry.py +77 -0
- raise_cli/mcp/schema.py +41 -0
- raise_cli/memory/__init__.py +58 -0
- raise_cli/memory/loader.py +247 -0
- raise_cli/memory/migration.py +241 -0
- raise_cli/memory/models.py +169 -0
- raise_cli/memory/writer.py +598 -0
- raise_cli/onboarding/__init__.py +103 -0
- raise_cli/onboarding/bootstrap.py +324 -0
- raise_cli/onboarding/claudemd.py +17 -0
- raise_cli/onboarding/conventions.py +742 -0
- raise_cli/onboarding/detection.py +374 -0
- raise_cli/onboarding/governance.py +443 -0
- raise_cli/onboarding/instructions.py +672 -0
- raise_cli/onboarding/manifest.py +201 -0
- raise_cli/onboarding/memory_md.py +399 -0
- raise_cli/onboarding/migration.py +207 -0
- raise_cli/onboarding/profile.py +624 -0
- raise_cli/onboarding/skill_conflict.py +100 -0
- raise_cli/onboarding/skill_manifest.py +176 -0
- raise_cli/onboarding/skills.py +437 -0
- raise_cli/onboarding/workflows.py +101 -0
- raise_cli/output/__init__.py +28 -0
- raise_cli/output/console.py +394 -0
- raise_cli/output/formatters/__init__.py +9 -0
- raise_cli/output/formatters/adapters.py +135 -0
- raise_cli/output/formatters/discover.py +439 -0
- raise_cli/output/formatters/skill.py +298 -0
- raise_cli/publish/__init__.py +3 -0
- raise_cli/publish/changelog.py +80 -0
- raise_cli/publish/check.py +179 -0
- raise_cli/publish/version.py +172 -0
- raise_cli/rai_base/__init__.py +22 -0
- raise_cli/rai_base/framework/__init__.py +7 -0
- raise_cli/rai_base/framework/methodology.yaml +233 -0
- raise_cli/rai_base/governance/__init__.py +1 -0
- raise_cli/rai_base/governance/architecture/__init__.py +1 -0
- raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
- raise_cli/rai_base/governance/architecture/system-context.md +34 -0
- raise_cli/rai_base/governance/architecture/system-design.md +24 -0
- raise_cli/rai_base/governance/backlog.md +8 -0
- raise_cli/rai_base/governance/guardrails.md +17 -0
- raise_cli/rai_base/governance/prd.md +25 -0
- raise_cli/rai_base/governance/vision.md +16 -0
- raise_cli/rai_base/identity/__init__.py +8 -0
- raise_cli/rai_base/identity/core.md +119 -0
- raise_cli/rai_base/identity/perspective.md +119 -0
- raise_cli/rai_base/memory/__init__.py +7 -0
- raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
- raise_cli/schemas/__init__.py +3 -0
- raise_cli/schemas/journal.py +49 -0
- raise_cli/schemas/session_state.py +117 -0
- raise_cli/session/__init__.py +5 -0
- raise_cli/session/bundle.py +820 -0
- raise_cli/session/close.py +268 -0
- raise_cli/session/journal.py +119 -0
- raise_cli/session/resolver.py +126 -0
- raise_cli/session/state.py +187 -0
- raise_cli/skills/__init__.py +44 -0
- raise_cli/skills/locator.py +141 -0
- raise_cli/skills/name_checker.py +199 -0
- raise_cli/skills/parser.py +145 -0
- raise_cli/skills/scaffold.py +212 -0
- raise_cli/skills/schema.py +132 -0
- raise_cli/skills/skillsets.py +195 -0
- raise_cli/skills/validator.py +197 -0
- raise_cli/skills_base/__init__.py +80 -0
- raise_cli/skills_base/contract-template.md +60 -0
- raise_cli/skills_base/preamble.md +37 -0
- raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
- raise_cli/skills_base/rai-debug/SKILL.md +171 -0
- raise_cli/skills_base/rai-discover/SKILL.md +167 -0
- raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
- raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
- raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
- raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
- raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
- raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
- raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
- raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
- raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
- raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
- raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
- raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
- raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
- raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
- raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
- raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
- raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
- raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
- raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
- raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
- raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
- raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
- raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
- raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
- raise_cli/skills_base/rai-research/SKILL.md +143 -0
- raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
- raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
- raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
- raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
- raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
- raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
- raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
- raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
- raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
- raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
- raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
- raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
- raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
- raise_cli/telemetry/__init__.py +42 -0
- raise_cli/telemetry/schemas.py +285 -0
- raise_cli/telemetry/writer.py +217 -0
- raise_cli/tier/__init__.py +0 -0
- raise_cli/tier/context.py +134 -0
- raise_cli/viz/__init__.py +7 -0
- raise_cli/viz/generator.py +406 -0
- raise_cli-2.2.1.dist-info/METADATA +433 -0
- raise_cli-2.2.1.dist-info/RECORD +264 -0
- raise_cli-2.2.1.dist-info/WHEEL +4 -0
- raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
- raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
- raise_cli-2.2.1.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
"""Context bundle assembly for session start.
|
|
2
|
+
|
|
3
|
+
Assembles a token-optimized context bundle (~600 tokens) from multiple sources:
|
|
4
|
+
1. ~/.rai/developer.yaml → developer model + coaching + deadlines
|
|
5
|
+
2. .raise/rai/session-state.yaml → current work state
|
|
6
|
+
3. Memory graph → foundational patterns, governance primes
|
|
7
|
+
4. .raise/rai/personal/sessions/index.jsonl → recent sessions
|
|
8
|
+
|
|
9
|
+
Note: Identity primes (RAI-VAL-*, RAI-BND-*) are no longer emitted here.
|
|
10
|
+
They live in CLAUDE.md as always-on content (ADR-012).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from datetime import date
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel
|
|
22
|
+
|
|
23
|
+
from raise_cli.graph.backends import get_active_backend
|
|
24
|
+
from raise_cli.onboarding.profile import DeveloperProfile
|
|
25
|
+
from raise_cli.schemas.session_state import SessionState
|
|
26
|
+
from raise_core.graph.models import GraphNode
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LiveBacklogStatus(BaseModel):
|
|
32
|
+
"""Live status fetched from backlog adapter during session-start."""
|
|
33
|
+
|
|
34
|
+
epic_status: str = ""
|
|
35
|
+
epic_summary: str = ""
|
|
36
|
+
story_status: str = ""
|
|
37
|
+
story_summary: str = ""
|
|
38
|
+
warning: str = ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _fetch_live_status(
|
|
42
|
+
state: SessionState | None,
|
|
43
|
+
timeout: float = 5.0,
|
|
44
|
+
) -> LiveBacklogStatus:
|
|
45
|
+
"""Query backlog adapter for live epic/story status.
|
|
46
|
+
|
|
47
|
+
Returns LiveBacklogStatus with warning on any failure.
|
|
48
|
+
Never raises — all errors are caught and surfaced as warnings.
|
|
49
|
+
"""
|
|
50
|
+
if state is None:
|
|
51
|
+
return LiveBacklogStatus()
|
|
52
|
+
|
|
53
|
+
epic_key = state.current_work.epic
|
|
54
|
+
story_key = state.current_work.story
|
|
55
|
+
|
|
56
|
+
if not epic_key and not story_key:
|
|
57
|
+
return LiveBacklogStatus()
|
|
58
|
+
|
|
59
|
+
return _query_adapter(epic_key, story_key, timeout)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _query_adapter(
|
|
63
|
+
epic_key: str,
|
|
64
|
+
story_key: str,
|
|
65
|
+
timeout: float,
|
|
66
|
+
) -> LiveBacklogStatus:
|
|
67
|
+
"""Resolve adapter and run queries with timeout. Never raises.
|
|
68
|
+
|
|
69
|
+
The entire operation (adapter resolution + issue fetches) runs inside
|
|
70
|
+
the ThreadPoolExecutor so that the timeout covers everything, including
|
|
71
|
+
slow adapter startup (e.g., MCP bridge initialization).
|
|
72
|
+
"""
|
|
73
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
74
|
+
from concurrent.futures import TimeoutError as FuturesTimeoutError
|
|
75
|
+
|
|
76
|
+
from raise_cli.adapters.models import IssueDetail
|
|
77
|
+
from raise_cli.adapters.protocols import ProjectManagementAdapter
|
|
78
|
+
from raise_cli.cli.commands._resolve import resolve_adapter
|
|
79
|
+
|
|
80
|
+
def _do_fetch() -> LiveBacklogStatus:
|
|
81
|
+
adapter: ProjectManagementAdapter = resolve_adapter(None)
|
|
82
|
+
result = LiveBacklogStatus()
|
|
83
|
+
if epic_key:
|
|
84
|
+
detail: IssueDetail = adapter.get_issue(epic_key)
|
|
85
|
+
result.epic_status = detail.status
|
|
86
|
+
result.epic_summary = detail.summary
|
|
87
|
+
if story_key:
|
|
88
|
+
detail = adapter.get_issue(story_key)
|
|
89
|
+
result.story_status = detail.status
|
|
90
|
+
result.story_summary = detail.summary
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
with ThreadPoolExecutor(max_workers=1) as pool:
|
|
95
|
+
future = pool.submit(_do_fetch)
|
|
96
|
+
return future.result(timeout=timeout)
|
|
97
|
+
except FuturesTimeoutError:
|
|
98
|
+
logger.debug("Live status fetch timed out after %.1fs", timeout)
|
|
99
|
+
return LiveBacklogStatus(
|
|
100
|
+
warning=f"Backlog query timeout ({timeout:.0f}s) — showing cached state"
|
|
101
|
+
)
|
|
102
|
+
except SystemExit:
|
|
103
|
+
# resolve_adapter() uses sys.exit() on failure; SystemExit is
|
|
104
|
+
# BaseException, not Exception, so we catch it explicitly.
|
|
105
|
+
logger.debug("Adapter unavailable (SystemExit)")
|
|
106
|
+
return LiveBacklogStatus(
|
|
107
|
+
warning="Backlog adapter unavailable — showing cached state"
|
|
108
|
+
)
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
logger.debug("Live status fetch failed: %s", exc)
|
|
111
|
+
return LiveBacklogStatus(
|
|
112
|
+
warning=f"Backlog query error: {exc} — showing cached state"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# Graph path relative to project root
|
|
117
|
+
GRAPH_REL_PATH = Path(".raise") / "rai" / "memory" / "index.json"
|
|
118
|
+
# Sessions index path relative to project root (personal = developer-specific)
|
|
119
|
+
SESSIONS_INDEX_REL_PATH = (
|
|
120
|
+
Path(".raise") / "rai" / "personal" / "sessions" / "index.jsonl"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_foundational_patterns(project_path: Path) -> list[GraphNode]:
|
|
125
|
+
"""Query memory graph for foundational patterns.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
project_path: Absolute path to the project root.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List of pattern GraphNodes with foundational=true metadata.
|
|
132
|
+
"""
|
|
133
|
+
graph_path = project_path / GRAPH_REL_PATH
|
|
134
|
+
if not graph_path.exists():
|
|
135
|
+
logger.debug("Graph not found: %s", graph_path)
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
graph = get_active_backend(graph_path).load()
|
|
140
|
+
except Exception:
|
|
141
|
+
logger.warning("Failed to load graph: %s", graph_path)
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
return [
|
|
145
|
+
node
|
|
146
|
+
for node in graph.iter_concepts()
|
|
147
|
+
if node.type == "pattern" and node.metadata.get("foundational") is True
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_always_on_primes(project_path: Path) -> list[GraphNode]:
|
|
152
|
+
"""Query memory graph for all always_on nodes (governance + identity).
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
project_path: Absolute path to the project root.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
List of GraphNodes with always_on=true metadata.
|
|
159
|
+
"""
|
|
160
|
+
graph_path = project_path / GRAPH_REL_PATH
|
|
161
|
+
if not graph_path.exists():
|
|
162
|
+
logger.debug("Graph not found: %s", graph_path)
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
graph = get_active_backend(graph_path).load()
|
|
167
|
+
except Exception:
|
|
168
|
+
logger.warning("Failed to load graph: %s", graph_path)
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
return [
|
|
172
|
+
node for node in graph.iter_concepts() if node.metadata.get("always_on") is True
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _format_developer_section(profile: DeveloperProfile) -> str:
|
|
177
|
+
"""Format developer identity line with non-default communication prefs."""
|
|
178
|
+
line = f"Developer: {profile.name} ({profile.experience_level.value})"
|
|
179
|
+
|
|
180
|
+
# Surface communication preferences that deviate from defaults
|
|
181
|
+
comm = profile.communication
|
|
182
|
+
prefs: list[str] = []
|
|
183
|
+
if comm.language != "en":
|
|
184
|
+
prefs.append(f"language: {comm.language}")
|
|
185
|
+
if comm.style.value != "balanced":
|
|
186
|
+
prefs.append(f"style: {comm.style.value}")
|
|
187
|
+
if comm.skip_praise:
|
|
188
|
+
prefs.append("skip_praise")
|
|
189
|
+
if comm.redirect_when_dispersing:
|
|
190
|
+
prefs.append("redirect_when_dispersing")
|
|
191
|
+
|
|
192
|
+
if prefs:
|
|
193
|
+
line += f"\nCommunication: {', '.join(prefs)}"
|
|
194
|
+
|
|
195
|
+
return line
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _find_release_for_current_epic(
|
|
199
|
+
project_path: Path, epic_id: str
|
|
200
|
+
) -> GraphNode | None:
|
|
201
|
+
"""Find release node for the current epic from the memory graph.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
project_path: Absolute path to the project root.
|
|
205
|
+
epic_id: Epic identifier (e.g., "E19").
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The release GraphNode, or None if not found or graph unavailable.
|
|
209
|
+
"""
|
|
210
|
+
if not epic_id:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
graph_path = project_path / GRAPH_REL_PATH
|
|
214
|
+
if not graph_path.exists():
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
from raise_cli.graph.backends import get_active_backend
|
|
219
|
+
from raise_core.graph.query import QueryEngine
|
|
220
|
+
|
|
221
|
+
graph = get_active_backend(graph_path).load()
|
|
222
|
+
engine = QueryEngine(graph)
|
|
223
|
+
return engine.find_release_for(f"epic-{epic_id.lower()}")
|
|
224
|
+
except Exception:
|
|
225
|
+
logger.debug("Failed to query release for epic %s", epic_id)
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _format_work_section(
|
|
230
|
+
state: SessionState | None,
|
|
231
|
+
release_node: GraphNode | None = None,
|
|
232
|
+
live: LiveBacklogStatus | None = None,
|
|
233
|
+
) -> str:
|
|
234
|
+
"""Format current work state with optional live backlog annotations."""
|
|
235
|
+
if state is None:
|
|
236
|
+
return "Work: (no previous session state)"
|
|
237
|
+
|
|
238
|
+
lines: list[str] = []
|
|
239
|
+
|
|
240
|
+
if release_node:
|
|
241
|
+
release_id = release_node.metadata.get("release_id", release_node.id)
|
|
242
|
+
name = release_node.metadata.get("name", "")
|
|
243
|
+
target = release_node.metadata.get("target", "")
|
|
244
|
+
release_parts = [f"Release: {release_id}"]
|
|
245
|
+
if name:
|
|
246
|
+
release_parts.append(f"({name})")
|
|
247
|
+
if target:
|
|
248
|
+
release_parts.append(f"— Target: {target}")
|
|
249
|
+
lines.append(" ".join(release_parts))
|
|
250
|
+
|
|
251
|
+
# Story line with optional live annotation
|
|
252
|
+
story_line = f"Story: {state.current_work.story} [{state.current_work.phase}]"
|
|
253
|
+
if live and live.story_status:
|
|
254
|
+
story_line += f" — {live.story_status} (live)"
|
|
255
|
+
lines.append(story_line)
|
|
256
|
+
|
|
257
|
+
# Epic line with optional live annotation
|
|
258
|
+
epic_line = f"Epic: {state.current_work.epic}"
|
|
259
|
+
if live and live.epic_status:
|
|
260
|
+
epic_line += f" — {live.epic_status} (live)"
|
|
261
|
+
lines.append(epic_line)
|
|
262
|
+
|
|
263
|
+
lines.append(f"Branch: {state.current_work.branch}")
|
|
264
|
+
|
|
265
|
+
# Warning line for degraded live status
|
|
266
|
+
if live and live.warning:
|
|
267
|
+
lines.append(f"⚠ {live.warning}")
|
|
268
|
+
|
|
269
|
+
return "\n".join(lines)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _format_last_session(state: SessionState | None) -> str:
|
|
273
|
+
"""Format last session summary."""
|
|
274
|
+
if state is None:
|
|
275
|
+
return ""
|
|
276
|
+
s = state.last_session
|
|
277
|
+
return f"Last: {s.id} ({s.date}, {s.developer}) — {s.summary}"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _format_deadlines(profile: DeveloperProfile) -> str:
|
|
281
|
+
"""Format deadlines with days remaining."""
|
|
282
|
+
if not profile.deadlines:
|
|
283
|
+
return ""
|
|
284
|
+
|
|
285
|
+
today = date.today()
|
|
286
|
+
lines = ["# Deadlines"]
|
|
287
|
+
for d in profile.deadlines:
|
|
288
|
+
days = (d.date - today).days
|
|
289
|
+
if days < 0:
|
|
290
|
+
suffix = f"({abs(days)}d overdue)"
|
|
291
|
+
elif days == 0:
|
|
292
|
+
suffix = "(today)"
|
|
293
|
+
elif days == 1:
|
|
294
|
+
suffix = "(1 day)"
|
|
295
|
+
else:
|
|
296
|
+
suffix = f"({days} days)"
|
|
297
|
+
line = f"{d.name}: {d.date.strftime('%b %d')} {suffix}"
|
|
298
|
+
if d.notes:
|
|
299
|
+
line += f" — {d.notes}"
|
|
300
|
+
lines.append(line)
|
|
301
|
+
return "\n".join(lines)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _format_governance_primes(always_on_nodes: list[GraphNode]) -> str:
|
|
305
|
+
"""Format governance primes (guardrails + non-identity principles).
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
always_on_nodes: All always_on nodes from the graph.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Formatted governance primes section, or empty string if none.
|
|
312
|
+
"""
|
|
313
|
+
governance = [
|
|
314
|
+
n
|
|
315
|
+
for n in always_on_nodes
|
|
316
|
+
if not n.id.startswith("RAI-VAL-") and not n.id.startswith("RAI-BND-")
|
|
317
|
+
]
|
|
318
|
+
if not governance:
|
|
319
|
+
return ""
|
|
320
|
+
|
|
321
|
+
lines = ["# Governance Primes"]
|
|
322
|
+
for n in governance:
|
|
323
|
+
content = n.content
|
|
324
|
+
if len(content) > 80:
|
|
325
|
+
content = content[:77] + "..."
|
|
326
|
+
lines.append(f"- {n.id}: {content}")
|
|
327
|
+
return "\n".join(lines)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _format_progress(state: SessionState | None) -> str:
|
|
331
|
+
"""Format epic progress section.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
state: Session state (may be None).
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Formatted progress section, or empty string if no progress.
|
|
338
|
+
"""
|
|
339
|
+
if state is None or state.progress is None:
|
|
340
|
+
return ""
|
|
341
|
+
|
|
342
|
+
p = state.progress
|
|
343
|
+
pct = round(p.sp_done / p.sp_total * 100) if p.sp_total > 0 else 0
|
|
344
|
+
lines = [
|
|
345
|
+
f"Progress: {p.epic} — {p.stories_done}/{p.stories_total} stories, {p.sp_done}/{p.sp_total} SP ({pct}%)"
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
if state.completed_epics:
|
|
349
|
+
lines.append(f"Completed: {', '.join(state.completed_epics)}")
|
|
350
|
+
|
|
351
|
+
return "\n".join(lines)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _format_recent_sessions(project_path: Path, limit: int = 3) -> str:
|
|
355
|
+
"""Format recent sessions from sessions/index.jsonl.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
project_path: Absolute path to the project root.
|
|
359
|
+
limit: Number of recent sessions to include.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Formatted recent sessions section, or empty string if none.
|
|
363
|
+
"""
|
|
364
|
+
index_path = project_path / SESSIONS_INDEX_REL_PATH
|
|
365
|
+
if not index_path.exists():
|
|
366
|
+
return ""
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
text = index_path.read_text(encoding="utf-8").strip()
|
|
370
|
+
if not text:
|
|
371
|
+
return ""
|
|
372
|
+
sessions = [json.loads(line) for line in text.splitlines() if line.strip()]
|
|
373
|
+
except Exception:
|
|
374
|
+
logger.warning("Failed to read sessions index: %s", index_path)
|
|
375
|
+
return ""
|
|
376
|
+
|
|
377
|
+
if not sessions:
|
|
378
|
+
return ""
|
|
379
|
+
|
|
380
|
+
recent = sessions[-limit:]
|
|
381
|
+
recent.reverse() # Most recent first
|
|
382
|
+
|
|
383
|
+
lines = ["Recent:"]
|
|
384
|
+
for s in recent:
|
|
385
|
+
topic = s.get("topic", "")
|
|
386
|
+
if len(topic) > 80:
|
|
387
|
+
topic = topic[:77] + "..."
|
|
388
|
+
lines.append(f"- {s['id']}: {topic}")
|
|
389
|
+
return "\n".join(lines)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _format_narrative(state: SessionState | None) -> str:
|
|
393
|
+
"""Format session narrative for cross-session continuity.
|
|
394
|
+
|
|
395
|
+
Narrative is loaded verbatim — no truncation. It contains structured
|
|
396
|
+
context (decisions, research, artifacts, branch state) that makes the
|
|
397
|
+
next session immediately resumable.
|
|
398
|
+
|
|
399
|
+
Adds a staleness caveat so the reader knows to verify volatile
|
|
400
|
+
state (git status, branch, uncommitted files) before acting on it.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
state: Session state (may be None).
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
Formatted narrative section, or empty string if no narrative.
|
|
407
|
+
"""
|
|
408
|
+
if state is None or not state.narrative:
|
|
409
|
+
return ""
|
|
410
|
+
|
|
411
|
+
captured_date = state.last_session.date
|
|
412
|
+
caveat = (
|
|
413
|
+
f"(Captured at session close on {captured_date}. "
|
|
414
|
+
"Git/branch state may be stale — verify before acting.)"
|
|
415
|
+
)
|
|
416
|
+
return f"# Session Narrative\n{caveat}\n{state.narrative}"
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _format_next_session_prompt(state: SessionState | None) -> str:
|
|
420
|
+
"""Format next session prompt for cross-session continuity.
|
|
421
|
+
|
|
422
|
+
This is forward-looking guidance from Rai to her future self,
|
|
423
|
+
written during session-close and presented at session-start.
|
|
424
|
+
|
|
425
|
+
Adds a staleness caveat so the reader knows to verify volatile
|
|
426
|
+
state (git status, branch, uncommitted files) before acting on it.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
state: Session state (may be None).
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
Formatted prompt section, or empty string if no prompt.
|
|
433
|
+
"""
|
|
434
|
+
if state is None or not state.next_session_prompt:
|
|
435
|
+
return ""
|
|
436
|
+
|
|
437
|
+
captured_date = state.last_session.date
|
|
438
|
+
caveat = (
|
|
439
|
+
f"(Captured at session close on {captured_date}. "
|
|
440
|
+
"Git/branch state may be stale — verify before acting.)"
|
|
441
|
+
)
|
|
442
|
+
return f"# Next Session Prompt\n{caveat}\n{state.next_session_prompt}"
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _format_primes(patterns: list[GraphNode]) -> str:
|
|
446
|
+
"""Format foundational patterns as behavioral primes."""
|
|
447
|
+
if not patterns:
|
|
448
|
+
return ""
|
|
449
|
+
|
|
450
|
+
lines = ["# Behavioral Primes"]
|
|
451
|
+
for p in patterns:
|
|
452
|
+
# Compact: PAT-ID: first sentence
|
|
453
|
+
content = p.content.split("—")[0].strip() if "—" in p.content else p.content
|
|
454
|
+
# Truncate long content
|
|
455
|
+
if len(content) > 80:
|
|
456
|
+
content = content[:77] + "..."
|
|
457
|
+
lines.append(f"- {p.id}: {content}")
|
|
458
|
+
return "\n".join(lines)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _format_coaching(profile: DeveloperProfile) -> str:
|
|
462
|
+
"""Format coaching context."""
|
|
463
|
+
coaching = profile.coaching
|
|
464
|
+
has_content = (
|
|
465
|
+
coaching.strengths
|
|
466
|
+
or coaching.growth_edge
|
|
467
|
+
or coaching.trust_level != "new"
|
|
468
|
+
or coaching.autonomy
|
|
469
|
+
or coaching.relationship.quality != "new"
|
|
470
|
+
)
|
|
471
|
+
if not has_content:
|
|
472
|
+
return ""
|
|
473
|
+
|
|
474
|
+
lines = ["# Coaching"]
|
|
475
|
+
if coaching.trust_level != "new":
|
|
476
|
+
lines.append(f"Trust: {coaching.trust_level}")
|
|
477
|
+
if coaching.strengths:
|
|
478
|
+
lines.append(f"Strengths: {', '.join(coaching.strengths)}")
|
|
479
|
+
if coaching.growth_edge:
|
|
480
|
+
lines.append(f"Growth edge: {coaching.growth_edge}")
|
|
481
|
+
if coaching.autonomy:
|
|
482
|
+
lines.append(f"Autonomy: {coaching.autonomy}")
|
|
483
|
+
if coaching.relationship.quality != "new":
|
|
484
|
+
rel = coaching.relationship
|
|
485
|
+
lines.append(f"Relationship: {rel.quality} ({rel.trajectory})")
|
|
486
|
+
# Corrections suppressed from session context — noise without specific
|
|
487
|
+
# consumption point. Revisit when /rai-story-review integrates them.
|
|
488
|
+
return "\n".join(lines)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _format_pending(state: SessionState | None) -> str:
|
|
492
|
+
"""Format pending items."""
|
|
493
|
+
if state is None:
|
|
494
|
+
return ""
|
|
495
|
+
|
|
496
|
+
pending = state.pending
|
|
497
|
+
if not pending.decisions and not pending.blockers and not pending.next_actions:
|
|
498
|
+
return ""
|
|
499
|
+
|
|
500
|
+
lines = ["# Pending"]
|
|
501
|
+
if pending.decisions:
|
|
502
|
+
lines.append("Decisions:")
|
|
503
|
+
for d in pending.decisions:
|
|
504
|
+
lines.append(f"- {d}")
|
|
505
|
+
if pending.blockers:
|
|
506
|
+
lines.append("Blockers:")
|
|
507
|
+
for b in pending.blockers:
|
|
508
|
+
lines.append(f"- {b}")
|
|
509
|
+
if pending.next_actions:
|
|
510
|
+
lines.append("Next:")
|
|
511
|
+
for n in pending.next_actions:
|
|
512
|
+
lines.append(f"- {n}")
|
|
513
|
+
return "\n".join(lines)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# ---------------------------------------------------------------------------
|
|
517
|
+
# Section registry and manifest
|
|
518
|
+
# ---------------------------------------------------------------------------
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class SectionManifest(BaseModel):
|
|
522
|
+
"""Manifest entry for a queryable context section."""
|
|
523
|
+
|
|
524
|
+
name: str
|
|
525
|
+
count: int
|
|
526
|
+
token_estimate: int
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _count_governance(project_path: Path) -> int:
|
|
530
|
+
"""Count governance items (always_on nodes minus identity)."""
|
|
531
|
+
nodes = get_always_on_primes(project_path)
|
|
532
|
+
return len(
|
|
533
|
+
[
|
|
534
|
+
n
|
|
535
|
+
for n in nodes
|
|
536
|
+
if not n.id.startswith("RAI-VAL-") and not n.id.startswith("RAI-BND-")
|
|
537
|
+
]
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _count_behavioral(project_path: Path) -> int:
|
|
542
|
+
"""Count foundational pattern items."""
|
|
543
|
+
return len(get_foundational_patterns(project_path))
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _count_coaching(profile: DeveloperProfile) -> int:
|
|
547
|
+
"""Count coaching items (1 if content exists, 0 otherwise)."""
|
|
548
|
+
coaching = profile.coaching
|
|
549
|
+
has_content = (
|
|
550
|
+
coaching.strengths
|
|
551
|
+
or coaching.growth_edge
|
|
552
|
+
or coaching.trust_level != "new"
|
|
553
|
+
or coaching.autonomy
|
|
554
|
+
or coaching.relationship.quality != "new"
|
|
555
|
+
)
|
|
556
|
+
return 1 if has_content else 0
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _count_deadlines(profile: DeveloperProfile) -> int:
|
|
560
|
+
"""Count deadline items."""
|
|
561
|
+
return len(profile.deadlines)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _count_progress(state: SessionState | None) -> int:
|
|
565
|
+
"""Count progress items (1 if exists, 0 otherwise)."""
|
|
566
|
+
if state is None or state.progress is None:
|
|
567
|
+
return 0
|
|
568
|
+
return 1
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
# Average tokens per item, estimated from real data
|
|
572
|
+
_TOKENS_PER_ITEM: dict[str, int] = {
|
|
573
|
+
"governance": 25,
|
|
574
|
+
"behavioral": 20,
|
|
575
|
+
"coaching": 80,
|
|
576
|
+
"deadlines": 30,
|
|
577
|
+
"progress": 40,
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def count_section_items(
|
|
582
|
+
section: str,
|
|
583
|
+
project_path: Path,
|
|
584
|
+
profile: DeveloperProfile,
|
|
585
|
+
state: SessionState | None,
|
|
586
|
+
) -> int:
|
|
587
|
+
"""Count items in a named section.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
section: Section name from SECTION_REGISTRY.
|
|
591
|
+
project_path: Absolute path to the project root.
|
|
592
|
+
profile: Developer profile.
|
|
593
|
+
state: Session state (may be None).
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
Number of items in the section.
|
|
597
|
+
|
|
598
|
+
Raises:
|
|
599
|
+
ValueError: If section name is not in SECTION_REGISTRY.
|
|
600
|
+
"""
|
|
601
|
+
if section not in SECTION_REGISTRY:
|
|
602
|
+
raise ValueError(
|
|
603
|
+
f"Unknown section: '{section}'. Valid: {sorted(SECTION_REGISTRY.keys())}"
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
if section == "governance":
|
|
607
|
+
return _count_governance(project_path)
|
|
608
|
+
if section == "behavioral":
|
|
609
|
+
return _count_behavioral(project_path)
|
|
610
|
+
if section == "coaching":
|
|
611
|
+
return _count_coaching(profile)
|
|
612
|
+
if section == "deadlines":
|
|
613
|
+
return _count_deadlines(profile)
|
|
614
|
+
if section == "progress":
|
|
615
|
+
return _count_progress(state)
|
|
616
|
+
|
|
617
|
+
return 0 # unreachable but satisfies pyright
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
# Registry: section name → format function
|
|
621
|
+
# Format functions have heterogeneous signatures; the registry maps names
|
|
622
|
+
# for validation and dispatch. Actual calling happens in assemble_sections().
|
|
623
|
+
SECTION_REGISTRY: dict[str, Callable[..., str]] = {
|
|
624
|
+
"governance": _format_governance_primes,
|
|
625
|
+
"behavioral": _format_primes,
|
|
626
|
+
"coaching": _format_coaching,
|
|
627
|
+
"deadlines": _format_deadlines,
|
|
628
|
+
"progress": _format_progress,
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def assemble_sections(
|
|
633
|
+
sections: list[str],
|
|
634
|
+
project_path: Path,
|
|
635
|
+
profile: DeveloperProfile,
|
|
636
|
+
state: SessionState | None,
|
|
637
|
+
) -> str:
|
|
638
|
+
"""Assemble formatted output for selected priming sections.
|
|
639
|
+
|
|
640
|
+
Each section independently loads its data source (graph, profile, or state)
|
|
641
|
+
and formats the output. Section names are validated against SECTION_REGISTRY.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
sections: List of section names to load (e.g., ["governance", "behavioral"]).
|
|
645
|
+
project_path: Absolute path to the project root.
|
|
646
|
+
profile: Developer profile.
|
|
647
|
+
state: Session state (may be None).
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
Formatted sections joined by blank lines, or empty string if no content.
|
|
651
|
+
|
|
652
|
+
Raises:
|
|
653
|
+
ValueError: If any section name is not in SECTION_REGISTRY.
|
|
654
|
+
"""
|
|
655
|
+
if not sections:
|
|
656
|
+
return ""
|
|
657
|
+
|
|
658
|
+
# Validate all section names first
|
|
659
|
+
for name in sections:
|
|
660
|
+
if name not in SECTION_REGISTRY:
|
|
661
|
+
raise ValueError(
|
|
662
|
+
f"Unknown section: '{name}'. Valid: {sorted(SECTION_REGISTRY.keys())}"
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
parts: list[str] = []
|
|
666
|
+
for name in sections:
|
|
667
|
+
if name == "governance":
|
|
668
|
+
always_on = get_always_on_primes(project_path)
|
|
669
|
+
part = _format_governance_primes(always_on)
|
|
670
|
+
elif name == "behavioral":
|
|
671
|
+
patterns = get_foundational_patterns(project_path)
|
|
672
|
+
part = _format_primes(patterns)
|
|
673
|
+
elif name == "coaching":
|
|
674
|
+
part = _format_coaching(profile)
|
|
675
|
+
elif name == "deadlines":
|
|
676
|
+
part = _format_deadlines(profile)
|
|
677
|
+
elif name == "progress":
|
|
678
|
+
part = _format_progress(state)
|
|
679
|
+
else:
|
|
680
|
+
continue # unreachable due to validation above
|
|
681
|
+
|
|
682
|
+
if part:
|
|
683
|
+
parts.append(part)
|
|
684
|
+
|
|
685
|
+
return "\n\n".join(parts)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _format_manifest(manifests: list[SectionManifest]) -> str:
|
|
689
|
+
"""Format manifest of available context sections.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
manifests: List of section manifest entries.
|
|
693
|
+
|
|
694
|
+
Returns:
|
|
695
|
+
Formatted manifest section, or empty string if no manifests.
|
|
696
|
+
"""
|
|
697
|
+
if not manifests:
|
|
698
|
+
return ""
|
|
699
|
+
|
|
700
|
+
lines = ["# Available Context"]
|
|
701
|
+
for m in manifests:
|
|
702
|
+
if m.count == 0:
|
|
703
|
+
lines.append(f"- {m.name}: 0 items")
|
|
704
|
+
else:
|
|
705
|
+
lines.append(f"- {m.name}: {m.count} items (~{m.token_estimate} tokens)")
|
|
706
|
+
return "\n".join(lines)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def assemble_orientation(
|
|
710
|
+
profile: DeveloperProfile,
|
|
711
|
+
state: SessionState | None,
|
|
712
|
+
project_path: Path,
|
|
713
|
+
session_id: str | None = None,
|
|
714
|
+
) -> str:
|
|
715
|
+
"""Assemble orientation-only context (always-on sections).
|
|
716
|
+
|
|
717
|
+
Orientation = "where are we?" — work state, continuity, pending.
|
|
718
|
+
Does NOT include priming sections (governance, behavioral, coaching,
|
|
719
|
+
deadlines, progress). Those are loaded separately via assemble_sections().
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
profile: Developer profile from ~/.rai/developer.yaml.
|
|
723
|
+
state: Session state from .raise/rai/session-state.yaml (may be None).
|
|
724
|
+
project_path: Absolute path to the project root.
|
|
725
|
+
session_id: Optional session identifier (e.g., "SES-177").
|
|
726
|
+
|
|
727
|
+
Returns:
|
|
728
|
+
Plain text orientation context.
|
|
729
|
+
"""
|
|
730
|
+
# Resolve release context for current epic
|
|
731
|
+
release_node: GraphNode | None = None
|
|
732
|
+
if state and state.current_work.epic:
|
|
733
|
+
release_node = _find_release_for_current_epic(
|
|
734
|
+
project_path, state.current_work.epic
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
# Session Context header
|
|
738
|
+
sections: list[str] = [
|
|
739
|
+
"# Session Context",
|
|
740
|
+
_format_developer_section(profile),
|
|
741
|
+
]
|
|
742
|
+
|
|
743
|
+
# Add session ID if provided
|
|
744
|
+
if session_id:
|
|
745
|
+
sections.append(f"Session: {session_id}")
|
|
746
|
+
|
|
747
|
+
# Fetch live backlog status (never blocks — degrades gracefully)
|
|
748
|
+
live = _fetch_live_status(state)
|
|
749
|
+
|
|
750
|
+
sections.append(_format_work_section(state, release_node=release_node, live=live))
|
|
751
|
+
|
|
752
|
+
# Last session + recent sessions
|
|
753
|
+
sections.append(_format_last_session(state))
|
|
754
|
+
recent = _format_recent_sessions(project_path)
|
|
755
|
+
if recent:
|
|
756
|
+
sections.append(recent)
|
|
757
|
+
|
|
758
|
+
# Session narrative (cross-session continuity — not truncated)
|
|
759
|
+
narrative = _format_narrative(state)
|
|
760
|
+
if narrative:
|
|
761
|
+
sections.append(narrative)
|
|
762
|
+
|
|
763
|
+
# Next session prompt (forward-looking guidance from Rai to future self)
|
|
764
|
+
next_prompt = _format_next_session_prompt(state)
|
|
765
|
+
if next_prompt:
|
|
766
|
+
sections.append(next_prompt)
|
|
767
|
+
|
|
768
|
+
# Pending
|
|
769
|
+
pending = _format_pending(state)
|
|
770
|
+
if pending:
|
|
771
|
+
sections.append(pending)
|
|
772
|
+
|
|
773
|
+
# Filter empty sections, join with blank lines
|
|
774
|
+
return "\n\n".join(s for s in sections if s)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def assemble_context_bundle(
|
|
778
|
+
profile: DeveloperProfile,
|
|
779
|
+
state: SessionState | None,
|
|
780
|
+
project_path: Path,
|
|
781
|
+
session_id: str | None = None,
|
|
782
|
+
) -> str:
|
|
783
|
+
"""Assemble lean context bundle: orientation + manifest.
|
|
784
|
+
|
|
785
|
+
Emits always-on orientation sections plus a manifest of available
|
|
786
|
+
priming sections (with counts and token estimates). Priming sections
|
|
787
|
+
are loaded separately via `rai session context --sections`.
|
|
788
|
+
|
|
789
|
+
Args:
|
|
790
|
+
profile: Developer profile from ~/.rai/developer.yaml.
|
|
791
|
+
state: Session state from .raise/rai/session-state.yaml (may be None).
|
|
792
|
+
project_path: Absolute path to the project root.
|
|
793
|
+
session_id: Optional session identifier (e.g., "SES-177").
|
|
794
|
+
|
|
795
|
+
Returns:
|
|
796
|
+
Plain text context bundle: orientation + manifest.
|
|
797
|
+
"""
|
|
798
|
+
# Orientation (always-on sections)
|
|
799
|
+
orientation = assemble_orientation(profile, state, project_path, session_id)
|
|
800
|
+
|
|
801
|
+
# Build manifest for available priming sections
|
|
802
|
+
manifests: list[SectionManifest] = []
|
|
803
|
+
for section_name in SECTION_REGISTRY:
|
|
804
|
+
count = count_section_items(section_name, project_path, profile, state)
|
|
805
|
+
tokens = count * _TOKENS_PER_ITEM.get(section_name, 20)
|
|
806
|
+
manifests.append(
|
|
807
|
+
SectionManifest(
|
|
808
|
+
name=section_name,
|
|
809
|
+
count=count,
|
|
810
|
+
token_estimate=tokens,
|
|
811
|
+
)
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
manifest = _format_manifest(manifests)
|
|
815
|
+
|
|
816
|
+
parts = [orientation]
|
|
817
|
+
if manifest:
|
|
818
|
+
parts.append(manifest)
|
|
819
|
+
|
|
820
|
+
return "\n\n".join(parts)
|