chapterhouse 0.3.26 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/api/server.js +12 -0
  2. package/dist/api/server.test.js +39 -0
  3. package/dist/config.js +70 -0
  4. package/dist/config.test.js +109 -0
  5. package/dist/copilot/agents.js +32 -6
  6. package/dist/copilot/agents.test.js +41 -0
  7. package/dist/copilot/oneshot.js +54 -0
  8. package/dist/copilot/orchestrator.js +224 -3
  9. package/dist/copilot/orchestrator.test.js +380 -0
  10. package/dist/copilot/prompt-date.js +8 -0
  11. package/dist/copilot/system-message.js +8 -0
  12. package/dist/copilot/system-message.test.js +58 -0
  13. package/dist/copilot/tools.agent.test.js +24 -0
  14. package/dist/copilot/tools.js +351 -4
  15. package/dist/copilot/tools.memory.test.js +297 -0
  16. package/dist/copilot/turn-event-log-env.test.js +19 -0
  17. package/dist/copilot/turn-event-log.js +22 -23
  18. package/dist/copilot/turn-event-log.test.js +61 -2
  19. package/dist/memory/active-scope.js +69 -0
  20. package/dist/memory/active-scope.test.js +76 -0
  21. package/dist/memory/checkpoint-prompt.js +71 -0
  22. package/dist/memory/checkpoint.js +257 -0
  23. package/dist/memory/checkpoint.test.js +255 -0
  24. package/dist/memory/decisions.js +53 -0
  25. package/dist/memory/decisions.test.js +92 -0
  26. package/dist/memory/entities.js +59 -0
  27. package/dist/memory/entities.test.js +65 -0
  28. package/dist/memory/eot.js +219 -0
  29. package/dist/memory/eot.test.js +263 -0
  30. package/dist/memory/hot-tier.js +187 -0
  31. package/dist/memory/hot-tier.test.js +197 -0
  32. package/dist/memory/housekeeping.js +352 -0
  33. package/dist/memory/housekeeping.test.js +280 -0
  34. package/dist/memory/inbox.js +73 -0
  35. package/dist/memory/index.js +11 -0
  36. package/dist/memory/observations.js +46 -0
  37. package/dist/memory/observations.test.js +86 -0
  38. package/dist/memory/recall.js +210 -0
  39. package/dist/memory/recall.test.js +238 -0
  40. package/dist/memory/scopes.js +89 -0
  41. package/dist/memory/scopes.test.js +201 -0
  42. package/dist/memory/tiering.js +193 -0
  43. package/dist/memory/types.js +2 -0
  44. package/dist/paths.js +7 -1
  45. package/dist/store/db.js +412 -8
  46. package/dist/store/db.test.js +83 -0
  47. package/dist/test/setup-env.js +16 -0
  48. package/dist/test/setup-env.test.js +4 -0
  49. package/package.json +1 -1
  50. package/web/dist/assets/{index-BRPJa1DK.js → index-DmYLALt0.js} +70 -70
  51. package/web/dist/assets/index-DmYLALt0.js.map +1 -0
  52. package/web/dist/index.html +1 -1
  53. package/web/dist/assets/index-BRPJa1DK.js.map +0 -1
@@ -7,7 +7,7 @@ import { homedir } from "os";
7
7
  import { listSkills, createSkill, removeSkill } from "./skills.js";
8
8
  import { config, persistModel } from "../config.js";
9
9
  import { agentEventBus } from "./agent-event-bus.js";
10
- import { getCurrentSourceChannel, getCurrentActivityCallback, getCurrentActiveProjectRules, getCurrentAuthenticatedUser, getLastAuthenticatedUser, getCurrentAuthorizationHeader, getCurrentSessionKey, switchSessionModel, } from "./orchestrator.js";
10
+ import { getCurrentSourceChannel, getCurrentActivityCallback, getCurrentActiveProjectRules, getCurrentAuthenticatedUser, getLastAuthenticatedUser, getCurrentAuthorizationHeader, getCurrentSessionKey, invalidateOrchestratorSession, maybeScheduleScopeChangeCheckpoint, resetCheckpointSessionState, switchSessionModel, } from "./orchestrator.js";
11
11
  import { getRouterConfig, updateRouterConfig } from "./router.js";
