@a-company/paradigm 3.5.0 → 3.7.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.
@@ -3,7 +3,8 @@ import {
3
3
  getPluginUpdateNotice,
4
4
  runPluginUpdateCheck,
5
5
  schedulePluginUpdateCheck
6
- } from "./chunk-4G54C4VM.js";
6
+ } from "./chunk-YT3QWWKP.js";
7
+ import "./chunk-MO4EEYFW.js";
7
8
  export {
8
9
  getPluginUpdateNotice,
9
10
  runPluginUpdateCheck,
@@ -9,7 +9,7 @@ async function sentinelCommand(path, options) {
9
9
  const shouldOpen = options.open !== false;
10
10
  console.log(chalk.cyan("\nStarting Sentinel...\n"));
11
11
  try {
12
- const { startServer } = await import("./server-MZBYDXJY.js");
12
+ const { startServer } = await import("./server-NN7WDAZJ.js");
13
13
  console.log(chalk.gray(`Project: ${projectDir}`));
14
14
  console.log(chalk.gray(`Port: ${port}`));
15
15
  console.log();
@@ -3,7 +3,7 @@ import {
3
3
  SentinelStorage,
4
4
  loadServerConfig,
5
5
  v4_default
6
- } from "./chunk-CRICL4FQ.js";
6
+ } from "./chunk-ADOBV4PH.js";
7
7
  import {
8
8
  __commonJS,
9
9
  __require,
@@ -8952,6 +8952,8 @@ import { Router as Router6 } from "express";
8952
8952
  import { Router as Router7 } from "express";
8953
8953
  import { Router as Router8 } from "express";
8954
8954
  import { Router as Router9 } from "express";
8955
+ import { Router as Router10 } from "express";
8956
+ import { Router as Router11 } from "express";
8955
8957
  var LOG_LEVEL = process.env.SENTINEL_LOG_LEVEL || process.env.LOG_LEVEL || "info";
8956
8958
  var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
8957
8959
  function shouldLog(level) {
@@ -10017,6 +10019,197 @@ function createTracesRouter(options) {
10017
10019
  });
10018
10020
  return router;
10019
10021
  }
10022
+ function createSchemasRouter(options) {
10023
+ const router = Router10();
10024
+ const { storage } = options;
10025
+ router.post("/", (req, res) => {
10026
+ try {
10027
+ const body = req.body;
10028
+ if (!body.id || !body.version || !body.name || !body.scope || !body.eventTypes) {
10029
+ res.status(400).json({
10030
+ error: "Missing required fields: id, version, name, scope, eventTypes"
10031
+ });
10032
+ return;
10033
+ }
10034
+ if (!body.scope.field || !body.scope.type || !body.scope.label || !body.scope.ordering) {
10035
+ res.status(400).json({
10036
+ error: "Invalid scope: requires field, type, label, ordering"
10037
+ });
10038
+ return;
10039
+ }
10040
+ if (!Array.isArray(body.eventTypes) || body.eventTypes.length === 0) {
10041
+ res.status(400).json({
10042
+ error: "eventTypes must be a non-empty array"
10043
+ });
10044
+ return;
10045
+ }
10046
+ for (let i = 0; i < body.eventTypes.length; i++) {
10047
+ const et = body.eventTypes[i];
10048
+ if (!et.type || !et.category) {
10049
+ res.status(400).json({
10050
+ error: `eventTypes[${i}]: missing required fields (type, category)`
10051
+ });
10052
+ return;
10053
+ }
10054
+ }
10055
+ const schema = storage.registerSchema(body);
10056
+ res.status(201).json(schema);
10057
+ } catch (error) {
10058
+ res.status(500).json({ error: "Failed to register schema" });
10059
+ }
10060
+ });
10061
+ router.get("/", (_req, res) => {
10062
+ try {
10063
+ const schemas = storage.listSchemas();
10064
+ res.json({ count: schemas.length, schemas });
10065
+ } catch (error) {
10066
+ res.status(500).json({ error: "Failed to list schemas" });
10067
+ }
10068
+ });
10069
+ router.get("/:id", (req, res) => {
10070
+ try {
10071
+ const schema = storage.getSchema(req.params.id);
10072
+ if (!schema) {
10073
+ res.status(404).json({ error: "Schema not found" });
10074
+ return;
10075
+ }
10076
+ res.json(schema);
10077
+ } catch (error) {
10078
+ res.status(500).json({ error: "Failed to get schema" });
10079
+ }
10080
+ });
10081
+ return router;
10082
+ }
10083
+ function createEventsRouter(options) {
10084
+ const router = Router11();
10085
+ const { storage, serverConfig, onEventReceived } = options;
10086
+ let insertsSincePrune2 = 0;
10087
+ router.post("/", (req, res) => {
10088
+ try {
10089
+ const body = req.body;
10090
+ if (!body.schemaId || !body.service || !Array.isArray(body.events)) {
10091
+ res.status(400).json({
10092
+ error: "Expected { schemaId, service, events: [...] }"
10093
+ });
10094
+ return;
10095
+ }
10096
+ const { schemaId, service, events } = body;
10097
+ const schema = storage.getSchema(schemaId);
10098
+ if (!schema) {
10099
+ res.status(404).json({
10100
+ error: `Schema "${schemaId}" not found. Register it first via POST /api/schemas.`
10101
+ });
10102
+ return;
10103
+ }
10104
+ if (events.length > serverConfig.maxBatchSize) {
10105
+ res.status(413).json({
10106
+ error: `Batch too large: ${events.length} events, max ${serverConfig.maxBatchSize}`
10107
+ });
10108
+ return;
10109
+ }
10110
+ for (let i = 0; i < events.length; i++) {
10111
+ const e = events[i];
10112
+ if (!e.type) {
10113
+ res.status(400).json({
10114
+ error: `events[${i}]: missing required field "type"`
10115
+ });
10116
+ return;
10117
+ }
10118
+ }
10119
+ const result = storage.insertEventBatch(schemaId, service, events);
10120
+ if (onEventReceived) {
10121
+ const typeMap = /* @__PURE__ */ new Map();
10122
+ for (const et of schema.eventTypes) {
10123
+ typeMap.set(et.type, et.category);
10124
+ }
10125
+ for (const input of events) {
10126
+ const evt = {
10127
+ id: input.id || "",
10128
+ schemaId,
10129
+ eventType: input.type,
10130
+ category: typeMap.get(input.type) || "unknown",
10131
+ timestamp: input.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
10132
+ scopeValue: input.scopeValue != null ? String(input.scopeValue) : void 0,
10133
+ sessionId: input.sessionId,
10134
+ service,
10135
+ data: input.data,
10136
+ severity: input.severity || "info",
10137
+ parentEventId: input.parentEventId,
10138
+ depth: input.depth
10139
+ };
10140
+ onEventReceived(evt);
10141
+ }
10142
+ }
10143
+ insertsSincePrune2 += result.accepted;
10144
+ if (serverConfig.maxLogs > 0 && insertsSincePrune2 >= serverConfig.pruneIntervalInserts) {
10145
+ insertsSincePrune2 = 0;
10146
+ storage.pruneEvents(serverConfig.maxLogs);
10147
+ }
10148
+ res.json({
10149
+ accepted: result.accepted,
10150
+ errors: result.errors.length > 0 ? result.errors : void 0
10151
+ });
10152
+ } catch (error) {
10153
+ res.status(500).json({ error: "Failed to ingest events" });
10154
+ }
10155
+ });
10156
+ router.get("/", (req, res) => {
10157
+ try {
10158
+ const query = {
10159
+ schemaId: req.query.schemaId,
10160
+ eventType: req.query.eventType,
10161
+ category: req.query.category,
10162
+ service: req.query.service,
10163
+ sessionId: req.query.sessionId,
10164
+ scopeValue: req.query.scopeValue,
10165
+ scopeFrom: req.query.scopeFrom,
10166
+ scopeTo: req.query.scopeTo,
10167
+ severity: req.query.severity,
10168
+ since: req.query.since,
10169
+ until: req.query.until,
10170
+ search: req.query.search,
10171
+ limit: req.query.limit ? parseInt(req.query.limit) : 100,
10172
+ offset: req.query.offset ? parseInt(req.query.offset) : 0
10173
+ };
10174
+ const events = storage.queryEvents(query);
10175
+ const total = storage.getEventCount(query);
10176
+ res.json({ count: events.length, total, events });
10177
+ } catch (error) {
10178
+ res.status(500).json({ error: "Failed to query events" });
10179
+ }
10180
+ });
10181
+ router.get("/scopes", (req, res) => {
10182
+ try {
10183
+ const schemaId = req.query.schemaId;
10184
+ if (!schemaId) {
10185
+ res.status(400).json({ error: "schemaId query parameter is required" });
10186
+ return;
10187
+ }
10188
+ const scopes = storage.getEventScopes(schemaId, {
10189
+ limit: req.query.limit ? parseInt(req.query.limit) : 100,
10190
+ offset: req.query.offset ? parseInt(req.query.offset) : 0,
10191
+ sessionId: req.query.sessionId
10192
+ });
10193
+ res.json({ count: scopes.length, scopes });
10194
+ } catch (error) {
10195
+ res.status(500).json({ error: "Failed to query scopes" });
10196
+ }
10197
+ });
10198
+ router.get("/scope/:value", (req, res) => {
10199
+ try {
10200
+ const schemaId = req.query.schemaId;
10201
+ if (!schemaId) {
10202
+ res.status(400).json({ error: "schemaId query parameter is required" });
10203
+ return;
10204
+ }
10205
+ const events = storage.queryEventsByScope(schemaId, req.params.value);
10206
+ res.json({ count: events.length, events });
10207
+ } catch (error) {
10208
+ res.status(500).json({ error: "Failed to query scope events" });
10209
+ }
10210
+ });
10211
+ return router;
10212
+ }
10020
10213
  function createAuthMiddleware(config) {
10021
10214
  return function authMiddleware(requiredPermission) {
10022
10215
  return (req, res, next) => {
@@ -10178,6 +10371,14 @@ function createApp(options) {
10178
10371
  app.use("/api/traces", rateLimiter, auth("write"), createTracesRouter({
10179
10372
  storage: options.storage
10180
10373
  }));
10374
+ app.use("/api/schemas", rateLimiter, auth("write"), createSchemasRouter({
10375
+ storage: options.storage
10376
+ }));
10377
+ app.use("/api/events", rateLimiter, auth("write"), createEventsRouter({
10378
+ storage: options.storage,
10379
+ serverConfig: config,
10380
+ onEventReceived: options.onEventReceived
10381
+ }));
10181
10382
  }
10182
10383
  app.get("/api/health", (_req, res) => {
10183
10384
  res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
@@ -10232,12 +10433,16 @@ async function startServer(options) {
10232
10433
  });
10233
10434
  }
10234
10435
  }
10436
+ function onEventReceived(event) {
10437
+ broadcast({ type: "event", event });
10438
+ }
10235
10439
  const app = createApp({
10236
10440
  ...options,
10237
10441
  storage,
10238
10442
  serverConfig,
10239
10443
  symbolIndex,
10240
- onLogReceived
10444
+ onLogReceived,
10445
+ onEventReceived
10241
10446
  });
10242
10447
  log3.component("sentinel-server").info("Starting server", { port: options.port });
10243
10448
  log3.component("sentinel-server").info("Project directory", { path: options.projectDir });
@@ -10273,6 +10478,21 @@ async function startServer(options) {
10273
10478
  result: { logs },
10274
10479
  id: msg.id
10275
10480
  }));
10481
+ } else if (msg.method === "query_events") {
10482
+ const events = storage.queryEvents(msg.params || {});
10483
+ ws.send(JSON.stringify({
10484
+ jsonrpc: "2.0",
10485
+ result: { events },
10486
+ id: msg.id
10487
+ }));
10488
+ } else if (msg.method === "query_scopes") {
10489
+ const { schemaId, ...rest } = msg.params || {};
10490
+ const scopes = schemaId ? storage.getEventScopes(schemaId, rest) : [];
10491
+ ws.send(JSON.stringify({
10492
+ jsonrpc: "2.0",
10493
+ result: { scopes },
10494
+ id: msg.id
10495
+ }));
10276
10496
  }
10277
10497
  } catch {
10278
10498
  }
