bitacora-cli 1.0.0 → 1.1.1

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,25 +14,8 @@ 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.
18
+ Command examples are available in `examples/cli-workflows.md`.
36
19
 
37
20
  ## What Each Bitacora File Stores
38
21
 
@@ -43,42 +26,58 @@ Use `--root <path>` on any command to target a different project directory.
43
26
  - `bitacora/ux-style-guide.md`: visual tokens and UX interaction rules.
44
27
  - `bitacora/tracks/tracks.md`: canonical track registry, current snapshot, and handoff summary.
45
28
  - `bitacora/tracks/TRACK-*/track.md`: per-track plan, tasks, decisions, and timestamped log.
29
+ - `bitacora/history/TRACK-*.md`: archived full track detail after compaction (read on demand).
30
+ - `bitacora/history/tracks-*.md`: archived snapshots of `tracks/tracks.md`.
46
31
  - `.agents/skills/bitacora/SKILL.md`: instructions agents follow to keep the memory system updated.
47
32
 
48
- ## How Agents Read and Update Bitacora
33
+ ## Compaction Model (v1.1.0)
49
34
 
50
- Recommended memory read order for agents:
35
+ Bitacora now supports compacting finished tracks to reduce active context size and token usage.
51
36
 
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`
37
+ ### How it works
59
38
 
60
- Typical agent write operations:
39
+ 1. Keep full details while implementing.
40
+ 2. When a track is fully done, compact it.
41
+ 3. Bitacora writes a short version in `tracks/TRACK-xxx/track.md`.
42
+ 4. Full detail is archived in `bitacora/history/TRACK-xxx.md`.
43
+ 5. `bitacora/tracks/tracks.md` is regenerated in compact form.
61
44
 
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.
45
+ ### Completion gates for `--complete`
67
46
 
68
- ## Agent Skill Integration
47
+ `bitacora compact --complete` only succeeds when:
69
48
 
70
- When you run `bitacora init`, Bitacora also creates an agent skill at:
49
+ - `# Tasks` has no unchecked checklist item (`- [ ]`).
50
+ - `# Log` contains at least one verification line with `TEST:`.
71
51
 
72
- ```text
73
- .agents/skills/bitacora/SKILL.md
74
- ```
52
+ If gates fail, command exits with code `1` and no compaction is applied.
75
53
 