12
12
  import { ensureWikiStructure, readPage, writePage, deletePage, writeRawSource, assertPagePath } from "../wiki/fs.js";
13
13
  import { searchIndex, addToIndex, removeFromIndex, buildIndexEntryForPage } from "../wiki/index-manager.js";
@@ -20,7 +20,8 @@ import { loadTaxonomy } from "../wiki/taxonomy.js";
20
20
  import { getCategoryDir, topicPagePath, slugify, entityCategories, FLAT_CATEGORIES } from "../wiki/topic-structure.js";
21
21
  import { withWikiWrite } from "../wiki/lock.js";
22
22
  import { readWikiPage, teamWikiSync } from "../wiki/team-sync.js";
23
- import { getAgentRegistry, getAgent, createEphemeralAgentSession, getAgentSessionStatus, getTask, registerTask, completeTask, failTask, createAgentFile, removeAgentFile, loadAgents, } from "./agents.js";
23
+ import { getAgentRegistry, getAgent, createEphemeralAgentSession, getAgentSessionStatus, getTask, registerTask, completeTask, failTask, createTaskId, createAgentFile, removeAgentFile, loadAgents, } from "./agents.js";
24
+ import * as agentsModule from "./agents.js";
24
25
  import { detectProjectRuleWarnings } from "./project-rule-warnings.js";
25
26
  import { renderDelegatedProjectRulesPreamble } from "./project-rules-injection.js";
26
27
  import { adoGetOkrs, adoOkrSummary, adoUpdateKr } from "../integrations/ado-skill.js";
@@ -28,6 +29,7 @@ import { TeamsNotifier } from "../integrations/teams-notify.js";
28
29
  import { TeamPushClient } from "../integrations/team-push.js";
29
30
  import { OKRMapper, parseOKRPageContent } from "./okr-mapper.js";
30
31
  import { childLogger } from "../util/logger.js";
32
+ import { getActiveScope as getMemoryActiveScope, getScope as getMemoryScope, inferScopeFromText, demoteToCold, demoteToWarm, queueMemoryProposal, recall as recallMemory, recordDecision, recordObservation, runHousekeeping, setActiveScope as setMemoryActiveScope, promoteToHot, upsertEntity, } from "../memory/index.js";
31
33
  const log = childLogger("tools");
32
34
  /** Escape a string for safe inclusion as a single-line YAML scalar value. */