@@ -16,7 +16,7 @@ import {
16
16
  } from "./chunk-M2XMTJHQ.js";
17
17
  import {
18
18
  initCommand
19
- } from "./chunk-IRVA7NKV.js";
19
+ } from "./chunk-BC6XKMUA.js";
20
20
  import {
21
21
  indexCommand
22
22
  } from "./chunk-UI3XXVJ6.js";
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ completeTask,
4
+ createTask,
5
+ loadTask,
6
+ loadTasks,
7
+ rebuildTaskIndex,
8
+ shelveTask,
9
+ updateTask
10
+ } from "./chunk-CSD7IHSN.js";
11
+ import "./chunk-MO4EEYFW.js";
12
+ export {
13
+ completeTask,
14
+ createTask,
15
+ loadTask,
16
+ loadTasks,
17
+ rebuildTaskIndex,
18
+ shelveTask,
19
+ updateTask
20
+ };
@@ -6,10 +6,10 @@ import {
6
6
  StatsCalculator,
7
7
  TimelineBuilder,
8
8
  loadAllSeedPatterns
9
- } from "./chunk-2E2RTBSM.js";
9
+ } from "./chunk-GY5KO3YZ.js";
10
10
  import {
11
11
  SentinelStorage
12
- } from "./chunk-CRICL4FQ.js";
12
+ } from "./chunk-ADOBV4PH.js";
13
13
  import "./chunk-MO4EEYFW.js";
