Skip to content

Commit cfe24d0

Browse files
committed
feat: add quick reference index to DECISIONS.md
- Auto-generated index table at top of DECISIONS.md - Index auto-updates on `ctx add decision` - New `ctx decisions reindex` command for manual regeneration - Normalized older entries to full timestamp format Helps AI agents locate decisions without reading entire file when context budget is limited. Signed-off-by: Jose Alekhinne <alekhinejose@gmail.com>
1 parent 0d182ce commit cfe24d0

6 files changed

Lines changed: 651 additions & 12 deletions

File tree

‎.context/DECISIONS.md‎

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
# Decisions
22

3+
<!-- INDEX:START -->
4+
| Date | Decision |
5+
|------------|----------------------------------------------------------------------------------|
6+
| 2026-01-29 | Add quick reference index to DECISIONS.md |
7+
| 2026-01-28 | No custom UI - IDE is the interface |
8+
| 2026-01-28 | ctx recall is Claude-first |
9+
| 2026-01-28 | Tasks must include explicit deliverables, not just implementation steps |
10+
| 2026-01-28 | Use tool-agnostic Session type with tool-specific parsers for recall system |
11+
| 2026-01-27 | Use reverse-chronological order (newest first) for DECISIONS.md and LEARNINGS.md |
12+
| 2026-01-25 | Removed AGENTS.md from project root |
13+
| 2026-01-25 | Keep CONSTITUTION Minimal |
14+
| 2026-01-25 | Centralize Constants with Semantic Prefixes |
15+
| 2026-01-21 | Separate Orchestrator Directive from Agent Tasks |
16+
| 2026-01-21 | Hooks Use ctx from PATH, Not Hardcoded Paths |
17+
| 2026-01-20 | Use SessionEnd Hook for Auto-Save |
18+
| 2026-01-20 | Handle CLAUDE.md Creation/Merge in ctx init |
19+
| 2026-01-20 | Auto-Save Before Compact |
20+
| 2026-01-20 | Session Filename Format: YYYY-MM-DD-HHMMSS-topic.md |
21+
| 2026-01-20 | Two-Tier Context Persistence Model |
22+
| 2026-01-20 | Always Generate Claude Hooks in Init (No Flag Needed) |
23+
| 2026-01-20 | Generic Core with Optional Claude Code Enhancements |
24+
<!-- INDEX:END -->
25+
26+
## [2026-01-29-044515] Add quick reference index to DECISIONS.md
27+
28+
**Status**: Accepted
29+
30+
**Context**: AI agents need to locate decisions quickly without reading the
31+
entire file when context budget is limited
32+
33+
**Decision**: Add quick reference index to DECISIONS.md
34+
35+
**Rationale**: Compact table at top allows scanning; agents can grep for full
36+
timestamp to jump to entry
37+
38+
**Consequences**: Index auto-updated on ctx add decision; ctx decisions
39+
reindex for manual edits
40+
41+
---
42+
343
## [2026-01-28-051426] No custom UI - IDE is the interface
444

545
**Status**: Accepted
@@ -114,7 +154,7 @@ token budget or whether AI uses ctx agent.
114154

115155
---
116156

117-
## [2026-01-25-2208] Removed AGENTS.md from project root
157+
## [2026-01-25-220800] Removed AGENTS.md from project root
118158

119159
**Status**: Accepted
120160

@@ -131,7 +171,7 @@ AGENT_PLAYBOOK.md provides ctx-specific instructions.
131171

132172
---
133173

134-
## [2026-01-25] Keep CONSTITUTION Minimal
174+
## [2026-01-25-180000] Keep CONSTITUTION Minimal
135175

136176
**Status**: Accepted
137177

@@ -158,7 +198,7 @@ Style preferences and best practices go in CONVENTIONS.md instead.
158198

159199
---
160200

161-
## [2026-01-25] Centralize Constants with Semantic Prefixes
201+
## [2026-01-25-170000] Centralize Constants with Semantic Prefixes
162202

163203
**Status**: Accepted (implemented)
164204