33
35
  function yamlEscape(value) {
@@ -56,6 +58,81 @@ function isTimeoutError(err) {
56
58
  function hasAdoPat() {
57
59
  return (process.env.ADO_PAT?.trim() || config.adoPat).length > 0;
58
60
  }
61
+ function requireOrchestratorMemoryWrite() {
62
+ const agentSlug = agentsModule.getCurrentToolAgentSlug?.();
63
+ if (agentSlug && agentSlug !== "chapterhouse") {
64
+ return "Memory writes are orchestrator-only. Use memory_propose instead.";
65
+ }
66
+ return null;
67
+ }
68
+ function resolveMemoryScopeForWrite(explicitScope, content) {
69
+ const explicit = explicitScope ? getMemoryScope(explicitScope) : undefined;
70
+ if (explicitScope && !explicit) {
71
+ throw new Error(`Unknown memory scope '${explicitScope}'.`);
72
+ }
73
+ if (explicit) {
74
+ return { scopeId: explicit.id, scopeSlug: explicit.slug };
75
+ }
76
+ const active = getMemoryActiveScope();
77
+ if (active) {
78
+ return { scopeId: active.id, scopeSlug: active.slug };
79
+ }
80
+ const inferred = inferScopeFromText(content);
81
+ if (inferred) {
82
+ const inferredScope = getMemoryScope(inferred.scope_id);
83
+ if (inferredScope) {
84
+ return { scopeId: inferredScope.id, scopeSlug: inferredScope.slug };
85
+ }
86
+ }
87
+ const validScopes = getDb().prepare(`
88
+ SELECT slug
89
+ FROM mem_scopes
90
+ WHERE active = 1
91
+ ORDER BY slug
92
+ `).all();
93
+ const activeScope = getMemoryActiveScope()?.slug ?? "none";
94
+ throw new Error(`No scope inferred. Active scope: ${activeScope}. Valid scopes: [${validScopes.map((row) => row.slug).join(", ")}]. `
95
+ + "Set active scope with memory_set_scope or pass scope explicitly.");
96
+ }
97
+ const observationProposalPayloadSchema = z.object({
98
+ content: z.string(),
99
+ entity_id: z.number().int().positive().optional(),
100
+ source: z.string().optional(),
101
+ });
102
+ const decisionProposalPayloadSchema = z.object({
103
+ title: z.string(),
104
+ rationale: z.string().optional(),
105
+ decided_at: z.string().optional(),
106
+ });
107
+ const entityProposalPayloadSchema = z.object({
108
+ name: z.string(),
109
+ kind: z.string(),
110
+ summary: z.string().optional(),
111
+ });
112
+ const memoryProposeArgsSchema = z.object({
113
+ kind: z.enum(["observation", "decision", "entity"]),
114
+ scope_slug: z.string().optional(),
115
+ payload: z.record(z.string(), z.unknown()),
116
+ confidence: z.number().min(0).max(1).optional(),
117
+ reason: z.string().optional(),
118
+ }).superRefine((value, context) => {
119
+ const schema = value.kind === "observation"
120
+ ? observationProposalPayloadSchema
121
+ : value.kind === "decision"
122
+ ? decisionProposalPayloadSchema
123
+ : entityProposalPayloadSchema;
124
+ const parsed = schema.safeParse(value.payload);
125
+ if (!parsed.success) {
126
+ for (const issue of parsed.error.issues) {
127
+ context.addIssue({
128
+ code: z.ZodIssueCode.custom,
129
+ message: issue.message,
130
+ path: ["payload", ...issue.path],
131
+ });
132
+ }
133
+ }
134
+ });
135
+ const memoryTierTableSchema = z.enum(["observation", "decision", "entity"]);
59
136
  function getCurrentQuarter(now = new Date()) {
60
137
  return `${now.getUTCFullYear()}-Q${Math.floor(now.getUTCMonth() / 3) + 1}`;
61
138
  }
@@ -110,16 +187,17 @@ export function createTools(deps) {
110
187
  return `Agent '${args.agent_name}' not found. Available agents: ${available}`;
111
188
  }
112
189
  const delegatedSlug = agent.slug;
190
+ const taskId = createTaskId();
113
191
  let session;
114
192
  try {
115
193
  const allTools = createTools(deps);
116
- session = await createEphemeralAgentSession(agent.slug, deps.client, allTools, args.model_override);
194
+ session = await createEphemeralAgentSession(agent.slug, deps.client, allTools, args.model_override, undefined, taskId);
117
195
  }
118
196
  catch (err) {
119
197
  const msg = err instanceof Error ? err.message : String(err);
120
198
  return `Failed to create session for @${delegatedSlug}: ${msg}`;
121
199
  }
122
- const task = registerTask(delegatedSlug, args.summary, getCurrentSourceChannel());
200
+ const task = registerTask(delegatedSlug, args.summary, getCurrentSourceChannel(), taskId);
123
201
  const activeProjectRules = getCurrentActiveProjectRules();
124
202
  const warningLines = activeProjectRules
125
203
  ? detectProjectRuleWarnings(args.task, activeProjectRules.rules.hard)
@@ -689,6 +767,275 @@ export function createTools(deps) {
689
767
  return `Auto-routing disabled. Using fixed model: ${config.copilotModel}`;
690
768
  },
691
769
  }),
