@opum-ai/lore 0.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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +306 -0
  3. package/bin/lore.cjs +109 -0
  4. package/package.json +67 -0
  5. package/src/adapters/backlog.ts +1084 -0
  6. package/src/adapters/git.ts +221 -0
  7. package/src/cli.ts +667 -0
  8. package/src/commands/agent.ts +301 -0
  9. package/src/commands/agents.ts +302 -0
  10. package/src/commands/args.ts +209 -0
  11. package/src/commands/changed.ts +70 -0
  12. package/src/commands/check.ts +1031 -0
  13. package/src/commands/codex-bridge.ts +49 -0
  14. package/src/commands/concurrency.ts +48 -0
  15. package/src/commands/context.ts +292 -0
  16. package/src/commands/discover.ts +89 -0
  17. package/src/commands/explorer.ts +253 -0
  18. package/src/commands/export.ts +93 -0
  19. package/src/commands/fswrite.ts +928 -0
  20. package/src/commands/graph.ts +291 -0
  21. package/src/commands/help.ts +151 -0
  22. package/src/commands/impact.ts +59 -0
  23. package/src/commands/init.ts +583 -0
  24. package/src/commands/instructions.ts +91 -0
  25. package/src/commands/link.ts +929 -0
  26. package/src/commands/new.ts +476 -0
  27. package/src/commands/orphans.ts +457 -0
  28. package/src/commands/path.ts +67 -0
  29. package/src/commands/provenance.ts +68 -0
  30. package/src/commands/query.ts +312 -0
  31. package/src/commands/reconcile-shared.ts +280 -0
  32. package/src/commands/rename.ts +585 -0
  33. package/src/commands/replace.ts +320 -0
  34. package/src/commands/scaffold.ts +346 -0
  35. package/src/commands/schema.ts +293 -0
  36. package/src/commands/snapshot.ts +130 -0
  37. package/src/commands/supersede.ts +400 -0
  38. package/src/commands/sync.ts +371 -0
  39. package/src/commands/tasks.ts +271 -0
  40. package/src/commands/traversal.ts +151 -0
  41. package/src/commands/validate.ts +226 -0
  42. package/src/config.ts +598 -0
  43. package/src/core/agent-bridge.ts +287 -0
  44. package/src/core/agent-context.ts +498 -0
  45. package/src/core/agent-profile.ts +447 -0
  46. package/src/core/bundle.ts +893 -0
  47. package/src/core/check.ts +853 -0
  48. package/src/core/codex-bridge.ts +100 -0
  49. package/src/core/concept.ts +597 -0
  50. package/src/core/consumer-scaffold.ts +433 -0
  51. package/src/core/context.ts +271 -0
  52. package/src/core/explorer-contract.ts +441 -0
  53. package/src/core/explorer-qualification.ts +58 -0
  54. package/src/core/explorer.ts +518 -0
  55. package/src/core/finding.ts +31 -0
  56. package/src/core/graph.ts +201 -0
  57. package/src/core/indexes.ts +436 -0
  58. package/src/core/instructions.ts +209 -0
  59. package/src/core/ladybug-driver.ts +1795 -0
  60. package/src/core/ladybug-lifecycle.ts +1178 -0
  61. package/src/core/ladybug-native.ts +95 -0
  62. package/src/core/ladybug-source.ts +667 -0
  63. package/src/core/links.ts +681 -0
  64. package/src/core/log.ts +253 -0
  65. package/src/core/managed-block.ts +540 -0
  66. package/src/core/manifest.ts +718 -0
  67. package/src/core/order.ts +13 -0
  68. package/src/core/profile.ts +1007 -0
  69. package/src/core/projection.ts +195 -0
  70. package/src/core/query.ts +542 -0
  71. package/src/core/reconcile.ts +236 -0
  72. package/src/core/replace.ts +419 -0
  73. package/src/core/retrieval.ts +213 -0
  74. package/src/core/rewrite.ts +940 -0
  75. package/src/core/scaffold.ts +255 -0
  76. package/src/core/schema.ts +366 -0
  77. package/src/core/snapshot-runtime.ts +52 -0
  78. package/src/core/snapshot-store.ts +287 -0
  79. package/src/core/snapshot.ts +711 -0
  80. package/src/core/template.ts +429 -0
  81. package/src/core/traversal.ts +487 -0
  82. package/src/core/validate.ts +517 -0
  83. package/src/core/workspace-contract.ts +473 -0
  84. package/src/core/workspace-projection.ts +365 -0
  85. package/src/core/workspace-retrieval.ts +196 -0
  86. package/src/core/workspace-source.ts +174 -0
  87. package/src/errors.ts +697 -0
  88. package/src/meta.ts +7 -0
  89. package/src/output.ts +589 -0
  90. package/src/scripts/upstream-backlog-watch.ts +288 -0
  91. package/src/state.ts +390 -0