76
- This skill guides agents to keep memory updated during implementation:
54
+ ## New in v1.1.1: Skill-only update
77
55
 
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`.
56
+ Use `bitacora skill` to install/update only `.agents/skills/bitacora/SKILL.md` and `skills-lock.json` without recreating `bitacora/` context files.
57
+
58
+ ## Typical flow
59
+
60
+ ```bash
61
+ # 1) bootstrap
62
+ bitacora init
63
+
64
+ # 2) update skill only (after CLI upgrades)
65
+ bitacora skill
66
+
67
+ # 3) create work
68
+ bitacora new-track
69
+
70
+ # 4) append progress
71
+ bitacora log --track-id TRACK-001 --message "implemented parser"
72
+ bitacora log --track-id TRACK-001 --message "TEST: npm test -- --run tests/core/parser.test.ts -> pass"
73
+
74
+ # 5) compact when fully completed
75
+ bitacora compact --track-id TRACK-001 --complete
76
+
77
+ # 6) inspect archive only when needed
78
+ bitacora history --track-id TRACK-001
79
+ bitacora history --track-id TRACK-001 --show
80
+ ```
82
81
 
83
82
  ## Commands
84
83
 
@@ -89,6 +88,14 @@ Creates the `bitacora/` memory structure in the project root.
89
88
  - `--force`: recreates memory files if they already exist.
90
89
  - `--root <path>`: sets the project root (default: current directory).
91
90
 
91
+ ### `bitacora skill [--root <path>]`
92
+
93
+ Installs or updates only the Bitacora local agent skill and its lock entry.
94
+
95
+ - Updates `.agents/skills/bitacora/SKILL.md`.
96
+ - Updates `skills-lock.json` entry for `bitacora` while preserving other skills.
97
+ - Does not recreate or modify `bitacora/` project memory files.
98
+
92
99
  ### `bitacora new-track [trackId] [--status <status>] [--priority <priority>] [--root <path>]`
93
100
 
94
101
  Creates a new track file. If `trackId` is omitted, Bitacora picks the next sequential ID.
@@ -106,6 +113,56 @@ Appends a timestamped log entry to an existing track.
106
113
  - `--message <text>`: required log message.
107
114
  - `--root <path>`: sets the project root.
108
115
 
116
+ ### `bitacora compact [--track-id <trackId> | --all] [--complete] [--dry-run] [--root <path>]`
117
+
118
+ Compacts tracks by summarizing content and archiving full detail.
119
+
120
+ - `--track-id <trackId>`: compact a specific track.
121
+ - `--all`: compact all eligible tracks.
122
+ - `--complete`: mark target tracks as completed (requires completion gates).
123
+ - `--dry-run`: report estimated byte/token savings without writing files.
124
+ - `--root <path>`: sets the project root.
125
+ - `--complete` gates:
126
+ - `# Tasks` must not contain unchecked items (`- [ ]`).
127
+ - `# Log` must include at least one `TEST:` line.
128
+ - rewrite/archive model:
129
+ - full source track is archived under `bitacora/history/TRACK-###.md`.
130
+ - active `tracks/TRACK-###/track.md` is rewritten into compact summary form.
131
+ - `tracks/tracks.md` is regenerated and the previous snapshot is archived in `bitacora/history/tracks-*.md`.
132
+
133
+ Examples:
134
+
135
+ ```bash
136
+ # Compact one already-completed track
137
+ bitacora compact --track-id TRACK-004
138
+
139
+ # Complete + compact in one step
140
+ bitacora compact --track-id TRACK-004 --complete
141
+
142
+ # Preview savings for all tracks
143
+ bitacora compact --all --dry-run
144
+
145
+ # Compact all completed tracks
146
+ bitacora compact --all
147
+ ```
148
+
149
+ ### `bitacora history --track-id <trackId> [--show] [--root <path>]`
150
+
151
+ Reads archived track history.
152
+
153
+ - `--track-id <trackId>`: required track identifier.
154
+ - `--show`: print full archived content (default prints only metadata/path).
155
+ - `--root <path>`: sets the project root.
156
+
157
+ ### `bitacora --help`
158
+
159
+ `bitacora --help` includes full command details for:
160
+ - `init`, `skill`, `new-track`, `validate`, `rebuild-state`, `log`
161
+ - `compact` (flags, completion gates, output/archive model)
162
+ - `history` (metadata mode vs `--show`)
163
+
164
+ For a maintained example output, see `examples/help-output.txt`.
165
+
109
166
  ### `bitacora validate [--json] [--root <path>]`
110
167
 
111
168
  Validates the Bitacora file/folder structure and reports errors.
package/dist/src/cli.js CHANGED
@@ -5,9 +5,12 @@ 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");
13
+ const skill_1 = require("./commands/skill");
11
14
  const validate_1 = require("./commands/validate");
12
15
  const COMMAND_HELP_OVERVIEW = `
13
16
  Command details:
@@ -17,6 +20,11 @@ Command details:
17
20
  --force Recreate bitacora memory if it already exists.
18
21
  --root <path> Project root path (default: current directory).
19
22
 
23
+ skill
24
+ Installs or updates only the bitacora agent skill and skills lock entry.
25
+ Options:
26
+ --root <path> Project root path (default: current directory).
27
+
20
28
  new-track [trackId]
21
29
  Creates a new track file, automatically picking the next track id when omitted.
22
30
  Options:
@@ -41,6 +49,32 @@ Command details:
41
49
  --track-id <trackId> Track identifier.
42
50
  --message <text> Log entry message.
43
51
  --root <path> Project root path (default: current directory).
52
+
53
+ compact
54
+ Compacts tracks by summarizing and archiving redundant details.
55
+ Options:
56
+ --track-id <trackId> Compact a specific track.
57
+ --all Compact all eligible tracks.
58
+ --complete Mark target tracks as completed (requires completion gates).
59
+ --dry-run Show savings without modifying files.
60
+ --root <path> Project root path (default: current directory).
61
+ Completion gates (--complete):
62
+ - # Tasks has no unchecked item (- [ ]).
63
+ - # Log includes at least one line containing TEST:.
64
+ Output model:
65
+ - Full source moves to bitacora/history/TRACK-###.md.
66
+ - Active track.md is rewritten into compact summary form.
67
+ - tracks/tracks.md is regenerated; previous snapshot is archived in history/.
68
+
69
+ history
70
+ Reads archived history for a compacted track.
71
+ Options:
72
+ --track-id <trackId> Track identifier.
73
+ --show Print full archived content.
74
+ --root <path> Project root path (default: current directory).
75
+ Notes:
76
+ - Without --show, prints metadata/path only.
77
+ - Use --show only when compact summary is insufficient.
44
78
  `;
