159 """Append a record of the completed goal to ``SESSION_LOG.md``.
161 **NEVER touches ``CHANGELOG.md``** — that file is registry-generated
162 (``roadmap render``) and ``render --check``-gated. A test pins this.
164 Follows the formatting rules defined in
165 ``oc_chat/docs/CHANGELOG-format.md`` (Rule 4 H2 timestamp, Rule 9
166 directory-style session link) and adopts the Keep-a-Changelog
167 ``### Changed`` H3 vocabulary used by ``oct git changelog`` so a
168 file edited by both tools stays structurally consistent (Phase
169 F.2c). A new section is prepended after the metadata block:
171 ## YYYY-MM-DD:HH:MM:SS — <scope>
173 **Session:** [`<name>`](oc_chat/sessions/<name>/)
180 func =
"append_session_log"
181 if not ctx.goal_files_changed:
185 timestamp = now.strftime(
"%Y-%m-%d:%H:%M:%S")
186 session_name = ctx.session.name
187 session_link = f
"oc_chat/sessions/{session_name}/"
193 scope_line = ctx.goal_description
194 lines: list[str] = []
195 lines.append(f
"## {timestamp} — {scope_line}")
196 lines.append(f
"**Scope:** {scope_line}")
197 lines.append(f
"**Session:** [`{session_name}`]({session_link})")
199 lines.append(
"### Changed")
201 for f
in sorted(ctx.goal_files_changed):
202 lines.append(f
"- `{f}`")
205 new_section =
"\n".join(lines)
209 with open(SESSION_LOG_PATH,
"r", encoding=
"utf-8")
as fh:
211 except FileNotFoundError:
213 f
"# OC Chat — {_PROJECT_DESCRIPTION} — SESSION LOG\n\n"
214 f
"**Version:** [{_project_version()}] - {timestamp}**\n"
215 f
"**Last Session:** [`{session_name}`]({session_link})\n\n"
219 existing_norm = existing.replace(
"\r\n",
"\n")
220 lines_existing = existing_norm.split(
"\n")
224 for i, line
in enumerate(lines_existing):
225 if line.startswith(
"**Last Session:**"):
228 if line.startswith(
"**Version:**"):
232 meta_end = min(3, len(lines_existing))
235 metadata_lines = lines_existing[:meta_end]
238 new_last_session = f
"**Last Session:** [`{session_name}`]({session_link})"
239 updated_meta: list[str] = []
240 last_session_replaced =
False
241 for line
in metadata_lines:
242 if line.startswith(
"**Last Session:**"):
243 updated_meta.append(new_last_session)
244 last_session_replaced =
True
245 elif line.startswith(
"**Version:**"):
246 updated_meta.append(line)
248 updated_meta.append(line)
250 if not last_session_replaced:
252 final_meta: list[str] = []
253 for line
in updated_meta:
254 final_meta.append(line)
255 if not inserted
and line.startswith(
"**Version:**"):
256 final_meta.append(new_last_session)
258 updated_meta = final_meta
261 body_lines = lines_existing[meta_end:]
264 result_lines = updated_meta + [
""] + [new_section]
265 body_has_content = any(line.strip()
for line
in body_lines)
267 result_lines.append(
"")
269 while first_nonblank < len(body_lines)
and not body_lines[first_nonblank].strip():
271 result_lines.extend(body_lines[first_nonblank:])
273 result =
"\n".join(result_lines)
279 SESSION_LOG_PATH.parent.mkdir(parents=
True, exist_ok=
True)
280 tmp = SESSION_LOG_PATH.with_name(SESSION_LOG_PATH.name + f
".tmp.{os.getpid()}")
282 with open(tmp,
"w", encoding=
"utf-8")
as fh:
285 os.fsync(fh.fileno())
286 os.replace(tmp, SESSION_LOG_PATH)
291 _dbg(
"OC_CHAT.MAIN", func,
292 f
"updated {len(ctx.goal_files_changed)} file(s) for goal: {scope_line!r}", 2)
293 ctx.presenter.show_info(
294 f
"SESSION_LOG.md updated — {len(ctx.goal_files_changed)} file(s) changed."