@stackbilt/aegis-core 0.3.0 → 0.5.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 (87) hide show
  1. package/package.json +5 -2
  2. package/schema.sql +5 -0
  3. package/src/adapters/voice/cloudflare-agent.ts +0 -0
  4. package/src/bluesky.ts +0 -0
  5. package/src/claude-tools/content.ts +0 -0
  6. package/src/claude-tools/email.ts +0 -0
  7. package/src/codebeast.ts +0 -0
  8. package/src/content/column.ts +0 -0
  9. package/src/content/hero-image.ts +0 -0
  10. package/src/content/index.ts +0 -0
  11. package/src/content/journal.ts +0 -0
  12. package/src/content/roundtable.ts +0 -0
  13. package/src/contracts/agenda-item.contract.ts +0 -0
  14. package/src/contracts/cc-task.contract.ts +0 -0
  15. package/src/contracts/goal.contract.ts +0 -0
  16. package/src/contracts/memory-entry.contract.ts +0 -0
  17. package/src/core.ts +0 -0
  18. package/src/dashboard.ts +0 -0
  19. package/src/decision-docs.ts +0 -0
  20. package/src/dispatch.ts +0 -0
  21. package/src/edge-env.ts +0 -0
  22. package/src/exports.ts +0 -0
  23. package/src/github-projects.ts +0 -0
  24. package/src/kernel/argus-actions.ts +0 -0
  25. package/src/kernel/argus-correlation.ts +0 -0
  26. package/src/kernel/board.ts +0 -0
  27. package/src/kernel/classify-memory-topic.ts +0 -0
  28. package/src/kernel/dynamic-tools.ts +0 -0
  29. package/src/kernel/executor-port.ts +0 -0
  30. package/src/kernel/insight-cache.ts +0 -0
  31. package/src/kernel/memory/index.ts +1 -1
  32. package/src/kernel/memory/insights.ts +0 -0
  33. package/src/kernel/memory/procedural.ts +34 -0
  34. package/src/kernel/memory-guardrails.ts +0 -0
  35. package/src/kernel/memory-service.ts +252 -0
  36. package/src/kernel/port.ts +0 -0
  37. package/src/kernel/resilience.ts +0 -0
  38. package/src/kernel/scheduled/agent-dispatch.ts +0 -0
  39. package/src/kernel/scheduled/argus-analytics.ts +0 -0
  40. package/src/kernel/scheduled/argus-heartbeat.ts +0 -0
  41. package/src/kernel/scheduled/argus-notify.ts +0 -0
  42. package/src/kernel/scheduled/board-sync.ts +0 -0
  43. package/src/kernel/scheduled/ci-watcher.ts +0 -0
  44. package/src/kernel/scheduled/content-drip.ts +0 -0
  45. package/src/kernel/scheduled/content.ts +0 -0
  46. package/src/kernel/scheduled/conversation-facts.ts +0 -0
  47. package/src/kernel/scheduled/cost-report.ts +0 -0
  48. package/src/kernel/scheduled/dev-activity.ts +0 -0
  49. package/src/kernel/scheduled/digest.ts +0 -0
  50. package/src/kernel/scheduled/dreaming/agenda-triage.ts +0 -0
  51. package/src/kernel/scheduled/dreaming/facts.ts +0 -0
  52. package/src/kernel/scheduled/dreaming/index.ts +0 -0
  53. package/src/kernel/scheduled/dreaming/llm.ts +0 -0
  54. package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +0 -0
  55. package/src/kernel/scheduled/dreaming/persona.ts +0 -0
  56. package/src/kernel/scheduled/dreaming/symbolic.ts +0 -0
  57. package/src/kernel/scheduled/dreaming/task-proposals.ts +0 -0
  58. package/src/kernel/scheduled/entropy.ts +0 -0
  59. package/src/kernel/scheduled/feed-watcher.ts +0 -0
  60. package/src/kernel/scheduled/inbox-processor.ts +0 -0
  61. package/src/kernel/scheduled/issue-proposer.ts +0 -0
  62. package/src/kernel/scheduled/issue-watcher.ts +0 -0
  63. package/src/kernel/scheduled/pr-automerge.ts +0 -0
  64. package/src/kernel/scheduled/product-health.ts +0 -0
  65. package/src/kernel/scheduled/self-improvement.ts +0 -0
  66. package/src/kernel/scheduled/social-engage.ts +0 -0
  67. package/src/kernel/scheduled/task-audit.ts +0 -0
  68. package/src/kernel/types.ts +6 -0
  69. package/src/landing.ts +0 -0
  70. package/src/lib/audit-chain/chain.ts +0 -0
  71. package/src/lib/audit-chain/types.ts +0 -0
  72. package/src/lib/observability/errors.ts +0 -0
  73. package/src/operator/config.ts +0 -0
  74. package/src/operator/persona.ts +0 -0
  75. package/src/pulse.ts +0 -0
  76. package/src/routes/bluesky.ts +0 -0
  77. package/src/routes/codebeast.ts +0 -0
  78. package/src/routes/content.ts +0 -0
  79. package/src/routes/dynamic-tools.ts +0 -0
  80. package/src/routes/observability.ts +0 -0
  81. package/src/routes/operator-logs.ts +0 -0
  82. package/src/routes/pages.ts +0 -0
  83. package/src/schema-enums.ts +0 -0
  84. package/src/task-intelligence.ts +0 -0
  85. package/src/ui.ts +0 -0
  86. package/src/wiki/client.ts +346 -0
  87. package/src/wiki/types.ts +90 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbilt/aegis-core",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Persistent AI agent framework for Cloudflare Workers. Multi-tier memory, autonomous goals, dreaming cycles, MCP native.",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
