bitacora-cli 1.0.0 → 1.1.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/README.md CHANGED
@@ -14,24 +14,6 @@ Show help:
14
14
  bitacora --help
15
15
  ```
16
16
 
17
- Typical flow:
18
-
19
- ```bash
20
- # init bitacora files structure and add SKILL
21
- bitacora init
22
-
23
- # ask your agent to complete bitacora
24
- # whith your project info. You can
25
- # complete bitacora by yourself if you wish.
26
- codex "$bitacora read and fill bitacora template with this project info"
27
-
28
- # plan some tasks
29
- codex (planner )"$bitacora plan the unit test for this repo and add separated tasks to bitacora"
30
-
31
- # use another agent
32
- codex "$bitacora implement the 003 track"
33
- ```
34
-
35
17
  Use `--root <path>` on any command to target a different project directory.
36
18
 
37
19
  ## What Each Bitacora File Stores
@@ -43,42 +25,51 @@ Use `--root <path>` on any command to target a different project directory.
43
25
  - `bitacora/ux-style-guide.md`: visual tokens and UX interaction rules.
44
26
  - `bitacora/tracks/tracks.md`: canonical track registry, current snapshot, and handoff summary.
45
27
  - `bitacora/tracks/TRACK-*/track.md`: per-track plan, tasks, decisions, and timestamped log.
28
+ - `bitacora/history/TRACK-*.md`: archived full track detail after compaction (read on demand).
29
+ - `bitacora/history/tracks-*.md`: archived snapshots of `tracks/tracks.md`.
46
30
  - `.agents/skills/bitacora/SKILL.md`: instructions agents follow to keep the memory system updated.
47
31
 
48
- ## How Agents Read and Update Bitacora
32
+ ## Compaction Model (v1.1.0)
49
33
 
50
- Recommended memory read order for agents:
34
+ Bitacora now supports compacting finished tracks to reduce active context size and token usage.
51
35
 
52
- 1. `bitacora/index.md`
53
- 2. `bitacora/product.md`
54
- 3. `bitacora/tech-stack.md`
55
- 4. `bitacora/workflow.md`
56
- 5. `bitacora/ux-style-guide.md`
57
- 6. `bitacora/tracks/tracks.md`
58
- 7. Active files in `bitacora/tracks/TRACK-*/track.md`
36
+ ### How it works
59
37
 
60
- Typical agent write operations:
38
+ 1. Keep full details while implementing.
39
+ 2. When a track is fully done, compact it.
40
+ 3. Bitacora writes a short version in `tracks/TRACK-xxx/track.md`.
41
+ 4. Full detail is archived in `bitacora/history/TRACK-xxx.md`.
42
+ 5. `bitacora/tracks/tracks.md` is regenerated in compact form.
61
43
 
62
- - `bitacora new-track`: create a new work unit.
63
- - `bitacora log --track-id ... --message ...`: append progress updates.
64
- - direct updates to `bitacora/tracks/TRACK-*/track.md`: tasks, decisions, execution details.
65
- - direct updates to `bitacora/tracks/tracks.md`: canonical project status and next action.
66
- - `bitacora validate` and `bitacora rebuild-state`: ensure memory remains valid and deterministic.
44
+ ### Completion gates for `--complete`
67
45
 
68
- ## Agent Skill Integration
46
+ `bitacora compact --complete` only succeeds when:
69
47
 
70
- When you run `bitacora init`, Bitacora also creates an agent skill at:
48
+ - `# Tasks` has no unchecked checklist item (`- [ ]`).
49
+ - `# Log` contains at least one verification line with `TEST:`.
71
50
 
72
- ```text
73
- .agents/skills/bitacora/SKILL.md
74
- ```
51
+ If gates fail, command exits with code `1` and no compaction is applied.
52
+
53
+ ## Typical flow
54
+
55
+ ```bash
56
+ # 1) bootstrap
57
+ bitacora init
58
+
59
+ # 2) create work
60
+ bitacora new-track
75
61
 
76
- This skill guides agents to keep memory updated during implementation:
62
+ # 3) append progress
63
+ bitacora log --track-id TRACK-001 --message "implemented parser"
64
+ bitacora log --track-id TRACK-001 --message "TEST: npm test -- --run tests/core/parser.test.ts -> pass"
77
65
 
78
- - Read project memory before coding.
79
- - Create and update tracks as work progresses.
80
- - Log decisions and progress.
81
- - Keep a consistent handoff summary in `bitacora/tracks/tracks.md`.
66
+ # 4) compact when fully completed
67
+ bitacora compact --track-id TRACK-001 --complete
68
+
69
+ # 5) inspect archive only when needed
70
+ bitacora history --track-id TRACK-001
71
+ bitacora history --track-id TRACK-001 --show
72
+ ```
82
73
 
