@iamem/amem 0.1.2 → 0.2.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 (47) hide show
  1. package/README.md +64 -1
  2. package/dist/api/routes.js +337 -3
  3. package/dist/attest.d.ts +13 -0
  4. package/dist/attest.js +44 -0
  5. package/dist/capture.js +14 -5
  6. package/dist/cli.js +291 -6
  7. package/dist/context.d.ts +10 -1
  8. package/dist/context.js +105 -3
  9. package/dist/db.d.ts +141 -0
  10. package/dist/db.js +398 -0
  11. package/dist/embed.js +5 -14
  12. package/dist/estimate.d.ts +25 -1
  13. package/dist/estimate.js +36 -3
  14. package/dist/freshness.d.ts +7 -0
  15. package/dist/freshness.js +8 -1
  16. package/dist/hook.js +8 -1
  17. package/dist/hygiene.d.ts +26 -2
  18. package/dist/hygiene.js +42 -3
  19. package/dist/install/hosts.d.ts +15 -0
  20. package/dist/install/hosts.js +82 -4
  21. package/dist/install/skills.js +10 -5
  22. package/dist/kinds.d.ts +18 -0
  23. package/dist/kinds.js +80 -3
  24. package/dist/license.d.ts +1 -0
  25. package/dist/license.js +21 -19
  26. package/dist/mcp.js +221 -0
  27. package/dist/platforms.js +6 -0
  28. package/dist/policy.d.ts +6 -0
  29. package/dist/policy.js +16 -1
  30. package/dist/remember-contract.js +13 -4
  31. package/dist/repo-identity.d.ts +10 -0
  32. package/dist/repo-identity.js +20 -1
  33. package/dist/skill-capture.d.ts +43 -0
  34. package/dist/skill-capture.js +146 -0
  35. package/dist/skills.d.ts +106 -0
  36. package/dist/skills.js +422 -0
  37. package/docs/backlog.md +9 -0
  38. package/package.json +2 -1
  39. package/scripts/mcp-launch.sh +26 -0
  40. package/skills/amem-tasks/SKILL.md +100 -0
  41. package/skills/amem-write-skill/SKILL.md +99 -0
  42. package/templates/cursor-rule.mdc +16 -6
  43. package/templates/policy.deny-default.toml +5 -0
  44. package/templates/policy.example.toml +8 -0
  45. package/ui-static/app.js +458 -233
  46. package/ui-static/index.html +11 -34
  47. package/ui-static/styles.css +310 -1
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import { mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { buildAttestReport, formatAttestHuman } from "./attest.js";
5
5
  import { handleApi, logContextUsage } from "./api/routes.js";
6
- import { closeDb, getRepoByCwd, getRepoByName, renameWorkspace, listClaims, listComponents, listFlows, requireRepo, setReportedOnLatest, setReportedTokensSaved, touchSession, upsertRepo, upsertSetupState, wipeAllRepos, wipeRepo, openDb, } from "./db.js";
6
+ import { closeDb, getRepoByCwd, getRepoByName, renameWorkspace, listClaims, listComponents, listFlows, listRepos, listTasks, listTasksAll, insertTask, updateTask, completeTask, deleteTask, findTaskAnyRepo, normalizeTaskStatus, requireRepo, setReportedOnLatest, setReportedTokensSaved, touchSession, upsertRepo, upsertSetupState, wipeAllRepos, wipeRepo, openDb, } from "./db.js";
7
7
  import { handleHookPayload } from "./hook.js";
8
8
  import { installClaude, claudeInstallHealth } from "./install/claude.js";
9
9
  import { installCursor, cursorInstallHealth } from "./install/cursor.js";
@@ -11,7 +11,7 @@ import { installHost } from "./install/hosts.js";
11
11
  import { amemHome, dbPath, tryEnsureDir } from "./paths.js";
12
12
  import { assertExportAllowed, assertPlatformAllowed, assertRemoteAllowed, assertUiAllowed, loadPolicy, } from "./policy.js";
13
13
  import { applyProposal, diffProposal, exportRepoMemory, formatProposalDiff, loadProposalFile, validateProposal, } from "./proposal.js";
14
- import { detectRepoIdentity, parseWorkspaceSlug, workspaceIdentity } from "./repo-identity.js";
14
+ import { detectRepoIdentity, parseWorkspaceSlug, unbindableRootReason, workspaceIdentity, } from "./repo-identity.js";
15
15
  import { runDesktopApp } from "./app-shell.js";
16
16
  import { startUiServer, buildUiLandingUrl, openUiInBrowser, isAddrInUse, probeUiHealth, } from "./ui/server.js";
17
17
  import { installLoginService, isServiceInstalled, isServiceSupported, uninstallLoginService } from "./service.js";
@@ -25,16 +25,18 @@ import { rememberContract } from "./remember-contract.js";
25
25
  import { vaultStatus } from "./vault.js";
26
26
  import { applyLicenseFile, clearLicense, generateLicenseKeys, licenseStatus, signLicense, } from "./license.js";
27
27
  import { embedIndexIssues, embedStatus, reindexAllEmbeds, setEmbedBackend, } from "./embed.js";
28
- import { acceptSafeCleanups, decayStaleClaims, hygieneReport, mergeDuplicate, runScheduledHygiene } from "./hygiene.js";
28
+ import { acceptSafeCleanups, decayStaleClaims, hygieneReport, mergeDuplicate, purgeNonFactClaims, runScheduledHygiene, } from "./hygiene.js";
29
29
  import { hygieneSchedulePath, installHygieneSchedule, isHygieneScheduleInstalled, uninstallHygieneSchedule, writeHygieneHelperScript, } from "./hygiene-schedule.js";
30
30
  import { syncPinnedRules } from "./rules-sync.js";
31
+ import { listSkillDrafts } from "./db.js";
32
+ import { deleteSkill, findSkillOnDisk, importSkillFromPath, isValidSkillName, listIndexedSkills, readSkillBody, renderSkillMarkdown, skillsDir, slugifySkillName, syncSkillIndex, writeSkill, } from "./skills.js";
31
33
  import { buildSbom, writeItPack } from "./it-pack.js";
32
34
  function usage() {
33
35
  console.log(`amem — local personal agent memory
34
36
 
35
37
  Usage:
36
38
  amem setup [--personal] [--platform <host>]
37
- amem init --platform cursor|claude|windsurf|continue|aider|zed
39
+ amem init --platform cursor|claude|claude-desktop|windsurf|continue|aider|zed
38
40
  amem init --workspace <name> [--path <dir>] [--platform …]
39
41
  amem init --personal
40
42
  amem rename "<display name>" --workspace <slug>
@@ -43,6 +45,11 @@ Usage:
43
45
  amem context "<query>" [--workspace <name>] [--platform cursor|claude|luna]
44
46
  amem remember "<text>" [--workspace <name>] [--kind session] [--anchor <path>]
45
47
  amem recipe [--json]
48
+ amem task [list] [--status <backlog|next|doing|blocked|done>] [--include-done] [--all] [--json]
49
+ amem task add <title> [--body <notes>] [--status <status>] [--anchor <file>]
50
+ amem task update <id> [--title <title>] [--body <notes>] [--status <status>]
51
+ amem task complete <id>
52
+ amem task delete <id>
46
53
  amem propose validate <file.json>
47
54
  amem propose diff <file.json>
48
55
  amem propose apply <file.json>
@@ -56,10 +63,13 @@ Usage:
56
63
  amem backup unschedule
57
64
  amem restore --file <backup.db|backup.db.enc> [--passphrase <secret>]
58
65
  amem hygiene [--days 90] [--decay] [--accept-safe] [--merge <keepId> <dropId>]
66
+ amem hygiene --purge-nonfacts [--all] [--apply] # default is a dry run
59
67
  amem hygiene --scheduled
60
68
  amem hygiene schedule [--hour <0-23>]
61
69
  amem hygiene unschedule
62
70
  amem rules sync
71
+ amem skills list|show <name>|new <name> [--desc <text>]|rm <name>|sync|import <path>
72
+ amem skills drafts|approve <id>|dismiss <id>
63
73
  amem it-pack [--out <dir>]
64
74
  amem doctor [--attest] [--sbom] [--json]
65
75
  amem session touch --platform cursor|claude [--session-id <id>]
@@ -221,11 +231,29 @@ async function main() {
221
231
  }
222
232
  const platform = flagString(flags, "platform");
223
233
  if (!platform) {
224
- throw new Error("amem init requires --platform cursor|claude|windsurf|continue|aider|zed, --workspace <name>, or --personal");
234
+ throw new Error("amem init requires --platform cursor|claude|claude-desktop|windsurf|continue|aider|zed, --workspace <name>, or --personal");
225
235
  }
226
236
  const policy = loadPolicy().policy;
227
237
  assertPlatformAllowed(platform, policy);
238
+ // Claude Desktop, Windsurf, Continue and Zed read a single machine-wide
239
+ // MCP config. There is nothing repo-specific to bind, and binding the
240
+ // cwd here is exactly how running this from a home directory
241
+ // registered the whole home as a repo.
242
+ if (HOST_INSTALL_IDS.has(platform) && platform !== "aider") {
243
+ const hostInfo = installHost(platform, { workspace: PERSONAL_SLUG });
244
+ console.log(`Configured ${hostInfo.host} (machine-wide, no repo binding)`);
245
+ for (const cfgPath of hostInfo.paths)
246
+ console.log(`Config: ${cfgPath}`);
247
+ for (const note of hostInfo.notes)
248
+ console.log(`Note: ${note}`);
249
+ break;
250
+ }
228
251
  const identity = detectRepoIdentity();
252
+ const unbindable = unbindableRootReason(identity.rootPath);
253
+ if (unbindable) {
254
+ throw new Error(`Refusing to bind ${unbindable} (${identity.rootPath}) as a repo. ` +
255
+ "cd into a project first, or use: amem init --workspace <name>");
256
+ }
229
257
  assertRemoteAllowed(identity.remoteUrl, policy);
230
258
  const repo = upsertRepo(identity, platform);
231
259
  let installInfo = { skills: [] };
@@ -646,6 +674,33 @@ async function main() {
646
674
  }
647
675
  break;
648
676
  }
677
+ if (flags.get("purge-nonfacts")) {
678
+ const all = Boolean(flags.get("all"));
679
+ const scope = all ? {} : { repoId: resolveBinding(flags).id };
680
+ // Dry run unless --apply is passed: deletion is not reversible.
681
+ const preview = purgeNonFactClaims({ ...scope, dryRun: true });
682
+ const byReason = new Map();
683
+ for (const row of preview.matched) {
684
+ byReason.set(row.reason, (byReason.get(row.reason) ?? 0) + 1);
685
+ }
686
+ console.log(`Scanned ${preview.scanned} claim(s)${all ? " across every repo" : ""}; ${preview.matched.length} look like clear junk.`);
687
+ for (const [reason, n] of [...byReason].sort((a, b) => b[1] - a[1])) {
688
+ console.log(` ${reason}: ${n}`);
689
+ }
690
+ for (const row of preview.matched.slice(0, 8)) {
691
+ console.log(` - [${row.repoName}] ${row.reason}: ${row.preview}`);
692
+ }
693
+ if (preview.matched.length > 8) {
694
+ console.log(` … and ${preview.matched.length - 8} more`);
695
+ }
696
+ if (!flags.get("apply")) {
697
+ console.log("Dry run. Re-run with --apply to delete. Back up first: amem backup");
698
+ break;
699
+ }
700
+ const result = purgeNonFactClaims({ ...scope, dryRun: false });
701
+ console.log(`Deleted ${result.deleted} claim(s).`);
702
+ break;
703
+ }
649
704
  const repo = resolveBinding(flags);
650
705
  const days = Number(flagString(flags, "days") ?? "90");
651
706
  const keep = flagString(flags, "merge");
@@ -691,6 +746,235 @@ async function main() {
691
746
  console.log("Keep this file out of shared git if it contains personal notes.");
692
747
  break;
693
748
  }
749
+ case "task":
750
+ case "tasks": {
751
+ const sub = positional[1] || "list";
752
+ if (sub === "list") {
753
+ const isAll = Boolean(flags.get("all"));
754
+ const statusRaw = flagString(flags, "status");
755
+ const status = statusRaw ? (normalizeTaskStatus(statusRaw) ?? undefined) : undefined;
756
+ const includeDone = Boolean(flags.get("include-done") || flags.get("all") || status === "done");
757
+ let tasks;
758
+ if (isAll) {
759
+ tasks = listTasksAll({ status, includeDone });
760
+ }
761
+ else {
762
+ let repo;
763
+ try {
764
+ repo = resolveBinding(flags);
765
+ }
766
+ catch {
767
+ repo = getRepoByCwd() || ensurePersonalWorkspace();
768
+ }
769
+ tasks = listTasks(repo.id, { status, includeDone });
770
+ }
771
+ if (flags.get("json")) {
772
+ console.log(JSON.stringify({ tasks }, null, 2));
773
+ break;
774
+ }
775
+ if (tasks.length === 0) {
776
+ console.log("No tasks found.");
777
+ break;
778
+ }
779
+ const repoMap = new Map(listRepos().map((r) => [r.id, r.repo_name]));
780
+ for (const t of tasks) {
781
+ const repoLabel = isAll ? ` [${repoMap.get(t.repo_id) || "repo"}]` : "";
782
+ const body = t.body ? ` — ${t.body}` : "";
783
+ console.log(`[${t.status}] ${t.title}${body}${repoLabel} (${t.id})`);
784
+ }
785
+ break;
786
+ }
787
+ if (sub === "add") {
788
+ const title = positional.slice(2).join(" ") || flagString(flags, "title");
789
+ if (!title || !title.trim()) {
790
+ throw new Error("Usage: amem task add <title> [--body <notes>] [--status <status>] [--anchor <file>]");
791
+ }
792
+ let repo;
793
+ try {
794
+ repo = resolveBinding(flags);
795
+ }
796
+ catch {
797
+ repo = getRepoByCwd() || ensurePersonalWorkspace();
798
+ }
799
+ const body = flagString(flags, "body") || "";
800
+ const status = flagString(flags, "status") || "backlog";
801
+ const anchor = flagString(flags, "anchor");
802
+ const anchors = anchor ? [anchor] : undefined;
803
+ const task = insertTask({
804
+ repoId: repo.id,
805
+ title: title.trim(),
806
+ body,
807
+ status,
808
+ anchors,
809
+ source: "cli",
810
+ });
811
+ console.log(`Created task ${task.id} [${task.status}]: ${task.title}`);
812
+ break;
813
+ }
814
+ if (sub === "update") {
815
+ const id = positional[2];
816
+ if (!id)
817
+ throw new Error("Usage: amem task update <id> [--title <title>] [--body <notes>] [--status <status>]");
818
+ const found = findTaskAnyRepo(id);
819
+ if (!found)
820
+ throw new Error(`Task not found: ${id}`);
821
+ const title = flagString(flags, "title");
822
+ const body = flagString(flags, "body");
823
+ const status = flagString(flags, "status");
824
+ const anchor = flagString(flags, "anchor");
825
+ const anchors = anchor ? [anchor] : undefined;
826
+ const updated = updateTask(found.repo_id, id, {
827
+ title,
828
+ body,
829
+ status,
830
+ anchors,
831
+ });
832
+ if (!updated)
833
+ throw new Error(`Task not found: ${id}`);
834
+ console.log(`Updated task ${updated.id} [${updated.status}]: ${updated.title}`);
835
+ break;
836
+ }
837
+ if (sub === "complete" || sub === "done") {
838
+ const id = positional[2];
839
+ if (!id)
840
+ throw new Error("Usage: amem task complete <id>");
841
+ const found = findTaskAnyRepo(id);
842
+ if (!found)
843
+ throw new Error(`Task not found: ${id}`);
844
+ const completed = completeTask(found.repo_id, id);
845
+ if (!completed)
846
+ throw new Error(`Task not found: ${id}`);
847
+ console.log(`Completed task ${completed.id}: ${completed.title}`);
848
+ break;
849
+ }
850
+ if (sub === "rm" || sub === "delete") {
851
+ const id = positional[2];
852
+ if (!id)
853
+ throw new Error("Usage: amem task delete <id>");
854
+ const found = findTaskAnyRepo(id);
855
+ if (!found)
856
+ throw new Error(`Task not found: ${id}`);
857
+ const ok = deleteTask(found.repo_id, id);
858
+ if (!ok)
859
+ throw new Error(`Task not found: ${id}`);
860
+ console.log(`Deleted task ${id}`);
861
+ break;
862
+ }
863
+ throw new Error("Usage: amem task list|add|update|complete|delete");
864
+ }
865
+ case "skills": {
866
+ const sub = positional[1] || "list";
867
+ if (sub === "list") {
868
+ const skills = listIndexedSkills();
869
+ if (skills.length === 0) {
870
+ console.log(`No skills yet. Create one with: amem skills new <name>`);
871
+ console.log(`Skills live in ${skillsDir()}`);
872
+ break;
873
+ }
874
+ for (const s of skills) {
875
+ const flag = s.modified ? " (edited)" : "";
876
+ const used = s.uses > 0 ? ` · used ${s.uses}×` : "";
877
+ console.log(`${s.name}${flag}${used}`);
878
+ if (s.description)
879
+ console.log(` ${s.description}`);
880
+ }
881
+ console.log(`\n${skills.length} skill(s) in ${skillsDir()}`);
882
+ break;
883
+ }
884
+ if (sub === "show") {
885
+ const name = positional[2];
886
+ if (!name)
887
+ throw new Error("Usage: amem skills show <name>");
888
+ const body = readSkillBody(name);
889
+ if (body === null)
890
+ throw new Error(`Skill not found: ${name}`);
891
+ console.log(body);
892
+ break;
893
+ }
894
+ if (sub === "new") {
895
+ const name = positional[2];
896
+ if (!name)
897
+ throw new Error("Usage: amem skills new <name> [--desc <text>]");
898
+ const slug = slugifySkillName(name);
899
+ if (!isValidSkillName(slug))
900
+ throw new Error(`Invalid skill name: ${name}`);
901
+ if (findSkillOnDisk(slug))
902
+ throw new Error(`Skill already exists: ${slug}`);
903
+ const markdown = renderSkillMarkdown({
904
+ name: slug,
905
+ description: flagString(flags, "desc") || `Procedure: ${slug.replace(/[-_]+/g, " ")}`,
906
+ });
907
+ const written = writeSkill(slug, markdown);
908
+ syncSkillIndex();
909
+ console.log(`Created ${written.path}`);
910
+ console.log("Edit it, then agents will see it in their skill index.");
911
+ break;
912
+ }
913
+ if (sub === "rm") {
914
+ const name = positional[2];
915
+ if (!name)
916
+ throw new Error("Usage: amem skills rm <name>");
917
+ if (!deleteSkill(name))
918
+ throw new Error(`Skill not found: ${name}`);
919
+ syncSkillIndex();
920
+ console.log(`Deleted skill ${slugifySkillName(name)}`);
921
+ break;
922
+ }
923
+ if (sub === "sync") {
924
+ const skills = syncSkillIndex();
925
+ console.log(`Indexed ${skills.length} skill(s) from ${skillsDir()}`);
926
+ const edited = skills.filter((s) => s.modified);
927
+ if (edited.length > 0) {
928
+ console.log(`${edited.length} locally edited: ${edited.map((s) => s.name).join(", ")}`);
929
+ }
930
+ break;
931
+ }
932
+ if (sub === "drafts") {
933
+ const drafts = listSkillDrafts({ status: "pending", limit: 50 });
934
+ if (drafts.length === 0) {
935
+ console.log("No pending skill drafts.");
936
+ break;
937
+ }
938
+ for (const d of drafts) {
939
+ const label = d.kind === "revision" ? `revise ${d.target_skill}` : d.kind === "create" ? "staged" : "suggestion";
940
+ console.log(`${d.id} [${label}] ${d.title}`);
941
+ const why = JSON.parse(d.reasons || "[]");
942
+ if (Array.isArray(why) && why.length)
943
+ console.log(` why: ${why.join(", ")}`);
944
+ }
945
+ console.log(`\n${drafts.length} pending · approve with: amem skills approve <id>`);
946
+ break;
947
+ }
948
+ if (sub === "approve" || sub === "dismiss") {
949
+ const id = positional[2];
950
+ if (!id)
951
+ throw new Error(`Usage: amem skills ${sub} <draft-id>`);
952
+ const path = sub === "approve" ? "/api/skills/drafts/apply" : "/api/skills/drafts/dismiss";
953
+ const result = handleApi({
954
+ method: "POST",
955
+ pathname: path,
956
+ searchParams: new URLSearchParams(),
957
+ body: { id },
958
+ cwd: process.cwd(),
959
+ });
960
+ if (result.status >= 400) {
961
+ throw new Error(String(result.body?.error || "Failed"));
962
+ }
963
+ console.log(sub === "approve" ? `Applied ${id}` : `Dismissed ${id}`);
964
+ break;
965
+ }
966
+ if (sub === "import") {
967
+ const src = positional[2];
968
+ if (!src)
969
+ throw new Error("Usage: amem skills import <path-to-skill-dir>");
970
+ const result = importSkillFromPath(src, flagString(flags, "name"));
971
+ syncSkillIndex();
972
+ console.log(`Imported ${result.name} → ${result.path}`);
973
+ console.log("Review it before trusting: skills are instructions agents follow.");
974
+ break;
975
+ }
976
+ throw new Error("Usage: amem skills list|show|new|rm|sync|import");
977
+ }
694
978
  case "it-pack": {
695
979
  const out = resolve(flagString(flags, "out") || join(amemHome(), "it-pack"));
696
980
  const result = writeItPack(out);
@@ -919,7 +1203,8 @@ async function main() {
919
1203
  break;
920
1204
  }
921
1205
  if (sub === "activate") {
922
- throw new Error("Self-activate is disabled. Buy Pro/IT at https://getamem.com then: amem license apply --file <amem-license.json>");
1206
+ console.log("amem is 100% free and open all features are already unlocked.");
1207
+ break;
923
1208
  }
924
1209
  if (sub === "clear") {
925
1210
  clearLicense();
package/dist/context.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { type ClaimRow, type ComponentRow, type ConversationNoteRow, type FlowRow, type UsageEventRow } from "./db.js";
1
+ import { type AgentTaskRow, type ClaimRow, type ComponentRow, type ConversationNoteRow, type FlowRow, type UsageEventRow, type SkillDraftRow } from "./db.js";
2
2
  import { type ClaimFreshness } from "./freshness.js";
3
+ import { type RankedSkill } from "./skills.js";
3
4
  export type RankedClaim = ClaimRow & {
4
5
  score: number;
5
6
  freshness: ClaimFreshness;
@@ -14,12 +15,20 @@ export type ContextPacket = {
14
15
  notes: Array<ConversationNoteRow & {
15
16
  score: number;
16
17
  }>;
18
+ /** Deferred Kanban tasks (non-done), compact for agents. */
19
+ tasks: AgentTaskRow[];
20
+ /** Index-only skill matches; bodies load on demand via `amem_skill_view`. */
21
+ skills: RankedSkill[];
22
+ /** At most one pending "worth writing up" nudge from a previous session. */
23
+ skillDrafts: SkillDraftRow[];
17
24
  };
18
25
  export type BuildContextOptions = {
19
26
  limit?: number;
20
27
  rootPath?: string;
21
28
  /** Include cross-repo personal prefs (default true). */
22
29
  includePersonal?: boolean;
30
+ /** Include the ranked skill index (default true). */
31
+ includeSkills?: boolean;
23
32
  };
24
33
  export declare function buildContext(repoId: string, query: string, limitOrOpts?: number | BuildContextOptions): ContextPacket;
25
34
  export type ShowdownClaim = {
package/dist/context.js CHANGED
@@ -1,10 +1,25 @@
1
- import { getRepoByName, listClaims, listComponents, listConversationNotes, listEdges, listFlows, openDb, } from "./db.js";
1
+ import { getRepoByName, listClaims, listComponents, listConversationNotes, listEdges, listFlows, listOpenTasksForContext, listTasks, openDb, listSkillDrafts, } from "./db.js";
2
2
  import { assessClaimFreshness, freshnessScoreMultiplier, } from "./freshness.js";
3
3
  import { kindRankBoost } from "./kinds.js";
4
4
  import { ftsBoostFromBm25, keywordScoreClaim, searchClaimsFts, tokenize, } from "./search.js";
5
5
  import { embedBoostFromScore, searchClaimsEmbed, searchClaimsEmbedLive } from "./embed.js";
6
6
  import { FEATURE_LOCAL_EMBED, hasFeature } from "./license.js";
7
7
  import { PERSONAL_SLUG } from "./personal.js";
8
+ import { listIndexedSkills, rankSkills } from "./skills.js";
9
+ import { loadPolicy } from "./policy.js";
10
+ /** Reasons are stored as JSON; a malformed row must not break the whole packet. */
11
+ function safeReasons(raw) {
12
+ try {
13
+ const list = JSON.parse(raw);
14
+ if (Array.isArray(list) && list.length > 0) {
15
+ return ` (${list.filter((r) => typeof r === "string").join(", ")})`;
16
+ }
17
+ }
18
+ catch {
19
+ /* fall through */
20
+ }
21
+ return "";
22
+ }
8
23
  function scoreNote(note, queryTokens) {
9
24
  if (queryTokens.length === 0)
10
25
  return 0;
@@ -16,6 +31,17 @@ function scoreNote(note, queryTokens) {
16
31
  }
17
32
  return score;
18
33
  }
34
+ function scoreTask(task, queryTokens) {
35
+ if (queryTokens.length === 0)
36
+ return 0;
37
+ const hay = `${task.title} ${task.body} ${task.anchors || ""}`.toLowerCase();
38
+ let score = 0;
39
+ for (const token of queryTokens) {
40
+ if (hay.includes(token))
41
+ score += token.length > 4 ? 3 : 2;
42
+ }
43
+ return score;
44
+ }
19
45
  function rankClaims(claims, query, queryTokens, opts) {
20
46
  return claims.map((c) => {
21
47
  const freshness = assessClaimFreshness(opts.rootPath, c);
@@ -152,7 +178,44 @@ export function buildContext(repoId, query, limitOrOpts = 12) {
152
178
  const notes = notesRaw.length > 0
153
179
  ? notesRaw
154
180
  : listConversationNotes(repoId, 5).map((n) => ({ ...n, score: 0 }));
155
- return { query, claims: selected, flows, components, notes };
181
+ const openTasks = listOpenTasksForContext(repoId, 8);
182
+ const doneTasks = queryTokens.length > 0
183
+ ? listTasks(repoId, { status: "done", includeDone: true, limit: 20 })
184
+ .map((t) => ({ ...t, score: scoreTask(t, queryTokens) }))
185
+ .filter((t) => t.score > 0)
186
+ .sort((a, b) => b.score - a.score || b.updated_at.localeCompare(a.updated_at))
187
+ .slice(0, 4)
188
+ : [];
189
+ const tasks = [...openTasks, ...doneTasks];
190
+ const skillsOn = opts.includeSkills !== false && loadPolicy().policy.skills_enabled;
191
+ const skills = skillsOn ? rankSkillsForQuery(query) : [];
192
+ const skillDrafts = skillsOn ? pendingSkillNudges(repoId) : [];
193
+ return { query, claims: selected, flows, components, notes, tasks, skills, skillDrafts };
194
+ }
195
+ /**
196
+ * amem has no model, so it cannot write a skill itself. The nudge is how a session-end
197
+ * detection reaches an agent that can. One at a time — this rides in every packet.
198
+ */
199
+ function pendingSkillNudges(repoId) {
200
+ try {
201
+ return listSkillDrafts({ status: "pending", repoId, limit: 1 });
202
+ }
203
+ catch {
204
+ return [];
205
+ }
206
+ }
207
+ /**
208
+ * Progressive disclosure, level 0. Only names and descriptions reach the packet; the
209
+ * agent pulls a body with `amem_skill_view` if it decides the procedure applies. A skills
210
+ * directory that cannot be read must never break context, so this stays best-effort.
211
+ */
212
+ function rankSkillsForQuery(query) {
213
+ try {
214
+ return rankSkills(listIndexedSkills(), query, 3);
215
+ }
216
+ catch {
217
+ return [];
218
+ }
156
219
  }
157
220
  function toShowdownClaims(ranked) {
158
221
  return ranked.map((c) => ({
@@ -258,7 +321,11 @@ export function decorateUsageEvents(events) {
258
321
  }
259
322
  export function renderContextMarkdown(packet) {
260
323
  const lines = ["# Agent Memory Context", "", `Query: ${packet.query}`, ""];
261
- if (packet.claims.length === 0 && packet.notes.length === 0) {
324
+ if (packet.claims.length === 0 &&
325
+ packet.notes.length === 0 &&
326
+ (packet.tasks?.length ?? 0) === 0 &&
327
+ (packet.skills?.length ?? 0) === 0 &&
328
+ (packet.skillDrafts?.length ?? 0) === 0) {
262
329
  lines.push("No claims stored for this repository yet.", "");
263
330
  lines.push("Seed memory with the `amem-bootstrap` skill or `amem propose apply <file>`.");
264
331
  return lines.join("\n");
@@ -266,6 +333,41 @@ export function renderContextMarkdown(packet) {
266
333
  if (packet.claims.length === 0) {
267
334
  lines.push("No durable claims yet — using recent conversation memory.", "");
268
335
  }
336
+ if (packet.tasks && packet.tasks.length > 0) {
337
+ const hasDone = packet.tasks.some((t) => t.status === "done");
338
+ const hasOpen = packet.tasks.some((t) => t.status !== "done");
339
+ const header = hasDone && !hasOpen ? "## Completed tasks" : (hasDone ? "## Tasks (open & completed)" : "## Open tasks");
340
+ lines.push(header, "");
341
+ lines.push("_Project Kanban tasks. Use `amem_task_*` to add/update/complete._", "");
342
+ for (const task of packet.tasks) {
343
+ const body = task.body ? ` — ${task.body.replace(/\s+/g, " ").slice(0, 120)}` : "";
344
+ lines.push(`- **[${task.status}]** ${task.title}${body} (\`${task.id}\`)`);
345
+ }
346
+ lines.push("");
347
+ }
348
+ if (packet.skills && packet.skills.length > 0) {
349
+ lines.push("## Relevant skills", "");
350
+ lines.push("_Procedures that may apply. Only the index is shown — call `amem_skill_view` with the name to load one before following it._", "");
351
+ for (const skill of packet.skills) {
352
+ const desc = skill.description
353
+ ? ` — ${skill.description.replace(/\s+/g, " ").slice(0, 140)}`
354
+ : "";
355
+ lines.push(`- **${skill.name}**${desc}`);
356
+ }
357
+ lines.push("");
358
+ }
359
+ const nudge = packet.skillDrafts?.[0];
360
+ if (nudge) {
361
+ const why = safeReasons(nudge.reasons);
362
+ if (nudge.kind === "revision" && nudge.target_skill) {
363
+ lines.push("## Skill worth revising", "");
364
+ lines.push(`_You followed **${nudge.target_skill}** in a recent session and the work still went sideways${why}. If you learned what it was missing, update it with \`amem_skill_save\`._`, "");
365
+ }
366
+ else {
367
+ lines.push("## Worth saving as a skill", "");
368
+ lines.push(`_A recent session looked like a repeatable procedure: "${nudge.title}"${why}. If you can write it up, save it with \`amem_skill_save\` so the next agent does not rediscover it._`, "");
369
+ }
370
+ }
269
371
  const staleCount = packet.claims.filter((c) => c.freshness.status === "stale").length;
270
372
  if (staleCount > 0) {
271
373
  lines.push(`_${staleCount} claim(s) marked stale — anchored files changed after the claim was written. Verify before trusting._`, "");