14
14
 
15
15
  // src/commands/triage/index.ts
@@ -679,6 +679,116 @@
679
679
  "explanation": "Zero-access aspects are candidates for review, not automatic deletion. Some may be legitimate but poorly named (rename to improve discoverability). Some may be truly stale with drifted anchors (remove or update). Some may have been superseded by newer aspects (consolidate with supersedes edges). The governance review evaluates each case individually."
680
680
  }
681
681
  ]
682
+ },
683
+ {
684
+ "id": "task-management",
685
+ "title": "Task Management",
686
+ "content": "## Why Tasks Exist\n\nAI agent sessions are stateless. You can discuss a plan, identify five things that need doing, and then the session ends. The next session starts blank — those five items are gone. Sticky notes on a monitor do not help when your developer is a language model.\n\nParadigm's Task Management system provides a persistent scratch pad that survives context windows. Tasks are lightweight, date-partitioned YAML entries that capture what needs doing, how urgent it is, and what project knowledge relates to it. They are not a full project management system — they are the missing short-term memory between sessions.\n\nThe key difference from lore: lore records what happened (past tense). Tasks record what should happen (future tense). Together they form a complete timeline — memory of the past and intention for the future.\n\n## Anatomy of a Task\n\nEvery task follows a consistent structure:\n\n```yaml\nid: T-2026-02-26-001\nblurb: \"Add rate limiting to the /api/payments endpoint\"\npriority: high\nstatus: open\ntags: [security, payments]\nrelated_lore: [L-2026-02-25-003]\ncreated: \"2026-02-26T10:15:00Z\"\nupdated: \"2026-02-26T10:15:00Z\"\n```\n\nThe `id` field is auto-generated: `T-{date}-{sequence}`, following the same date-partitioned pattern as lore entries. The `blurb` is the only required field — a concise description of what needs to be done. Everything else is optional but useful.\n\nThree priority levels exist: `high` (do this soon), `medium` (do this eventually), and `low` (nice to have). Tasks without an explicit priority default to `medium`.\n\nThree statuses track lifecycle: `open` (needs doing), `done` (completed), and `shelved` (parked for later — not abandoned, just deferred).\n\n## Storage: Date-Partitioned YAML\n\nTasks live in `.paradigm/tasks/entries/` organized by creation date:\n\n```\n.paradigm/tasks/\n entries/\n 2026-02-25/\n T-2026-02-25-001.yaml\n T-2026-02-25-002.yaml\n 2026-02-26/\n T-2026-02-26-001.yaml\n```\n\nDate partitioning keeps directories small. Each task is a standalone YAML file, making them easy to read, edit, and version-control. The date in the path matches the date in the task ID.\n\n## The Five MCP Tools\n\n**`paradigm_task_create`** — Create a new task. The `blurb` field is required — a short description of what needs to be done. Optional fields include `priority` (high/medium/low), `tags` (for categorization and filtering), and `related_lore` (linking to lore entries that provide context). The task is written to the correct date directory with an auto-incremented ID and starts with status `open`.\n\n**`paradigm_task_list`** — List tasks with filters. Filter by `status` (open/done/shelved/all), `priority` (high/medium/low), or `tag`. Results are sorted by priority (high first) then by date (newest first). Without filters, it returns all open tasks.\n\n**`paradigm_task_update`** — Update any field on an existing task by ID. You can change the blurb, priority, status, tags, or related_lore. Only specified fields are modified — everything else is preserved.\n\n**`paradigm_task_done`** — Shorthand to mark a task as complete. Pass the task ID and the status changes to `done` with an updated timestamp. This is equivalent to `paradigm_task_update` with `status: done` but more ergonomic for the common case.\n\n**`paradigm_task_shelve`** — Shorthand to shelve a task for later. Pass the task ID and the status changes to `shelved`. Shelved tasks are not deleted — they remain searchable and can be reopened by updating their status back to `open`.\n\n## Session Recovery Integration\n\nThe top 5 open tasks are automatically surfaced during session recovery. When a new session starts and the agent calls any Paradigm MCP tool, the recovery data includes the highest-priority open tasks alongside the usual breadcrumbs and checkpoint data.\n\nThis means every session begins with awareness of outstanding work. The agent does not need to ask \"what should I work on?\" — the task list is already there, sorted by priority. This is the scratch-pad-that-survives pattern: write tasks in one session, see them in the next.\n\n## When to Create Tasks\n\nCreate tasks when:\n- You identify work that cannot be completed in the current session\n- A code review surfaces follow-up items\n- You discover a bug or improvement while working on something else\n- The user mentions something that should be tracked but is not the current focus\n- A handoff needs to communicate specific next steps\n\nDo not use tasks for:\n- Tracking completed work (that is what lore is for)\n- Long-term roadmap items (use your project management tool)\n- Architectural decisions (use lore entries with type `decision`)\n\nTasks are ephemeral intentions — they should be created quickly, completed or shelved promptly, and never allowed to accumulate into a backlog of hundreds. If your task list grows beyond 20-30 open items, it is time to triage: shelve the low-priority items and focus on what matters.",
687
+ "keyConcepts": [
688
+ "Tasks are persistent scratch-pad items that survive context windows",
689
+ "Auto-generated IDs: T-{date}-{sequence}, date-partitioned in .paradigm/tasks/entries/{YYYY-MM-DD}/",
690
+ "Three priority levels: high, medium (default), low",
691
+ "Three statuses: open, done, shelved",
692
+ "Five MCP tools: paradigm_task_create, paradigm_task_list, paradigm_task_update, paradigm_task_done, paradigm_task_shelve",
693
+ "Top 5 open tasks surfaced automatically on session recovery",
694
+ "Tasks record future intention; lore records past action"
695
+ ],
696
+ "quiz": [
697
+ {
698
+ "id": "q1",
699
+ "question": "You are midway through adding authentication when you notice the payment service has a null-check bug. You cannot fix it now. What is the correct action?",
700
+ "choices": {
701
+ "A": "Record a lore entry describing the bug for future reference",
702
+ "B": "Call `paradigm_task_create` with a blurb describing the null-check bug, priority high, and tags [bug, payments]",
703
+ "C": "Add a TODO comment in the payment service code and move on",
704
+ "D": "Call `paradigm_sentinel_record` to log it as an incident",
705
+ "E": "Mention it in the session's handoff summary and hope the next session picks it up"
706
+ },
707
+ "correct": "B",
708
+ "explanation": "This is exactly what tasks are for — capturing work you cannot complete in the current session. A task with priority `high` ensures it surfaces at the top of the next session's recovery data. A lore entry (A) records what happened, not what needs to happen. A TODO comment (C) is invisible to Paradigm. Sentinel (D) is for production incidents. A handoff mention (E) is fragile — tasks are persistent."
709
+ },
710
+ {
711
+ "id": "q2",
712
+ "question": "A new session starts. The agent calls `paradigm_status`. Among the recovery data, it sees 3 high-priority tasks and 12 medium-priority tasks. How were these surfaced?",
713
+ "choices": {
714
+ "A": "The agent must explicitly call `paradigm_task_list` to see tasks — they are not in recovery data",
715
+ "B": "All 15 tasks appear in the recovery data automatically",
716
+ "C": "The top 5 open tasks (sorted by priority then date) are automatically included in recovery data on the first Paradigm tool call",
717
+ "D": "Only high-priority tasks are surfaced during recovery",
718
+ "E": "Tasks are only surfaced if the previous session created a handoff"
719
+ },
720
+ "correct": "C",
721
+ "explanation": "Session recovery automatically includes the top 5 open tasks, sorted by priority (high first) then by date (newest first). So the agent would see the 3 high-priority tasks plus 2 of the most recent medium-priority tasks. The remaining 10 medium-priority tasks are available via `paradigm_task_list` but are not in the initial recovery data."
722
+ },
723
+ {
724
+ "id": "q3",
725
+ "question": "Your task list has grown to 35 open items. What should you do?",
726
+ "choices": {
727
+ "A": "Delete the oldest tasks to keep the list manageable",
728
+ "B": "Increase the session recovery limit from 5 to 35 so all tasks are visible",
729
+ "C": "Triage: shelve low-priority items with `paradigm_task_shelve` and focus on what matters — tasks should not accumulate into a large backlog",
730
+ "D": "Convert all tasks to lore entries since the list is too long for task management",
731
+ "E": "Tasks have no practical limit — 35 is fine, just filter by priority when working"
732
+ },
733
+ "correct": "C",
734
+ "explanation": "Tasks are meant to be a lightweight scratch pad, not a project management backlog. When the list grows beyond 20-30 items, it is time to triage. Use `paradigm_task_shelve` to park items that are not immediately relevant. Shelved tasks are not deleted — they remain searchable and can be reopened. This keeps the active list focused and the session recovery data meaningful."
735
+ }
736
+ ]
737
+ },
738
+ {
739
+ "id": "assessment-loops",
740
+ "title": "Assessment Loops",
741
+ "content": "## The Three Layers of Project Memory\n\nParadigm captures project knowledge at three distinct layers, each with a different nature, author, and granularity:\n\n| Layer | Nature | Created by | Granularity |\n|---|---|---|---|\n| Commits | Raw facts | Git | Per-change |\n| Lore | Session events | Agent + user | Per-session |\n| Assessments | Synthesized insight | Agent (mostly) + user | Per-arc milestone |\n\nCommits record what changed in the code. Lore records what happened in a session — the context, decisions, and learnings. Assessments synthesize patterns across multiple sessions and commits into higher-order insights: retrospectives, strategic decisions, and milestone reflections.\n\nAssessments reference downward. An assessment entry can link to specific lore entries and commits, creating a hierarchy: assessments summarize lore, lore summarizes commits. This layering means you can zoom in from a high-level insight to the specific session that produced it, and from there to the exact code change.\n\n## Arcs: Grouping Related Assessments\n\nAssessments are organized into arcs. An arc is a named grouping that represents a theme, project phase, or ongoing concern — think of it as a folder for related reflections.\n\n```yaml\nid: arc-auth-hardening\nname: Authentication Hardening\ndescription: Assessments related to the Q1 auth security initiative\nstatus: active\ncreated: \"2026-02-01T09:00:00Z\"\n```\n\nArc IDs use kebab-case with an `arc-` prefix: `arc-auth-hardening`, `arc-performance-tuning`, `arc-v2-migration`. The status is either `active` (ongoing), `complete` (finished successfully), or `archived` (closed without completion).\n\nArcs are stored at `.paradigm/assessments/arcs/{arc-id}/arc.yaml`, with their entries in a sibling `entries/` directory:\n\n```\n.paradigm/assessments/\n arcs/\n arc-auth-hardening/\n arc.yaml\n entries/\n A-2026-02-10-001.yaml\n A-2026-02-15-002.yaml\n arc-performance-tuning/\n arc.yaml\n entries/\n A-2026-02-12-001.yaml\n```\n\n## Anatomy of an Assessment Entry\n\nEach assessment entry captures a synthesized reflection:\n\n```yaml\nid: A-2026-02-15-002\narc: arc-auth-hardening\ntype: retro\ntimestamp: \"2026-02-15T16:00:00Z\"\ntitle: \"JWT refresh token rotation — what we learned\"\ncontent: |\n After three sessions implementing refresh token rotation,\n the key insight is that storing refresh tokens in httpOnly\n cookies eliminates an entire class of XSS vulnerabilities.\n The initial approach of localStorage was abandoned after\n session L-2026-02-12-001 surfaced the risk.\nsymbols: [\"#refresh-token-handler\", \"^authenticated\"]\nrelated_lore: [L-2026-02-10-003, L-2026-02-12-001, L-2026-02-14-002]\nrelated_commits: [a1b2c3d, e4f5g6h]\ntags: [security, auth, jwt]\n```\n\nEntry IDs follow the pattern `A-{YYYY-MM-DD}-{NNN}` and are globally unique across all arcs. The sequence number increments globally, not per-arc, so you will never have two entries with the same ID in different arcs.\n\nFour entry types capture different kinds of synthesized insight:\n\n| Type | When to Use |\n|---|---|\n| `retro` | Looking back at a completed piece of work — what went well, what did not, what to change |\n| `insight` | A realization or pattern discovered across multiple sessions |\n| `decision` | A strategic or architectural decision with rationale and alternatives considered |\n| `milestone` | A significant achievement or phase completion worth marking |\n\n## The Six MCP Tools\n\n**`paradigm_assessment_record`** — Add a reflection to an arc. If the arc does not exist, it is auto-created from the arc ID. Pass `arc` (the arc ID), `type` (retro/insight/decision/milestone), `title`, `content`, and optionally `symbols`, `related_lore`, `related_commits`, and `tags`. The entry is written with an auto-generated globally unique ID.\n\n**`paradigm_assessment_list`** — List arcs or entries within an arc. Without an arc ID, it returns all arcs with their status and entry count. With an arc ID, it returns entries within that arc sorted by date.\n\n**`paradigm_assessment_get`** — Fetch full detail for a single entry (pass an `A-*` ID) or a full arc with all its entries (pass an `arc-*` ID). This is the deep-dive tool — use it when you need the complete content and all linked references.\n\n**`paradigm_assessment_search`** — Cross-arc search with filters: by symbol, tag, entry type, and date range. This searches across all arcs simultaneously, making it easy to find every retrospective that mentions `#payment-service` regardless of which arc it lives in.\n\n**`paradigm_assessment_arc_create`** — Explicitly create an arc with a name, description, and initial status. While `paradigm_assessment_record` auto-creates arcs, this tool lets you set up an arc with a proper description before adding entries to it.\n\n**`paradigm_assessment_arc_close`** — Mark an arc as `complete` or `archived`. Complete arcs represent finished initiatives. Archived arcs represent work that was abandoned or superseded. Closed arcs remain searchable — closing is a status change, not a deletion.\n\n## The Assessment Feedback Loop\n\nAssessments do not exist in isolation. They connect to the rest of Paradigm through two feedback mechanisms:\n\n**Downward references.** Every assessment entry can link to specific lore entries (`related_lore`) and commits (`related_commits`). This creates traceability: an insight about authentication can point to the three sessions that produced it and the five commits that implemented it. When someone reads the assessment, they can drill down to the raw evidence.\n\n**Task-to-assessment nudge.** When a task is marked as complete via `paradigm_task_done`, the system nudges the agent to consider recording an assessment. Completing a task is a natural reflection point — was this harder than expected? Did we learn something? Should we change our approach? The nudge is advisory, not mandatory, but it creates a habit of reflection tied to completion.\n\n## When to Record Assessments\n\nAssessments are higher-order than lore. While lore should be recorded for any session that modifies 3+ files, assessments should be recorded at natural reflection points:\n\n- **After completing a multi-session feature** — What did we learn across the sessions? (retro)\n- **When a pattern emerges across unrelated work** — \"Every time we touch the auth system, we find expired token edge cases\" (insight)\n- **When making a decision that affects future work** — \"We are switching from REST to GraphQL for the admin API\" (decision)\n- **When reaching a project milestone** — \"v2.0 shipped to production\" (milestone)\n\nThe general rule: if the knowledge would be valuable in 3 months, it belongs in an assessment. If it is only relevant for the next session, a task or lore entry is sufficient.\n\n## Arc Lifecycle\n\nArcs follow a natural lifecycle:\n\n1. **Creation** — An arc is created (explicitly or auto-created) when a theme emerges. The name should be descriptive: `arc-auth-hardening`, not `arc-misc`.\n2. **Active collection** — Entries accumulate as work progresses. Retros after implementations, insights as patterns emerge, decisions when direction changes.\n3. **Closure** — When the initiative is complete or the theme is no longer active, close the arc with `paradigm_assessment_arc_close`. Use `complete` for successful initiatives, `archived` for abandoned ones.\n4. **Reference** — Closed arcs remain fully searchable. Future sessions can reference them when similar themes arise. The arc becomes part of institutional memory.\n\nWell-managed arcs have 5-15 entries over weeks or months. An arc with 1 entry should probably be merged into a broader arc. An arc with 50+ entries should probably be split into sub-themes.",
742
+ "keyConcepts": [
743
+ "Three-layer model: commits (raw facts) -> lore (session events) -> assessments (synthesized insight)",
744
+ "Arcs group related assessments by theme or initiative with arc-{kebab-case} IDs",
745
+ "Entry IDs are globally unique: A-{YYYY-MM-DD}-{NNN}, not scoped per arc",
746
+ "Four entry types: retro, insight, decision, milestone",
747
+ "Six MCP tools: paradigm_assessment_record, paradigm_assessment_list, paradigm_assessment_get, paradigm_assessment_search, paradigm_assessment_arc_create, paradigm_assessment_arc_close",
748
+ "Assessments reference downward — linking to lore entries and commits for traceability",
749
+ "Task completion nudges assessment recording as a natural reflection point"
750
+ ],
751
+ "quiz": [
752
+ {
753
+ "id": "q1",
754
+ "question": "You have completed a three-session effort to add rate limiting. Across those sessions, you recorded three lore entries and completed two tasks. What is the most appropriate assessment entry type and why?",
755
+ "choices": {
756
+ "A": "`milestone` — completing a multi-session feature is always a milestone",
757
+ "B": "`retro` — this is a natural reflection point after completing a body of work, capturing what went well and what to change",
758
+ "C": "`decision` — you decided to use rate limiting, so record the decision",
759
+ "D": "`insight` — you gained insight into rate limiting through three sessions",
760
+ "E": "`retro` and `milestone` — record both entries in the same arc"
761
+ },
762
+ "correct": "B",
763
+ "explanation": "A retro is the right type for reflecting on completed work — it captures what happened across sessions, what went well, what did not, and what to change next time. A milestone (A) marks a significant project event like a release, not a feature completion. A decision (C) would be appropriate if the assessment focused on choosing the rate limiting strategy, but the question asks about the post-implementation reflection. Recording both (E) could be valid but the retro is the primary and most appropriate single entry."
764
+ },
765
+ {
766
+ "id": "q2",
767
+ "question": "An assessment entry has `related_lore: [L-2026-02-10-003, L-2026-02-12-001]` and `related_commits: [a1b2c3d]`. What does this downward referencing enable?",
768
+ "choices": {
769
+ "A": "It automatically updates the lore entries and commits with a backlink to the assessment",
770
+ "B": "It creates traceability — readers can drill from the synthesized insight down to the specific sessions that produced it and the exact code changes",
771
+ "C": "It prevents the referenced lore entries from being deleted",
772
+ "D": "It triggers Sentinel to check those commits for incidents",
773
+ "E": "It merges the lore entries into the assessment content automatically"
774
+ },
775
+ "correct": "B",
776
+ "explanation": "Downward references create a traceability chain: assessment -> lore -> commits. A reader encountering the insight can follow the `related_lore` links to see the full session context, and the `related_commits` links to see the exact code changes. This layered referencing is the core design of the three-layer model — each layer summarizes the one below it, with links to drill down for detail."
777
+ },
778
+ {
779
+ "id": "q3",
780
+ "question": "You want to find every retrospective that mentions `#payment-service` across all arcs in your project. Which tool and approach is correct?",
781
+ "choices": {
782
+ "A": "`paradigm_assessment_list` for each arc, then manually filter entries mentioning the symbol",
783
+ "B": "`paradigm_assessment_search` with `symbol: '#payment-service'` and `type: 'retro'` — it searches across all arcs simultaneously",
784
+ "C": "`paradigm_assessment_get` with the arc ID containing payment-related work",
785
+ "D": "`paradigm_lore_search` with `symbol: '#payment-service'` — lore and assessments share the same search",
786
+ "E": "`paradigm_search` with `query: 'payment retro'` — the general search covers assessments"
787
+ },
788
+ "correct": "B",
789
+ "explanation": "`paradigm_assessment_search` is designed for cross-arc queries with filters. Passing `symbol: '#payment-service'` and `type: 'retro'` returns all retrospective entries that reference the payment service, regardless of which arc they belong to. Listing per-arc (A) works but is tedious. `paradigm_lore_search` (D) only searches lore entries, not assessments — they are separate systems. `paradigm_search` (E) searches the symbol index, not assessment content."
790
+ }
791
+ ]
682
792
  }
