@basou/core 0.14.0 → 0.15.0

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.
package/dist/index.d.ts CHANGED
@@ -3170,6 +3170,7 @@ type DecisionRecord = {
3170
3170
  decisionId: string;
3171
3171
  title: string;
3172
3172
  occurredAt: string;
3173
+ sessionId: string;
3173
3174
  };
3174
3175
  type NoteRecord = {
3175
3176
  body: string;
@@ -5001,15 +5002,26 @@ declare function readManifest(paths: BasouPaths): Promise<Manifest>;
5001
5002
  declare const GENERATED_START = "<!-- BASOU:GENERATED:START -->";
5002
5003
  /** Marker line that ends the auto-generated region. */
5003
5004
  declare const GENERATED_END = "<!-- BASOU:GENERATED:END -->";
5005
+ /** Marker line that begins a managed protocol block in a foreign instruction file. */
5006
+ declare const PROTOCOL_START = "<!-- BASOU:PROTOCOLS:START -->";
5007
+ /** Marker line that ends a managed protocol block. */
5008
+ declare const PROTOCOL_END = "<!-- BASOU:PROTOCOLS:END -->";
5009
+ /** A start/end marker pair. Both lines are matched whole-line, exact. */
5010
+ type Markers = {
5011
+ start: string;
5012
+ end: string;
5013
+ };
5004
5014
  /**
5005
5015
  * Result of parsing a markdown body for the BASOU:GENERATED marker region.
5006
5016
  *
5007
5017
  * The spec mandates strict line-level matching (see
5008
5018
  * `docs/spec/generated-markdown.md#102-marker-convention`): a marker is
5009
5019
  * only recognized when an entire line is exactly the marker string.
5010
- * Leading/trailing whitespace, comment compression, and BOM are treated as
5011
- * legacy formats (`no_markers`) so that re-generation refuses to silently
5012
- * overwrite a mismatched manual edit.
5020
+ * Leading/trailing whitespace and comment compression are treated as legacy
5021
+ * formats (`no_markers`) so that re-generation refuses to silently overwrite a
5022
+ * mismatched manual edit. A leading UTF-8 BOM is the one exception: it is
5023
+ * tolerated (stripped for matching, re-prepended on render) so a BOM-prefixed
5024
+ * file round-trips instead of duplicating the block.
5013
5025
  */
5014
5026
  type MarkerSection = {
5015
5027
  kind: "ok";
@@ -5056,11 +5068,13 @@ declare function writeMarkdownFile(filePath: string, body: string): Promise<void
5056
5068
  * - `multiple_pairs`: more than one START or END line.
5057
5069
  * - `wrong_order`: END appears before START.
5058
5070
  *
5059
- * Matching is strict: leading/trailing whitespace, BOM, and comment
5060
- * compression (`<!--BASOU:...-->`) all bypass the marker and are treated
5061
- * as legacy content.
5071
+ * Matching is strict: leading/trailing whitespace and comment compression
5072
+ * (`<!--BASOU:...-->`) bypass the marker and are treated as legacy content. A
5073
+ * leading UTF-8 BOM is the exception: it is stripped before matching and
5074
+ * re-prepended to `before`, so a marker on the first line of a BOM-prefixed
5075
+ * file still matches and the file round-trips.
5062
5076
  */
5063
- declare function parseMarkers(content: string): MarkerSection;
5077
+ declare function parseMarkers(content: string, markers?: Markers): MarkerSection;
5064
5078
  /**
5065
5079
  * Build the final markdown body by replacing the BASOU:GENERATED region.
5066
5080
  *
@@ -5072,7 +5086,18 @@ declare function parseMarkers(content: string): MarkerSection;
5072
5086
  * The caller passes `fileLabel` (e.g. `"handoff.md"` or `"decisions.md"`)
5073
5087
  * so the error message is informative without leaking an absolute path.
5074
5088
  */
5075
- declare function renderWithMarkers(existing: string | null, generated: string, fileLabel: string): string;
5089
+ declare function renderWithMarkers(existing: string | null, generated: string, fileLabel: string, markers?: Markers): string;
5090
+ /**
5091
+ * Remove a marker region from `existing`, returning the body without the block.
5092
+ *
5093
+ * - `no_markers`: returns `existing` unchanged (nothing to remove).
5094
+ * - `ok`: drops both marker lines and the generated region, collapsing the
5095
+ * single newline that terminated the END marker line so no stray blank line
5096
+ * is left behind.
5097
+ * - any other parse result: throws a pathless error referencing `fileLabel`
5098
+ * (mismatched markers must not be silently rewritten).
5099
+ */
5100
+ declare function removeMarkerSection(existing: string, fileLabel: string, markers?: Markers): string;
5076
5101
 
5077
5102
  /**
5078
5103
  * Options for {@link importSessionFromJson}. All fields are optional.
@@ -5299,4 +5324,4 @@ declare function overwriteYamlFile(filePath: string, value: unknown): Promise<vo
5299
5324
  */
5300
5325
  declare const BASOU_CORE_VERSION = "0.1.0";
5301
5326
 
5302
- export { ACTIVE_GAP_CAP_MS, type ActiveTimeBasis, type AdapterOutputEvent, type AdoptCandidate, type AdoptCandidateKind, type AppendBasouGitignoreOptions, type AppendBasouGitignoreResult, type AppendEventToExistingInput, type AppendEventToExistingResult, type Approval, type ApprovalApprovedEvent, type ApprovalExpiredEvent, ApprovalIdSchema, type ApprovalLocation, type ApprovalRejectedEvent, type ApprovalRequestedEvent, ApprovalSchema, type ApprovalStatus, ApprovalStatusSchema, type ArchivePlan, type ArchiveTaskInput, type ArchiveTaskResult, type AttachTaskInput, type AttachUpdateTaskStatusInput, type AttachableStatus, BASOU_CORE_VERSION, type BasouPaths, type BulkChainResult, CLAUDE_IMPORT_SOURCE, CODEX_IMPORT_SOURCE, type CaptureMode, type ChainBreakReason, type ChainTailState, type ChainVerdict, type ChainVerdictStatus, type ChainedEvents, ChildProcessRunner, type CitedReview, type ClaudeTranscriptRecord, type ClaudeTranscriptToPayloadOptions, type CodexRolloutRecord, type CodexRolloutToPayloadOptions, type CommandExecutedEvent, type CommandLookup, type CreateAdHocSessionInput, type CreateAdHocSessionResult, type CreateAdHocTaskInput, type CreateManifestInput, type CreateTaskInput, type CreateTaskResult, type DayWorkStats, DecisionIdSchema, type DecisionRecordedEvent, type DecisionsRendererInput, type DecisionsRendererResult, type DeleteTaskInput, type DeleteTaskResult, type DiffResult, type EditTaskInput, type EditTaskResult, type Event, EventIdSchema, EventSchema, EventSourceSchema, type ExistingViewLink, FailedToFinalizeError, type FileChange, type FileChangeStatus, type FileChangedEvent, GENERATED_END, GENERATED_START, type GitSnapshot, type GitSnapshotEvent, type GitignorePlanSummary, type HandoffRendererInput, type HandoffRendererResult, ID_PREFIXES, type IdPrefix, type ImportSessionOptions, type ImportSessionResult, type InstructionFileFact, type InstructionSymlinkFact, type InstructionSymlinkState, IsoTimestampSchema, JSON_SCHEMA_VERSION, type JsonSchemaArtifact, type LoadSessionEntriesOptions, type LoadTaskEntriesOptions, type LoadedApproval, type LockHandle, type LockScope, type Manifest, ManifestSchema, type MarkerSection, type MeasureAvailability, type NoteAddedEvent, type OrientationRendererInput, type OrientationRendererResult, type OrientationSummary, type PrefixedId, type PresetAction, type PresetCollision, type PresetMarkerConflict, type PresetMarkerKind, type PresetPlanSummary, type PresetRepo, type ProcessRunner, type PublishKind, type PublishTarget, type RechainOptions, type RechainResult, type ReconcileAllResult, type ReconcileAllTasksInput, type ReconcileAllTasksOptions, type ReconcileFailure, type ReconcileResult, type ReconcileTaskInput, type RefreshLinkageInput, type RefreshLinkageResult, type ReimportOptions, type ReimportResult, type RenamePlan, type ReplayOptions, type ReplayWarning, type RepoEntry, type RepoGitignoreFacts, type RepoGitignorePlan, type RepoLanguage, type RepoPresetFacts, type RepoPresetPlan, type RepoSymlinkFacts, type RepoSymlinkPlan, type RepoVisibility, type RepoWiringFacts, type ReportApprovalItem, type ReportData, type ReportDecisionItem, type ReportRendererInput, type ReportRendererResult, type ReportSessionItem, type ReportTaskItem, type ReviewGapRepoSummary, type ReviewGapUnit, type ReviewGapVerdict, type ReviewGapsInput, type ReviewGapsSummary, type RiskLevel, RiskLevelSchema, type RosterAdoptionPlan, type RosterDriftSummary, type RunOptions, type RunResult, STUCK_THRESHOLD_MS, type SanitizePathOptions, type SanitizeRelatedFilesResult, SchemaVersionSchema, type Session, type SessionEndedEvent, type SessionEntry, SessionIdSchema, type SessionImportPayload, SessionImportPayloadSchema, type SessionInnerImportInput, SessionInnerImportSchema, type SessionIntegrity, SessionIntegritySchema, type SessionMetrics, SessionMetricsSchema, SessionSchema, type SessionSkipReason, type SessionSourceKind, SessionSourceKindSchema, type SessionStartedEvent, type SessionStatus, type SessionStatusChangedEvent, SessionStatusSchema, type SessionWorkStats, type SourceRootsReconcile, type SourceWorkStats, type StatusCount, StatusSchema, type StatusSnapshot, type SuspectReason, type SymlinkCollision, type SymlinkConflict, type SymlinkPlanSummary, type Task, type TaskArchivedEvent, type TaskCreatedEvent, type TaskDeletedEvent, type TaskDocument, TaskIdSchema, type TaskLinkageRefreshedEvent, type TaskReconciledEvent, TaskSchema, type TaskSkipReason, type TaskStatus, type TaskStatusChangedEvent, type TaskStatusCount, TaskStatusSchema, TaskWriteAfterEventError, type TaskWriteAfterEventPhase, type TokenTotals, type UpdateAdHocTaskStatusInput, type UpdateTaskStatusInput, type UpdateTaskStatusResult, type ViewCollision, type ViewConflict, type ViewLinkState, type ViewRepoFact, type ViewStrayUnknown, type WiringRisk, type WiringSummary, type WorkStatsInput, type WorkStatsResult, type WorkStatsTotals, WorkspaceIdSchema, type WorkspaceViewPlan, type WriteEventsBulkOptions, type WriteTaskFileMode, acquireLock, appendBasouGitignore, appendChainedEvent, appendChainedEventLocked, appendEvent, appendEventToExistingSession, archiveTask, assertBasouRootSafe, basouPaths, buildJsonSchemas, buildStatusSnapshot, chainEvents, chainRawJsonLines, classifySuspect, claudeCodeAdapterMetadata, claudeTranscriptToImportPayload, codexRolloutToImportPayload, computeWorkStats, createAdHocSessionWithEvent, createManifest, createTaskWithEvent, deleteTask, editTask, ensureBasouDirectory, enumerateApprovals, enumerateArchivedTaskIds, enumerateSessionDirs, enumerateTaskIds, finalizeSessionYaml, findErrorCode, findReviewGaps, formatDurationMs, genesisHash, getDiff, getSnapshot, importSessionFromJson, inspectChainTail, isGitNotFound, isImportDerivedSource, isLazyExpired, isRenderable, isValidPrefixedId, lineHash, linkYamlFile, loadApproval, loadSessionEntries, loadTaskEntries, normalizeRepoKey, normalizeRepoPath, overwriteYamlFile, parseDuration, parseMarkers, pathBasename, planArchive, planGitignore, planRename, planRosterAdoption, planWorkspaceView, prefixedUlid, readAllEvents, readManifest, readMarkdownFile, readSessionYaml, readStatus, readTaskFile, readTaskFileWithArchiveFallback, readYamlFile, rechainSessionInPlace, reconcileAllTasks, reconcileSourceRoots, reconcileTask, refreshTaskLinkedSessions, reimportPreservingId, renderDecisions, renderHandoff, renderOrientation, renderPresetBlock, renderReport, renderWithMarkers, replayEvents, resolveBasouRepositoryRoot, resolveClaudeCodeCommand, resolveRepositoryRoot, resolveSessionId, resolveTaskId, safeSimpleGit, sanitizePath, sanitizeRelatedFiles, sanitizeWorkingDirectory, serializeEventLine, serializeJsonSchema, sessionWorkStatsFromEvents, summarizeAdapterOutput, summarizeOrientation, summarizePresetPlan, summarizeRosterDrift, summarizeSymlinkPlan, summarizeWiring, tryRemoteUrl, ulid, unknownManifestKeys, updateTaskStatusWithEvent, verifyEventsChain, writeEventsBulk, writeManifest, writeMarkdownFile, writeStatus, writeTaskFile, writeYamlFile };
5327
+ export { ACTIVE_GAP_CAP_MS, type ActiveTimeBasis, type AdapterOutputEvent, type AdoptCandidate, type AdoptCandidateKind, type AppendBasouGitignoreOptions, type AppendBasouGitignoreResult, type AppendEventToExistingInput, type AppendEventToExistingResult, type Approval, type ApprovalApprovedEvent, type ApprovalExpiredEvent, ApprovalIdSchema, type ApprovalLocation, type ApprovalRejectedEvent, type ApprovalRequestedEvent, ApprovalSchema, type ApprovalStatus, ApprovalStatusSchema, type ArchivePlan, type ArchiveTaskInput, type ArchiveTaskResult, type AttachTaskInput, type AttachUpdateTaskStatusInput, type AttachableStatus, BASOU_CORE_VERSION, type BasouPaths, type BulkChainResult, CLAUDE_IMPORT_SOURCE, CODEX_IMPORT_SOURCE, type CaptureMode, type ChainBreakReason, type ChainTailState, type ChainVerdict, type ChainVerdictStatus, type ChainedEvents, ChildProcessRunner, type CitedReview, type ClaudeTranscriptRecord, type ClaudeTranscriptToPayloadOptions, type CodexRolloutRecord, type CodexRolloutToPayloadOptions, type CommandExecutedEvent, type CommandLookup, type CreateAdHocSessionInput, type CreateAdHocSessionResult, type CreateAdHocTaskInput, type CreateManifestInput, type CreateTaskInput, type CreateTaskResult, type DayWorkStats, DecisionIdSchema, type DecisionRecordedEvent, type DecisionsRendererInput, type DecisionsRendererResult, type DeleteTaskInput, type DeleteTaskResult, type DiffResult, type EditTaskInput, type EditTaskResult, type Event, EventIdSchema, EventSchema, EventSourceSchema, type ExistingViewLink, FailedToFinalizeError, type FileChange, type FileChangeStatus, type FileChangedEvent, GENERATED_END, GENERATED_START, type GitSnapshot, type GitSnapshotEvent, type GitignorePlanSummary, type HandoffRendererInput, type HandoffRendererResult, ID_PREFIXES, type IdPrefix, type ImportSessionOptions, type ImportSessionResult, type InstructionFileFact, type InstructionSymlinkFact, type InstructionSymlinkState, IsoTimestampSchema, JSON_SCHEMA_VERSION, type JsonSchemaArtifact, type LoadSessionEntriesOptions, type LoadTaskEntriesOptions, type LoadedApproval, type LockHandle, type LockScope, type Manifest, ManifestSchema, type MarkerSection, type Markers, type MeasureAvailability, type NoteAddedEvent, type OrientationRendererInput, type OrientationRendererResult, type OrientationSummary, PROTOCOL_END, PROTOCOL_START, type PrefixedId, type PresetAction, type PresetCollision, type PresetMarkerConflict, type PresetMarkerKind, type PresetPlanSummary, type PresetRepo, type ProcessRunner, type PublishKind, type PublishTarget, type RechainOptions, type RechainResult, type ReconcileAllResult, type ReconcileAllTasksInput, type ReconcileAllTasksOptions, type ReconcileFailure, type ReconcileResult, type ReconcileTaskInput, type RefreshLinkageInput, type RefreshLinkageResult, type ReimportOptions, type ReimportResult, type RenamePlan, type ReplayOptions, type ReplayWarning, type RepoEntry, type RepoGitignoreFacts, type RepoGitignorePlan, type RepoLanguage, type RepoPresetFacts, type RepoPresetPlan, type RepoSymlinkFacts, type RepoSymlinkPlan, type RepoVisibility, type RepoWiringFacts, type ReportApprovalItem, type ReportData, type ReportDecisionItem, type ReportRendererInput, type ReportRendererResult, type ReportSessionItem, type ReportTaskItem, type ReviewGapRepoSummary, type ReviewGapUnit, type ReviewGapVerdict, type ReviewGapsInput, type ReviewGapsSummary, type RiskLevel, RiskLevelSchema, type RosterAdoptionPlan, type RosterDriftSummary, type RunOptions, type RunResult, STUCK_THRESHOLD_MS, type SanitizePathOptions, type SanitizeRelatedFilesResult, SchemaVersionSchema, type Session, type SessionEndedEvent, type SessionEntry, SessionIdSchema, type SessionImportPayload, SessionImportPayloadSchema, type SessionInnerImportInput, SessionInnerImportSchema, type SessionIntegrity, SessionIntegritySchema, type SessionMetrics, SessionMetricsSchema, SessionSchema, type SessionSkipReason, type SessionSourceKind, SessionSourceKindSchema, type SessionStartedEvent, type SessionStatus, type SessionStatusChangedEvent, SessionStatusSchema, type SessionWorkStats, type SourceRootsReconcile, type SourceWorkStats, type StatusCount, StatusSchema, type StatusSnapshot, type SuspectReason, type SymlinkCollision, type SymlinkConflict, type SymlinkPlanSummary, type Task, type TaskArchivedEvent, type TaskCreatedEvent, type TaskDeletedEvent, type TaskDocument, TaskIdSchema, type TaskLinkageRefreshedEvent, type TaskReconciledEvent, TaskSchema, type TaskSkipReason, type TaskStatus, type TaskStatusChangedEvent, type TaskStatusCount, TaskStatusSchema, TaskWriteAfterEventError, type TaskWriteAfterEventPhase, type TokenTotals, type UpdateAdHocTaskStatusInput, type UpdateTaskStatusInput, type UpdateTaskStatusResult, type ViewCollision, type ViewConflict, type ViewLinkState, type ViewRepoFact, type ViewStrayUnknown, type WiringRisk, type WiringSummary, type WorkStatsInput, type WorkStatsResult, type WorkStatsTotals, WorkspaceIdSchema, type WorkspaceViewPlan, type WriteEventsBulkOptions, type WriteTaskFileMode, acquireLock, appendBasouGitignore, appendChainedEvent, appendChainedEventLocked, appendEvent, appendEventToExistingSession, archiveTask, assertBasouRootSafe, basouPaths, buildJsonSchemas, buildStatusSnapshot, chainEvents, chainRawJsonLines, classifySuspect, claudeCodeAdapterMetadata, claudeTranscriptToImportPayload, codexRolloutToImportPayload, computeWorkStats, createAdHocSessionWithEvent, createManifest, createTaskWithEvent, deleteTask, editTask, ensureBasouDirectory, enumerateApprovals, enumerateArchivedTaskIds, enumerateSessionDirs, enumerateTaskIds, finalizeSessionYaml, findErrorCode, findReviewGaps, formatDurationMs, genesisHash, getDiff, getSnapshot, importSessionFromJson, inspectChainTail, isGitNotFound, isImportDerivedSource, isLazyExpired, isRenderable, isValidPrefixedId, lineHash, linkYamlFile, loadApproval, loadSessionEntries, loadTaskEntries, normalizeRepoKey, normalizeRepoPath, overwriteYamlFile, parseDuration, parseMarkers, pathBasename, planArchive, planGitignore, planRename, planRosterAdoption, planWorkspaceView, prefixedUlid, readAllEvents, readManifest, readMarkdownFile, readSessionYaml, readStatus, readTaskFile, readTaskFileWithArchiveFallback, readYamlFile, rechainSessionInPlace, reconcileAllTasks, reconcileSourceRoots, reconcileTask, refreshTaskLinkedSessions, reimportPreservingId, removeMarkerSection, renderDecisions, renderHandoff, renderOrientation, renderPresetBlock, renderReport, renderWithMarkers, replayEvents, resolveBasouRepositoryRoot, resolveClaudeCodeCommand, resolveRepositoryRoot, resolveSessionId, resolveTaskId, safeSimpleGit, sanitizePath, sanitizeRelatedFiles, sanitizeWorkingDirectory, serializeEventLine, serializeJsonSchema, sessionWorkStatsFromEvents, summarizeAdapterOutput, summarizeOrientation, summarizePresetPlan, summarizeRosterDrift, summarizeSymlinkPlan, summarizeWiring, tryRemoteUrl, ulid, unknownManifestKeys, updateTaskStatusWithEvent, verifyEventsChain, writeEventsBulk, writeManifest, writeMarkdownFile, writeStatus, writeTaskFile, writeYamlFile };
package/dist/index.js CHANGED
@@ -2081,6 +2081,21 @@ function parseDiffNameStatus(raw) {
2081
2081
  // src/handoff/handoff-renderer.ts
2082
2082
  import { join as join13 } from "path";
2083
2083
 
2084
+ // src/lib/recency.ts
2085
+ var DECISION_TRAILING_ACTIVITY_GAP_MS = 60 * 60 * 1e3;
2086
+ function isTrailingStale(latestActivityAt, recordedAt) {
2087
+ if (latestActivityAt === null) return false;
2088
+ return Date.parse(latestActivityAt) - Date.parse(recordedAt) > DECISION_TRAILING_ACTIVITY_GAP_MS;
2089
+ }
2090
+ function pickLatestSubstantiveEntry(entries) {
2091
+ return [...entries].sort((a, b) => {
2092
+ const aSubstantive = (a.session.session.related_files?.length ?? 0) > 0 ? 1 : 0;
2093
+ const bSubstantive = (b.session.session.related_files?.length ?? 0) > 0 ? 1 : 0;
2094
+ if (aSubstantive !== bSubstantive) return bSubstantive - aSubstantive;
2095
+ return Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at);
2096
+ })[0];
2097
+ }
2098
+
2084
2099
  // src/storage/tasks.ts
2085
2100
  import { createHash as createHash2 } from "crypto";
2086
2101
  import { mkdir as mkdir3, readdir as readdir4, readFile as readFile7, rename as rename2, stat as stat3, unlink as unlink3 } from "fs/promises";
@@ -3799,12 +3814,21 @@ async function renderHandoff(input) {
3799
3814
  const decisions = [];
3800
3815
  const tasksCreated = [];
3801
3816
  const tasksStatusChanged = [];
3817
+ let latestActivityAt = null;
3818
+ const noteActivity = (iso) => {
3819
+ if (latestActivityAt === null || Date.parse(iso) > Date.parse(latestActivityAt)) {
3820
+ latestActivityAt = iso;
3821
+ }
3822
+ };
3802
3823
  for (const entry of entries) {
3803
3824
  const sessionDir = join13(input.paths.sessions, entry.sessionId);
3825
+ const counted = entry.session.session.status !== "archived";
3826
+ if (counted) noteActivity(entry.session.session.ended_at ?? entry.session.session.started_at);
3804
3827
  try {
3805
3828
  for await (const ev of replayEvents(sessionDir, {
3806
3829
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
3807
3830
  })) {
3831
+ if (counted) noteActivity(ev.occurred_at);
3808
3832
  if (ev.type === "decision_recorded") {
3809
3833
  decisions.push({
3810
3834
  decisionId: ev.decision_id,
@@ -3864,9 +3888,7 @@ async function renderHandoff(input) {
3864
3888
  const liveEntries = entries.filter(
3865
3889
  (e) => e.session.session.status !== "archived" && e.session.session.source.kind !== "import"
3866
3890
  );
3867
- const latestSession = [...liveEntries].sort(
3868
- (a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
3869
- )[0];
3891
+ const latestSession = pickLatestSubstantiveEntry(liveEntries);
3870
3892
  const latestFiles = latestSession?.session.session.related_files ?? [];
3871
3893
  const sortedFiles = [...new Set(latestFiles)].sort();
3872
3894
  const displayedFiles = sortedFiles.slice(0, limit);
@@ -3880,6 +3902,7 @@ async function renderHandoff(input) {
3880
3902
  sessionRange,
3881
3903
  sessionCount: entries.length,
3882
3904
  latestSession,
3905
+ latestActivityAt,
3883
3906
  decisions,
3884
3907
  pendingApprovalsCount,
3885
3908
  suspectCount,
@@ -3952,6 +3975,16 @@ function formatHandoffBody(args) {
3952
3975
  } else {
3953
3976
  const last = args.decisions[args.decisions.length - 1];
3954
3977
  lines.push(`- ${last.title} [${shortIdWithPrefix(last.decisionId)}]`);
3978
+ if (args.latestActivityAt !== null && isTrailingStale(args.latestActivityAt, last.occurredAt)) {
3979
+ lines.push(
3980
+ " - \u6CE8: \u6700\u7D42\u6D3B\u52D5\u306F\u3053\u306E\u5224\u65AD\u3088\u308A\u5F8C\u3067\u3059\u3002\u4F1A\u8A71\u3067\u65E2\u306B\u89E3\u6C7A\u6E08\u307F\u306E\u53EF\u80FD\u6027\u304C\u3042\u308B\u305F\u3081\u3001\u518D\u958B\u524D\u306B\u7D99\u7D9A\u70B9\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044(\u4F1A\u8A71\u3067\u306E\u610F\u601D\u6C7A\u5B9A\u306F\u81EA\u52D5\u8A18\u9332\u3055\u308C\u307E\u305B\u3093\u3002`basou decision capture` \u3067\u8A18\u9332\u3067\u304D\u307E\u3059)\u3002"
3981
+ );
3982
+ }
3983
+ if (args.latestSession !== void 0 && last.sessionId !== args.latestSession.sessionId) {
3984
+ lines.push(
3985
+ ` - \u6CE8: \u3053\u306E\u5224\u65AD\u306F\u6700\u7D42 session \u3068\u306F\u5225\u306E session [${shortIdWithPrefix(last.sessionId)}] \u306E\u3082\u306E\u3067\u3059\u3002`
3986
+ );
3987
+ }
3955
3988
  lines.push("");
3956
3989
  lines.push(`(${args.decisions.length} decisions total \u2014 see decisions.md)`);
3957
3990
  }
@@ -4305,7 +4338,6 @@ function hasErrorCode3(error) {
4305
4338
  }
4306
4339
 
4307
4340
  // src/orientation/orientation-renderer.ts
4308
- var DECISION_TRAILING_ACTIVITY_GAP_MS = 60 * 60 * 1e3;
4309
4341
  async function summarizeOrientation(input) {
4310
4342
  const limit = input.relatedFilesLimit ?? 10;
4311
4343
  const now = new Date(input.nowIso);
@@ -4333,7 +4365,8 @@ async function summarizeOrientation(input) {
4333
4365
  decisions.push({
4334
4366
  decisionId: ev.decision_id,
4335
4367
  title: ev.title,
4336
- occurredAt: ev.occurred_at
4368
+ occurredAt: ev.occurred_at,
4369
+ sessionId: entry.sessionId
4337
4370
  });
4338
4371
  }
4339
4372
  if (counted && ev.type === "note_added" && ev.kind === "next_step") {
@@ -4386,9 +4419,7 @@ async function summarizeOrientation(input) {
4386
4419
  const liveEntries = entries.filter(
4387
4420
  (e) => e.session.session.status !== "archived" && e.session.session.source.kind !== "import"
4388
4421
  );
4389
- const latestEntry = [...liveEntries].sort(
4390
- (a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
4391
- )[0];
4422
+ const latestEntry = pickLatestSubstantiveEntry(liveEntries);
4392
4423
  const latestSession = latestEntry !== void 0 ? {
4393
4424
  sessionId: latestEntry.sessionId,
4394
4425
  label: latestEntry.session.session.label ?? null,
@@ -4474,16 +4505,20 @@ function formatOrientationBody(summary, opts) {
4474
4505
  lines.push("- \u6700\u7D42 session: (no live sessions)");
4475
4506
  }
4476
4507
  if (summary.latestDecision !== null) {
4477
- const decAge = relativeAgeJa(summary.latestDecision.occurredAt, now);
4478
- lines.push(
4479
- `- \u76F4\u8FD1\u306E\u5224\u65AD: ${summary.latestDecision.title} [${shortId(summary.latestDecision.decisionId)}] (${decAge})`
4480
- );
4508
+ const dec = summary.latestDecision;
4509
+ const decAge = relativeAgeJa(dec.occurredAt, now);
4510
+ lines.push(`- \u76F4\u8FD1\u306E\u5224\u65AD: ${dec.title} [${shortId(dec.decisionId)}] (${decAge})`);
4481
4511
  const activityAt = summary.freshness.latestActivityAt;
4482
- if (activityAt !== null && Date.parse(activityAt) - Date.parse(summary.latestDecision.occurredAt) > DECISION_TRAILING_ACTIVITY_GAP_MS) {
4512
+ if (activityAt !== null && isTrailingStale(activityAt, dec.occurredAt)) {
4483
4513
  lines.push(
4484
4514
  ` - \u6CE8: \u3053\u308C\u306F\u6700\u5F8C\u306B\u300C\u8A18\u9332\u3055\u308C\u305F\u300D\u5224\u65AD\u3067\u3059\u3002\u6700\u7D42\u6D3B\u52D5 (${relativeAgeJa(activityAt, now)}) \u306F\u3053\u308C\u3088\u308A\u5F8C\u306E\u305F\u3081\u3001\u73FE\u5728\u306E\u65B9\u91DD\u304C\u53CD\u6620\u3055\u308C\u3066\u3044\u306A\u3044\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059(\u4F1A\u8A71\u3067\u306E\u610F\u601D\u6C7A\u5B9A\u306F\u81EA\u52D5\u8A18\u9332\u3055\u308C\u307E\u305B\u3093\u3002\`basou decision capture\` \u3067\u3053\u306E session \u306E\u5224\u65AD\u3092\u8A18\u9332\u3067\u304D\u307E\u3059)\u3002`
4485
4515
  );
4486
4516
  }
4517
+ if (summary.latestSession !== null && dec.sessionId !== summary.latestSession.sessionId) {
4518
+ lines.push(
4519
+ ` - \u6CE8: \u3053\u306E\u5224\u65AD\u306F\u6700\u7D42 session \u3068\u306F\u5225\u306E session [${shortId(dec.sessionId)}] \u306E\u3082\u306E\u3067\u3059\u3002`
4520
+ );
4521
+ }
4487
4522
  if (summary.decisionCount > 1) {
4488
4523
  lines.push(` - ${summary.decisionCount} decisions total \u2014 see decisions.md`);
4489
4524
  }
@@ -4539,7 +4574,7 @@ function formatOrientationBody(summary, opts) {
4539
4574
  `- \u6B21\u306E\u8D77\u70B9 (\u8A18\u9332\u6E08\u307F, ${noteAge}): ${noteSummary(summary.latestNote.body)} [session ${shortId(summary.latestNote.sessionId)}]`
4540
4575
  );
4541
4576
  const activityAt = summary.freshness.latestActivityAt;
4542
- if (activityAt !== null && Date.parse(activityAt) - Date.parse(summary.latestNote.occurredAt) > DECISION_TRAILING_ACTIVITY_GAP_MS) {
4577
+ if (activityAt !== null && isTrailingStale(activityAt, summary.latestNote.occurredAt)) {
4543
4578
  lines.push(
4544
4579
  ` - \u6CE8: \u3053\u306E\u8D77\u70B9\u306E\u8A18\u9332\u5F8C (\u6700\u7D42\u6D3B\u52D5 ${relativeAgeJa(activityAt, now)}) \u3082\u4F5C\u696D\u304C\u7D9A\u3044\u3066\u3044\u307E\u3059\u3002\u518D\u958B\u70B9\u304C\u53E4\u3044\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`
4545
4580
  );
@@ -4549,9 +4584,17 @@ function formatOrientationBody(summary, opts) {
4549
4584
  lines.push(`- ${t.title} [${shortId(t.id)}]`);
4550
4585
  }
4551
4586
  if (summary.latestNote === null && summary.plannedTasks.length === 0) {
4552
- lines.push("- (no planned tasks \u2014 direction is inferred from recent decisions)");
4553
- if (summary.latestDecision !== null) {
4554
- lines.push(` - \u76F4\u8FD1\u306E\u5224\u65AD: ${summary.latestDecision.title}`);
4587
+ const dec = summary.latestDecision;
4588
+ if (dec === null) {
4589
+ lines.push("- (no planned tasks or recorded next step yet)");
4590
+ } else if (isTrailingStale(summary.freshness.latestActivityAt, dec.occurredAt)) {
4591
+ lines.push(
4592
+ "- (no planned tasks or recorded next step \u2014 \u6700\u7D42\u6D3B\u52D5\u306F\u76F4\u8FD1\u306E\u5224\u65AD\u3088\u308A\u5F8C\u3067\u3059\u3002\u7D99\u7D9A\u70B9\u3092\u30E6\u30FC\u30B6\u306B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044)"
4593
+ );
4594
+ lines.push(` - \u53C2\u8003 (\u53E4\u3044\u53EF\u80FD\u6027\u30FB\u65B9\u91DD\u3067\u306F\u306A\u3044): ${dec.title}`);
4595
+ } else {
4596
+ lines.push("- (no planned tasks \u2014 direction is inferred from recent decisions)");
4597
+ lines.push(` - \u76F4\u8FD1\u306E\u5224\u65AD: ${dec.title}`);
4555
4598
  }
4556
4599
  }
4557
4600
  lines.push("");
@@ -6462,6 +6505,9 @@ function hasErrorCode5(error) {
6462
6505
  import { readFile as readFile9 } from "fs/promises";
6463
6506
  var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
6464
6507
  var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
6508
+ var PROTOCOL_START = "<!-- BASOU:PROTOCOLS:START -->";
6509
+ var PROTOCOL_END = "<!-- BASOU:PROTOCOLS:END -->";
6510
+ var DEFAULT_MARKERS = { start: GENERATED_START, end: GENERATED_END };
6465
6511
  async function readMarkdownFile(filePath) {
6466
6512
  try {
6467
6513
  return await readFile9(filePath, "utf8");
@@ -6477,13 +6523,15 @@ async function writeMarkdownFile(filePath, body) {
6477
6523
  throw new Error("Failed to write markdown file", { cause: error });
6478
6524
  }
6479
6525
  }
6480
- function parseMarkers(content) {
6481
- const lines = content.split(/\r?\n/);
6526
+ function parseMarkers(content, markers = DEFAULT_MARKERS) {
6527
+ const bom = content.charCodeAt(0) === 65279 ? "\uFEFF" : "";
6528
+ const body = bom === "" ? content : content.slice(1);
6529
+ const lines = body.split(/\r?\n/);
6482
6530
  const startLines = [];
6483
6531
  const endLines = [];
6484
6532
  for (let i = 0; i < lines.length; i++) {
6485
- if (lines[i] === GENERATED_START) startLines.push(i);
6486
- else if (lines[i] === GENERATED_END) endLines.push(i);
6533
+ if (lines[i] === markers.start) startLines.push(i);
6534
+ else if (lines[i] === markers.end) endLines.push(i);
6487
6535
  }
6488
6536
  if (startLines.length === 0 && endLines.length === 0) return { kind: "no_markers" };
6489
6537
  if (startLines.length === 0) return { kind: "missing_start" };
@@ -6492,30 +6540,30 @@ function parseMarkers(content) {
6492
6540
  const startLineIdx = startLines[0];
6493
6541
  const endLineIdx = endLines[0];
6494
6542
  if (endLineIdx < startLineIdx) return { kind: "wrong_order" };
6495
- const startOffset = lineStartOffset(content, startLineIdx);
6496
- const endLineStart = lineStartOffset(content, endLineIdx);
6497
- const startLineEnd = startOffset + GENERATED_START.length;
6498
- const endLineEnd = endLineStart + GENERATED_END.length;
6499
- const before = content.slice(0, startOffset);
6500
- const afterStartNewline = skipOneNewline(content, startLineEnd);
6501
- const beforeEndNewline = trimOneNewline(content, endLineStart);
6502
- const generated = content.slice(afterStartNewline, beforeEndNewline);
6503
- const after = content.slice(endLineEnd);
6543
+ const startOffset = lineStartOffset(body, startLineIdx);
6544
+ const endLineStart = lineStartOffset(body, endLineIdx);
6545
+ const startLineEnd = startOffset + markers.start.length;
6546
+ const endLineEnd = endLineStart + markers.end.length;
6547
+ const before = bom + body.slice(0, startOffset);
6548
+ const afterStartNewline = skipOneNewline(body, startLineEnd);
6549
+ const beforeEndNewline = trimOneNewline(body, endLineStart);
6550
+ const generated = body.slice(afterStartNewline, beforeEndNewline);
6551
+ const after = body.slice(endLineEnd);
6504
6552
  return { kind: "ok", before, generated, after };
6505
6553
  }
6506
- function renderWithMarkers(existing, generated, fileLabel) {
6554
+ function renderWithMarkers(existing, generated, fileLabel, markers = DEFAULT_MARKERS) {
6507
6555
  const normalized = generated.endsWith("\n") ? generated : `${generated}
6508
6556
  `;
6509
6557
  if (existing === null) {
6510
- return `${GENERATED_START}
6511
- ${normalized}${GENERATED_END}
6558
+ return `${markers.start}
6559
+ ${normalized}${markers.end}
6512
6560
  `;
6513
6561
  }
6514
- const section = parseMarkers(existing);
6562
+ const section = parseMarkers(existing, markers);
6515
6563
  switch (section.kind) {
6516
6564
  case "ok":
6517
- return `${section.before}${GENERATED_START}
6518
- ${normalized}${GENERATED_END}${section.after}`;
6565
+ return `${section.before}${markers.start}
6566
+ ${normalized}${markers.end}${section.after}`;
6519
6567
  case "no_markers":
6520
6568
  throw new Error(`Markers missing in ${fileLabel}`);
6521
6569
  case "missing_start":
@@ -6525,6 +6573,22 @@ ${normalized}${GENERATED_END}${section.after}`;
6525
6573
  throw new Error(`Markers mismatched in ${fileLabel}`);
6526
6574
  }
6527
6575
  }
6576
+ function removeMarkerSection(existing, fileLabel, markers = DEFAULT_MARKERS) {
6577
+ const section = parseMarkers(existing, markers);
6578
+ switch (section.kind) {
6579
+ case "no_markers":
6580
+ return existing;
6581
+ case "ok": {
6582
+ const after = section.after.replace(/^\r?\n/, "");
6583
+ return section.before + after;
6584
+ }
6585
+ case "missing_start":
6586
+ case "missing_end":
6587
+ case "multiple_pairs":
6588
+ case "wrong_order":
6589
+ throw new Error(`Markers mismatched in ${fileLabel}`);
6590
+ }
6591
+ }
6528
6592
  function lineStartOffset(content, lineIdx) {
6529
6593
  if (lineIdx === 0) return 0;
6530
6594
  let offset = 0;
@@ -6965,6 +7029,8 @@ export {
6965
7029
  IsoTimestampSchema,
6966
7030
  JSON_SCHEMA_VERSION,
6967
7031
  ManifestSchema,
7032
+ PROTOCOL_END,
7033
+ PROTOCOL_START,
6968
7034
  RiskLevelSchema,
6969
7035
  STUCK_THRESHOLD_MS,
6970
7036
  SchemaVersionSchema,
@@ -7055,6 +7121,7 @@ export {
7055
7121
  reconcileTask,
7056
7122
  refreshTaskLinkedSessions,
7057
7123
  reimportPreservingId,
7124
+ removeMarkerSection,
7058
7125
  renderDecisions,
7059
7126
  renderHandoff,
7060
7127
  renderOrientation,