770
+ defineTool("memory_remember", {
771
+ description: "Write scoped agent memory (observation or decision) into the SQLite memory store. " +
772
+ "Use this for the new agent-memory system, not the legacy wiki-backed remember tool.",
773
+ parameters: z.object({
774
+ content: z.string().describe("Observation content or decision rationale."),
775
+ scope: z.string().optional().describe("Optional memory scope slug. Defaults to active scope, then keyword inference."),
776
+ kind: z.enum(["observation", "decision"]).optional().describe("Memory entry kind. Defaults to observation."),
777
+ entity_name: z.string().optional().describe("Optional entity name to attach. Auto-upserts the entity if provided."),
778
+ entity_kind: z.string().optional().describe("Required when entity_name is provided."),
779
+ title: z.string().optional().describe("Required for decision entries."),
780
+ decided_at: z.string().optional().describe("Decision date. Defaults to today."),
781
+ tier: z.enum(["hot", "warm", "cold"]).optional().describe("Storage tier. Defaults to warm."),
782
+ }),
783
+ handler: async (args) => {
784
+ const denied = requireOrchestratorMemoryWrite();
785
+ if (denied)
786
+ return denied;
787
+ if (args.entity_name && !args.entity_kind) {
788
+ return "entity_kind is required when entity_name is provided.";
789
+ }
790
+ try {
791
+ const { scopeId, scopeSlug } = resolveMemoryScopeForWrite(args.scope, args.content);
792
+ const kind = args.kind ?? "observation";
793
+ const entity = args.entity_name
794
+ ? upsertEntity({
795
+ scope_id: scopeId,
796
+ kind: args.entity_kind,
797
+ name: args.entity_name,
798
+ tier: args.tier ?? "warm",
799
+ })
800
+ : undefined;
801
+ if (kind === "decision") {
802
+ if (!args.title) {
803
+ return "title is required when kind='decision'.";
804
+ }
805
+ const decision = recordDecision({
806
+ scope_id: scopeId,
807
+ entity_id: entity?.id,
808
+ title: args.title,
809
+ rationale: args.content,
810
+ decided_at: args.decided_at ?? new Date().toISOString().slice(0, 10),
811
+ tier: args.tier ?? "warm",
812
+ });
813
+ return {
814
+ ok: true,
815
+ id: decision.id,
816
+ scope: scopeSlug,
817
+ kind,
818
+ entity_id: entity?.id,
819
+ };
820
+ }
821
+ const observation = recordObservation({
822
+ scope_id: scopeId,
823
+ entity_id: entity?.id,
824
+ content: args.content,
825
+ source: `agent:${agentsModule.getCurrentToolAgentSlug?.() ?? "chapterhouse"}`,
826
+ tier: args.tier ?? "warm",
827
+ });
828
+ return {
829
+ ok: true,
830
+ id: observation.id,
831
+ scope: scopeSlug,
832
+ kind,
833
+ entity_id: entity?.id,
834
+ };
835
+ }
836
+ catch (err) {
837
+ return err instanceof Error ? err.message : String(err);
838
+ }
839
+ },
840
+ }),
841
+ defineTool("memory_propose", {
842
+ description: "Queue a proposed scoped memory item for orchestrator review at end-of-task. " +
843
+ "Available to all agents; writes land in mem_inbox as pending proposals.",
844
+ parameters: memoryProposeArgsSchema,
845
+ handler: async (args) => {
846
+ try {
847
+ const parsedArgs = memoryProposeArgsSchema.parse(args);
848
+ const payload = parsedArgs.kind === "observation"
849
+ ? observationProposalPayloadSchema.parse(parsedArgs.payload)
850
+ : parsedArgs.kind === "decision"
851
+ ? decisionProposalPayloadSchema.parse(parsedArgs.payload)
852
+ : entityProposalPayloadSchema.parse(parsedArgs.payload);
853
+ const proposal = queueMemoryProposal({
854
+ kind: parsedArgs.kind,
855
+ scopeSlug: parsedArgs.scope_slug,
856
+ payload,
857
+ confidence: parsedArgs.confidence ?? 0.5,
858
+ reason: parsedArgs.reason,
859
+ sourceAgent: agentsModule.getCurrentToolAgentSlug?.() ?? "chapterhouse",
860
+ sourceTaskId: agentsModule.getCurrentToolTaskId?.(),
861
+ });
862
+ return {
863
+ proposal_id: proposal.id,
864
+ status: "queued",
865
+ };
866
+ }
867
+ catch (err) {
868
+ if (err instanceof z.ZodError) {
869
+ return err.issues.map((issue) => issue.message).join("; ");
870
+ }
871
+ return err instanceof Error ? err.message : String(err);
872
+ }
873
+ },
874
+ }),
875
+ defineTool("memory_recall", {
876
+ description: "Search scoped agent memory with FTS-backed recall. Use this for the new agent-memory store, not the wiki-backed recall tool.",
877
+ parameters: z.object({
878
+ query: z.string().describe("Query text to search for."),
879
+ scope: z.string().optional().describe("Optional scope slug. Defaults to the active scope when available."),
880
+ kinds: z.array(z.enum(["observation", "decision", "entity"])).optional()
881
+ .describe("Optional filter for memory entry kinds."),
882
+ limit: z.number().int().positive().optional().describe("Maximum number of ranked hits to return. Defaults to 10."),
883
+ includeSuperseded: z.boolean().optional().describe("Include superseded observations and decisions. Defaults to false."),
884
+ includeArchived: z.boolean().optional().describe("Include archived observations and decisions. Defaults to false."),
885
+ includeCold: z.boolean().optional().describe("Include cold-tier rows. Defaults to false."),
886
+ }),
887
+ handler: async (args) => {
888
+ const requestedScope = args.scope ? getMemoryScope(args.scope) : undefined;
889
+ if (args.scope && !requestedScope) {
890
+ return `Unknown memory scope '${args.scope}'.`;
891
+ }
892
+ const result = recallMemory({
893
+ query: args.query,
894
+ scope_id: requestedScope?.id,
895
+ kinds: args.kinds,
896
+ limit: args.limit ?? 10,
897
+ includeSuperseded: args.includeSuperseded,
898
+ includeArchived: args.includeArchived,
899
+ includeCold: args.includeCold,
900
+ });
901
+ return {
902
+ active_scope: result.activeScope
903
+ ? { slug: result.activeScope.slug, title: result.activeScope.title }
904
+ : null,
905
+ hot_tier: result.hotTier,
906
+ hits: result.hits.map((hit) => ({
907
+ kind: hit.kind,
908
+ id: hit.id,
909
+ scope: hit.scope,
910
+ content: hit.content,
911
+ decided_at: hit.decidedAt,
912
+ score: hit.score,
913
+ snippet: hit.snippet,
914
+ })),
915
+ };
916
+ },
917
+ }),
918
+ defineTool("memory_housekeep", {
919
+ description: "Run the scoped agent-memory housekeeping pipeline. Orchestrator-only write-tier maintenance tool.",
920
+ parameters: z.object({
921
+ scope_slug: z.string().optional().describe("Optional scope slug. Defaults to the active scope."),
922
+ all_scopes: z.boolean().optional().describe("Run scoped passes for all active scopes instead of one scope."),
923
+ passes: z.array(z.string()).optional().describe("Optional pass names: dedup_observations, dedup_decisions, orphan_cleanup, decay, compact_inbox."),
924
+ }),
925
+ handler: async (args) => {
926
+ const denied = requireOrchestratorMemoryWrite();
927
+ if (denied)
928
+ return denied;
929
+ if (args.scope_slug && args.all_scopes) {
930
+ return "Pass either scope_slug or all_scopes, not both.";
931
+ }
932
+ try {
933
+ const requestedScope = args.scope_slug ? getMemoryScope(args.scope_slug) : undefined;
934
+ if (args.scope_slug && !requestedScope) {
935
+ return `Unknown memory scope '${args.scope_slug}'.`;
936
+ }
937
+ const result = runHousekeeping({
938
+ scopeIds: requestedScope ? [requestedScope.id] : undefined,
939
+ allScopes: args.all_scopes,
940
+ passes: args.passes,
941
+ });
942
+ return {
943
+ ok: result.summaries.every((summary) => summary.errors.length === 0),
944
+ scope_ids: result.scopeIds,
945
+ total_examined: result.totalExamined,
946
+ total_modified: result.totalModified,
947
+ duration_ms: result.durationMs,
948
+ summaries: result.summaries,
949
+ };
950
+ }
951
+ catch (err) {
952
+ return err instanceof Error ? err.message : String(err);
953
+ }
954
+ },
955
+ }),
956
+ defineTool("memory_promote", {
957
+ description: "Promote a memory row to the hot tier. Orchestrator-only manual override.",
958
+ parameters: z.object({
959
+ table: memoryTierTableSchema,
960
+ id: z.number().int().positive(),
961
+ reason: z.string().min(1),
962
+ }),
963
+ handler: async (args) => {
964
+ const denied = requireOrchestratorMemoryWrite();
965
+ if (denied)
966
+ return denied;
967
+ try {
968
+ promoteToHot(args.table, args.id, args.reason);
969
+ return { ok: true, table: args.table, id: args.id, tier: "hot" };
970
+ }
971
+ catch (err) {
972
+ return err instanceof Error ? err.message : String(err);
973
+ }
974
+ },
975
+ }),
976
+ defineTool("memory_demote", {
977
+ description: "Demote a memory row to warm or cold tier. Orchestrator-only manual override.",
978
+ parameters: z.object({
979
+ table: memoryTierTableSchema,
980
+ id: z.number().int().positive(),
981
+ reason: z.string().min(1),
982
+ tier: z.enum(["warm", "cold"]).optional().describe("Target demotion tier. Defaults to warm."),
983
+ }),
984
+ handler: async (args) => {
985
+ const denied = requireOrchestratorMemoryWrite();
986
+ if (denied)
987
+ return denied;
988
+ try {
989
+ if (args.tier === "cold") {
990
+ demoteToCold(args.table, args.id, args.reason);
991
+ return { ok: true, table: args.table, id: args.id, tier: "cold" };
992
+ }
993
+ demoteToWarm(args.table, args.id, args.reason);
994
+ return { ok: true, table: args.table, id: args.id, tier: "warm" };
995
+ }
996
+ catch (err) {
997
+ return err instanceof Error ? err.message : String(err);
998
+ }
999
+ },
1000
+ }),
1001
+ defineTool("memory_set_scope", {
1002
+ description: "Set or clear the active scope for the agent-memory system. This affects default routing for memory_remember and memory_recall.",
1003
+ parameters: z.object({
1004
+ slug: z.string().nullable().describe("Scope slug to activate, or null to clear the active scope."),
1005
+ }),
1006
+ handler: async (args) => {
1007
+ const denied = requireOrchestratorMemoryWrite();
1008
+ if (denied)
1009
+ return denied;
1010
+ try {
1011
+ const previousScope = getMemoryActiveScope();
1012
+ const nextScope = args.slug === null ? null : (getMemoryScope(args.slug) ?? null);
1013
+ if (args.slug !== null && !nextScope) {
1014
+ return `Unknown memory scope '${args.slug}'.`;
1015
+ }
1016
+ const previousSlug = previousScope?.slug ?? null;
1017
+ const nextSlug = nextScope?.slug ?? null;
1018
+ const didChange = previousSlug !== nextSlug;
1019
+ const sessionKey = getCurrentSessionKey();
1020
+ if (didChange) {
1021
+ maybeScheduleScopeChangeCheckpoint(sessionKey, previousScope, nextScope);
1022
+ }
1023
+ const activeScope = setMemoryActiveScope(args.slug);
1024
+ if (didChange) {
1025
+ invalidateOrchestratorSession(sessionKey);
1026
+ resetCheckpointSessionState(sessionKey);
1027
+ }
1028
+ return {
1029
+ active_scope: activeScope
1030
+ ? { slug: activeScope.slug, title: activeScope.title }
1031
+ : null,
1032
+ };
1033
+ }
1034
+ catch (err) {
1035
+ return err instanceof Error ? err.message : String(err);
1036
+ }
1037
+ },
1038
+ }),
692
1039
  // ----- Wiki-backed memory facades (preserve existing remember/recall/forget UX) -----
693
1040
  defineTool("remember", {
694
1041
  description: "Save a fact, preference, or detail to the wiki. Routes to topic pages automatically. " +