83
74
  ## Commands
84
75
 
@@ -106,6 +97,40 @@ Appends a timestamped log entry to an existing track.
106
97
  - `--message <text>`: required log message.
107
98
  - `--root <path>`: sets the project root.
108
99
 
100
+ ### `bitacora compact [--track-id <trackId> | --all] [--complete] [--dry-run] [--root <path>]`
101
+
102
+ Compacts tracks by summarizing content and archiving full detail.
103
+
104
+ - `--track-id <trackId>`: compact a specific track.
105
+ - `--all`: compact all eligible tracks.
106
+ - `--complete`: mark target tracks as completed (requires completion gates).
107
+ - `--dry-run`: report estimated byte/token savings without writing files.
108
+ - `--root <path>`: sets the project root.
109
+
110
+ Examples:
111
+
112
+ ```bash
113
+ # Compact one already-completed track
114
+ bitacora compact --track-id TRACK-004
115
+
116
+ # Complete + compact in one step
117
+ bitacora compact --track-id TRACK-004 --complete
118
+
119
+ # Preview savings for all tracks
120
+ bitacora compact --all --dry-run
121
+
122
+ # Compact all completed tracks
123
+ bitacora compact --all
124
+ ```
125
+
126
+ ### `bitacora history --track-id <trackId> [--show] [--root <path>]`
127
+
128
+ Reads archived track history.
129
+
130
+ - `--track-id <trackId>`: required track identifier.
131
+ - `--show`: print full archived content (default prints only metadata/path).
132
+ - `--root <path>`: sets the project root.
133
+
109
134
  ### `bitacora validate [--json] [--root <path>]`
110
135
 
111
136
  Validates the Bitacora file/folder structure and reports errors.
package/dist/src/cli.js CHANGED
@@ -5,6 +5,8 @@ exports.createCliProgram = createCliProgram;
5
5
  exports.runCli = runCli;
6
6
  const commander_1 = require("commander");
7
7
  const init_1 = require("./commands/init");
8
+ const compact_1 = require("./commands/compact");
9
+ const history_1 = require("./commands/history");
8
10
  const log_1 = require("./commands/log");
9
11
  const new_track_1 = require("./commands/new-track");
10
12
  const rebuild_state_1 = require("./commands/rebuild-state");
@@ -41,6 +43,22 @@ Command details:
41
43
  --track-id <trackId> Track identifier.
42
44
  --message <text> Log entry message.
43
45
  --root <path> Project root path (default: current directory).
