@joenandez/academy 0.4.0 → 0.5.12

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 (121) hide show
  1. package/.claude-plugin/marketplace.json +5 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +390 -0
  4. package/README.md +67 -122
  5. package/hooks/backstory/.claude-plugin/plugin.json +11 -0
  6. package/hooks/backstory/SOURCE +2 -0
  7. package/hooks/backstory/bin/backstory +9 -0
  8. package/hooks/backstory/hooks/hooks.json +45 -0
  9. package/hooks/backstory/scripts/knowledge.mjs +87 -0
  10. package/hooks/backstory/scripts/memory.mjs +685 -0
  11. package/hooks/backstory/scripts/tool-tick.mjs +1 -0
  12. package/hooks/backstory/skills/capture/SKILL.md +71 -0
  13. package/hooks/backstory/skills/capture/references/knowledge-capture-input.json +16 -0
  14. package/hooks/backstory/skills/capture/references/tagging-policy.md +29 -0
  15. package/hooks/backstory/skills/memory/SKILL.md +70 -0
  16. package/hooks/backstory/skills/setup/SKILL.md +85 -0
  17. package/hooks/context_metrics.mjs +67 -0
  18. package/hooks/hook_runtime.mjs +43 -2
  19. package/hooks/memory_bridge.mjs +286 -52
  20. package/hooks/memory_store.mjs +78 -26
  21. package/hooks/register_session.mjs +76 -14
  22. package/hooks/roque/.claude-plugin/plugin.json +6 -0
  23. package/hooks/roque/LICENSE +21 -0
  24. package/hooks/roque/SOURCE +1 -0
  25. package/hooks/roque/hooks.json +38 -0
  26. package/hooks/roque/package.json +3 -0
  27. package/hooks/roque/roque.js +1690 -0
  28. package/hooks/sync_memory.mjs +11 -7
  29. package/package.json +7 -3
  30. package/scripts/cli/agent-gate.mjs +70 -0
  31. package/scripts/cli/agent-id.mjs +37 -0
  32. package/scripts/cli/archive.mjs +39 -38
  33. package/scripts/cli/archived.mjs +4 -2
  34. package/scripts/cli/args-kinds.mjs +33 -0
  35. package/scripts/cli/args-notes.mjs +42 -0
  36. package/scripts/cli/args-owner.mjs +37 -0
  37. package/scripts/cli/args-run.mjs +38 -0
  38. package/scripts/cli/args.mjs +47 -104
  39. package/scripts/cli/beads.mjs +57 -0
  40. package/scripts/cli/codex.mjs +66 -12
  41. package/scripts/cli/core.mjs +117 -41
  42. package/scripts/cli/create.mjs +224 -76
  43. package/scripts/cli/curator-feed-codex.mjs +81 -0
  44. package/scripts/cli/curator-feed-observations.mjs +97 -0
  45. package/scripts/cli/curator-feed-scope.mjs +124 -0
  46. package/scripts/cli/curator-feed-sources.mjs +172 -0
  47. package/scripts/cli/curator-feed.mjs +318 -0
  48. package/scripts/cli/default-name.mjs +73 -0
  49. package/scripts/cli/doctor.mjs +67 -2
  50. package/scripts/cli/eventlog.mjs +6 -2
  51. package/scripts/cli/exit-code.mjs +9 -0
  52. package/scripts/cli/first-run.mjs +115 -0
  53. package/scripts/cli/heartbeat.mjs +135 -0
  54. package/scripts/cli/helm-skip.mjs +20 -0
  55. package/scripts/cli/hire-headless.mjs +37 -22
  56. package/scripts/cli/hire.mjs +8 -2
  57. package/scripts/cli/inspect.mjs +36 -18
  58. package/scripts/cli/lifecycle.mjs +95 -49
  59. package/scripts/cli/main.mjs +32 -2
  60. package/scripts/cli/memory-env.mjs +124 -0
  61. package/scripts/cli/migrate.mjs +13 -7
  62. package/scripts/cli/next-steps.mjs +13 -0
  63. package/scripts/cli/nightly-launch.mjs +118 -0
  64. package/scripts/cli/nightly.mjs +228 -0
  65. package/scripts/cli/notes.mjs +58 -8
  66. package/scripts/cli/owner-config.mjs +181 -0
  67. package/scripts/cli/owner-jobs.mjs +201 -0
  68. package/scripts/cli/prompt-append.mjs +135 -0
  69. package/scripts/cli/rename.mjs +90 -109
  70. package/scripts/cli/run-context.mjs +50 -0
  71. package/scripts/cli/run.mjs +236 -79
  72. package/scripts/cli/scaffold.mjs +119 -126
  73. package/scripts/cli/schedule-errors.mjs +70 -0
  74. package/scripts/cli/sessions.mjs +10 -3
  75. package/scripts/cli/skills-check.mjs +147 -0
  76. package/scripts/cli/skills.mjs +170 -0
  77. package/scripts/cli/start.mjs +201 -0
  78. package/scripts/cli/templates.mjs +119 -45
  79. package/scripts/cli/tldr.mjs +184 -0
  80. package/scripts/cli/toml-string.mjs +76 -0
  81. package/scripts/cli/user-work-view.mjs +48 -0
  82. package/scripts/cli/writable-dirs.mjs +23 -0
  83. package/scripts/cli/yaml.mjs +1 -1
  84. package/skills/hire/SKILL.md +112 -100
  85. package/skills/hire/references/owner.md +68 -0
  86. package/templates/agents/claude-code/knowledge-curator.md +5 -2
  87. package/templates/agents/codex/knowledge-curator.toml +2 -1
  88. package/templates/operating-model/base.md +78 -0
  89. package/templates/operating-model/owner.md +11 -0
  90. package/templates/operating-model/principal.md +9 -0
  91. package/templates/operating-model/specialist.md +9 -0
  92. package/templates/packs/owner/craft.md +32 -0
  93. package/templates/packs/owner/identity.md +13 -0
  94. package/templates/packs/owner/mandate.md +26 -0
  95. package/templates/packs/owner/owner/SKILL.md +118 -0
  96. package/templates/packs/owner/owner-feedback/SKILL.md +49 -0
  97. package/templates/packs/owner/owner-heartbeat/SKILL.md +35 -0
  98. package/templates/packs/owner/owner-nightly/SKILL.md +114 -0
  99. package/templates/packs/owner/owner-review/SKILL.md +70 -0
  100. package/templates/packs/owner/owner-work/SKILL.md +53 -0
  101. package/templates/packs/principal/craft.md +27 -0
  102. package/templates/packs/principal/identity.md +13 -0
  103. package/templates/packs/principal/mandate.md +23 -0
  104. package/templates/packs/principal/principal-brief/SKILL.md +44 -0
  105. package/templates/packs/principal/principal-intro/SKILL.md +95 -0
  106. package/templates/packs/principal/principal-nightly/SKILL.md +152 -0
  107. package/templates/skills/check-in/SKILL.md +34 -24
  108. package/templates/skills/knowledge-curation/SKILL.md +89 -48
  109. package/templates/skills/local-rules/SKILL.md +86 -0
  110. package/templates/skills/nightly-consolidation/SKILL.md +131 -100
  111. package/templates/skills/skill-method/SKILL.md +79 -0
  112. package/templates/skills/user-nightly/SKILL.md +143 -0
  113. package/templates/skills/work-graph/SKILL.md +75 -0
  114. package/conformance/README.md +0 -60
  115. package/conformance/discovery.test.mjs +0 -140
  116. package/conformance/envelope.test.mjs +0 -185
  117. package/conformance/error-codes.test.mjs +0 -125
  118. package/conformance/harness.mjs +0 -180
  119. package/conformance/identity.test.mjs +0 -125
  120. package/docs/integration-guide.md +0 -1042
  121. package/templates/skills/self-update/SKILL.md +0 -121