683
793
  ]
684
794
  }
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "version": "3.0",
3
3
  "frameworkVersion": "2.0",
4
- "timeLimit": 5160,
5
- "totalSlots": 86,
4
+ "timeLimit": 5400,
5
+ "totalSlots": 90,
6
6
  "passThreshold": 0.8,
7
7
  "title": "The PLSAT \u2014 Paradigm Licensure Standardized Assessment Test",
8
- "description": "86 questions. 86 minutes. 80% to pass. Good luck, scholar.",
8
+ "description": "90 questions. 90 minutes. 80% to pass. Good luck, scholar.",
9
9
  "items": [
10
10
  {
11
11
  "type": "standalone",
@@ -2195,6 +2195,90 @@
2195
2195
  "explanation": "A 63% concentration in the `rule` category (95 out of 150) suggests over-classification. Many numeric limits (which should be constraints) and architectural choices (which should be decisions) may be categorized as rules. The low decision count (12) is a red flag \u2014 a project with 150 aspects likely made more than 12 strategic decisions. The governance review should reclassify mistyped aspects and document missing decisions. Zero-access aspects (40) are a separate concern requiring individual evaluation."
2196
2196
  }
2197
2197
  ]
2198
+ },
2199
+ {
2200
+ "type": "standalone",
2201
+ "slot": "slot-090",
2202
+ "course": "para-501",
2203
+ "variants": [
2204
+ {
2205
+ "id": "plsat-090",
2206
+ "scenario": "Your team uses Paradigm task management to track work across context windows. An agent creates three tasks on 2026-03-10: a high-priority auth bug, a medium-priority docs update, and a low-priority refactor. Later that day, a fourth task is created. The next morning (2026-03-11), a new agent session starts and calls `paradigm_session_recover`.",
2207
+ "question": "What task ID is assigned to the fourth task on 2026-03-10, and how are tasks surfaced in the new session?",
2208
+ "choices": {
2209
+ "A": "T-2026-03-10-004. All four tasks are fully displayed in the recovery payload.",
2210
+ "B": "T-2026-03-10-004. The top 5 open tasks sorted by priority are surfaced in the session recovery, so all four appear (high first, then medium, then low).",
2211
+ "C": "T-004. Only tasks explicitly pinned to the session are recovered.",
2212
+ "D": "T-2026-03-11-001. Task IDs reset per calendar day, so recovery re-numbers them.",
2213
+ "E": "T-2026-03-10-004. Tasks are not surfaced automatically \u2014 the agent must call `paradigm_task_list` to see them."
2214
+ },
2215
+ "correct": "B",
2216
+ "explanation": "Task IDs follow the format T-YYYY-MM-DD-NNN with per-date sequential numbering, so the fourth task on 2026-03-10 is T-2026-03-10-004. Tasks are designed to survive context windows \u2014 on session recovery, the top 5 open tasks (sorted by priority: high > medium > low) are automatically surfaced. Since there are only four open tasks, all four appear. This ensures continuity without requiring the agent to manually query task state."
2217
+ }
2218
+ ]
2219
+ },
2220
+ {
2221
+ "type": "standalone",
2222
+ "slot": "slot-091",
2223
+ "course": "para-501",
2224
+ "variants": [
2225
+ {
2226
+ "id": "plsat-091",
2227
+ "scenario": "An agent finishes a debugging session and wants to record the root cause and resolution as a lasting insight. The team already has an assessment arc called `arc-auth-hardening` tracking authentication improvements. The agent calls `paradigm_assessment_record` with `type: \"insight\"`, `arc_id: \"arc-auth-hardening\"`, and a summary of the fix. Three entries already exist across all arcs today (2026-04-02).",
2228
+ "question": "What entry ID is assigned, and where is the file stored?",
2229
+ "choices": {
2230
+ "A": "A-2026-04-02-001 stored in `.paradigm/assessments/arcs/arc-auth-hardening/entries/A-2026-04-02-001.yaml` \u2014 entry IDs are sequential per arc.",
2231
+ "B": "A-2026-04-02-004 stored in `.paradigm/assessments/arcs/arc-auth-hardening/entries/A-2026-04-02-004.yaml` \u2014 entry IDs are globally unique across all arcs.",
2232
+ "C": "I-2026-04-02-004 stored in `.paradigm/assessments/insights/I-2026-04-02-004.yaml` \u2014 insights have their own prefix and directory.",
2233
+ "D": "A-auth-hardening-004 stored in `.paradigm/assessments/entries/A-auth-hardening-004.yaml` \u2014 entry IDs embed the arc name.",
2234
+ "E": "A-2026-04-02-004 stored in `.paradigm/assessments/entries/2026-04-02/A-2026-04-02-004.yaml` \u2014 entries are grouped by date, not arc."
2235
+ },
2236
+ "correct": "B",
2237
+ "explanation": "Assessment entry IDs follow the format A-YYYY-MM-DD-NNN where NNN is globally unique across ALL arcs \u2014 the ID generator scans every arc's entries directory to determine the next sequence number. Since three entries already exist today, this one gets A-2026-04-02-004. The file is stored under the arc's own entries directory at `.paradigm/assessments/arcs/arc-auth-hardening/entries/A-2026-04-02-004.yaml`. This global uniqueness guarantees that an entry ID alone is sufficient to locate any assessment entry without knowing which arc it belongs to."
2238
+ }
2239
+ ]
2240
+ },
2241
+ {
2242
+ "type": "standalone",
2243
+ "slot": "slot-092",
2244
+ "course": "para-501",
2245
+ "variants": [
2246
+ {
2247
+ "id": "plsat-092",
2248
+ "scenario": "A developer asks their agent to record a retrospective about a failed deployment. The agent calls `paradigm_assessment_record` with `type: \"retro\"`, `arc_name: \"Q1 Platform Stability\"`, but does NOT provide an `arc_id`. No arc for platform stability exists yet.",
2249
+ "question": "What happens when `arc_name` is provided without an `arc_id` to `paradigm_assessment_record`?",
2250
+ "choices": {
2251
+ "A": "The call fails with an error \u2014 `arc_id` is always required when recording an entry.",
2252
+ "B": "The entry is recorded as an orphan in `.paradigm/assessments/unassigned/`.",
2253
+ "C": "The tool auto-creates a new arc (deriving an arc_id like `arc-q1-platform-stability` from the name) and records the entry inside it.",
2254
+ "D": "The tool searches existing arcs by name and fails if no exact match is found.",
2255
+ "E": "The entry is recorded with a temporary arc_id that must be manually linked later."
2256
+ },
2257
+ "correct": "C",
2258
+ "explanation": "When `arc_name` is provided without an `arc_id`, `paradigm_assessment_record` auto-creates the arc. The arc_id is derived by converting the name to kebab-case and prefixing with `arc-` (validated against `/^arc-[a-z0-9-]+$/`). This creates the arc directory structure at `.paradigm/assessments/arcs/arc-q1-platform-stability/` with an `arc.yaml` and the entry in its `entries/` subdirectory. This convenience behavior reduces friction \u2014 agents can record assessments without a separate arc creation step."
2259
+ }
2260
+ ]
2261
+ },
2262
+ {
2263
+ "type": "standalone",
2264
+ "slot": "slot-093",
2265
+ "course": "para-501",
2266
+ "variants": [
2267
+ {
2268
+ "id": "plsat-093",
2269
+ "scenario": "Your project has been running for six months. The codebase has 200+ commits (raw facts in git), 45 lore entries (session-level events captured by the post-commit hook), and 12 assessment entries across 3 arcs (synthesized insights from retrospectives and decisions). A new team member asks how these three layers relate to each other.",
2270
+ "question": "Which statement BEST describes Paradigm's three-layer knowledge model?",
2271
+ "choices": {
2272
+ "A": "Commits, lore, and assessments are independent systems \u2014 they share no data and serve different audiences.",
2273
+ "B": "Lore replaces commit messages with richer context, and assessments replace lore when insights are finalized.",
2274
+ "C": "Commits capture raw facts (what changed), lore captures session-level events (what happened and why), and assessments synthesize cross-session insights (what we learned) \u2014 each layer builds on the one below it.",
2275
+ "D": "Assessments are the authoritative layer \u2014 commits and lore are temporary and should be pruned once assessments exist.",
2276
+ "E": "Lore and assessments are both subsets of the commit history, just filtered by different criteria."
2277
+ },
2278
+ "correct": "C",
2279
+ "explanation": "Paradigm's three-layer knowledge model is: Commits (raw facts \u2014 what code changed, captured by git), Lore (session events \u2014 what happened during a work session and why, captured by the post-commit hook into `.paradigm/history/`), and Assessments (synthesized insight \u2014 what the team learned across multiple sessions, recorded into `.paradigm/assessments/`). Each layer adds interpretation: commits are mechanical diffs, lore adds narrative context, and assessments distill patterns and decisions. All three layers persist and remain valuable \u2014 none replaces or supersedes the others."
2280
+ }
2281
+ ]
2198
2282
  }
2199
2283
  ]
2200
2284
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a-company/paradigm",
3
- "version": "3.5.0",
3
+ "version": "3.7.0",
4
4
  "description": "Unified CLI for Paradigm developer tools",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",