46
+
47
+ compact
48
+ Compacts tracks by summarizing and archiving redundant details.
49
+ Options:
50
+ --track-id <trackId> Compact a specific track.
51
+ --all Compact all eligible tracks.
52
+ --complete Mark target tracks as completed (requires completion gates).
53
+ --dry-run Show savings without modifying files.
54
+ --root <path> Project root path (default: current directory).
55
+
56
+ history
57
+ Reads archived history for a compacted track.
58
+ Options:
59
+ --track-id <trackId> Track identifier.
60
+ --show Print full archived content.
61
+ --root <path> Project root path (default: current directory).
44
62
  `;
45
63
  function writeLine(writer, message) {
46
64
  writer(message.endsWith("\n") ? message : `${message}\n`);
@@ -142,6 +160,48 @@ function createCliProgram(runtime = {}, onCommandExitCode) {
142
160
  });
143
161
  onCommandExitCode?.(exitCode);
144
162
  });
163
+ program
164
+ .command("compact")
165
+ .description("Compact tracks and archive redundant details.")
166
+ .option("--track-id <trackId>", "track identifier")
167
+ .option("--all", "compact all eligible tracks")
168
+ .option("--complete", "mark track(s) as completed before compaction")
169
+ .option("--dry-run", "report savings without writing files")
170
+ .option("--root <path>", "project root path")
171
+ .action((options) => {
172
+ const compactOptions = {
173
+ rootDir: options.root ?? cwd(),
174
+ all: Boolean(options.all),
175
+ complete: Boolean(options.complete),
176
+ dryRun: Boolean(options.dryRun)
177
+ };
178
+ if (options.trackId !== undefined) {
179
+ compactOptions.trackId = options.trackId;
180
+ }
181
+ const exitCode = (0, compact_1.runCompactCommand)(compactOptions, {
182
+ now,
183
+ onOutput: (message) => writeLine(stdout, message),
184
+ onError: (message) => writeLine(stderr, message)
185
+ });
186
+ onCommandExitCode?.(exitCode);
187
+ });
188
+ program
189
+ .command("history")
190
+ .description("Read archived history for a compacted track.")
191
+ .requiredOption("--track-id <trackId>", "track identifier")
192
+ .option("--show", "print full archived content")
193
+ .option("--root <path>", "project root path")
194
+ .action((options) => {
195
+ const exitCode = (0, history_1.runHistoryCommand)({
196
+ rootDir: options.root ?? cwd(),
197
+ trackId: options.trackId,
198
+ show: Boolean(options.show)
199
+ }, {
200
+ onOutput: (message) => writeLine(stdout, message),
201
+ onError: (message) => writeLine(stderr, message)
202
+ });
203
+ onCommandExitCode?.(exitCode);
204
+ });
145
205
  program.addHelpText("after", COMMAND_HELP_OVERVIEW);
146
206
  return program;
147
207
  }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runCompactCommand = runCompactCommand;
4
+ const compaction_1 = require("../core/compaction");
5
+ const validator_1 = require("../core/validator");
6
+ function formatSavings(label, before, after, tokensBefore, tokensAfter) {
7
+ const bytesSaved = before - after;
8
+ const tokensSaved = tokensBefore - tokensAfter;
9
+ return `${label}: bytes ${before} -> ${after} (${bytesSaved >= 0 ? "-" : "+"}${Math.abs(bytesSaved)}), tokens ${tokensBefore} -> ${tokensAfter} (${tokensSaved >= 0 ? "-" : "+"}${Math.abs(tokensSaved)})`;
10
+ }
11
+ function runCompactCommand(options, dependencies = {}) {
12
+ const now = dependencies.now ?? (() => new Date().toISOString());
13
+ const onOutput = dependencies.onOutput ?? (() => { });
14
+ const onError = dependencies.onError ?? (() => { });
15
+ if (options.all && options.trackId) {
16
+ onError("Use either --all or --track-id, not both");
17
+ return 1;
18
+ }
19
+ if (!options.all && !options.trackId) {
20
+ onError("Missing target. Provide --track-id <TRACK-###> or --all");
21
+ return 1;
22
+ }
23
+ const initialValidation = (0, validator_1.validateMemory)(options.rootDir);
24
+ if (!initialValidation.ok) {
25
+ onError(`Memory structure is invalid: ${initialValidation.errors.join("; ")}`);
26
+ return 1;
27
+ }
28
+ const targets = options.trackId
29
+ ? [options.trackId]
30
+ : initialValidation.tracks.map((track) => track.frontmatter.track_id).sort((left, right) => left.localeCompare(right));
31
+ const timestamp = now();
32
+ let compactedCount = 0;
33
+ for (const trackId of targets) {
34
+ try {
35
+ const result = (0, compaction_1.compactTrack)({
36
+ rootDir: options.rootDir,
37
+ trackId,
38
+ complete: options.complete,
39
+ dryRun: options.dryRun,
40
+ now: timestamp
41
+ });
42
+ if (!result.compacted) {
43
+ onOutput(`${trackId}: skipped (${result.skippedReason ?? "not eligible"})`);
44
+ continue;
45
+ }
46
+ compactedCount += 1;
47
+ onOutput(formatSavings(trackId, result.bytesBefore, result.bytesAfter, result.estimatedTokensBefore, result.estimatedTokensAfter));
48
+ }
49
+ catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ onError(message);
52
+ if (options.trackId) {
53
+ return 1;
54
+ }
55
+ }
56
+ }
57
+ if (compactedCount === 0) {
58
+ onOutput("No tracks compacted");
59
+ return options.trackId ? 1 : 0;
60
+ }
61
+ const finalValidation = (0, validator_1.validateMemory)(options.rootDir);
62
+ if (!finalValidation.ok) {
63
+ onError(`Memory structure is invalid: ${finalValidation.errors.join("; ")}`);
64
+ return 1;
65
+ }
66
+ const registrySavings = (0, compaction_1.compactTracksRegistry)({
67
+ rootDir: options.rootDir,
68
+ now: timestamp,
69
+ tracks: finalValidation.tracks,
70
+ dryRun: options.dryRun
71
+ });
72
+ onOutput(formatSavings("tracks.md", registrySavings.bytesBefore, registrySavings.bytesAfter, registrySavings.estimatedTokensBefore, registrySavings.estimatedTokensAfter));
73
+ onOutput(`Compacted tracks: ${compactedCount}`);
74
+ return 0;
75
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runHistoryCommand = runHistoryCommand;
4
+ const compaction_1 = require("../core/compaction");
5
+ function runHistoryCommand(options, dependencies = {}) {
6
+ const onOutput = dependencies.onOutput ?? (() => { });
7
+ const onError = dependencies.onError ?? (() => { });
8
+ try {
9
+ const result = (0, compaction_1.readTrackHistory)(options.rootDir, options.trackId, options.show);
10
+ if (!result.exists) {
11
+ onError(`History not found for ${options.trackId}: ${result.historyPath}`);
12
+ return 1;
13
+ }
14
+ if (!options.show) {
15
+ onOutput(`Track: ${result.trackId}`);
16
+ onOutput(`History path: ${result.historyPath}`);
17
+ onOutput("Use --show to print history contents");
18
+ return 0;
19
+ }
20
+ onOutput(result.content ?? "");
21
+ return 0;
22
+ }
23
+ catch (error) {
24
+ const message = error instanceof Error ? error.message : String(error);
25
+ onError(message);
26
+ return 1;
27
+ }
28
+ }
@@ -78,6 +78,7 @@ function runInitCommand(options, dependencies = {}) {
78
78
  const createdOnDate = createdAt.slice(0, 10);
79
79
  const trackId = "TRACK-001";
80
80
  node_fs_1.default.mkdirSync(node_path_1.default.join(memoryRoot, "tracks", trackId), { recursive: true });
81
+ node_fs_1.default.mkdirSync(node_path_1.default.join(memoryRoot, "history"), { recursive: true });
81
82
  node_fs_1.default.mkdirSync(node_path_1.default.join(options.rootDir, ".agents", "skills", "bitacora"), { recursive: true });
82
83
  node_fs_1.default.writeFileSync(node_path_1.default.join(memoryRoot, "product.md"), (0, templates_1.createProductTemplate)(), "utf8");
83
84
  node_fs_1.default.writeFileSync(node_path_1.default.join(memoryRoot, "tech-stack.md"), (0, templates_1.createTechStackTemplate)(), "utf8");
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.estimateTokenCount = estimateTokenCount;
7
+ exports.compactTrack = compactTrack;
8
+ exports.buildCompactedTracksRegistry = buildCompactedTracksRegistry;
9
+ exports.compactTracksRegistry = compactTracksRegistry;
10
+ exports.readTrackHistory = readTrackHistory;
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const parser_1 = require("./parser");
14
+ const MAX_DECISIONS_IN_COMPACT_TRACK = 3;
15
+ const PENDING_TASK_REGEX = /^-\s*\[\s\]/m;
16
+ const TEST_LOG_REGEX = /^-\s+[^|]+\s+\|\s+TEST:\s+.+$/m;
17
+ function uniqueOrdered(lines) {
18
+ const seen = new Set();
19
+ const result = [];
20
+ for (const line of lines) {
21
+ if (seen.has(line)) {
22
+ continue;
23
+ }
24
+ seen.add(line);
25
+ result.push(line);
26
+ }
27
+ return result;
28
+ }
29
+ function extractLines(section) {
30
+ return section
31
+ .split("\n")
32
+ .map((line) => line.trim())
33
+ .filter((line) => line.length > 0);
34
+ }
35
+ function summarizeOverview(overview) {
36
+ const lines = extractLines(overview);
37
+ if (lines.length === 0) {
38
+ return "- Summary unavailable.";
39
+ }
40
+ return lines.slice(0, 2).map((line) => `- ${line.replace(/^-\s*/, "")}`).join("\n");
41
+ }
42
+ function summarizeTasks(tasks) {
43
+ const lines = extractLines(tasks);
44
+ const checkboxes = lines.filter((line) => /^-\s*\[[ xX]\]/.test(line));
45
+ const done = checkboxes.filter((line) => /^-\s*\[[xX]\]/.test(line)).length;
46
+ if (checkboxes.length === 0) {
47
+ return "- No checklist tasks found.";
48
+ }
49
+ return `- Checklist completed: ${done}/${checkboxes.length}`;
50
+ }
51
+ function summarizeDecisions(decisions) {
52
+ const normalizedLines = extractLines(decisions)
53
+ .map((line) => line.replace(/^-\s*/, ""));
54
+ const uniqueLines = uniqueOrdered(normalizedLines);
55
+ const tail = uniqueLines.slice(-MAX_DECISIONS_IN_COMPACT_TRACK);
56
+ if (tail.length === 0) {
57
+ return "- No decisions captured.";
58
+ }
59
+ return tail.map((line) => `- ${line}`).join("\n");
60
+ }
61
+ function assertCanMarkAsCompleted(trackPath, tasks, log) {
62
+ if (PENDING_TASK_REGEX.test(tasks)) {
63
+ throw new Error(`Cannot mark track as completed: pending tasks found (${trackPath})`);
64
+ }
65
+ if (!TEST_LOG_REGEX.test(log)) {
66
+ throw new Error(`Cannot mark track as completed: missing TEST: evidence in log (${trackPath})`);
67
+ }
68
+ }
69
+ function renderFrontmatter(fields) {
70
+ const lines = Object.entries(fields).map(([key, value]) => `${key}: ${value}`);
71
+ return `---\n${lines.join("\n")}\n---`;
72
+ }
73
+ function ensureHistoryRoot(rootDir) {
74
+ const historyRoot = node_path_1.default.join(rootDir, "bitacora", "history");
75
+ node_fs_1.default.mkdirSync(historyRoot, { recursive: true });
76
+ return historyRoot;
77
+ }
78
+ function buildCompactedTrackMarkdown(input) {
79
+ const frontmatter = renderFrontmatter({
80
+ track_id: input.trackId,
81
+ status: input.status,
82
+ priority: input.priority,
83
+ created_at: input.createdAt,
84
+ updated_at: input.updatedAt,
85
+ completion: String(input.completion),
86
+ compacted_at: input.compactedAt,
87
+ history_path: input.historyPath
88
+ });
89
+ return `${frontmatter}
90
+
91
+ # Overview
92
+ ${summarizeOverview(input.overview)}
93
+ - Full history: ${input.historyPath}
94
+
95
+ # Tasks
96
+ ${summarizeTasks(input.tasks)}
97
+
98
+ # Decisions
99
+ ${summarizeDecisions(input.decisions)}
100
+
101
+ # Log
102
+ - ${input.compactedAt} | compacted track and archived full history
103
+ `;
104
+ }
105
+ function estimateTokenCount(text) {
106
+ return Math.ceil(text.length / 4);
107
+ }
108
+ function compactTrack(request) {
109
+ const trackPath = node_path_1.default.join(request.rootDir, "bitacora", "tracks", request.trackId, "track.md");
110
+ if (!node_fs_1.default.existsSync(trackPath)) {
111
+ throw new Error(`Track not found: ${request.trackId}`);
112
+ }
113
+ const source = node_fs_1.default.readFileSync(trackPath, "utf8").replace(/\r\n/g, "\n");
114
+ const parsed = (0, parser_1.parseTrackMarkdown)(source);
115
+ if (request.complete) {
116
+ assertCanMarkAsCompleted(trackPath, parsed.sections.tasks, parsed.sections.log);
117
+ }
118
+ else if (parsed.frontmatter.status !== "completed") {
119
+ return {
120
+ trackId: request.trackId,
121
+ compacted: false,
122
+ skippedReason: "Track is not completed",
123
+ bytesBefore: source.length,
124
+ bytesAfter: source.length,
125
+ estimatedTokensBefore: estimateTokenCount(source),
126
+ estimatedTokensAfter: estimateTokenCount(source)
127
+ };
128
+ }
129
+ const compactedAt = request.now;
130
+ const updatedAt = request.now;
131
+ const completion = 100;
132
+ const status = "completed";
133
+ const historyRoot = ensureHistoryRoot(request.rootDir);
134
+ const historyFileName = `${request.trackId}.md`;
135
+ const historyAbsolutePath = node_path_1.default.join(historyRoot, historyFileName);
136
+ const historyRelativePath = node_path_1.default.join("bitacora", "history", historyFileName).replace(/\\/g, "/");
137
+ const compactedMarkdown = buildCompactedTrackMarkdown({
138
+ trackId: parsed.frontmatter.track_id,
139
+ status,
140
+ priority: parsed.frontmatter.priority,
141
+ createdAt: parsed.frontmatter.created_at,
142
+ updatedAt,
143
+ completion,
144
+ compactedAt,
145
+ historyPath: historyRelativePath,
146
+ overview: parsed.sections.overview,
147
+ tasks: parsed.sections.tasks,
148
+ decisions: parsed.sections.decisions
149
+ });
150
+ if (!request.dryRun) {
151
+ node_fs_1.default.writeFileSync(historyAbsolutePath, source, "utf8");
152
+ node_fs_1.default.writeFileSync(trackPath, compactedMarkdown, "utf8");
153
+ }
154
+ return {
155
+ trackId: request.trackId,
156
+ compacted: true,
157
+ bytesBefore: source.length,
158
+ bytesAfter: compactedMarkdown.length,
159
+ estimatedTokensBefore: estimateTokenCount(source),
160
+ estimatedTokensAfter: estimateTokenCount(compactedMarkdown)
161
+ };
162
+ }
163
+ function extractTrackRows(tracks) {
164
+ const sorted = [...tracks].sort((left, right) => left.frontmatter.track_id.localeCompare(right.frontmatter.track_id));
165
+ return sorted
166
+ .map((track) => {
167
+ const status = track.frontmatter.status;
168
+ const completion = track.frontmatter.completion ?? (status === "completed" ? 100 : 0);
169
+ const notes = track.frontmatter.compacted_at ? "compacted" : "active detail";
170
+ return `| ${track.frontmatter.track_id} | ${status} | ${completion}% | ${track.frontmatter.updated_at} | ${notes} |`;
171
+ })
172
+ .join("\n");
173
+ }
174
+ function buildCompactedTracksRegistry(now, tracks) {
175
+ const active = tracks.filter((track) => track.frontmatter.status === "active").length;
176
+ const blocked = tracks.filter((track) => track.frontmatter.status === "blocked").length;
177
+ const completed = tracks.filter((track) => track.frontmatter.status === "completed").length;
178
+ const archived = tracks.filter((track) => track.frontmatter.status === "archived").length;
179
+ const rows = extractTrackRows(tracks);
180
+ return `# Tracks
181
+
182
+ > Canonical project status and handoff registry.
183
+ >
184
+ > Last updated: ${now.slice(0, 10)}
185
+ >
186
+ > Rule: update this file after meaningful implementation changes.
187
+
188
+ ## Snapshot
189
+
190
+ - Active: ${active}
191
+ - Blocked: ${blocked}
192
+ - Completed: ${completed}
193
+ - Archived: ${archived}
194
+
195
+ ## Track Registry
196
+
197
+ | ID | Status | Completion | Last Update | Notes |
198
+ | --- | --- | --- | --- | --- |
199
+ ${rows.length > 0 ? rows : "| - | - | - | - | - |"}
200
+
201
+ ## Session Handoff (Required)
202
+
203
+ - Track(s) touched
204
+ - Tests run (exact command + result)
205
+ - Current TDD phase
206
+ - Blockers/assumptions
207
+ - Next recommended action
208
+ `;
209
+ }
210
+ function compactTracksRegistry(options) {
211
+ const tracksRegistryPath = node_path_1.default.join(options.rootDir, "bitacora", "tracks", "tracks.md");
212
+ const before = node_fs_1.default.existsSync(tracksRegistryPath) ? node_fs_1.default.readFileSync(tracksRegistryPath, "utf8") : "";
213
+ const next = buildCompactedTracksRegistry(options.now, options.tracks);
214
+ if (!options.dryRun) {
215
+ const historyRoot = ensureHistoryRoot(options.rootDir);
216
+ const backupFileName = `tracks-${options.now.replace(/[:.]/g, "-")}.md`;
217
+ node_fs_1.default.writeFileSync(node_path_1.default.join(historyRoot, backupFileName), before, "utf8");
218
+ node_fs_1.default.writeFileSync(tracksRegistryPath, next, "utf8");
219
+ }
220
+ return {
221
+ bytesBefore: before.length,
222
+ bytesAfter: next.length,
223
+ estimatedTokensBefore: estimateTokenCount(before),
224
+ estimatedTokensAfter: estimateTokenCount(next)
225
+ };
226
+ }
227
+ function readTrackHistory(rootDir, trackId, includeContent) {
228
+ const trackPath = node_path_1.default.join(rootDir, "bitacora", "tracks", trackId, "track.md");
229
+ if (!node_fs_1.default.existsSync(trackPath)) {
230
+ throw new Error(`Track not found: ${trackId}`);
231
+ }
232
+ const parsed = (0, parser_1.parseTrackMarkdown)(node_fs_1.default.readFileSync(trackPath, "utf8"));
233
+ const historyPath = parsed.frontmatter.history_path ?? node_path_1.default.join("bitacora", "history", `${trackId}.md`).replace(/\\/g, "/");
234
+ const absoluteHistoryPath = node_path_1.default.join(rootDir, historyPath);
235
+ const exists = node_fs_1.default.existsSync(absoluteHistoryPath);
236
+ if (!includeContent) {
237
+ return {
238
+ trackId,
239
+ historyPath,
240
+ exists
241
+ };
242
+ }
243
+ return {
244
+ trackId,
245
+ historyPath,
246
+ exists,
247
+ ...(exists ? { content: node_fs_1.default.readFileSync(absoluteHistoryPath, "utf8") } : {})
248
+ };
249
+ }
@@ -78,6 +78,9 @@ function parseTrackMarkdown(markdown) {
78
78
  const trackId = getRequiredField("track_id");
79
79
  const createdAt = getRequiredField("created_at");
80
80
  const updatedAt = getRequiredField("updated_at");
81
+ const completionRaw = frontmatter.completion;
82
+ const compactedAtRaw = frontmatter.compacted_at;
83
+ const historyPathRaw = frontmatter.history_path;
81
84
  if (!types_1.TRACK_STATUSES.includes(status)) {
82
85
  throw new Error("Invalid status");
83
86
  }
@@ -91,13 +94,24 @@ function parseTrackMarkdown(markdown) {
91
94
  throw new Error(`Missing section: ${heading}`);
92
95
  }
93
96
  }
97
+ let completion;
98
+ if (completionRaw !== undefined) {
99
+ const parsedCompletion = Number.parseInt(completionRaw, 10);
100
+ if (!Number.isInteger(parsedCompletion)) {
101
+ throw new Error("Invalid completion");
102
+ }
103
+ completion = parsedCompletion;
104
+ }
94
105
  return {
95
106
  frontmatter: {
96
107
  track_id: trackId,
97
108
  status: status,
98
109
  priority: priority,
99
110
  created_at: createdAt,
100
- updated_at: updatedAt
111
+ updated_at: updatedAt,
112
+ ...(completion !== undefined ? { completion } : {}),
113
+ ...(compactedAtRaw !== undefined ? { compacted_at: compactedAtRaw } : {}),
114
+ ...(historyPathRaw !== undefined ? { history_path: historyPathRaw } : {})
101
115
  },
102
116
  sections: {
103
117
  overview: sections.overview ?? "",
@@ -22,6 +22,7 @@ Quick map of where to find each kind of project memory.
22
22
  4. \`ux-style-guide.md\` (visual style tokens and UX constraints)
23
23
  5. \`tracks/tracks.md\` (canonical project status and next actions)
24
24
  6. \`tracks/TRACK-*/track.md\` (details for active or relevant tracks)
25
+ 7. \`history/\` (archived detail, read only when needed)
25
26
 
26
27
  ## File Index
27
28
 
@@ -37,11 +38,16 @@ Quick map of where to find each kind of project memory.
37
38
  - \`tracks/TRACK-001/track.md\`: first active track created by \`bitacora init\`.
38
39
  - \`tracks/TRACK-*/track.md\`: per-track execution details (overview, tasks, decisions, log).
39
40
 
41
+ ### \`history/\`
42
+ - \`history/TRACK-*.md\`: archived full detail after compaction.
43
+ - \`history/tracks-*.md\`: archived snapshots of \`tracks/tracks.md\`.
44
+
40
45
  Mandatory behavior:
41
46
 
42
47
  - Always read this index at session start.
43
48
  - Always update memory before session end.
44
49
  - Always keep \`tracks/tracks.md\` aligned with track-level changes.
50
+ - Read \`history/\` only when active context is insufficient.
45
51
  `;
