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 +68 -43
- package/dist/src/cli.js +60 -0
- package/dist/src/commands/compact.js +75 -0
- package/dist/src/commands/history.js +28 -0
- package/dist/src/commands/init.js +1 -0
- package/dist/src/core/compaction.js +249 -0
- package/dist/src/core/parser.js +15 -1
- package/dist/src/core/templates.js +11 -0
- package/dist/src/core/validator.js +22 -0
- package/dist/src/index.js +5 -1
- package/package.json +1 -1
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
|
-
##
|
|
32
|
+
## Compaction Model (v1.1.0)
|
|
49
33
|
|
|
50
|
-
|
|
34
|
+
Bitacora now supports compacting finished tracks to reduce active context size and token usage.
|
|
51
35
|
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
`bitacora compact --complete` only succeeds when:
|
|
69
47
|
|
|
70
|
-
|
|
48
|
+
- `# Tasks` has no unchecked checklist item (`- [ ]`).
|
|
49
|
+
- `# Log` contains at least one verification line with `TEST:`.
|
|
71
50
|
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
}
|
package/dist/src/core/parser.js
CHANGED
|
@@ -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.
|
|
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",
|