@@ -0,0 +1 @@
1
+ import{spawn as se}from"node:child_process";import{readFileSync as ce,statSync as ae}from"node:fs";import de from"node:path";import{fileURLToPath as ue}from"node:url";import{existsSync as M,realpathSync as x}from"node:fs";import{homedir as m}from"node:os";import u from"node:path";var f="BACKSTORY_HOME";var _="BACKSTORY_INTERNAL";var g=".backstory";var y="setup.json";function S(e={}){let r=e.env??process.env,n=(e.platform??process.platform)==="win32"?u.win32:u,o=r[f],i=o===void 0?n.join(e.homeDirectory??m(),g):o;if(!i||!n.isAbsolute(i))throw new Error(`${f} must be absolute`);let s=n.resolve(i);return b(s,n),D(s,e.cacheRoots??v(r,e.homeDirectory??m(),n),n),s}function b(e,r){if(e===r.parse(e).root)throw new Error("memory home must not be a filesystem root");if(r===u&&e===u.resolve(m()))throw new Error("memory home must not be the user home")}function v(e,r,t){return[t.join(r,".cache"),e.XDG_CACHE_HOME,e.CLAUDE_PLUGIN_ROOT,e.CODEX_PLUGIN_ROOT].filter(n=>!!n).map(n=>{if(!t.isAbsolute(n))throw new Error("cache and plugin roots must be absolute");return n})}function D(e,r,t){let n=R(e,t);if(r.map(i=>{if(!t.isAbsolute(i))throw new Error("cache and plugin roots must be absolute");return R(i,t)}).some(i=>P(n,i,t))||/(?:^|[/\\])plugins[/\\]cache(?:[/\\]|$)/i.test(n))throw new Error("memory home must not be inside a cache or plugin root")}function R(e,r){let t=[],n=r.resolve(e);for(;!M(n);){let o=r.dirname(n);if(o===n)return r.resolve(e);t.unshift(r.basename(n)),n=o}return r.join(x(n),...t)}function P(e,r,t=u){let n=t.relative(r,e);return n===""||!n.startsWith("..")&&!t.isAbsolute(n)}import{closeSync as Pe,fsyncSync as Ce,lstatSync as Y,mkdirSync as Le,openSync as He,readFileSync as B,renameSync as Ye,unlinkSync as Be,writeFileSync as Ke}from"node:fs";import T from"node:path";var A=2e3,N=45e3;var c="configuration is invalid";function k(e){try{return C(JSON.parse(e))}catch{throw new Error(c)}}function C(e){if(!p(e)||!d(e,["capture","provider","rag","revision","updatedAt","version"]))throw new Error(c);let r=e.revision;if(e.version!==1||!Number.isSafeInteger(r)||typeof r!="number"||r<0)throw new Error(c);if(typeof e.updatedAt!="string"||Number.isNaN(Date.parse(e.updatedAt)))throw new Error(c);if(!p(e.capture)||!d(e.capture,["enabled"])||typeof e.capture.enabled!="boolean")throw new Error(c);return{version:1,revision:r,capture:{enabled:e.capture.enabled},provider:H(e.provider),rag:L(e.rag),updatedAt:e.updatedAt}}function L(e){if(e===void 0)return{enabled:!0};if(!p(e)||!d(e,["deadlineMs","enabled"])||typeof e.enabled!="boolean")throw new Error(c);let{deadlineMs:r}=e;if(r===void 0)return{enabled:e.enabled};if(typeof r!="number"||!Number.isInteger(r)||r<A||r>N)throw new Error(c);return{enabled:e.enabled,deadlineMs:r}}function H(e){if(!p(e)||typeof e.kind!="string")throw new Error(c);if(e.kind==="native"&&d(e,["kind"]))return{kind:"native"};if(e.kind!=="openai"&&e.kind!=="openrouter")throw new Error(c);if(!d(e,["credential","kind"]))throw new Error(c);let r=e.kind==="openai"?"OPENAI_API_KEY":"OPENROUTER_API_KEY";if(!p(e.credential)||!d(e.credential,["kind","name"])||e.credential.kind!=="environment"||e.credential.name!==r)throw new Error(c);return{kind:e.kind,credential:{kind:"environment",name:r}}}function p(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function d(e,r){return Object.keys(e).every(t=>r.includes(t))}var K="config.json",j=["completedAt","managedBy","mode","provider","version"],F=64,a="setup marker is invalid";function h(e){try{let r=G(e);return r!==null&&U(r,X(e))}catch{return!1}}function G(e){let r=O(V(e));if(r===null)return null;try{return W(JSON.parse(r))}catch{throw new Error(a)}}function V(e){return T.join(e,y)}function U(e,r){return e.mode==="cloud"?r===e.provider:r==="native"}function X(e){let r=O(T.join(e,K));return r===null?"native":k(r).provider.kind}function W(e){if(!$(e)||!Object.keys(e).every(s=>j.includes(s)))throw new Error(a);let{completedAt:r,managedBy:t,mode:n,provider:o,version:i}=e;if(i!==1||n!=="cloud"&&n!=="local")throw new Error(a);if(typeof r!="string"||Number.isNaN(Date.parse(r)))throw new Error(a);if(n==="cloud"?o!=="openai"&&o!=="openrouter":o!==void 0)throw new Error(a);if(t!==void 0&&(typeof t!="string"||t===""||t.length>F))throw new Error(a);return{version:1,mode:n,...o===void 0?{}:{provider:o},completedAt:r,...t===void 0?{}:{managedBy:t}}}function O(e){let r;try{r=Y(e)}catch(t){if(J(t,"ENOENT"))return null;throw t}if(!r.isFile()||r.isSymbolicLink())throw new Error(a);return B(e,"utf8")}function $(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function J(e,r){return typeof e=="object"&&e!==null&&"code"in e&&e.code===r}import{createHash as z}from"node:crypto";import{appendFileSync as Q,closeSync as q,mkdirSync as Z,openSync as ee,readdirSync as We,rmSync as $e,statSync as re,utimesSync as Je}from"node:fs";import E from"node:path";var ne=25,te="tool-ticks";var Qe=1440*6e4,qe=4320*6e4;function w(e,r=ne){let t=ie(e);Z(t,{recursive:!0,mode:448});let n=E.join(t,"ticks");Q(n,".",{mode:384});let o=re(n).size,i=Math.floor(o/r);return{calls:o,due:i>=1&&oe(t,i)}}function oe(e,r){try{return q(ee(E.join(e,`r${r}`),"wx",384)),!0}catch{return!1}}function ie(e){let r=z("sha256").update(e.sessionId).digest("hex").slice(0,32);return E.join(e.dataHome,te,`${e.host}-${r}`)}var I=8*1024*1024,pe=2e3,le=5e3;async function fe(){let e=ge(process.argv.slice(2)),r=await ye();if(!e||!r||process.env[_]==="1")return;let t=l(r.session_id),n=l(r.transcript_path),o=l(r.cwd);if(!t||!n||!o||l(r.agent_id))return;let i={dataHome:S(),host:e,sessionId:t};if(!me(i.dataHome)||!h(i.dataHome)||!w(i).due||Ee(i.dataHome))return;let s=l(r.turn_id);await _e(e,o,{hook_event_name:"Checkpoint",session_id:t,transcript_path:n,cwd:o,...s?{turn_id:s}:{}})}function me(e){try{return ae(e).isDirectory()}catch{return!1}}function Ee(e){try{return JSON.parse(ce(de.join(e,"config.json"),"utf8")).capture?.enabled===!1}catch{return!1}}function _e(e,r,t){let n=ue(new URL("./memory.mjs",import.meta.url)),o=se(process.execPath,[n,"hook","--host",e,"--event","Checkpoint"],{cwd:r,detached:!0,env:process.env,stdio:["pipe","ignore","ignore"]});return new Promise(i=>{let s=()=>{o.unref(),i()};o.on("error",s),o.stdin.on("error",s),o.stdin.end(JSON.stringify(t),s)})}function ge(e){let r=e[e.indexOf("--host")+1];return r==="claude"||r==="codex"?r:null}function ye(){return new Promise(e=>{let r=[],t=0,n=!1,o=s=>{n||(n=!0,clearTimeout(i),e(s))},i=setTimeout(()=>o(null),pe);process.stdin.on("data",s=>{t+=s.length,t<=I&&r.push(s)}).on("error",()=>o(null)).on("end",()=>o(t>I?null:Re(Buffer.concat(r))))})}function Re(e){try{let r=JSON.parse(e.toString("utf8"));return typeof r=="object"&&r!==null&&!Array.isArray(r)?r:null}catch{return null}}function l(e){return typeof e=="string"&&e.trim().length>0&&!e.includes("\0")?e:null}setTimeout(()=>process.exit(0),le).unref();fe().then(()=>process.exit(0),()=>process.exit(0));
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: "capture"
3
+ description: "Use whenever a message carries something the project must still know next week—record it immediately and unasked; the user will never say ‘remember this.’ Fires on: a decision made, reaffirmed, or reversed (‘we settled on X’, ‘not sharding after all’, ‘that’s the call’); a correction of you or standing guidance, however blunt or repeated (‘no, that’s wrong; X applies only to Y’); a gotcha, root cause, version pin, constraint, or convention worth handing to whoever hits it next (‘for future reference’, ‘so nobody else hits this’); maintained guidance now disproved; or a persistent blocker newly cleared, confirmed real, or otherwise changed state, even in passing. The user’s word alone is sufficient authority; never wait for repo evidence or an explicit save request. Also search and load saved project knowledge when prior decisions or gotchas may matter. Not for routine progress or per-turn activity (Backstory observations cover that)."
4
+ user-invocable: true
5
+ ---
6
+
7
+ # capture
8
+
9
+ ## Purpose
10
+
11
+ Proactively preserve consequential project knowledge without waiting for a user request, and find it again later. Knowledge records are typed, revisioned, and tagged by durable project area. They live in their own store beside Backstory observations: capture writes only knowledge records, while `search` returns knowledge records and observations together (`--kind knowledge` narrows to records) and `load` reads either.
12
+
13
+ ## Inputs
14
+
15
+ - A qualifying durable fact, with the authority or evidence behind it.
16
+ - The current working directory. The project comes from it; commands take no project flag.
17
+ - For an update: the record's exact ID and its loaded `revisionToken`.
18
+ - Tag intent: canonical tags to reuse, or a genuinely new tag with a short area description.
19
+
20
+ ## Running commands
21
+
22
+ - Claude Code: `backstory <command>` (the plugin's `bin/` is on PATH).
23
+ - Codex: `node "<plugin root>/scripts/knowledge.mjs" <command>`. The SessionStart knowledge context names the exact path to use.
24
+ - Pass `--json` to every command. Exit 0 is success; exit 1 carries `code`, `message`, and, for a failed capture, `recoveryInput`.
25
+
26
+ ## Working Set
27
+
28
+ - Bounded tag and task search results, exact loads, and current revision tokens from the commands below.
29
+ - `references/tagging-policy.md` and `references/knowledge-capture-input.json`.
30
+ - Data lives under `${BACKSTORY_HOME:-~/.backstory}/projects/<projectKey>/knowledge/`. Never read or edit its files directly: records, `tags.json`, `index.json`, `activity.json`, and `history/` are written only by these commands.
31
+
32
+ ## Commands
33
+
34
+ | Command | Use |
35
+ |---|---|
36
+ | `tags search '<subject>'` | Find existing canonical tags, aliases, and descriptions (`--limit N`, at most 5). Use it for unresolved aliases, omitted tags, or genuinely new tag intent. |
37
+ | `capture --kind knowledge --input - [--expected-revision <token>]` | Save or update one record from semantic JSON on standard input. `--input <path>` only for a manual capture or a returned `recoveryInput` file. |
38
+ | `search '<task>' [--kind knowledge] [--tag <tag>] [--path <path>] [--include-historical] [--cursor <cursor>]` | Search knowledge records and observations together; returns one compact page of up to 10 one-line rows (record: `id`, category, title, `useWhen`, tag; observation: `observation:<id>`, date, status, files) and a footer with counts, a `next:` cursor line, and the `load` form. A query or `--tag` is required; `--tag` alone browses by exact tag. `--kind knowledge` returns records only; historical records are hidden unless `--include-historical`. |
39
+ | `load <id> [--inspect-historical]` | Load one record in full and verified. A record that is not `active` needs `--inspect-historical` (search lists it only with `--include-historical`). An `observation:<id>` result loads the same way, as evidence rather than maintained guidance. |
40
+ | `history <id>` / `inspect <id> --revision <token>` | List or read prior revisions of a record. |
41
+ | `tags ensure\|merge\|apply --input <path>` | Explicit tag maintenance. `merge` needs the catalog revision it read; run it only on an explicit request. |
42
+ | `registry [--host claude\|codex]` | Preview exactly what SessionStart injects. |
43
+
44
+ ## Outputs + DONE
45
+
46
+ - A saved, updated, superseded, retired, no-op, skipped, or surfaced-failure knowledge result with ID, revision, and canonical `recordPath` where applicable.
47
+ - For a lookup: the loaded records that answer the question, or a statement that none apply.
48
+
49
+ **DONE when:** a supported durable fact is recorded through the typed path, or a skip/no-op/failure is reported truthfully; or a lookup is answered from loaded records.
50
+
51
+ ## Method / guardrails
52
+
53
+ 1. Capture accepted lasting decisions, explicit corrections, verified reusable patterns/gotchas/constraints, disproved maintained guidance, and confirmed persistent-blocker transitions. Do not infer a durable fact from incidental code, a transient command failure, a task outcome, or routine progress.
54
+ 2. Read `references/tagging-policy.md`, then reuse the visible tag catalog (SessionStart lists it) and current-request tag results. Run `tags search` only for unresolved aliases, omitted tags, or genuinely new tag intent. Select or create tags at the policy's durable area altitude. An explicit user statement that a lasting decision is current, corrected, or superseded is accepted authoritative evidence; absence of corroborating repository evidence does not block the save. Retain disagreeing repository statements as stale or historical context.
55
+ 3. Read `references/knowledge-capture-input.json`, fill its semantic JSON outside the store, then submit it on standard input with `capture --kind knowledge --input - --json`. Fields: `id` (lowercase hyphenated), `title`, `summary`, `category` (`decision`, `pattern`, `gotcha`, or `blocker`), `useWhen`, `content`, `evidence`, `tags`; optional `status` (`active`, `disputed`, `superseded`, `archived`), `relatedRecordIds`, and, for a `blocker`, `blocker: { condition, resolutionCriterion }`. New input needs non-empty tag intent.
56
+ 4. An unchanged retry is a no-op. A changed record needs its loaded `revisionToken` as `--expected-revision`; omit `tags` on an update to preserve the stored ones. To retire or supersede guidance, update the record's `status` rather than deleting anything.
57
+ 5. For a lookup, search with the actual task, assess the rows for applicability, `load` only the applicable IDs, and answer from loaded records. Treat records as claims from earlier sessions: verify against the code before acting on them. Never repeat equivalent searches; refine only unresolved questions.
58
+ 6. Return the tag and record outcome, canonical `recordPath`, and ID/revision. A failed write returns `recoveryInput` for manual recovery and is reported, never retried with other options.
59
+
60
+ ## Handoff
61
+
62
+ | Handoff | Details |
63
+ |---|---|
64
+ | Current phase | Done |
65
+ | What was just done | Trigger or skip reason, knowledge ID, revision or conflict, canonical `recordPath`, tag outcomes |
66
+ | Proposed next step | Render resolved action. |
67
+
68
+ ## Escalate-If
69
+
70
+ - Explicit authoritative directions conflict without a clear latest decision.
71
+ - A write cannot recover through the available path; report the `recoveryInput` file.
@@ -0,0 +1,16 @@
1
+ {
2
+ "inputVersion": 1,
3
+ "id": "<canonical-record-id>",
4
+ "title": "<durable knowledge title>",
5
+ "summary": "<short durable summary>",
6
+ "category": "pattern",
7
+ "useWhen": "<when this guidance applies>",
8
+ "content": "<maintained guidance>",
9
+ "evidence": "<accepted evidence or authority>",
10
+ "tags": [
11
+ {
12
+ "id": "<canonical-or-new-tag>",
13
+ "description": "<required only when this is a genuinely new tag>"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,29 @@
1
+ # Tagging Policy
2
+
3
+ ## Purpose
4
+
5
+ Tags are durable, wiki-level project areas that connect multiple independent knowledge records. They are not alternate names for a task, branch, implementation, bug, or individual record.
6
+
7
+ ## Selection
8
+
9
+ - Assign one primary tag at the broadest stable level that still identifies a meaningful product capability, subsystem, or workflow.
10
+ - Add at most one secondary tag when the record materially connects another independently browsable area.
11
+ - Reuse a canonical tag when it already represents the concept. Put record-specific detail in the record ID, title, summary, and body.
12
+ - Prefer the durable area over a compound restatement of the current work: `memory` over `memory-hooks` or `codex-hooks-display`; `plan` over `plan-simplification`.
13
+
14
+ ## Creation
15
+
16
+ Create a new canonical tag when no existing tag represents the area and the proposed tag:
17
+
18
+ 1. Makes sense as an independently browsable wiki page.
19
+ 2. Is expected to group multiple independent records.
20
+ 3. Will survive completion or renaming of the current task.
21
+ 4. Adds useful navigation that its nearest broader tag cannot provide.
22
+
23
+ Give every new tag a short area description. If the candidate fails this test, use the nearest applicable canonical area and keep the specificity on the record.
24
+
25
+ ## Elevation
26
+
27
+ - Split downward only after the narrower concept has multiple independent records and demonstrated retrieval or navigation value.
28
+ - Merge upward when a tag remains task-shaped, mirrors one record, lacks an independent conceptual identity, or adds no useful distinction from its broader area.
29
+ - Preserve merged names through aliases or redirects.
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: "memory"
3
+ description: "Search and read Backstory observations for prior work in this project, and administer it: check capture health, configure the observation model, prune or delete memory data. Use when the user asks what was done or decided before, or asks about memory status."
4
+ user-invocable: true
5
+ ---
6
+
7
+ # memory
8
+
9
+ ## Purpose
10
+
11
+ Backstory records what each agent turn accomplished as a short observation, stores it locally, and returns it three ways: SessionStart injects a brief of recent status and observations; the first substantive prompt of a session adds a few one-line pointers to related past work (a `## Memory from past work` block); and this skill's commands search and read the full history on demand. Use it to recover prior decisions, work, and context for the current project.
12
+
13
+ ## Inputs
14
+
15
+ - `$ARGUMENTS` or the request: a question about prior work, or a memory administration command with its options.
16
+ - The current working directory. The project and branch scope come from it; no command takes a project flag.
17
+
18
+ ## Running commands
19
+
20
+ - Claude Code: `backstory <command>` (the plugin's `bin/` is on PATH).
21
+ - Codex: `node "<plugin root>/scripts/knowledge.mjs" search|load ...` for search and load, `node "<plugin root>/scripts/memory.mjs" <command>` for every other command. The SessionStart context names the exact path to use.
22
+ - Pass `--json` to every command. Exit 0 is success, 1 is unavailable or not ready, 2 is an invalid request.
23
+
24
+ ## Working Set
25
+
26
+ - Command JSON only: `state`, `reason`, `errors`, and the result fields. Never read or edit corpus files directly.
27
+ - Data lives under `${BACKSTORY_HOME:-~/.backstory}`: `projects/<projectKey>/contexts/<contextKey>/memory/` holds observations per branch, `memory-search.db` and `index-generations/` hold the derived search index, and `config.json` and `logs/` hold settings and bounded diagnostics.
28
+
29
+ ## Commands
30
+
31
+ | Command | Use |
32
+ |---|---|
33
+ | `search "<query>"` | One search over this project's observations and project knowledge records, ranked together in one compact page of up to 10 one-line rows with a footer (counts, `next:` cursor, `load` form); a query or `--tag` is required. `--kind observation` or `--kind knowledge` narrows to one kind; `--tag <tag>` (repeatable) filters by project knowledge tag, which observations carry as `area:<tag>`; `--path <path>` boosts matches on a file path; `--include-historical` also lists historical knowledge records; `--cursor <cursor>` reads the next page. Observation results have ids `observation:<id>`; a warning `OBSERVATION_INDEX_UNAVAILABLE` means only knowledge answered. |
34
+ | `load <id>` | Load one result in full: `load observation:<id>` for an observation, `load <record-id>` for a knowledge record. |
35
+ | `project-search "<query>"` / `read-observation <id>` | Observation-only aliases kept from earlier releases (`--limit N`, `--token-budget N`, `--tag <exact tag>`); `read-observation` takes the bare id without `observation:`. |
36
+ | `branch-search "<query>"` / `global-search "<query>"` | Limit to the current branch, or search every project in the memory home. |
37
+ | `status` | Branch status and recent history (`--history N`, at most 20). |
38
+ | `timeline` | Recent observations for this branch (`--limit N`). |
39
+ | `doctor` | `state: ready` with exit 0 when memory works. Otherwise `errors` and `nodeFloor` name the cause, for example Node older than 22.16.0 or SQLite without FTS5. |
40
+ | `config get` | Read capture, provider, and per-prompt memory settings (`ragEnabled`). `config set --capture-enabled true\|false --provider native\|openai\|openrouter` chooses the observation model; `config set --rag on\|off` turns per-prompt memory on or off on its own, and `config set --rag-deadline-ms N` (2000 to 45000) overrides how long its model steps may run (default 7 s with an API key, 30 s through Codex). Add `--expected-revision N` to refuse a concurrent change. `openai` and `openrouter` read `OPENAI_API_KEY` or `OPENROUTER_API_KEY` from the environment. |
41
+ | `prune plan` | List low-value observations. `prune apply --backup-path <new file>` removes them and writes that restore file; `prune restore --backup-path <same file>` puts them back. The file must be outside the memory home. |
42
+ | `delete --scope project --confirm delete-project:<projectKey> --recovery-path <new dir>` | Move this project's memory to recovery. `<projectKey>` is `identity.projectKey` from `doctor`. The recovery directory must not exist, must be outside the memory home, and must be on the same filesystem. |
43
+
44
+ ## Outputs + DONE
45
+
46
+ - For a prior-work question: the relevant observations and knowledge records, quoted from `search` or `load`, with their dates and branch.
47
+ - For an administration command: the result summarized from its JSON, with the exact command that ran.
48
+ - DONE when the question is answered from loaded observations, or the command returns, or its `state` and `reason` are reported.
49
+
50
+ ## Method / guardrails
51
+
52
+ 1. For a prior-work question, `search` with specific terms, then `load` the best hits before relying on them. Search rows are short and omit detail. If nothing matches, say so rather than guessing.
53
+ 2. Treat observations as claims from earlier sessions, not as current truth: verify against the code before acting on them. A `## Memory from past work` block in the prompt context is pointers only (date, status, files): it carries no ids and no detail. When a line matters, `search` for its status words, then `load observation:<id>` the hit before relying on it.
54
+ - It appears at most once per session, on the first substantive prompt; `ok`, `go`, and slash commands do not use it up. With an OpenAI or OpenRouter key configured as the provider, the notes are chosen by a model; otherwise they are the top lexical matches. It never repeats what SessionStart already showed.
55
+ 3. Run `prune apply`, `prune restore`, `delete`, and `config set` only on an explicit user request. Show `prune plan` or `doctor` output first.
56
+ 4. If a command reports `unavailable`, run `doctor` and report its cause. Do not retry with other options.
57
+ 5. `index_unavailable` before the first capture completes means there is nothing to search yet. A project that has had no session start has no corpus; the next session start creates it.
58
+
59
+ ## Handoff
60
+
61
+ | Handoff | Details |
62
+ |---|---|
63
+ | Current phase | Done |
64
+ | What was just done | Result |
65
+ | Proposed next step | Render resolved action. |
66
+
67
+ ## Escalate-If
68
+
69
+ - `delete` or `prune apply` is implied but not explicitly requested.
70
+ - `doctor` reports a cause the user must fix, such as the Node version.
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: "setup"
3
+ description: "Set Backstory up: the user chooses cloud (an OpenAI or OpenRouter API key; recommended) or local (their own Claude or Codex subscription). Run it as soon as the SessionStart notice says Backstory is installed but not set up, when a command returns setup_required, or when the user asks to change Backstory's mode or provider."
4
+ user-invocable: true
5
+ ---
6
+
7
+ # setup
8
+
9
+ ## Purpose
10
+
11
+ Backstory captures, searches, and injects nothing until the user chooses how its models run. This skill checks the current state, explains the two modes, gets the user's choice, and completes setup through the CLI. It never chooses for the user and never handles an API key.
12
+
13
+ ## Inputs
14
+
15
+ - The SessionStart notice, a `setup_required` result, or a user request to set up or change mode or provider.
16
+ - An optional preference in `$ARGUMENTS` (`cloud`, `local`, `openai`, `openrouter`); still confirm it with the user.
17
+
18
+ ## Running commands
19
+
20
+ - Claude Code: `backstory <command>` (the plugin's `bin/` is on PATH).
21
+ - Codex: `node "<plugin root>/scripts/memory.mjs" <command>`, where `<plugin root>` is the directory two levels above this file (`<plugin root>/skills/setup/SKILL.md`). Whenever you show the user a command to run, write it with the resolved absolute path.
22
+ - Exit 0 is success, 1 is a refusal with `reason` and `text`, 2 is an invalid request.
23
+
24
+ ## Working Set
25
+
26
+ - Only the output of `setup --status --json` and `setup --mode …`. Never read or edit `setup.json`, `config.json`, `credentials`, or any credential-store item, and never use `config set --provider` to switch modes.
27
+
28
+ ## Outputs + DONE
29
+
30
+ **DONE when:** status reports `"configured": true` for the mode the user chose; or setup was already complete and the skill exited silently; or no user is present and the skill stopped after one line; or the user declined or deferred.
31
+
32
+ ## Modes
33
+
34
+ | | Cloud (recommended): OpenAI or OpenRouter, direct | Local, Codex host | Local, Claude host |
35
+ |---|---|---|---|
36
+ | Observations (background, after each turn) | gpt-6-luna through the chosen provider's API | `codex exec --model gpt-6-luna` (Codex subscription) | `claude --model haiku` (Claude subscription) |
37
+ | Per-prompt query expansion and rerank | Same API, 7 s budget | `codex exec`, 12.45 s end to end (measured 2026-10-05 in a live Codex hook run, recorded in project memory), 30 s budget | `codex exec` through the Codex login when Codex is installed (same as left); otherwise keyword-only |
38
+ | Search | Local, instant | Local, instant | Local, instant |
39
+
40
+ Budgets: `rag/models.ts`. The 12.45 s (expansion 5.6 s, rerank 5.2 s) was measured 2026-10-05 in a live Codex hook run, recorded in project memory. Cloud latency is not measured yet, so no figure is given for it.
41
+
42
+ Tell the user plainly:
43
+
44
+ - Cloud calls go **directly** from this machine to OpenAI or OpenRouter, with no Backstory server in between. They carry the turn text (observations) and the prompt plus up to 30 short candidate notes (per-prompt retrieval).
45
+ - Local uses the user's own subscriptions and is slower.
46
+ - On a Claude host, local retrieval borrows the Codex login when Codex is installed; otherwise it is keyword-only and sends nothing.
47
+ - A key stored with `key set` goes into the OS credential store (macOS Keychain, Linux Secret Service) and is never written to Backstory's config, `setup.json`, logs, or any environment. The exception: a key the user already exports as `OPENAI_API_KEY` or `OPENROUTER_API_KEY` is used first, is visible to this agent session, and is inherited by Backstory's background capture worker. Setup cannot make an exported key secret.
48
+
49
+ ## Why gpt-6-luna (not Claude Haiku)
50
+
51
+ Figures retrieved 2026-10-06 unless dated otherwise; nothing here is estimated.
52
+
53
+ - **Cost.** gpt-6-luna on the OpenAI API: $0.10 input, $0.01 cached input, $0.50 output per 1M tokens (up to 272K context) ([OpenAI pricing](https://developers.openai.com/api/docs/pricing), [gpt-6-luna](https://developers.openai.com/api/docs/models/gpt-6-luna), retrieved 2026-10-06).
54
+ - On OpenRouter, `openai/gpt-6-luna`: $0.10 input, $0.50 output per 1M tokens ([OpenRouter](https://openrouter.ai/openai/gpt-6-luna), retrieved 2026-10-06).
55
+ - Claude Haiku 4.5 (`claude-haiku-4-5-20251001`): $1 input, $5 output per 1M tokens, about 10× gpt-6-luna ([Anthropic pricing](https://platform.claude.com/docs/en/about-claude/pricing), retrieved 2026-10-06).
56
+ - **Speed.** OpenAI positions gpt-6-luna as its fastest and most cost-effective model and documents reasoning effort `none`, which Backstory uses ([latest model guide](https://developers.openai.com/api/docs/guides/latest-model), retrieved 2026-10-06). OpenAI publishes no latency figures.
57
+ - Measured locally: `codex exec` gpt-6-luna expansion plus rerank took 12.45 s end to end for one prompt (expansion 5.6 s, rerank 5.2 s; measured 2026-10-05 in a live Codex hook run, recorded in project memory). The budgets are 7 s direct and 30 s native (`rag/models.ts`).
58
+ - **Quality.** Haiku is not used for query expansion and rerank because Claude's CLI has no low-latency structured-output path inside the per-prompt hook budget (`rag/prompt-memory.ts`). Haiku stays the local observation model on a Claude host.
59
+ - **Subscription billing (time-sensitive; re-check before relying on it).** Codex CLI signed in with ChatGPT draws on the ChatGPT plan's credits ([ChatGPT pricing](https://learn.chatgpt.com/docs/pricing), retrieved 2026-10-06). `claude -p` draws from the Claude subscription's usage limits unless `ANTHROPIC_API_KEY` is set ([Claude support](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan), dated 2026-06-16).
60
+
61
+ ## Method / guardrails
62
+
63
+ 1. Run `backstory setup --status --json`. If it reports `"configured": true`, exit silently: say nothing about setup and continue with what the user asked for. Continue past this step only when the user explicitly asked to change the mode or provider.
64
+ 2. If no user is present (a headless run such as `claude -p` or `codex exec`, or you are a subagent), stop after one line: "Backstory is not set up; run its setup skill in an interactive session." Never pick a mode on the user's behalf.
65
+ 3. Present the modes table, the plain statements under it, and the "Why gpt-6-luna" note. Recommend cloud.
66
+ 4. Ask the user to choose: cloud with OpenAI, cloud with OpenRouter, or local. Wait for the answer.
67
+ 5. Cloud:
68
+ - Ask the user to run `backstory key set --provider openai` (or `--provider openrouter`) in a separate terminal, outside this session, and to tell you when it printed `valid · <n> ms`. It asks for the key with hidden input, checks it with one live call, and stores it. On Linux without Secret Service it refuses unless the user adds `--allow-file-storage`, which keeps the key in a plain-text 0600 file in the Backstory home; say so before they choose it. A user who already exports the provider's key variable may skip this step (see the exception above).
69
+ - You never ask for, read, or echo the key. If the user pastes a key into the conversation anyway, do not repeat or use it; tell them it is now in this transcript, suggest rotating it, and point them back to the terminal command.
70
+ - After they confirm, run `backstory setup --mode cloud --provider <provider>`. On `key_required` no key is stored yet: repeat the terminal instruction. On `invalid_key` the provider rejected the key: ask them to run `key set` again with a working key.
71
+ 6. Local: confirm the user agrees that Backstory will use their Claude or Codex subscription, then run `backstory setup --mode local --confirm-subscription-use` and relay the per-host routes it prints.
72
+ 7. Run `backstory setup --status` and report its one line.
73
+
74
+ ## Handoff
75
+
76
+ | Handoff | Details |
77
+ |---|---|
78
+ | Current phase | Done |
79
+ | What was just done | Mode and provider chosen, key source, or why setup stopped |
80
+ | Proposed next step | Continue with the user's original request. |
81
+
82
+ ## Escalate-If
83
+
84
+ - `setup` returns `setup_failed`: report its text and suggest `backstory doctor`.
85
+ - The user wants a provider other than OpenAI or OpenRouter, or a mode other than cloud or local: neither exists in this release.
@@ -0,0 +1,67 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export const TOKENIZER = 'estimated:chars-and-words-v1';
5
+
6
+ export function estimateTokens(text) {
7
+ const normalized = text.trim();
8
+ if (!normalized) return 0;
9
+ const chunks = normalized.match(/[\p{L}\p{N}]+|[^\s\p{L}\p{N}]/gu) ?? [];
10
+ return Math.max(chunks.length, Math.ceil(text.length / 4));
11
+ }
12
+
13
+ export function tokenRecord(text) {
14
+ return { estimatedTokens: estimateTokens(text), chars: text.length };
15
+ }
16
+
17
+ const SURFACE_MARKER = /^<!-- academy:surface:([a-z-]+) -->\n/gm;
18
+
19
+ // Measures a rendered Academy prompt: the operating model and marker lines are
20
+ // overhead, everything between one marker and the next belongs to that surface.
21
+ // `academy tokens` and the SessionStart hook both measure through this.
22
+ export function measureRenderedPrompt(prompt) {
23
+ const marks = [...prompt.matchAll(SURFACE_MARKER)];
24
+ const head = prompt.slice(0, marks[0]?.index ?? prompt.length);
25
+ const surfaces = marks.map((mark, i) => {
26
+ const start = mark.index + mark[0].length;
27
+ const end = marks[i + 1]?.index ?? prompt.length;
28
+ // Each surface is rendered as `content\n\n`; the last loses one newline to the file end.
29
+ const content = prompt.slice(start, end).replace(i + 1 < marks.length ? /\n\n$/ : /\n$/, '');
30
+ return { name: mark[1], marker: mark[0].trimEnd(), ...tokenRecord(content) };
31
+ });
32
+ const overheadText = head + surfaces.map((s) => `${s.marker}\n`).join('\n');
33
+ return { total: tokenRecord(prompt), overhead: tokenRecord(overheadText), surfaces };
34
+ }
35
+
36
+ function skillListing(skillFile) {
37
+ const front = readFileSync(skillFile, 'utf8').match(/^---\n([\s\S]*?)\n---/)?.[1] ?? '';
38
+ const field = (key) => front.match(new RegExp(`^${key}:\\s*(.*)$`, 'm'))?.[1].trim() ?? '';
39
+ return `${field('name')}: ${field('description')}`;
40
+ }
41
+
42
+ // The skills Academy rendered for this agent, from its package-skills manifest.
43
+ export function measureRenderedSkills(agentDir) {
44
+ const manifestPath = join(agentDir, '.academy', 'generated', 'package-skills.json');
45
+ const names = existsSync(manifestPath)
46
+ ? (JSON.parse(readFileSync(manifestPath, 'utf8')).skills?.['.claude/skills'] ?? [])
47
+ : [];
48
+ const listingTokens = names.reduce(
49
+ (sum, name) =>
50
+ sum + estimateTokens(skillListing(join(agentDir, '.claude', 'skills', name, 'SKILL.md'))),
51
+ 0,
52
+ );
53
+ return { count: names.length, listingTokens };
54
+ }
55
+
56
+ export function measureSessionContext(agentDir) {
57
+ const promptPath = join(agentDir, '.academy', 'generated', 'academy-system-prompt.md');
58
+ if (!existsSync(promptPath)) return null;
59
+ const { total, overhead, surfaces } = measureRenderedPrompt(readFileSync(promptPath, 'utf8'));
60
+ return {
61
+ tokenizer: TOKENIZER,
62
+ total: total.estimatedTokens,
63
+ overhead: overhead.estimatedTokens,
64
+ surfaces: Object.fromEntries(surfaces.map((s) => [s.name, s.estimatedTokens])),
65
+ skills: measureRenderedSkills(agentDir),
66
+ };
67
+ }
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
@@ -57,6 +57,43 @@ function sleep(ms) {
57
57
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
58
58
  }
59
59
 
60
+ const LOCK_WAIT_MS = 5000;
61
+ // Every hold is one short file section (a sweep is the longest, well under a
62
+ // second), and waiters give up after LOCK_WAIT_MS. A lock dir older than this
63
+ // was left by a holder that died, so a waiter may remove it.
64
+ const LOCK_ABANDONED_MS = 60_000;
65
+
66
+ function isAbandoned(lockDir) {
67
+ try {
68
+ return Date.now() - statSync(lockDir).mtimeMs > LOCK_ABANDONED_MS;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ // The age check and the removal share a break lock, so a waiter never removes
75
+ // a lock that another waiter has just taken.
76
+ // shortcut: two waiters that both find a dead breaker's dir can each take the
77
+ // break lock; upgrade to an owner pid in the lock dir if that is ever seen.
78
+ function breakAbandonedLock(lockDir) {
79
+ if (!isAbandoned(lockDir)) return;
80
+ const breakDir = `${lockDir}.break`;
81
+ if (isAbandoned(breakDir)) rmSync(breakDir, { recursive: true, force: true });
82
+ try {
83
+ mkdirSync(breakDir);
84
+ } catch {
85
+ return;
86
+ }
87
+ try {
88
+ if (!isAbandoned(lockDir)) return;
89
+ rmSync(lockDir, { recursive: true, force: true });
90
+ console.error(JSON.stringify({ event: 'lock.abandoned_removed', params: { lockDir } }));
91
+ } finally {
92
+ rmSync(breakDir, { recursive: true, force: true });
93
+ }
94
+ }
95
+
96
+ // `fn` never runs without the lock: a timeout throws `lock_timeout` first.
60
97
  export function withFileLock(lockDir, fn) {
61
98
  const started = Date.now();
62
99
  while (true) {
@@ -64,7 +101,11 @@ export function withFileLock(lockDir, fn) {
64
101
  mkdirSync(lockDir, { recursive: false });
65
102
  break;
66
103
  } catch {
67
- if (Date.now() - started > 5000) return fn();
104
+ if (Date.now() - started > LOCK_WAIT_MS) {
105
+ const error = new Error(`Timed out acquiring lock: ${lockDir}`);
106
+ throw Object.assign(error, { code: 'lock_timeout' });
107
+ }
108
+ breakAbandonedLock(lockDir);
68
109
  sleep(25);
69
110
  }
70
111
  }