46
52
  }
47
53
  function createProductTemplate() {
@@ -179,11 +185,13 @@ Canonical skill file path: \`.agents/skills/bitacora/SKILL.md\`.
179
185
  - Always read \`bitacora/product.md\`, \`bitacora/tech-stack.md\`, \`bitacora/workflow.md\`, and \`bitacora/ux-style-guide.md\` before making code changes.
180
186
  - Always read \`bitacora/tracks/tracks.md\` and the active \`bitacora/tracks/TRACK-*/track.md\` files before implementation.
181
187
  - Always write memory updates before ending the session.
188
+ - Do not read \`bitacora/history/\` unless needed to recover full detail.
182
189
 
183
190
  ## What To Update During Work
184
191
  - Update \`bitacora/tracks/TRACK-*/track.md\` while work progresses (tasks, decisions, logs).
185
192
  - Update \`bitacora/tracks/tracks.md\` after meaningful changes, including current status and next action.
186
193
  - Update root docs when product/tech/workflow/ux assumptions change.
194
+ - Before closing a fully completed track, append a \`TEST:\` verification log and run \`bitacora compact --track-id <id> --complete\`.
187
195
 
188
196
  ## File Map (Where To Look)
189
197
  - \`bitacora/product.md\`: scope, goals, constraints, non-goals.
@@ -193,6 +201,7 @@ Canonical skill file path: \`.agents/skills/bitacora/SKILL.md\`.
193
201
  - \`bitacora/tracks/tracks.md\`: canonical status registry and handoff summary.
194
202
  - \`bitacora/tracks/tracks-template.md\`: template for new tracks.
195
203
  - \`bitacora/tracks/TRACK-*/track.md\`: per-track execution details.
204
+ - \`bitacora/history/\`: archived detail to inspect only when needed.
196
205
 
197
206
  ## Manual Bootstrap (No CLI Required)
198
207
  If the CLI is unavailable, create this exact structure manually:
@@ -204,6 +213,7 @@ bitacora/
204
213
  tech-stack.md
205
214
  workflow.md
206
215
  ux-style-guide.md
216
+ history/
207
217
  tracks/
208
218
  tracks.md
209
219
  tracks-template.md
@@ -226,6 +236,7 @@ Required track sections in \`track.md\`:
226
236
  ## End Of Session Checklist
227
237
  - Confirm \`bitacora/tracks/tracks.md\` is updated.
228
238
  - Confirm active \`TRACK-*/track.md\` files include latest decisions and logs.
239
+ - Confirm compacted tracks have history in \`bitacora/history/\`.
229
240
  `;
230
241
  }