45
79
  function writeLine(writer, message) {
46
80
  writer(message.endsWith("\n") ? message : `${message}\n`);
@@ -73,6 +107,18 @@ function createCliProgram(runtime = {}, onCommandExitCode) {
73
107
  });
74
108
  onCommandExitCode?.(exitCode);
75
109
  });
110
+ program
111
+ .command("skill")
112
+ .description("Install or update only the bitacora agent skill.")
113
+ .option("--root <path>", "project root path")
114
+ .action((options) => {
115
+ const exitCode = (0, skill_1.runSkillCommand)({
116
+ rootDir: options.root ?? cwd()
117
+ }, {
118
+ onError: (message) => writeLine(stderr, message)
119
+ });
120
+ onCommandExitCode?.(exitCode);
121
+ });
76
122
  program
77
123
  .command("new-track [trackId]")
78
124
  .description("Create a new track with optional id, status, and priority.")
@@ -142,6 +188,48 @@ function createCliProgram(runtime = {}, onCommandExitCode) {
142
188
  });
143
189
  onCommandExitCode?.(exitCode);
144
190
  });
191
+ program
192
+ .command("compact")
193
+ .description("Compact tracks and archive redundant details.")
194
+ .option("--track-id <trackId>", "track identifier")
195
+ .option("--all", "compact all eligible tracks")
196
+ .option("--complete", "mark track(s) as completed before compaction")
197
+ .option("--dry-run", "report savings without writing files")
198
+ .option("--root <path>", "project root path")
199
+ .action((options) => {
200
+ const compactOptions = {
201
+ rootDir: options.root ?? cwd(),
202
+ all: Boolean(options.all),
203
+ complete: Boolean(options.complete),
204
+ dryRun: Boolean(options.dryRun)
205
+ };
206
+ if (options.trackId !== undefined) {
207
+ compactOptions.trackId = options.trackId;
208
+ }
209
+ const exitCode = (0, compact_1.runCompactCommand)(compactOptions, {
210
+ now,
211
+ onOutput: (message) => writeLine(stdout, message),
212
+ onError: (message) => writeLine(stderr, message)
213
+ });
214
+ onCommandExitCode?.(exitCode);
215
+ });
216
+ program
217
+ .command("history")
218
+ .description("Read archived history for a compacted track.")
219
+ .requiredOption("--track-id <trackId>", "track identifier")
220
+ .option("--show", "print full archived content")
221
+ .option("--root <path>", "project root path")
222
+ .action((options) => {
223
+ const exitCode = (0, history_1.runHistoryCommand)({
224
+ rootDir: options.root ?? cwd(),
225
+ trackId: options.trackId,
226
+ show: Boolean(options.show)
227
+ }, {
228
+ onOutput: (message) => writeLine(stdout, message),
229
+ onError: (message) => writeLine(stderr, message)
230
+ });
231
+ onCommandExitCode?.(exitCode);
232
+ });
145
233
  program.addHelpText("after", COMMAND_HELP_OVERVIEW);
146
234
  return program;
147
235
  }
@@ -157,6 +245,9 @@ async function runCli(argv, runtime = {}) {
157
245
  }
158
246
  catch (error) {
159
247
  if (error instanceof commander_1.CommanderError) {
248
+ if (error.code === "commander.helpDisplayed") {
249
+ return 0;
250
+ }
160
251
  return commandExitCode !== 0 ? commandExitCode : 1;
161
252
  }
162
253
  throw error;
@@ -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
+ }
@@ -6,63 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runInitCommand = runInitCommand;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const node_crypto_1 = require("node:crypto");
10
9
  const templates_1 = require("../core/templates");