@@ -59,7 +59,10 @@
59
59
  "./contracts/goal": "./src/contracts/goal.contract.ts",
60
60
  "./contracts/agenda-item": "./src/contracts/agenda-item.contract.ts",
61
61
  "./contracts/cc-task": "./src/contracts/cc-task.contract.ts",
62
- "./contracts/memory-entry": "./src/contracts/memory-entry.contract.ts"
62
+ "./contracts/memory-entry": "./src/contracts/memory-entry.contract.ts",
63
+ "./wiki/client": "./src/wiki/client.ts",
64
+ "./wiki/types": "./src/wiki/types.ts",
65
+ "./kernel/memory-service": "./src/kernel/memory-service.ts"
63
66
  },
64
67
  "scripts": {
65
68
  "dev": "wrangler dev",
package/schema.sql CHANGED
@@ -65,7 +65,9 @@ CREATE TABLE IF NOT EXISTS episodic_memory (
65
65
  reclassified INTEGER NOT NULL DEFAULT 0,
66
66
  thread_id TEXT, -- conversation thread for dreaming cycle
67
67
  executor TEXT, -- which executor handled this
68
+ court_card TEXT, -- composite court card (king/queen/knight/page)
68
69
  complexity_tier TEXT, -- aegis#563: procedureKey complement (low|mid|high); NULL for non-dispatcher producers
70
+ executor_config TEXT, -- aegis#563: config snapshot at emit time (evaluator-replay fidelity)
69
71
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
70
72
  );
71
73
 
@@ -111,6 +113,8 @@ CREATE TABLE IF NOT EXISTS procedural_memory (
111
113
  last_used TEXT,
112
114
  candidate_executor TEXT, -- probation: untrusted executor being tested
113
115
  candidate_successes INTEGER NOT NULL DEFAULT 0, -- consecutive successes of candidate
116
+ gap_signal_count INTEGER NOT NULL DEFAULT 0, -- aegis#497: unresolved grounding-gap signals; >0 triggers tier escalation
117
+ gap_last_seen TEXT,
114
118
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
115
119
  );
116
120
 
@@ -204,6 +208,7 @@ CREATE INDEX IF NOT EXISTS idx_memory_validation_stage ON memory_entries(validat
204
208
  CREATE INDEX IF NOT EXISTS idx_episodic_class ON episodic_memory(intent_class);
205
209
  CREATE INDEX IF NOT EXISTS idx_episodic_created ON episodic_memory(created_at);
206
210
  CREATE INDEX IF NOT EXISTS idx_episodic_thread ON episodic_memory(thread_id);
211
+ CREATE INDEX IF NOT EXISTS idx_episodic_class_complexity ON episodic_memory(intent_class, complexity_tier);
207
212
  CREATE INDEX IF NOT EXISTS idx_procedural_pattern ON procedural_memory(task_pattern);
208
213
  CREATE INDEX IF NOT EXISTS idx_procedural_status ON procedural_memory(status);
209
214
  CREATE INDEX IF NOT EXISTS idx_heartbeat_created ON heartbeat_results(created_at);
File without changes
package/src/bluesky.ts CHANGED
File without changes
File without changes
File without changes
package/src/codebeast.ts CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/core.ts CHANGED
File without changes
package/src/dashboard.ts CHANGED
File without changes
File without changes
package/src/dispatch.ts CHANGED
File without changes
package/src/edge-env.ts CHANGED
File without changes
package/src/exports.ts CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,5 +1,5 @@
1
1
  export { recordEpisode, retrogradeEpisode, sanitizeEpisodicOutcome, getRecentEpisodes, getEpisodeStats, getEpisodeStatsByComplexity, getAllEpisodeStatsByComplexity, type EpisodeStatsAggregate, getConversationHistory, estimateTokens, budgetConversationHistory } from './episodic.js';
2
- export { PROCEDURE_MIN_SUCCESSES, PROCEDURE_MIN_SUCCESS_RATE, complexityTier, procedureKey, getProcedure, getAllProcedures, getProcedureWithDerivedStats, getAllProceduresWithDerivedStats, type DriftLogOpts, findNearMiss, upsertProcedure, addRefinement, degradeProcedure, maintainProcedures } from './procedural.js';
2
+ export { PROCEDURE_MIN_SUCCESSES, PROCEDURE_MIN_SUCCESS_RATE, complexityTier, procedureKey, getProcedure, getAllProcedures, getProcedureWithDerivedStats, getAllProceduresWithDerivedStats, type DriftLogOpts, findNearMiss, upsertProcedure, addRefinement, degradeProcedure, recordGapSignal, clearGapSignal, maintainProcedures } from './procedural.js';
3
3
  export { normalizeTopic, tokenize, jaccardSimilarity, recordMemory, searchMemoryByKeywords, getMemoryEntries, recallMemory, computeEwaScore, getAllMemoryForContext } from './semantic.js';
4
4
  export { pruneMemory } from './pruning.js';
5
5
  export { consolidateEpisodicToSemantic } from './consolidation.js';
File without changes
@@ -465,3 +465,37 @@ export async function maintainProcedures(db: D1Database): Promise<void> {
465
465
  console.log(`[procedures] Utility-pruned: ${proc.task_pattern} (${(rate * 100).toFixed(0)}% success over ${proc.success_count + proc.fail_count} invocations)`);
466
466
  }
467
467
  }
468
+
469
+ /** Increment gap signal for a procedure. Called when the response carried
470
+ * unverified_claims[] or unknowns[] on a grounding-gated classification. */
471
+ export async function recordGapSignal(db: D1Database, taskPattern: string): Promise<void> {
472
+ const procedure = await getProcedure(db, taskPattern);
473
+ if (!procedure) return;
474
+
475
+ const newCount = (procedure.gap_signal_count ?? 0) + 1;
476
+ await db.prepare(
477
+ "UPDATE procedural_memory SET gap_signal_count = ?, gap_last_seen = datetime('now') WHERE task_pattern = ?"
478
+ ).bind(newCount, taskPattern).run();
479
+
480
+ console.log(`[procedures] Gap signal recorded: ${taskPattern} (count=${newCount})`);
481
+ }
482
+
483
+ /** Decrement gap signal for a procedure (min 0). Called on clean grounded
484
+ * response. When the counter returns to 0, gap_last_seen is cleared. */
485
+ export async function clearGapSignal(db: D1Database, taskPattern: string): Promise<void> {
486
+ const procedure = await getProcedure(db, taskPattern);
487
+ if (!procedure) return;
488
+
489
+ const current = procedure.gap_signal_count ?? 0;
490
+ if (current <= 0) return;
491
+
492
+ const newCount = current - 1;
493
+ const newLastSeen = newCount === 0 ? null : procedure.gap_last_seen ?? null;
494
+ await db.prepare(
495
+ 'UPDATE procedural_memory SET gap_signal_count = ?, gap_last_seen = ? WHERE task_pattern = ?'
496
+ ).bind(newCount, newLastSeen, taskPattern).run();
497
+
498
+ if (newCount === 0) {
499
+ console.log(`[procedures] Gap signal cleared: ${taskPattern}`);
500
+ }
501
+ }
File without changes
@@ -0,0 +1,252 @@
1
+ /**
2
+ * MemoryService — unified facade over the memory tiers (episodic,
3
+ * procedural, semantic, insights) + goals. Phase 1B of the god-object
4
+ * decomposition epic (aegis#532 / aegis#534).
5
+ *
6
+ * Why: the audit flagged that dispatch, self-improvement, ambient-capture
7
+ * each reach into memory/*.ts and memory-adapter.ts directly, repeating
8
+ * the `{db, memoryBinding}` threading and obscuring which tier each call
9
+ * actually hits. This service hands callers one object and hides the
10
+ * backing-store distinction.
11
+ *
12
+ * Tier → backing store:
13
+ * - episodic, procedural, insights, goals → D1 (env.db)
14
+ * - semantic, context-recall → memory-worker (env.memoryBinding) via
15
+ * memory-adapter (aegis-core re-export)
16
+ *
17
+ * Prefer this over the raw tier functions in new code. Existing callers
18
+ * migrate incrementally — Phase 2/3 decomps will naturally fold the rest
19
+ * as those files get broken up.
20
+ */
21
+ import {
22
+ recordEpisode as _recordEpisode,
23
+ retrogradeEpisode as _retrogradeEpisode,
24
+ getRecentEpisodes as _getRecentEpisodes,
25
+ getEpisodeStatsByComplexity as _getEpisodeStatsByComplexity,
26
+ upsertProcedure as _upsertProcedure,
27
+ getProcedure as _getProcedure,
28
+ getProcedureWithDerivedStats as _getProcedureWithDerivedStats,
29
+ getAllProceduresWithDerivedStats as _getAllProceduresWithDerivedStats,
30
+ type DriftLogOpts,
31
+ addRefinement as _addRefinement,
32
+ degradeProcedure as _degradeProcedure,
33
+ findNearMiss as _findNearMiss,
34
+ recordGapSignal as _recordGapSignal,
35
+ clearGapSignal as _clearGapSignal,
36
+ publishInsight as _publishInsight,
37
+ validateInsight as _validateInsight,
38
+ recordGoalAction as _recordGoalAction,
39
+ type InsightPayload,
40
+ type ValidationStage,
41
+ type AgentAction,
42
+ } from './memory/index.js';
43
+ import {
44
+ recordMemory as _recordMemoryViaAdapter,
45
+ searchMemoryByKeywords as _searchMemoryByKeywordsViaAdapter,
46
+ getAllMemoryForContext as _getAllMemoryForContextViaAdapter,
47
+ } from './memory-adapter.js';
48
+ import type { MemoryServiceBinding } from '../types.js';
49
+ import type { EpisodicEntry, ProceduralEntry, Refinement } from './types.js';
50
+
51
+ export type { ValidationStage, InsightPayload };
52
+
53
+ export interface DispatchOutcomeArgs {
54
+ episode: Omit<EpisodicEntry, 'id' | 'created_at'>;
55
+ procedureKey: string;
56
+ executor: string;
57
+ executorConfig: string;
58
+ outcome: 'success' | 'failure' | 'partial_failure';
59
+ latencyMs: number;
60
+ cost: number;
61
+ /** Optional refinement log — added when a replan promotes a degraded procedure. */
62
+ refinement?: Refinement;
63
+ }
64
+
65
+ export interface GoalActionOpts {
66
+ autoExecuted?: boolean;
67
+ authorityLevel?: string;
68
+ toolCalled?: string;
69
+ toolArgsJson?: string;
70
+ toolResultJson?: string;
71
+ }
72
+
73
+ export class MemoryService {
74
+ constructor(
75
+ private readonly db: D1Database,
76
+ private readonly memoryBinding?: MemoryServiceBinding,
77
+ ) {}
78
+
79
+ // ─── Episodic ──────────────────────────────────────────────
80
+
81
+ recordEpisode(entry: Omit<EpisodicEntry, 'id' | 'created_at'>): Promise<void> {
82
+ return _recordEpisode(this.db, entry);
83
+ }
84
+
85
+ retrogradeEpisode(threadId: string): Promise<EpisodicEntry | null> {
86
+ return _retrogradeEpisode(this.db, threadId);
87
+ }
88
+
89
+ getRecentEpisodes(intentClass: string, limit = 5): Promise<EpisodicEntry[]> {
90
+ return _getRecentEpisodes(this.db, intentClass, limit);
91
+ }
92
+
93
+ // aegis#563: aggregates keyed by (intent_class, complexity_tier), matching
94
+ // procedural_memory's procedureKey shape. Foundation for the #564
95
+ // projection-vs-cache decomposition.
96
+ getEpisodeStatsByComplexity(intentClass: string, complexityTier: string) {
97
+ return _getEpisodeStatsByComplexity(this.db, intentClass, complexityTier);
98
+ }
99
+
100
+ // ─── Procedural ────────────────────────────────────────────
101
+
102
+ upsertProcedure(
103
+ taskPattern: string,
104
+ executor: string,
105
+ executorConfig: string,
106
+ outcome: 'success' | 'failure' | 'partial_failure',
107
+ latencyMs: number,
108
+ cost: number,
109
+ ): Promise<void> {
110
+ return _upsertProcedure(this.db, taskPattern, executor, executorConfig, outcome, latencyMs, cost);
111
+ }
112
+
113
+ getProcedure(taskPattern: string): Promise<ProceduralEntry | null> {
114
+ return _getProcedure(this.db, taskPattern);
115
+ }
116
+
117
+ // aegis#564 Phase 1 — callers that read the five cached aggregate
118
+ // columns (success_count, fail_count, avg_latency_ms, avg_cost,
119
+ // last_used) should route through this helper. Phase 1 is a pass-
120
+ // through; Phase 2 adds shadow-read logging; Phase 3 flips to derived.
121
+ getProcedureWithDerivedStats(taskPattern: string, opts?: DriftLogOpts): Promise<ProceduralEntry | null> {
122
+ return _getProcedureWithDerivedStats(this.db, taskPattern, opts);
123
+ }
124
+
125
+ getAllProceduresWithDerivedStats(opts?: DriftLogOpts): Promise<ProceduralEntry[]> {
126
+ return _getAllProceduresWithDerivedStats(this.db, opts);
127
+ }
128
+
129
+ addRefinement(taskPattern: string, refinement: Refinement): Promise<void> {
130
+ return _addRefinement(this.db, taskPattern, refinement);
131
+ }
132
+
133
+ degradeProcedure(taskPattern: string, executor: string): Promise<void> {
134
+ return _degradeProcedure(this.db, taskPattern, executor);
135
+ }
136
+
137
+ findNearMiss(taskPattern: string) {
138
+ return _findNearMiss(this.db, taskPattern);
139
+ }
140
+
141
+ recordGapSignal(taskPattern: string): Promise<void> {
142
+ return _recordGapSignal(this.db, taskPattern);
143
+ }
144
+
145
+ clearGapSignal(taskPattern: string): Promise<void> {
146
+ return _clearGapSignal(this.db, taskPattern);
147
+ }
148
+
149
+ // ─── Semantic (memory-worker via adapter) ──────────────────
150
+
151
+ async recordMemory(
152
+ topic: string,
153
+ fact: string,
154
+ confidence: number,
155
+ source: string,
156
+ metadata?: Record<string, unknown>,
157
+ ) {
158
+ this.requireBinding('recordMemory');
159
+ return _recordMemoryViaAdapter(this.memoryBinding!, topic, fact, confidence, source, metadata);
160
+ }
161
+
162
+ async searchMemory(query: string, limit?: number) {
163
+ this.requireBinding('searchMemory');
164
+ return _searchMemoryByKeywordsViaAdapter(this.memoryBinding!, query, limit);
165
+ }
166
+
167
+ async getMemoryForContext(query?: string) {
168
+ this.requireBinding('getMemoryForContext');
169
+ return _getAllMemoryForContextViaAdapter(this.memoryBinding!, query);
170
+ }
171
+
172
+ // ─── Insights ──────────────────────────────────────────────
173
+
174
+ publishInsight(payload: InsightPayload) {
175
+ return _publishInsight(this.db, payload, this.memoryBinding);
176
+ }
177
+
178
+ validateInsight(factHash: string, validatingRepo: string, confirmed: boolean) {
179
+ return _validateInsight(this.db, factHash, validatingRepo, confirmed);
180
+ }
181
+
182
+ // ─── Goals ─────────────────────────────────────────────────
183
+
184
+ recordGoalAction(
185
+ goalId: string | null,
186
+ actionType: AgentAction['action_type'],
187
+ description: string,
188
+ outcome: AgentAction['outcome'] = 'success',
189
+ opts?: GoalActionOpts,
190
+ ): Promise<void> {
191
+ return _recordGoalAction(this.db, goalId, actionType, description, outcome, opts);
192
+ }
193
+
194
+ // ─── Domain: dispatch outcome bookkeeping ──────────────────
195
+
196
+ /**
197
+ * Record a dispatch outcome: episode + procedural upsert, plus an
198
+ * optional refinement log when a replan happens on a degraded procedure.
199
+ * Collapses the three-call pattern that dispatch.recordOutcome carries
200
+ * inline.
201
+ */
202
+ async recordDispatchOutcome(args: DispatchOutcomeArgs): Promise<void> {
203
+ // aegis#563: enrich episode with complexity_tier (derived from
204
+ // procedureKey = `classification:complexity_tier`) and executor_config
205
+ // snapshot. Caller-supplied values win; otherwise derive from args so
206
+ // dispatcher call sites don't need to pass these explicitly.
207
+ const tierFromKey = args.procedureKey.includes(':')
208
+ ? args.procedureKey.slice(args.procedureKey.indexOf(':') + 1)
209
+ : null;
210
+ const enrichedEpisode: Omit<EpisodicEntry, 'id' | 'created_at'> = {
211
+ ...args.episode,
212
+ complexity_tier: args.episode.complexity_tier ?? tierFromKey,
213
+ executor_config: args.episode.executor_config ?? args.executorConfig,
214
+ };
215
+
216
+ await _recordEpisode(this.db, enrichedEpisode);
217
+ await _upsertProcedure(
218
+ this.db,
219
+ args.procedureKey,
220
+ args.executor,
221
+ args.executorConfig,
222
+ args.outcome,
223
+ args.latencyMs,
224
+ args.cost,
225
+ );
226
+ if (args.refinement) {
227
+ await _addRefinement(this.db, args.procedureKey, args.refinement);
228
+ }
229
+ }
230
+
231
+ // ─── Internal ──────────────────────────────────────────────
232
+
233
+ private requireBinding(method: string): void {
234
+ if (!this.memoryBinding) {
235
+ throw new Error(
236
+ `[memory-service] ${method}() requires memoryBinding — constructor received none`,
237
+ );
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Factory: construct a MemoryService from any env-shape carrying
244
+ * `db` and (optionally) `memoryBinding`. The daemon's EdgeEnv satisfies
245
+ * this shape; scheduled tasks and tests can pass a narrower object.
246
+ */
247
+ export function memoryServiceFor(env: {
248
+ db: D1Database;
249
+ memoryBinding?: MemoryServiceBinding;
250
+ }): MemoryService {
251
+ return new MemoryService(env.db, env.memoryBinding);
252
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -38,6 +38,9 @@ export interface EpisodicEntry {
38
38
  reclassified?: boolean;
39
39
  thread_id?: string | null;
40
40
  executor?: string | null;
41
+ court_card?: string | null;
42
+ complexity_tier?: string | null;
43
+ executor_config?: string | null;
41
44
  created_at?: string;
42
45
  }
43
46
 
@@ -65,6 +68,9 @@ export interface ProceduralEntry {
65
68
  last_used?: string;
66
69
  candidate_executor?: string | null;
67
70
  candidate_successes?: number;
71
+ // aegis#497 — gap-driven tier escalation
72
+ gap_signal_count?: number;
73
+ gap_last_seen?: string | null;
68
74
  created_at?: string;
69
75
  }
70
76
 
package/src/landing.ts CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/pulse.ts CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/ui.ts CHANGED
File without changes
@@ -0,0 +1,346 @@
1
+ import type {
2
+ ListPagesResult,
3
+ PageSummary,
4
+ ReadPageResult,
5
+ SearchPagesResult,
6
+ WikiPage,
7
+ WriteInput,
8
+ WritePageResult,
9
+ } from './types.js';
10
+
11
+ export interface WikiClientEnv {
12
+ wikiBaseUrl?: string;
13
+ wikiBinding?: Fetcher;
14
+ wikiToken?: string;
15
+ }
16
+
17
+ export class WikiClientError extends Error {
18
+ constructor(
19
+ message: string,
20
+ public readonly status: number,
21
+ public readonly statusText: string,
22
+ public readonly details?: unknown,
23
+ ) {
24
+ super(message);
25
+ this.name = 'WikiClientError';
26
+ }
27
+ }
28
+
29
+ function requireWikiToken(env: WikiClientEnv): string {
30
+ if (!env.wikiToken?.trim()) {
31
+ throw new WikiClientError('AEGIS_WIKI_TOKEN is not configured', 503, 'Service Unavailable');
32
+ }
33
+ return env.wikiToken;
34
+ }
35
+
36
+ function todayIsoDate(): string {
37
+ return new Date().toISOString().slice(0, 10);
38
+ }
39
+
40
+ function asObject(value: unknown): Record<string, unknown> | null {
41
+ return value && typeof value === 'object' && !Array.isArray(value)
42
+ ? value as Record<string, unknown>
43
+ : null;
44
+ }
45
+
46
+ function coerceBoolean(value: unknown): boolean | undefined {
47
+ if (typeof value === 'boolean') return value;
48
+ if (typeof value === 'number') return value !== 0;
49
+ if (value === undefined || value === null) return undefined;
50
+ if (typeof value === 'string') {
51
+ if (value === 'true' || value === '1') return true;
52
+ if (value === 'false' || value === '0') return false;
53
+ }
54
+ return Boolean(value);
55
+ }
56
+
57
+ // Emdash stores body as portableText (an array of blocks). Agents want readable
58
+ // prose, so serialize back to markdown-ish text. Walks blocks, applies heading
59
+ // prefixes by style, drops marks/links — lossy but agent-readable.
60
+ function portableTextToMarkdown(value: unknown): string | undefined {
61
+ if (typeof value === 'string') return value;
62
+ if (!Array.isArray(value)) return undefined;
63
+ const lines: string[] = [];
64
+ for (const block of value) {
65
+ if (!block || typeof block !== 'object') continue;
66
+ const b = block as Record<string, unknown>;
67
+ const style = typeof b.style === 'string' ? b.style : 'normal';
68
+ const children = Array.isArray(b.children) ? b.children : [];
69
+ const text = children
70
+ .map(c => (c && typeof c === 'object' && typeof (c as Record<string, unknown>).text === 'string'
71
+ ? String((c as Record<string, unknown>).text)
72
+ : ''))
73
+ .join('');
74
+ if (!text) { lines.push(''); continue; }
75
+ if (style === 'h1') lines.push(`# ${text}`);
76
+ else if (style === 'h2') lines.push(`## ${text}`);
77
+ else if (style === 'h3') lines.push(`### ${text}`);
78
+ else if (style === 'h4') lines.push(`#### ${text}`);
79
+ else if (style === 'h5') lines.push(`##### ${text}`);
80
+ else if (style === 'h6') lines.push(`###### ${text}`);
81
+ else if (style === 'blockquote') lines.push(`> ${text}`);
82
+ else lines.push(text);
83
+ lines.push('');
84
+ }
85
+ return lines.join('\n').trim();
86
+ }
87
+
88
+ // Emdash response envelope: { data: { item: ContentItem, _rev?: string } }
89
+ // ContentItem shape: { id, type, slug, status, data: <frontmatter+body>, ... }
90
+ // Returns merged WikiPage with frontmatter spread + top-level id/slug/status/_rev.
91
+ function normalizePage(value: unknown): WikiPage | null {
92
+ const envelope = asObject(value);
93
+ if (!envelope) return null;
94
+
95
+ // Drill into the wrapper. Accept both { data: { item } } and a bare ContentItem
96
+ // (in case the response shape ever flattens). Walk both shapes safely.
97
+ const inner = asObject(envelope.data) ?? envelope;
98
+ const item = asObject(inner.item) ?? inner;
99
+ if (!item.slug && !item.id) return null;
100
+
101
+ const data = asObject(item.data) ?? {};
102
+ const canonical = coerceBoolean(data.canonical);
103
+
104
+ const merged: WikiPage = {
105
+ id: String(item.id ?? ''),
106
+ slug: String(item.slug ?? data.slug ?? ''),
107
+ status: typeof item.status === 'string' ? item.status : undefined,
108
+ revision_id: typeof inner._rev === 'string' ? inner._rev : undefined,
109
+ title: String(data.title ?? ''),
110
+ scope: String(data.scope ?? ''),
111
+ type: String(data.type ?? ''),
112
+ confidence: String(data.confidence ?? ''),
113
+ summary: typeof data.summary === 'string' ? data.summary : '',
114
+ body: portableTextToMarkdown(data.body),
115
+ canonical,
116
+ last_verified: typeof data.last_verified === 'string' ? data.last_verified : undefined,
117
+ sources: Array.isArray(data.sources) ? (data.sources as WikiPage['sources']) : undefined,
118
+ related: Array.isArray(data.related) ? (data.related as string[]) : undefined,
119
+ supersedes: Array.isArray(data.supersedes) ? (data.supersedes as string[]) : undefined,
120
+ owners: Array.isArray(data.owners) ? (data.owners as string[]) : undefined,
121
+ consumers: Array.isArray(data.consumers) ? (data.consumers as string[]) : undefined,
122
+ guarded_paths: Array.isArray(data.guarded_paths) ? (data.guarded_paths as string[]) : undefined,
123
+ updated_at: typeof item.updatedAt === 'string' ? item.updatedAt : '',
124
+ };
125
+
126
+ // Preserve any additional fields from data that we didn't explicitly map.
127
+ for (const [k, v] of Object.entries(data)) {
128
+ if (!(k in merged)) merged[k] = v;
129
+ }
130
+
131
+ return merged;
132
+ }
133
+
134
+ // Handles both shapes:
135
+ // - ContentItem (list endpoint): { id, slug, data: { title, scope, type, ... } }
136
+ // - SearchHit (search endpoint): { collection, id, slug, title, snippet, score }
137
+ // Search hits don't carry scope/type/confidence — those fields stay empty and the
138
+ // agent can call wiki_read for full details on any hit.
139
+ function normalizeContentItemToSummary(item: Record<string, unknown>): PageSummary {
140
+ const data = asObject(item.data) ?? {};
141
+ const summary: PageSummary = {
142
+ slug: String(item.slug ?? ''),
143
+ title: String(item.title ?? data.title ?? ''),
144
+ scope: String(item.scope ?? data.scope ?? ''),
145
+ type: typeof data.type === 'string' ? data.type : '', // top-level item.type is the collection name, not the wiki type
146
+ confidence: String(data.confidence ?? ''),
147
+ summary: typeof data.summary === 'string' ? data.summary : '',
148
+ updated_at: typeof item.updatedAt === 'string' ? item.updatedAt : '',
149
+ };
150
+ if (typeof item.snippet === 'string') summary.snippet = item.snippet;
151
+ return summary;
152
+ }
153
+
154
+ function normalizeSummaryArray(value: unknown): PageSummary[] {
155
+ const envelope = asObject(value);
156
+ if (!envelope) return [];
157
+ const inner = asObject(envelope.data) ?? envelope;
158
+
159
+ // Cursor-based list: { data: { items: [...] } }
160
+ // Search hits: { data: { results: [...] } }
161
+ const arr =
162
+ (Array.isArray(inner.items) && inner.items) ||
163
+ (Array.isArray(inner.results) && inner.results) ||
164
+ (Array.isArray(inner.pages) && inner.pages) ||
165
+ (Array.isArray(envelope.items) && envelope.items) ||
166
+ (Array.isArray(envelope.results) && envelope.results) ||
167
+ [];
168
+
169
+ return (arr as unknown[])
170
+ .map(item => asObject(item))
171
+ .filter((item): item is Record<string, unknown> => !!item)
172
+ .map(normalizeContentItemToSummary);
173
+ }
174
+
175
+ function normalizeWriteResult(value: unknown, fallback: { slug: string; id?: string }): WritePageResult {
176
+ const page = normalizePage(value);
177
+ const id = page?.id || fallback.id || '';
178
+ if (!id) {
179
+ throw new WikiClientError('Wiki write response did not include an id', 502, 'Bad Gateway', value);
180
+ }
181
+ return {
182
+ slug: page?.slug || fallback.slug,
183
+ id,
184
+ revision_id: page?.revision_id,
185
+ };
186
+ }
187
+
188
+ async function requestJson<T>(env: WikiClientEnv, path: string, init: RequestInit = {}): Promise<T> {
189
+ const token = requireWikiToken(env);
190
+ const headers = new Headers(init.headers);
191
+ headers.set('Authorization', `Bearer ${token}`);
192
+ if (init.body && !headers.has('Content-Type')) {
193
+ headers.set('Content-Type', 'application/json');
194
+ }
195
+ headers.set('Accept', 'application/json');
196
+
197
+ const base = env.wikiBaseUrl ?? '';
198
+ const request = new Request(`${base}${path}`, { ...init, headers });
199
+
200
+ let response: Response;
201
+ if (env.wikiBinding) {
202
+ response = await env.wikiBinding.fetch(request);
203
+ } else if (env.wikiBaseUrl) {
204
+ response = await fetch(request);
205
+ } else {
206
+ throw new WikiClientError(
207
+ 'WikiClientEnv requires wikiBaseUrl or wikiBinding',
208
+ 503,
209
+ 'Service Unavailable',
210
+ );
211
+ }
212
+
213
+ const text = await response.text();
214
+ let data: unknown = null;
215
+ if (text) {
216
+ try {
217
+ data = JSON.parse(text);
218
+ } catch {
219
+ data = text;
220
+ }
221
+ }
222
+
223
+ if (!response.ok) {
224
+ throw new WikiClientError(
225
+ `Wiki request failed: ${response.status} ${response.statusText}`,
226
+ response.status,
227
+ response.statusText,
228
+ data,
229
+ );
230
+ }
231
+
232
+ return data as T;
233
+ }
234
+
235
+ export async function readPage(env: WikiClientEnv, slug: string): Promise<ReadPageResult> {
236
+ try {
237
+ const data = await requestJson<unknown>(env, `/content/wiki/${encodeURIComponent(slug)}`);
238
+ return { page: normalizePage(data) };
239
+ } catch (err) {
240
+ if (err instanceof WikiClientError && err.status === 404) {
241
+ return { page: null };
242
+ }
243
+ throw err;
244
+ }
245
+ }
246
+
247
+ export async function searchPages(
248
+ env: WikiClientEnv,
249
+ query: string,
250
+ options: { limit?: number } = {},
251
+ ): Promise<SearchPagesResult> {
252
+ const params = new URLSearchParams({ q: query, collections: 'wiki' });
253
+ params.set('limit', String(options.limit ?? 10));
254
+
255
+ const data = await requestJson<unknown>(env, `/search?${params.toString()}`);
256
+ const results = normalizeSummaryArray(data);
257
+
258
+ // EmDash search hits don't carry scope/type in the response — client-side
259
+ // filtering on those fields would silently discard all results. Use
260
+ // wiki_list for scope-scoped enumeration (aegis#575).
261
+ return { results };
262
+ }
263
+
264
+ // Emdash's write endpoints land content in a draft revision. A separate
265
+ // POST /publish call is required to promote the draft to live — otherwise
266
+ // GET returns the previous live revision and the write is silently invisible.
267
+ // See aegis#475 for the forensic trace.
268
+ async function publishRevision(env: WikiClientEnv, id: string): Promise<void> {
269
+ await requestJson<unknown>(
270
+ env,
271
+ `/content/wiki/${encodeURIComponent(id)}/publish`,
272
+ { method: 'POST', body: JSON.stringify({}) },
273
+ );
274
+ }
275
+
276
+ export async function writePage(env: WikiClientEnv, input: WriteInput): Promise<WritePageResult> {
277
+ const { slug, ...rest } = input;
278
+ const dataPayload = {
279
+ canonical: false,
280
+ confidence: 'drifting' as const,
281
+ last_verified: todayIsoDate(),
282
+ sources: [],
283
+ related: [],
284
+ supersedes: [],
285
+ ...rest,
286
+ };
287
+
288
+ const existing = await readPage(env, slug);
289
+
290
+ if (existing.page?.id) {
291
+ const updateBody = {
292
+ slug,
293
+ status: existing.page.status ?? 'published',
294
+ data: dataPayload,
295
+ ...(existing.page.revision_id ? { _rev: existing.page.revision_id } : {}),
296
+ };
297
+ const data = await requestJson<unknown>(
298
+ env,
299
+ `/content/wiki/${encodeURIComponent(existing.page.id)}`,
300
+ {
301
+ method: 'PUT',
302
+ body: JSON.stringify(updateBody),
303
+ },
304
+ );
305
+ await publishRevision(env, existing.page.id);
306
+ return normalizeWriteResult(data, { slug, id: existing.page.id });
307
+ }
308
+
309
+ const createBody = {
310
+ slug,
311
+ status: 'published',
312
+ data: dataPayload,
313
+ };
314
+ const data = await requestJson<unknown>(env, '/content/wiki', {
315
+ method: 'POST',
316
+ body: JSON.stringify(createBody),
317
+ });
318
+ const result = normalizeWriteResult(data, { slug });
319
+ await publishRevision(env, result.id);
320
+ return result;
321
+ }
322
+
323
+ export async function listPages(
324
+ env: WikiClientEnv,
325
+ options: { scope?: string; limit?: number; cursor?: string } = {},
326
+ ): Promise<ListPagesResult> {
327
+ const params = new URLSearchParams();
328
+ params.set('limit', String(options.limit ?? 50));
329
+ if (options.cursor) params.set('cursor', options.cursor);
330
+
331
+ const raw = await requestJson<unknown>(env, `/content/wiki?${params.toString()}`);
332
+
333
+ let pages = normalizeSummaryArray(raw);
334
+ // Emdash list endpoint doesn't filter by inner data fields — scope is client-side.
335
+ if (options.scope) pages = pages.filter(p => p.scope === options.scope);
336
+
337
+ const envelope = asObject(raw);
338
+ const inner = asObject(envelope?.data) ?? envelope ?? {};
339
+ const nextCursor = typeof inner.nextCursor === 'string' ? inner.nextCursor : undefined;
340
+
341
+ return {
342
+ pages,
343
+ total: pages.length,
344
+ nextCursor,
345
+ };
346
+ }
@@ -0,0 +1,90 @@
1
+ // Second source of truth vs emdash seed.json (stackbilt-emdash/seed/seed.json).
2
+ // Intentional: this is the client-side MCP validator gate — narrower-than-server
3
+ // is fail-closed (daemon rejects, surfaces loudly). Keep in sync with seed.json
4
+ // when extending. Auto-generation from the seed schema is a separate epic.
5
+ export const WIKI_SCOPES = ['aegis', 'concepts', 'entities', 'decisions', 'wiki', 'dreams', 'contracts'] as const;
6
+ export const WIKI_TYPES = ['state', 'architecture', 'decision', 'concept', 'entity', 'agenda', 'synthesis'] as const;
7
+ export const WIKI_CONFIDENCES = ['stable', 'drifting', 'unverified', 'contested'] as const;
8
+ export const WIKI_STATUSES = ['experimental', 'stable', 'deprecated'] as const;
9
+
10
+ export type WikiScope = typeof WIKI_SCOPES[number];
11
+ export type WikiType = typeof WIKI_TYPES[number];
12
+ export type WikiConfidence = typeof WIKI_CONFIDENCES[number];
13
+ export type WikiStatus = typeof WIKI_STATUSES[number];
14
+
15
+ export interface WikiSource {
16
+ type: string;
17
+ ref: string;
18
+ verified_date: string;
19
+ }
20
+
21
+ export interface WriteInput {
22
+ slug: string;
23
+ scope: WikiScope;
24
+ type: WikiType;
25
+ title: string;
26
+ summary: string;
27
+ body: string;
28
+ canonical?: boolean;
29
+ confidence?: WikiConfidence;
30
+ last_verified?: string;
31
+ sources?: WikiSource[];
32
+ related?: string[];
33
+ supersedes?: string[];
34
+ // Contracts-scope frontmatter (Nexus Gate A, aegis#523).
35
+ // Required when scope === 'contracts'; enforced in mcp/handlers.ts.
36
+ // `contract_status` disambiguates from emdash's built-in `status` content-
37
+ // lifecycle column (draft/published/archived) which would collide.
38
+ owners?: string[];
39
+ consumers?: string[];
40
+ guarded_paths?: string[];
41
+ contract_status?: WikiStatus;
42
+ }
43
+
44
+ export interface PageSummary {
45
+ slug: string;
46
+ title: string;
47
+ scope: string;
48
+ type: string;
49
+ confidence: string;
50
+ summary: string;
51
+ updated_at: string;
52
+ snippet?: string;
53
+ }
54
+
55
+ export interface WikiPage extends PageSummary {
56
+ id: string;
57
+ status?: string;
58
+ revision_id?: string;
59
+ body?: string;
60
+ canonical?: boolean;
61
+ last_verified?: string;
62
+ sources?: WikiSource[];
63
+ related?: string[];
64
+ supersedes?: string[];
65
+ // Contracts-scope frontmatter (Nexus Gate A, aegis#523).
66
+ owners?: string[];
67
+ consumers?: string[];
68
+ guarded_paths?: string[];
69
+ [key: string]: unknown;
70
+ }
71
+
72
+ export interface ReadPageResult {
73
+ page: WikiPage | null;
74
+ }
75
+
76
+ export interface SearchPagesResult {
77
+ results: PageSummary[];
78
+ }
79
+
80
+ export interface WritePageResult {
81
+ slug: string;
82
+ id: string;
83
+ revision_id?: string;
84
+ }
85
+
86
+ export interface ListPagesResult {
87
+ pages: PageSummary[];
88
+ total: number;
89
+ nextCursor?: string;
90
+ }