231
242
  function createTracksRegistryTemplate(date) {
@@ -112,6 +112,27 @@ function validateUpdatedAtAgainstLogEntries(tracks, errors) {
112
112
  }
113
113
  }
114
114
  }
115
+ function validateCompactionMetadata(rootDir, tracks, errors) {
116
+ for (const track of tracks) {
117
+ const { completion, status, history_path: historyPath } = track.frontmatter;
118
+ if (completion !== undefined && (completion < 0 || completion > 100)) {
119
+ errors.push(`Invalid completion value for ${track.frontmatter.track_id}: ${completion}`);
120
+ }
121
+ if (status === "completed" && completion !== undefined && completion !== 100) {
122
+ errors.push(`Completed track must have completion 100: ${track.frontmatter.track_id}`);
123
+ }
124
+ if (track.frontmatter.compacted_at !== undefined) {
125
+ if (!historyPath) {
126
+ errors.push(`Compacted track missing history_path: ${track.frontmatter.track_id}`);
127
+ continue;
128
+ }
129
+ const historyAbsolutePath = node_path_1.default.join(rootDir, historyPath);
130
+ if (!node_fs_1.default.existsSync(historyAbsolutePath)) {
131
+ errors.push(`Compacted track history file not found for ${track.frontmatter.track_id}`);
132
+ }
133
+ }
134
+ }
135
+ }
115
136
  function validateMemory(rootDir) {
116
137
  const errors = [];
117
138
  const trackDirectories = getTrackDirectories(rootDir);
@@ -119,6 +140,7 @@ function validateMemory(rootDir) {
119
140
  const tracks = loadTracks(rootDir, errors);
120
141
  validateDuplicateTrackIds(tracks, errors);
121
142
  validateUpdatedAtAgainstLogEntries(tracks, errors);
143
+ validateCompactionMetadata(rootDir, tracks, errors);
122
144
  errors.sort((left, right) => left.localeCompare(right));
123
145
  return {
124
146
  ok: errors.length === 0,
package/dist/src/index.js CHANGED
@@ -1,8 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateMemory = exports.buildStateFromTracks = exports.buildIndexFromTracks = exports.parseTrackMarkdown = exports.boot = void 0;
3
+ exports.validateMemory = exports.buildStateFromTracks = exports.buildIndexFromTracks = exports.parseTrackMarkdown = exports.readTrackHistory = exports.compactTracksRegistry = exports.compactTrack = exports.boot = void 0;
4
4
  var boot_1 = require("./core/boot");
5
5
  Object.defineProperty(exports, "boot", { enumerable: true, get: function () { return boot_1.boot; } });
6
+ var compaction_1 = require("./core/compaction");
7
+ Object.defineProperty(exports, "compactTrack", { enumerable: true, get: function () { return compaction_1.compactTrack; } });
8
+ Object.defineProperty(exports, "compactTracksRegistry", { enumerable: true, get: function () { return compaction_1.compactTracksRegistry; } });
9
+ Object.defineProperty(exports, "readTrackHistory", { enumerable: true, get: function () { return compaction_1.readTrackHistory; } });
6
10
  var parser_1 = require("./core/parser");
7
11
  Object.defineProperty(exports, "parseTrackMarkdown", { enumerable: true, get: function () { return parser_1.parseTrackMarkdown; } });
8
12
  var state_builder_1 = require("./core/state-builder");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitacora-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Deterministic agent-oriented project memory system. Provides structured Markdown-based long-term memory, strict validation, state reconstruction, and an enforcement CLI with agent skill integration to guide and limit agent behavior.",
5
5
  "repository": {
6
6
  "type": "git",