11
- function computeHash(content) {
12
- return (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
13
- }
14
- function readSkillsLockFile(filePath) {
15
- if (!node_fs_1.default.existsSync(filePath)) {
16
- return {
17
- version: 1,
18
- skills: {}
19
- };
20
- }
21
- try {
22
- const parsed = JSON.parse(node_fs_1.default.readFileSync(filePath, "utf8"));
23
- if (typeof parsed !== "object" || parsed === null) {
24
- return { version: 1, skills: {} };
25
- }
26
- const versionRaw = parsed.version;
27
- const skillsRaw = parsed.skills;
28
- const normalized = {};
29
- if (typeof skillsRaw === "object" && skillsRaw !== null) {
30
- for (const [name, value] of Object.entries(skillsRaw)) {
31
- if (typeof value !== "object" || value === null) {
32
- continue;
33
- }
34
- const source = value.source;
35
- const sourceType = value.sourceType;
36
- const computedHash = value.computedHash;
37
- if (typeof source === "string" &&
38
- typeof sourceType === "string" &&
39
- typeof computedHash === "string") {
40
- normalized[name] = { source, sourceType, computedHash };
41
- }
42
- }
43
- }
44
- return {
45
- version: typeof versionRaw === "number" ? versionRaw : 1,
46
- skills: normalized
47
- };
48
- }
49
- catch {
50
- return {
51
- version: 1,
52
- skills: {}
53
- };
54
- }
55
- }
56
- function writeSkillsLockFile(rootDir, skillContent) {
57
- const lockPath = node_path_1.default.join(rootDir, "skills-lock.json");
58
- const lock = readSkillsLockFile(lockPath);
59
- lock.skills.bitacora = {
60
- source: ".agents/skills/bitacora/SKILL.md",
61
- sourceType: "local",
62
- computedHash: computeHash(skillContent)
63
- };
64
- node_fs_1.default.writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
65
- }
10
+ const skill_installer_1 = require("../core/skill-installer");
66
11
  function runInitCommand(options, dependencies = {}) {
67
12
  const now = dependencies.now ?? (() => new Date().toISOString());
68
13
  const onError = dependencies.onError ?? (() => { });
@@ -78,15 +23,13 @@ function runInitCommand(options, dependencies = {}) {
78
23
  const createdOnDate = createdAt.slice(0, 10);
79
24
  const trackId = "TRACK-001";
80
25
  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(options.rootDir, ".agents", "skills", "bitacora"), { recursive: true });
26
+ node_fs_1.default.mkdirSync(node_path_1.default.join(memoryRoot, "history"), { recursive: true });
82
27
  node_fs_1.default.writeFileSync(node_path_1.default.join(memoryRoot, "product.md"), (0, templates_1.createProductTemplate)(), "utf8");
83
28
  node_fs_1.default.writeFileSync(node_path_1.default.join(memoryRoot, "tech-stack.md"), (0, templates_1.createTechStackTemplate)(), "utf8");
84
29
  node_fs_1.default.writeFileSync(node_path_1.default.join(memoryRoot, "workflow.md"), (0, templates_1.createWorkflowTemplate)(), "utf8");
85
30
  node_fs_1.default.writeFileSync(node_path_1.default.join(memoryRoot, "ux-style-guide.md"), (0, templates_1.createUxStyleGuideTemplate)(), "utf8");
86
31
  node_fs_1.default.writeFileSync(node_path_1.default.join(memoryRoot, "index.md"), (0, templates_1.createIndexTemplate)(), "utf8");
87
- const skillContent = (0, templates_1.createAgentSkillTemplate)();
88
- node_fs_1.default.writeFileSync(node_path_1.default.join(options.rootDir, ".agents", "skills", "bitacora", "SKILL.md"), skillContent, "utf8");
89
- writeSkillsLockFile(options.rootDir, skillContent);
32
+ (0, skill_installer_1.installBitacoraSkill)(options.rootDir);
90
33
  node_fs_1.default.writeFileSync(node_path_1.default.join(memoryRoot, "tracks", "tracks.md"), (0, templates_1.createTracksRegistryTemplate)(createdOnDate), "utf8");
91
34
  node_fs_1.default.writeFileSync(node_path_1.default.join(memoryRoot, "tracks", "tracks-template.md"), (0, templates_1.createTracksTemplate)(), "utf8");
92
35
  node_fs_1.default.writeFileSync(node_path_1.default.join(memoryRoot, "tracks", trackId, "track.md"), (0, templates_1.createTrackTemplate)(trackId, "active", "medium", createdAt), "utf8");
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runSkillCommand = runSkillCommand;
4
+ const skill_installer_1 = require("../core/skill-installer");
5
+ function runSkillCommand(options, dependencies = {}) {
6
+ const onError = dependencies.onError ?? (() => { });
7
+ try {
8
+ (0, skill_installer_1.installBitacoraSkill)(options.rootDir);
9
+ return 0;
10
+ }
11
+ catch (error) {
12
+ const message = error instanceof Error ? error.message : String(error);
13
+ onError(message);
14
+ return 1;
15
+ }
16
+ }
@@ -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 ?? "",
@@ -0,0 +1,72 @@
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.installBitacoraSkill = installBitacoraSkill;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_crypto_1 = require("node:crypto");
10
+ const templates_1 = require("./templates");
11
+ function computeHash(content) {
12
+ return (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
13
+ }
14
+ function readSkillsLockFile(filePath) {
15
+ if (!node_fs_1.default.existsSync(filePath)) {
16
+ return {
17
+ version: 1,
18
+ skills: {}
19
+ };
20
+ }
21
+ try {
22
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(filePath, "utf8"));
23
+ if (typeof parsed !== "object" || parsed === null) {
24
+ return { version: 1, skills: {} };
25
+ }
26
+ const versionRaw = parsed.version;
27
+ const skillsRaw = parsed.skills;
28
+ const normalized = {};
29
+ if (typeof skillsRaw === "object" && skillsRaw !== null) {
30
+ for (const [name, value] of Object.entries(skillsRaw)) {
31
+ if (typeof value !== "object" || value === null) {
32
+ continue;
33
+ }
34
+ const source = value.source;
35
+ const sourceType = value.sourceType;
36
+ const computedHash = value.computedHash;
37
+ if (typeof source === "string" &&
38
+ typeof sourceType === "string" &&
39
+ typeof computedHash === "string") {
40
+ normalized[name] = { source, sourceType, computedHash };
41
+ }
42
+ }
43
+ }
44
+ return {
45
+ version: typeof versionRaw === "number" ? versionRaw : 1,
46
+ skills: normalized
47
+ };
48
+ }
49
+ catch {
50
+ return {
51
+ version: 1,
52
+ skills: {}
53
+ };
54
+ }
55
+ }
56
+ function writeSkillsLockFile(rootDir, skillContent) {
57
+ const lockPath = node_path_1.default.join(rootDir, "skills-lock.json");
58
+ const lock = readSkillsLockFile(lockPath);
59
+ lock.skills.bitacora = {
60
+ source: ".agents/skills/bitacora/SKILL.md",
61
+ sourceType: "local",
62
+ computedHash: computeHash(skillContent)
63
+ };
64
+ node_fs_1.default.writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
65
+ }
66
+ function installBitacoraSkill(rootDir) {
67
+ const skillDir = node_path_1.default.join(rootDir, ".agents", "skills", "bitacora");
68
+ node_fs_1.default.mkdirSync(skillDir, { recursive: true });
69
+ const skillContent = (0, templates_1.createAgentSkillTemplate)();
70
+ node_fs_1.default.writeFileSync(node_path_1.default.join(skillDir, "SKILL.md"), skillContent, "utf8");
71
+ writeSkillsLockFile(rootDir, skillContent);
72
+ }
@@ -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() {
@@ -163,7 +169,7 @@ function createAgentSkillTemplate() {
163
169
  return `---
164
170
  name: bitacora
165
171
  description: Keep deterministic project memory in bitacora/ and update it continuously during implementation sessions.
166
- version: 1.0.0
172
+ version: 1.1.1
167
173
  type: local
168
174
  source: .agents/skills/bitacora/SKILL.md
169
175
  ---
@@ -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.1",
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",