@@ -194,7 +234,7 @@ var FileType = map[string]string{
194234

195235
---
196236

197-
## [2026-01-21] Separate Orchestrator Directive from Agent Tasks
237+
## [2026-01-21-140000] Separate Orchestrator Directive from Agent Tasks
198238

199239
**Status**: Accepted
200240

@@ -226,7 +266,7 @@ agent to check its own mind.
226266

227267
---
228268

229-
## [2026-01-21] Hooks Use ctx from PATH, Not Hardcoded Paths
269+
## [2026-01-21-120000] Hooks Use ctx from PATH, Not Hardcoded Paths
230270

231271
**Status**: Accepted (implemented)
232272

@@ -255,7 +295,7 @@ This breaks when:
255295

256296
---
257297

258-
## [2026-01-20] Use SessionEnd Hook for Auto-Save
298+
## [2026-01-20-200000] Use SessionEnd Hook for Auto-Save
259299

260300
**Status**: Accepted (implemented)
261301

@@ -279,7 +319,7 @@ This breaks when:
279319

280320
---
281321

282-
## [2026-01-20] Handle CLAUDE.md Creation/Merge in ctx init
322+
## [2026-01-20-180000] Handle CLAUDE.md Creation/Merge in ctx init
283323

284324
**Status**: Accepted (to be implemented)
285325

@@ -311,7 +351,7 @@ but may already have a CLAUDE.md from `claude init`.
311351

312352
---
313353

314-
## [2026-01-20] Auto-Save Before Compact
354+
## [2026-01-20-160000] Auto-Save Before Compact
315355

316356
**Status**: Accepted (to be implemented)
317357

@@ -334,7 +374,7 @@ lost if not captured.
334374

335375
---
336376

337-
## [2026-01-20] Session Filename Format: YYYY-MM-DD-HHMMSS-topic.md
377+
## [2026-01-20-140000] Session Filename Format: YYYY-MM-DD-HHMMSS-topic.md
338378

339379
**Status**: Accepted
340380

@@ -359,7 +399,7 @@ Two file types:
359399

360400
---
361401

362-
## [2026-01-20] Two-Tier Context Persistence Model
402+
## [2026-01-20-120000] Two-Tier Context Persistence Model
363403

364404
**Status**: Accepted
365405

@@ -386,7 +426,7 @@ what can be loaded. But nothing should be truly lost.
386426

387427
---
388428

389-
## [2026-01-20] Always Generate Claude Hooks in Init (No Flag Needed)
429+
## [2026-01-20-100000] Always Generate Claude Hooks in Init (No Flag Needed)
390430

391431
**Status**: Accepted (to be implemented)
392432

@@ -413,7 +453,7 @@ ctx init # Creates BOTH .context/ AND .claude/hooks/
413453

414454
---
415455

416-
## [2026-01-20] Generic Core with Optional Claude Code Enhancements
456+
## [2026-01-20-080000] Generic Core with Optional Claude Code Enhancements
417457

418458
**Status**: Accepted
419459

‎internal/bootstrap/bootstrap.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/ActiveMemory/ctx/internal/cli/agent"
2323
"github.com/ActiveMemory/ctx/internal/cli/compact"
2424
"github.com/ActiveMemory/ctx/internal/cli/complete"
25+
"github.com/ActiveMemory/ctx/internal/cli/decisions"
2526
"github.com/ActiveMemory/ctx/internal/cli/drift"
2627
"github.com/ActiveMemory/ctx/internal/cli/hook"
2728
"github.com/ActiveMemory/ctx/internal/cli/initialize"
@@ -80,6 +81,7 @@ func Initialize(cmd *cobra.Command) *cobra.Command {
8081
cmd.AddCommand(drift.Cmd())
8182
cmd.AddCommand(sync.Cmd())
8283
cmd.AddCommand(compact.Cmd())
84+
cmd.AddCommand(decisions.Cmd())
8385
cmd.AddCommand(watch.Cmd())
8486
cmd.AddCommand(hook.Cmd())
8587
cmd.AddCommand(session.Cmd())

‎internal/cli/add/index.go‎

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// / Context: https://ctx.ist
2+
// ,'`./ do you remember?
3+
// `.,'\
4+
// \ Copyright 2026-present Context contributors.
5+
// SPDX-License-Identifier: Apache-2.0
6+
7+
package add
8+
9+
import (
10+
"regexp"
11+
"strings"
12+
)
13+
14+
// Index markers for DECISIONS.md
15+
const (
16+
IndexStart = "<!-- INDEX:START -->"
17+
IndexEnd = "<!-- INDEX:END -->"
18+
)
19+
20+
// DecisionEntry represents a parsed decision header.
21+
type DecisionEntry struct {
22+
Timestamp string // Full timestamp: YYYY-MM-DD-HHMMSS
23+
Date string // Date only: YYYY-MM-DD
24+
Title string // Decision title
25+
}
26+
27+
// decisionHeaderRegex matches decision headers like "## [2026-01-28-051426] Title here"
28+
var decisionHeaderRegex = regexp.MustCompile(`## \[(\d{4}-\d{2}-\d{2})-(\d{6})\] (.+)`)
29+
30+
// ParseDecisionHeaders extracts all decision entries from file content.
31+
//
32+
// It scans for headers matching the pattern "## [YYYY-MM-DD-HHMMSS] Title"
33+
// and returns them in the order they appear in the file.
34+
//
35+
// Parameters:
36+
// - content: The full content of DECISIONS.md
37+
//
38+
// Returns:
39+
// - []DecisionEntry: Slice of parsed entries (may be empty)
40+
func ParseDecisionHeaders(content string) []DecisionEntry {
41+
var entries []DecisionEntry
42+
43+
matches := decisionHeaderRegex.FindAllStringSubmatch(content, -1)
44+
for _, match := range matches {
45+
if len(match) == 4 {
46+
date := match[1]
47+
time := match[2]
48+
title := match[3]
49+
entries = append(entries, DecisionEntry{
50+
Timestamp: date + "-" + time,
51+
Date: date,
52+
Title: title,
53+
})
54+
}
55+
}
56+
57+
return entries
58+
}
59+
60+
// GenerateIndex creates a markdown table index from decision entries.
61+
//
62+
// The table has two columns: Date and Decision title.
63+
// If there are no entries, returns an empty string.
64+
//
65+
// Parameters:
66+
// - entries: Slice of decision entries to include
67+
//
68+
// Returns:
69+
// - string: Markdown table (without markers) or empty string
70+
func GenerateIndex(entries []DecisionEntry) string {
71+
if len(entries) == 0 {
72+
return ""
73+
}
74+
75+
var sb strings.Builder
76+
sb.WriteString("| Date | Decision |\n")
77+
sb.WriteString("|------|----------|\n")
78+
79+
for _, e := range entries {
80+
// Escape pipe characters in title
81+
title := strings.ReplaceAll(e.Title, "|", "\\|")
82+
sb.WriteString("| ")
83+
sb.WriteString(e.Date)
84+
sb.WriteString(" | ")
85+
sb.WriteString(title)
86+
sb.WriteString(" |\n")
87+
}
88+
89+
return sb.String()
90+
}
91+
92+
// UpdateIndex regenerates the decision index in file content.
93+
//
94+
// If INDEX:START and INDEX:END markers exist, the content between them
95+
// is replaced. Otherwise, the index is inserted after "# Decisions".
96+
// If there are no decision entries, any existing index is removed.
97+
//
98+
// Parameters:
99+
// - content: The full content of DECISIONS.md
100+
//
101+
// Returns:
102+
// - string: Updated content with regenerated index
103+
func UpdateIndex(content string) string {
104+
entries := ParseDecisionHeaders(content)
105+
indexContent := GenerateIndex(entries)
106+
107+
// Check if markers already exist
108+
startIdx := strings.Index(content, IndexStart)
109+
endIdx := strings.Index(content, IndexEnd)
110+
111+
if startIdx != -1 && endIdx != -1 && endIdx > startIdx {
112+
// Replace existing index
113+
if indexContent == "" {
114+
// No entries - remove index entirely (including markers and surrounding whitespace)
115+
before := strings.TrimRight(content[:startIdx], "\n")
116+
after := content[endIdx+len(IndexEnd):]
117+
after = strings.TrimLeft(after, "\n")
118+
if after != "" {
119+
return before + "\n\n" + after
120+
}
121+
return before + "\n"
122+
}
123+
// Replace content between markers
124+
before := content[:startIdx+len(IndexStart)]
125+
after := content[endIdx:]
126+
return before + "\n" + indexContent + after
127+
}
128+
129+
// No existing markers - insert after "# Decisions" header
130+
if indexContent == "" {
131+
// No entries, nothing to insert
132+
return content
133+
}
134+
135+
headerIdx := strings.Index(content, "# Decisions")
136+
if headerIdx == -1 {
137+
// No header found, return unchanged
138+
return content
139+
}
140+
141+
// Find end of header line
142+
lineEnd := strings.Index(content[headerIdx:], "\n")
143+
if lineEnd == -1 {
144+
// Header is at end of file
145+
return content + "\n\n" + IndexStart + "\n" + indexContent + IndexEnd + "\n"
146+
}
147+
148+
insertPoint := headerIdx + lineEnd + 1
149+
150+
// Build new content with index
151+
var sb strings.Builder
152+
sb.WriteString(content[:insertPoint])
153+
sb.WriteString("\n")
154+
sb.WriteString(IndexStart)
155+
sb.WriteString("\n")
156+
sb.WriteString(indexContent)
157+
sb.WriteString(IndexEnd)
158+
sb.WriteString("\n")
159+
sb.WriteString(content[insertPoint:])
160+
161+
return sb.String()
162+
}

0 commit comments

Comments
 (0)