@@ -0,0 +1,287 @@
1
+ /**
2
+ * core/agent-bridge.ts — the pure content + planning engine behind `lore agents` (LORE-36).
3
+ *
4
+ * `lore agents` generates the Claude Code "agent bridge" (docs/adr/0004-cli-first-skill-bridge):
5
+ * a `.claude/skills/lore/SKILL.md` that teaches an agent how to drive lore, and a tiny
6
+ * marker-delimited `CLAUDE.md` nudge that points at that skill and at `lore instructions`. Both
7
+ * are *generated*, not hand-maintained, so they stay in lockstep with the CLI surface.
8
+ *
9
+ * This module holds everything pure: the generated bytes ({@link buildSkillDoc},
10
+ * {@link buildNudgeBody}), the canonical command surface ({@link LORE_COMMANDS}), and the pure
11
+ * {@link planBridge planner} that — given each file's current bytes (or absence) and the `--force`
12
+ * flag — decides what each file's next state should be. All filesystem side effects live in the
13
+ * command layer (`commands/agents.ts`), per lore-design §2.1.
14
+ *
15
+ * ### Content is grounded in live source, not the runbook
16
+ *
17
+ * Every factual claim here (the command list, the `--json` envelope shape, the exit-code table, the
18
+ * canonical loop) is grounded in what the CLI *actually* does today — `src/cli.ts`, `src/output.ts`,
19
+ * `src/errors.ts`, and the shipped `core/instructions.ts` OVERVIEW — never in an aspirational
20
+ * runbook/ADR that also describes not-yet-shipped state. In particular the generated content names
21
+ * only **shipped** commands the real dispatcher handles (the LORE-37 phantom-command trap); the
22
+ * `agents.test.ts` lockstep guard re-checks {@link LORE_COMMANDS} against the real dispatcher.
23
+ */
24
+
25
+ import { upsertManagedBlock } from "./managed-block";
26
+
27
+ /** The repo-relative path of the generated skill bridge (pinned by ADR-0004 §3 and cli-surface §agents). */
28
+ export const SKILL_REL_PATH = ".claude/skills/lore/SKILL.md";
29
+
30
+ /** The repo-relative path of the project memory file the nudge block is upserted into. */
31
+ export const CLAUDE_MD_REL_PATH = "CLAUDE.md";
32
+
33
+ /**
34
+ * The label naming the `CLAUDE.md` nudge's managed block, consumed by
35
+ * {@link upsertManagedBlock} to build `<!-- lore:agents:begin -->` / `<!-- lore:agents:end -->`.
36
+ * Chosen to sit in lore's existing `lore:tasks` / `lore:index` marker namespace; kept stable
37
+ * because `--check` idempotency depends on the exact bytes.
38
+ */
39
+ export const AGENT_BLOCK_LABEL = "lore:agents";
40
+
41
+ /** One `lore` subcommand and its one-line summary, as advertised to an agent by the generated bridge. */
42
+ export interface CommandSummary {
43
+ /** The subcommand token (`"sync"`), exactly as `src/cli.ts` dispatches it. */
44
+ readonly name: string;
45
+ /** A one-line description, distilled from the authoritative `cli.ts` USAGE text. */
46
+ readonly summary: string;
47
+ }
48
+
49
+ /**
50
+ * The canonical `lore` command surface the bridge advertises — the single source both
51
+ * {@link buildSkillDoc} and the `agents.test.ts` lockstep guard read. Names and order mirror
52
+ * `src/cli.ts`'s Commander handler registry / USAGE; the guard runs each name through the real CLI to prove
53
+ * none is a phantom (the exact LORE-37 failure mode: teaching an agent a command that does not exist).
54
+ */
55
+ export const LORE_COMMANDS: readonly CommandSummary[] = [
56
+ {
57
+ name: "init",
58
+ summary: "Scaffold an OKF bundle; a bare TTY run also wizards the agent bridge/scaffolds/backlog check",
59
+ },
60
+ { name: "new", summary: "Scaffold a typed concept from a template" },
61
+ { name: "validate", summary: "Check concept files against OKF + the lore profile (per-file)" },
62
+ { name: "check", summary: "Validate links/anchors + reconciliation drift across the bundle (CI gate)" },
63
+ { name: "replace", summary: "Find-and-replace across the bundle, skipping managed regions" },
64
+ { name: "rename", summary: "Move a concept and repoint every inbound link + ref" },
65
+ { name: "supersede", summary: "Mark a concept superseded by another, wiring both ways" },
66
+ { name: "link", summary: "Add task ids to a concept's tasks: + the doc: back-ref" },
67
+ { name: "unlink", summary: "Remove task ids from a concept's tasks: + the doc: back-ref" },
68
+ { name: "sync", summary: "Reconcile status + managed task blocks, regen index/log, commit backlog/" },
69
+ { name: "tasks", summary: "Show the live status rollup for a concept's linked tasks" },
70
+ { name: "orphans", summary: "Report tasks with no owning doc + docs whose linked task vanished" },
71
+ { name: "schema", summary: "Export the profile's editor JSON Schemas to .lore/schemas/" },
72
+ { name: "scaffold", summary: "Generate a downstream docs consumer's config, additively outside docs/" },
73
+ { name: "graph", summary: "Emit the bundle's cross-link graph as json or dot" },
74
+ { name: "path", summary: "Find bounded paths across exact authored concept and task edges" },
75
+ { name: "impact", summary: "Expand bounded impact across exact authored concept and task edges" },
76
+ { name: "snapshot", summary: "Explicitly retain, list, or delete bounded projection snapshots" },
77
+ { name: "changed", summary: "Compare two retained snapshots with bounded authored-fact deltas" },
78
+ { name: "provenance", summary: "Trace one retained concept, task, or edge to exact source evidence" },
79
+ { name: "explorer", summary: "Build a deterministic self-contained local graph explorer" },
80
+ { name: "export", summary: "Emit a deterministic, consumer-neutral OKF projection as JSONL" },
81
+ { name: "query", summary: "Full-text search the bundle with frontmatter filters" },
82
+ { name: "context", summary: "Assemble a concept + neighbor summaries within a token budget" },
83
+ { name: "agent", summary: "List context profiles or compile bounded task-scoped evidence" },
84
+ { name: "instructions", summary: "Print task-scoped agent guidance on demand" },
85
+ { name: "agents", summary: "Regenerate this bridge (SKILL.md + the CLAUDE.md nudge)" },
86
+ { name: "help", summary: "Show help, or the machine-readable command manifest under --json" },
87
+ ];
88
+
89
+ /** The four detail topics `lore instructions <topic>` serves, mirrored (not restated) by the bridge. */
90
+ const INSTRUCTION_DETAIL_TOPICS: ReadonlyArray<{ key: string; blurb: string }> = [
91
+ { key: "linking", blurb: "Story <-> Task coupling (`lore link` / `lore unlink`)" },
92
+ { key: "sync", blurb: "reconcile status + managed blocks (`lore sync`)" },
93
+ { key: "check", blurb: "the CI gate: drift, links, anchors, portability (`lore check`)" },
94
+ { key: "validation", blurb: "per-file OKF/schema conformance (`lore validate`)" },
95
+ ];
96
+
97
+ /**
98
+ * Render a left-aligned two-column list: a tight `` `left` `` code span, then padding *after* the
99
+ * backtick (never inside it, which would render as trailing whitespace in the code span) so the
100
+ * right column aligns in the raw text an agent reads.
101
+ */
102
+ function twoColumn(rows: ReadonlyArray<{ left: string; right: string }>): string {
103
+ const width = Math.max(...rows.map((row) => row.left.length));
104
+ return rows.map((row) => `- \`${row.left}\`${" ".repeat(width - row.left.length + 2)}${row.right}`).join("\n");
105
+ }
106
+
107
+ /**
108
+ * Build the full `SKILL.md` bytes: YAML frontmatter (the `name` + when-to-use `description` Claude
109
+ * Code loads the skill by) followed by a small, live-source-grounded teacher body. Deterministic and
110
+ * timestamp-free, so regenerating with no change is byte-identical (AC#1). Kept a thin pointer:
111
+ * `lore instructions` is the source of truth, and this file directs the agent there (AC#2).
112
+ */
113
+ export function buildSkillDoc(): string {
114
+ const description =
115
+ "Author and maintain this repo's OKF documentation bundle (docs/) with the lore CLI. " +
116
+ "Use whenever reading, writing, linking, moving, or checking docs, so Story/Task coupling, " +
117
+ "managed blocks, and cross-links stay coherent. Run `lore instructions` for the canonical " +
118
+ "agent loop and `lore instructions <topic>` for just-in-time detail.";
119
+
120
+ const topicList = twoColumn(INSTRUCTION_DETAIL_TOPICS.map((t) => ({ left: t.key, right: t.blurb })));
121
+ const commandList = twoColumn(LORE_COMMANDS.map((c) => ({ left: c.name, right: c.summary })));
122
+
123
+ return `---
124
+ name: lore
125
+ description: "${description}"
126
+ ---
127
+
128
+ # lore — OKF documentation CLI
129
+
130
+ \`lore\` is this repo's documentation engine: a deterministic, CLI-first tool (no LLM dependency)
131
+ for authoring and maintaining the OKF bundle under \`docs/\`. This skill is a thin pointer —
132
+ **\`lore instructions\` is the source of truth for how to drive lore.**
133
+
134
+ ## When to use it
135
+
136
+ Reach for \`lore\` — not a plain editor — whenever you read, write, link, move, or verify docs in
137
+ this repo, so Story <-> Task coupling, managed blocks, and cross-links stay coherent.
138
+
139
+ ## Start here
140
+
141
+ Run \`lore instructions\` for the canonical agent loop and the topic index, then pull just-in-time
142
+ detail with \`lore instructions <topic>\`:
143
+
144
+ ${topicList}
145
+
146
+ ## Commands
147
+
148
+ ${commandList}
149
+
150
+ ## Machine contract
151
+
152
+ Every command supports \`--json\` (the \`{schemaVersion, kind, data}\` envelope) and \`--plain\`
153
+ (ANSI-free, auto-selected off a TTY). Branch on the semantic exit code, never on prose:
154
+ \`0\` ok · \`2\` usage · \`3\` not_found · \`4\` denied · \`5\` conflict · \`6\` validation/drift.
155
+
156
+ ## Optional task-scoped context
157
+
158
+ When native Claude Code instructions name a committed Lore profile, use this stable opt-in line:
159
+
160
+ > Lore profile: \`<name>\`. Before working, run \`lore agent context <name> --task "<assigned task>"\`
161
+ > and ground decisions in the returned source IDs.
162
+
163
+ Lore supplies evidence only. It does not create or patch native agents, prompts, tools, models,
164
+ permissions, or execution settings.
165
+
166
+ <!-- Generated by \`lore agents\`; do not hand-edit. Re-run \`lore agents --force\` to refresh. -->
167
+ `;
168
+ }
169
+
170
+ /**
171
+ * Build the inner body of the `CLAUDE.md` nudge — the bytes {@link upsertManagedBlock} places
172
+ * between the `lore:agents` markers. Deliberately tiny (ADR-0004: "the SKILL.md bridge costs ~30–50
173
+ * idle tokens"; the nudge carries no substantive guidance itself): its only job is discoverability —
174
+ * "lore exists, here is the skill, here is where to pull just-in-time detail." No trailing newline.
175
+ */
176
+ export function buildNudgeBody(): string {
177
+ return `This repo uses **lore** — an OKF-native documentation CLI — for the docs bundle under \`docs/\`.
178
+ When working on documentation, drive it through \`lore\` (not a plain editor) so Story <-> Task
179
+ coupling, managed blocks, and cross-links stay coherent.
180
+
181
+ - **Skill:** \`${SKILL_REL_PATH}\` — how to drive lore.
182
+ - **Just-in-time detail:** run \`lore instructions\` for the canonical agent loop, then
183
+ \`lore instructions <topic>\` (\`linking\`, \`sync\`, \`check\`, \`validation\`).`;
184
+ }
185
+
186
+ /** What {@link planBridge} decided a single bridge file's next state should be. */
187
+ export type BridgeAction =
188
+ /** The file did not exist and will be created. */
189
+ | "created"
190
+ /** The file (or its managed block) differs and will be rewritten. */
191
+ | "updated"
192
+ /** The on-disk bytes already match — nothing to write. */
193
+ | "unchanged"
194
+ /**
195
+ * A whole lore-owned file (SKILL.md) differs but was left untouched: either it looks hand-edited
196
+ * and `--force` was not given, or this is a `--check` run — which never writes, so `--force`
197
+ * cannot actually take effect and must not be reported as if it had (LORE-129).
198
+ */
199
+ | "protected";
200
+
201
+ /** One file's entry in a {@link BridgePlan}. */
202
+ export interface BridgeFilePlan {
203
+ /** Repo-relative path (`.claude/skills/lore/SKILL.md` or `CLAUDE.md`). */
204
+ readonly path: string;
205
+ /** The decided next state. */
206
+ readonly action: BridgeAction;
207
+ /** The full bytes to write, or `null` when nothing should be written (`unchanged`/`protected`). */
208
+ readonly contents: string | null;
209
+ }
210
+
211
+ /** The planned next state of every bridge file, computed purely from the on-disk bytes + `--force`. */
212
+ export interface BridgePlan {
213
+ readonly files: readonly BridgeFilePlan[];
214
+ }
215
+
216
+ /** The current on-disk bytes of each bridge file (LF-normalized), or `null` when the file is absent, plus `--force`/`--check`. */
217
+ export interface PlanBridgeInput {
218
+ /** Current `SKILL.md` bytes, or `null` if it does not exist. */
219
+ readonly skillOnDisk: string | null;
220
+ /** Current `CLAUDE.md` bytes, or `null` if it does not exist. */
221
+ readonly claudeOnDisk: string | null;
222
+ /** Whether `--force` was given (permits overwriting a differing, possibly hand-edited SKILL.md). */
223
+ readonly force: boolean;
224
+ /**
225
+ * Whether this is a `--check` (report-only) run. `--check` never writes, so `force` must not be
226
+ * honored while planning: a `--check --force` run against a differing SKILL.md still reports
227
+ * `protected` (never `updated`, which would claim a write that never happens) — see
228
+ * {@link planSkill} (LORE-129).
229
+ */
230
+ readonly check: boolean;
231
+ }
232
+
233
+ /**
234
+ * Decide the next state of both bridge files, purely (no IO). The two files have deliberately
235
+ * different write disciplines:
236
+ *
237
+ * - **SKILL.md is a whole lore-owned file.** Absent → `created`. Byte-equal to the generated
238
+ * content → `unchanged`. Differs → `updated` only under `--force` *and not* `--check` (a
239
+ * `--check` run never writes, so `--force` cannot take effect and must not be reported as if it
240
+ * had — LORE-129), else `protected` (left untouched — a differing generated file may be a
241
+ * hand-edit, and lore never silently clobbers it; the command reports "run with --force"). This
242
+ * is exactly the "overwrite hand-edited generated files" role cli-surface pins on `--force`.
243
+ * - **The CLAUDE.md nudge is a managed block**, so lore only ever rewrites the bytes between its own
244
+ * markers. Refreshing that block is by-design and needs no `--force`: absent file → `created`;
245
+ * block absent or stale → `updated` (the surrounding prose and any unrelated block, e.g.
246
+ * Backlog.md's, are preserved by {@link upsertManagedBlock}); already current → `unchanged`.
247
+ *
248
+ * {@link upsertManagedBlock} throws a `validation` {@link LoreError} if CLAUDE.md carries a malformed
249
+ * `lore:agents` marker pair — a fail-loud propagation the command surfaces (exit 6), never a guess.
250
+ */
251
+ export function planBridge(input: PlanBridgeInput): BridgePlan {
252
+ return { files: [planSkill(input), planNudge(input)] };
253
+ }
254
+
255
+ /**
256
+ * Plan the SKILL.md file (whole-file, `--force`-protected discipline). `--force` only ever permits
257
+ * an actual write, so it only ever yields `updated` on a real (non-`--check`) run: `--check` is
258
+ * read-only by contract, and `--check --force` against a differing file must still report
259
+ * `protected` — the check-safe, non-mutating label — never `updated`, which would falsely claim a
260
+ * write that this run never performs (LORE-129).
261
+ */
262
+ function planSkill(input: PlanBridgeInput): BridgeFilePlan {
263
+ const desired = buildSkillDoc();
264
+ if (input.skillOnDisk === null) {
265
+ return { path: SKILL_REL_PATH, action: "created", contents: desired };
266
+ }
267
+ if (input.skillOnDisk === desired) {
268
+ return { path: SKILL_REL_PATH, action: "unchanged", contents: null };
269
+ }
270
+ if (input.force && !input.check) {
271
+ return { path: SKILL_REL_PATH, action: "updated", contents: desired };
272
+ }
273
+ return { path: SKILL_REL_PATH, action: "protected", contents: null };
274
+ }
275
+
276
+ /** Plan the CLAUDE.md nudge (managed-block, always-refresh discipline). */
277
+ function planNudge(input: PlanBridgeInput): BridgeFilePlan {
278
+ const base = input.claudeOnDisk ?? "";
279
+ const desired = upsertManagedBlock(base, { label: AGENT_BLOCK_LABEL, body: buildNudgeBody() });
280
+ if (input.claudeOnDisk === null) {
281
+ return { path: CLAUDE_MD_REL_PATH, action: "created", contents: desired };
282
+ }
283
+ if (desired === input.claudeOnDisk) {
284
+ return { path: CLAUDE_MD_REL_PATH, action: "unchanged", contents: null };
285
+ }
286
+ return { path: CLAUDE_MD_REL_PATH, action: "updated", contents: desired };
287
+ }