Repository navigation
v3.0.0
·
57 commits
to develop
since this release
Immutable
release. Only release title and notes can be modified.
Breaking
- The lean
codeprofile is the default. A server with no setting lists 6 tools (find_symbol,get_file_context,who_imports,exports_in,search_files,query): about 850 tokens of tool definitions per model call instead of about 12,300. Every positive benchmark run used this profile. The old default, 97 tools (toolsetscode,code-admin,scrum), is now thefullprofile. (#178) - The sprint process is opt-in.
CODE_CONTEXT_PROFILEpicks the profile:code(default),fullorall(103 tools, addsfunandstream).CODE_CONTEXT_TOOLSETSstill adds single toolsets (code-admin,scrum,fun,stream,all); it only adds and never removes a toolset of the profile. Withoutfull, the 2.x meaning ofCODE_CONTEXT_TOOLSETS=fun,stream(the default plusfunandstream) becomescodeplusfunandstream. - Flair follows the sprint tools. The flair header and footer are on when the
scrumtoolset is on (full,all) and off otherwise, so they are off by default now.OVERDRIVE_FLAIR=0/1still forces it. - Two entry points in one package: the code index and the sprint process.
code-context(dist/server/index.js) is the index: its tool contract is frozen, with no setting it lists the 6codetools.code-context-sprint(dist/server/sprint.js) loads the same server withCODE_CONTEXT_PROFILE=fullas its default (97 tools); the scrum, bridge and dump/restore tools and the dashboard ride with it. A knownCODE_CONTEXT_PROFILEstill wins on both entries. (#179) - The browser logs in to the dashboard with an HttpOnly cookie from a one-time launch URL. The page no longer carries the token:
window.__DASHBOARD_TOKEN__is gone, andGET /without a session answers 401 with a short page that says how to log in. The dashboard prints a launch URL at startup (http://localhost:<port>/auth/launch?code=<random>); a bearer holder mints a fresh one withPOST /api/auth/launch, andnode scripts/dashboard-launch-url.mjs <db>does that after the/healthnonce check from #246. A code works once and expires after 5 minutes. Opening it starts a session with a random 32-byte id, held in dashboard memory only, and setscc_dashboard_session_<port>(HttpOnly; SameSite=Strict; Path=/; Max-Age=43200), then redirects to/. A session ends after 12 h, onPOST /api/auth/logout, or when the dashboard restarts. The cookie never holds the token: cookies reach every localhost port, so another local service that sees it gets one session for at most 12 h, not the token./api/eventstakes the cookie or theAuthorizationheader;?token=is refused with 401. Because a cookie reaches every localhost port, a cookie-authenticated write must send the exact dashboardOrigin, and a request the browser marksSec-Fetch-Site: same-siteorcross-siteis refused. The bearer header keeps working for the MCP server, the hooks and scripts (TM-01, TM-02). (#243) - The index entry no longer creates, migrates or seeds the scrum tables. Under the
codeprofile it openscontext.dbwith the code-index tables only and never loadssrc/scrum,src/dashboardorsrc/bridge. A sprint toolset (full,all, orCODE_CONTEXT_TOOLSETSwithscrum,funorstream) loads them throughsrc/server/sprint-toolsets.ts.test/boundary.test.tsfails on any other import fromsrc/serverinto the sprint side. (#179) - The
custombridge action is gone.POST /api/bridge/actionsanswers 400 forcustom, and each forwarded action needs a payload that matches its schema; see Security. (#244)
Added
healthreportsprofileandtoolsets, andignored_settingsfor a value the server did not understand. The server logs the profile at start and a WARN line for an ignored value.setupwrites the profile into.mcp.json. It setsCODE_CONTEXT_PROFILE=fullin thecode-contextentryenv, because setup installs the sprint process.--profile code|full|allpicks another one. An existingCODE_CONTEXT_PROFILEis kept, a 2.xCODE_CONTEXT_TOOLSETS=codestays lean, and other keys of the entry (env,type, …) are no longer dropped.--nameand--profilevalues are no longer taken as the target path./kickoffchecks the profile first. Step 0a callshealthand stops with the exact setting when thescrumorcode-admintoolset is off. It names thecode-context-sprintentry as the fix.- New bins
code-context,code-context-sprintandvlm-code-context-mcp.vlm-code-context-mcpis an alias ofcode-context, sonpx -y vlm-code-context-mcp ./context.dbstarts the index.code-context-mcpstays the setup command. (#179) setupinstalls the entry that matches the profile.fullandallwritedist/server/sprint.jsinto.mcp.json,codewritesdist/server/index.js. Setup still writesCODE_CONTEXT_PROFILE: the setting is explicit,healthshows it, and the entry still works when pointed atindex.js. (#179)- CI runs on pull requests to release branches (
release/**), not only onmain. (#179) - The public contract is written down and CI enforces it.
docs/CONTRACT.mdfreezes the 3.x surface: the bins and their flags, the profiles and env settings, the names, input schemas and default output of the 6codetools, the names and input schemas of the sprint tools (their output is best effort), the server instructions and thecontext.dbrules (who owns which table; a minor release may add tables and columns, never drop or rename). Snapshots intest/contract/snapshots/pin it;npm run contract:updaterewrites them. The new CI jobcontractfails when a PR changes a snapshot without a major version bump inpackage.jsonor the labelcontract-change-approved(scripts/check-contract-change.mjs). A change to a tool description only is not a contract change. (#180)
Migration
- An install that uses the sprint tools sets
CODE_CONTEXT_PROFILE=fullin the serverenv(.mcp.json→mcpServers["code-context"].env, or--mcp-config, or the shell).npx code-context-mcp setup .does it for a project: in update mode it writesfullwhen the entry has no profile setting yet. CODE_CONTEXT_TOOLSETS=codekeeps working and changes nothing now; remove it when you like./kickoffin an existing project keeps its old copy (setup never overwrites a command file). Delete.claude/commands/kickoff.mdand run setup again to get Step 0a.- Pick an entry (#179).
code-context-sprintis canonical for the sprint process,code-contextfor the index:- You use the sprint process: change the command to
code-context-sprint(or the args to…/dist/server/sprint.js <db>).npx code-context-mcp setup .does it in update mode. - You only read code: keep
dist/server/index.js(nowcode-context) and removeCODE_CONTEXT_PROFILE. You get the lean profile. - An existing
.mcp.jsonwithdist/server/index.jsandCODE_CONTEXT_PROFILE=fullkeeps working in 3.x: the index entry loads the sprint toolsets when the profile asks for them.
- You use the sprint process: change the command to
- Open the dashboard through a launch URL (#243). Plain
http://localhost:3333no longer logs a browser in. Run/ccui, or open the launch URL the dashboard prints at startup; mint a new one withnode node_modules/vlm-code-context-mcp/scripts/dashboard-launch-url.mjs <db>. A script that used/api/events?token=sendsAuthorization: Bearer <token>instead. A session lasts 12 h; after that, or after a dashboard restart, run/ccuiagain. - One shared
context.db. Both entries open the same file. The index entry owns the code-index tables (files,exports,dependencies,directories,changes,indexed_repos); the sprint entry owns the scrum tables and theschema_versionsstamp. A database the index entry created gets its scrum tables, stamped at the latest version, on the first start of the sprint entry. Both entries refuse a database stamped by a newer version.
Fixed
- The live bench scores only the final answer block. The task checkers read the answer from the last fenced code block (or the text after an
Answer:/Result:/Final answer:line), so a name a session mentions in explanation prose no longer counts as a wrong answer;--score-proserestores the old behaviour for comparison. The tool counter is the server'scodeprofile (now includingwho_importsandexports_in) instead of a hand-kept list, andnpm run bench:rescoreprints the old and new success rate per arm. (#261) - The VitePress docs build passes again. The three dead links that failed
npm run buildindocs/—../READMEindocs/ARCHITECTURE.mdanddocs/REFERENCE.md,../BENCHMARK-GUIDEindocs/user-guide.md— point at the GitHub files now, and CI builds the docs on pull requests that touchdocs/, so a dead link cannot land unnoticed again. (#264) - The MCP server and the dashboard share one auth token again. Both read or create
.code-context/dashboard.tokenin the directory of thecontext.dbthey open (dashboardTokenRootinsrc/dashboard/auth.ts); earlier each used its own cwd, so the two processes held different tokens and every MCP notify call got401and the dashboard did not refresh live. The MCP server passes its db path to the notify code at start (setDashboardDbPath); the cwd is used only when no db path is known.CODE_CONTEXT_DASHBOARD_TOKENstill wins when set. Migration: an old.code-context/dashboard.tokenbeside a launcher cwd stays in place and is now ignored; delete it, or setCODE_CONTEXT_DASHBOARD_TOKENfor both processes. (#235) - Restore and import never replace an existing row.
restore_database,import_from_fileandPOST /api/restoreshare one restore (src/scrum/restore.ts) that inserts withON CONFLICT DO NOTHINGinstead ofINSERT OR REPLACE. Earlier a dump row with a clashing id, or since v27 a clashing ticket ref, silently replaced the existing ticket, and the delete behind REPLACE also removed the children of a replaced row (a replaced sprint lost its tickets). A row equal to the existing one counts as unchanged; any other clash is listed and the existing row stays. By default a clash refuses the whole restore (MCP error, HTTP 409) and nothing is written;on_conflict: "skip"keeps the existing rows and restores the rest;dry_run: trueonly reports. (#230) - Every ticket ref is unique across the database. Schema v27 adds a unique index on
tickets.ticket_ref; earlier onlyUNIQUE(sprint_id, ticket_ref)held, so two tickets without a sprint could share aT-<n>. The MCPcreate_ticketandstart_sprinttools andPOST /api/ticketstake refs from one function (nextTicketRef, in the locked transaction from #215);create_ticketwithoutticket_refnow gets the next freeT-<n>, and a hand-set ref that another ticket has is refused. Existing installs migrate on the next start: per shared ref the oldest ticket keeps it, the others get the next free ref, and the ticket history (event_log) keeps the old ref. All installs that share one database must update together, because an older version refuses a v27 database. (#222) - The ticket notes and description e2e specs pass without retries.
test/e2e/ticket-notes.spec.tsandtest/e2e/ticket-description.spec.tseach create their own done sprint and ticket and open the board on that sprint; earlier they added tickets to the shared seeded sprint withDate.now()titles, and failed 6 of 20 runs under--repeat-each=10 --retries=0. The sprint setup moves tocreateOwnSprintintest/e2e/helpers.ts, which the Kanban spec uses too. (#229) - The Kanban drag e2e spec passes without retries.
test/e2e/kanban-drag.spec.tscreates its own sprint and ticket, opens the board on that sprint and drags with the mouse on the card's row; earlier it shared the seeded sprint with the other specs, so the card moved below the fold and the drop missed when it ran after them or with--repeat-each. (#220) - Every dashboard view shows the stored milestone status. The MILESTONES skill fallback maps an
ACTIVEorIN PROGRESSheading toactive, and the Planning page milestone card has noin_progresscolour; the dashboard now uses onlyplanned,activeandcompleted. (#221) - A milestone update never reports success for a deleted milestone.
PUT /api/milestone/:idchecks the row and writes it in one statement (UPDATE … WHERE deleted_at IS NULL RETURNING *) and answers 404 when no row changed; earlier a delete between the check and the update gave 200 for a write that changed nothing. The handler moves tosrc/dashboard/handlers/planning.ts. (#214) - An active milestone on the Dashboard sprint list shows the active colour.
SprintListcolours each milestone header and progress bar by the status the database stores (planned,active,completed); earlier it checkedin_progress, which the database never stores, so active milestones showed the planned colour. (#211) - A new ticket never gets the ref of a deleted ticket.
POST /api/ticketscounts all ticket rows, deleted rows too, to make the nextT-<n>ref, and skips a ref that a row already has; earlier a soft-deleted newest ticket gave its ref again and the insert failed with 500. (#210) - A ticket description can be cleared from the dashboard editor.
PATCH /api/ticket/:idacceptsdescriptionas a string or null, the same rule asnotes; earlier it refused thenullthat the editor sends for an empty description, so the old text stayed. The change gets a revision row and an event log row with a NULL new value. (#206) - A milestone edit from the dashboard is saved. Each milestone card on the Planning page has an Edit form for the status and the target date. The store sends
statusandtarget_datetoPUT /api/milestone/:id, with the values the database accepts (planned,active,completed); earlier it senttargetDate, which the route ignored, andin_progress, which the route refused.nullclears the target date or the description. The route returns the saved row and answers 404 for a missing milestone. (#205) - A new ticket without a priority is saved.
POST /api/ticketsusesP2, the default of the database column and of the MCPcreate_tickettool; earlier it used0, which the database rule refuses. An empty or unknown priority gets 400. The route returns the saved row. (#204) - A new milestone from the dashboard is saved. The Planning page has a New milestone form (name, optional target date). It sends
nameandtarget_date, the fieldsPOST /api/milestonesparses; earlier the store senttitle, so the route answered 400. The route returns the saved row. (#199) - A Kanban drag changes the ticket status. The board sends
PATCH /api/ticket/:idwith the new status, the same route as the ticket editor. Earlier it calledPATCH /api/ticket/:id/status, which does not exist. A drop on Done or Not Done goes back to its column with the server error, because the QA gate sets these statuses. (#198) - Ticket notes from the dashboard edit form are saved.
PATCH /api/ticket/:idacceptsnotes(a string or null) and records the change like the other fields. The edit form sends the notes through this route; earlier it sent them to a route without anotesfield, so the edit was lost. (#197) - A bad POST body no longer stops the dashboard.
/api/notify/event,/api/claude-outputand/api/claude-stepanswer 400 on invalid JSON, an empty body or a body that is not an object, and 413 on a body over 5 MB. Other/api/*write routes answer 400 on invalid JSON instead of 500. An outer guard answers any other request error with 500, so the process never gets an unhandled rejection. (#188) reset_agentsandreset_skillsdo not runnpm run buildin your project. The tools spawn no process, so they work in a project without abuildscript and write nothing to the JSON-RPC stream. The delete and the re-seed run in one transaction; when a step fails, the old rows stay. (#187)
Changed
- CLI change:
code-context-reindex --rootdefaults to the current directory. Earlier the default was~/Desktop/Workspace/dev, a path that exists only on the author's machine. Pass--root <dir>to scan another directory. The help text (--help) names the default. (#176) - Current Claude models in the agent catalog.
KNOWN_AGENT_MODELS(src/scrum/agent-model.ts) and the dashboard picker (src/dashboard/app/src/lib/constants.ts) offerclaude-fable-5-1,claude-opus-5-5,claude-sonnet-5-5andclaude-haiku-5-5. Factory seeds move toclaude-fable-5-1(dev roles),claude-opus-5-5(QA) andclaude-sonnet-5-5(other roles, and the default for a new agent without a model). The earlier ids (claude-fable-5,claude-opus-5,claude-sonnet-5,claude-haiku-4-5,claude-opus-4-8,claude-sonnet-4-6) stay accepted, so existing agents keep their model. Seeding never overwrites an existing agents table: existing installs keep their models until you change them withupdate_agent, in the dashboard, or withreset_agents. Tier routing is unchanged. (#174) - Node.js 22 or newer is the supported range.
package.jsondeclares"engines": { "node": ">=22" }; Node.js 24 LTS is recommended. CI tests 22, 24 and the current LTS. The minimum stays 22 until 3.0.0. (#173) @remotion/playeris no longer a direct dev dependency, and the stale navigation guide is gone. Only the deletedVisionPlayerimported the player;@remotion/clistill installs it fornpx remotion render.docs/ux/navigation-migration-guide.mdis deleted, because its samples imported removed modules; the UX index, CONTRIBUTING, STYLEGUIDE and two dated plans no longer name removed modules. (#231)- Unreachable dashboard modules are removed, and CI fails on new ones. 27 files that no page reached are deleted, among them
CapacityPlanningView,GanttChart,GoogleAdsStrategy,SprintPlanner,VisionPlayer,hooks/usePlanning.tsand thecomponents/molecules,components/organisms,hooksandstoresbarrel files. The newnpm run check:dashboard-reachablefollows the imports fromsrc/main.tsxwith the TypeScript module resolver and the app tsconfig, and the CI lint job runs it. No page changes. (#223) - The pre-commit hook type-checks, and
package.jsondrops unused entries. lint-staged (nowlint-staged.config.mjs) runstsc --noEmitonce per commit when a staged file is undersrc/(-p src/dashboard/appfor dashboard app files), then ESLint and Prettier on the staged files; it never runsnpm run build. Earlier the hook ran only ESLint and Prettier, while CONTRIBUTING.md and STYLEGUIDE.md said it rantsc --noEmitandnpm run build; the docs now match the hook. The unusedgemma-braindev dependency and thestart-gemmascript are removed. The Remotion dev dependencies stay:generate_vision_animationprintsnpx remotioncommands forsrc/remotion/, anddocs/guide/architecture.mdnow says so. (#175) - CI type-checks the dashboard app. The
testjob runsnpm run typecheck:dashboard(tsc --noEmit -p src/dashboard/app), so a type error in the dashboard app fails CI; the roottsc --noEmitdoes not include the app. The 9 type errors it found are fixed: Vite client types forimport.meta.env, thesteps(2)easing function instead of a string, the optionalsprint_idonRetroFinding, a margin wrapper instead of an unknownSkeletonprop, and an agent check inTeamManagementModal. (#213) - Unused planning components are removed from the dashboard.
SprintPlanningView,PlanningWizard,MilestoneList,PlanningDashboardwithEnhancedGanttChart,SprintBurndownView,EnhancedCapacityViewandSprintTableView, andPlanningInsightswere not shown on any page and still used the old milestone statusin_progressandPUTfor ticket edits. No page changes. (#212) - CI runs lint, format, the dashboard tests and the Playwright specs on every PR. A new
lintjob fails on ESLint errors and runs Prettier on the files the change touches (npm run format:changed). Thetestjob runsnpm run test:frontend. A newe2ejob runs the 35 Playwright specs against a seeded dashboard on a throwaway database (npm run test:e2e, port 4317, never a reused server). Every workflow declarespermissions:and pins actions by commit SHA. A tracked.husky/pre-commitruns lint-staged (ESLint + Prettier on staged files). (#191) - One transactional function moves a sprint to its next phase.
update_sprint,advance_sprintand the dashboard advance and sprint update routes calltransitionSprint(src/scrum/sprint-transition.ts). It writes the status, the frozen commitment,velocity_completed, one auto-analysis finding (rolesystem), the discovery archive, completed epics and milestone progress in one transaction. When a step fails, nothing changes and the error names the step; the dashboard answers 500. The MCP tools now also complete epics and update milestone progress onrest.get_discovery_coveragewrites nothing. (#189) - Typed rows and request bodies in the MCP server, the scrum tools and the dashboard write routes.
src/scrum/rows.tshas one row type per scrum table, and the queries insrc/serverandsrc/scrumuse them;@typescript-eslint/no-explicit-anyis an error there. Each dashboard POST, PUT and PATCH route parses its body with zod and answers 400 with the field name when a type is wrong; earlier a wrong type gave a 500 or a bad row. (#190)
Security
- The bridge forwards only schema-checked actions to the agent, never free-form ones. The
customaction ("Execute custom action based on payload", free JSON) is removed, so a leaked dashboard token can no longer give the agent arbitrary instructions (TM-03). The hook forwardsrun_kickoff,advance_sprint,assign_ticket,update_ticket,create_ticketandplan_sprintonly; each has a strict payload schema insrc/bridge/actions.ts(known keys, typed, strings length-capped, no control characters or backticks), and every payload is capped at 4 KB.POST /api/bridge/actionsanswers 400 for an unknown action or a payload that does not match, and the hook checks every row again before injection, because a row can also come fromexecute, an import or a restore; a row that fails is markedfailedand never reachesadditionalContext. The injected text frames the payload as data and tells the agent to confirm each action with the user before it acts. The hook now marks forwarded rowsclaimed, notcompleted, and expires a claim after one hour. Other actions, such asticket_changed, are no longer forwarded. (#244) - The dashboard token never reaches
.env.localor the built assets. On start the dashboard writes onlyVITE_DASHBOARD_PORTtosrc/dashboard/app/.env.local(mode 0600, npm 2.13.0 shipped a test token thatvite buildhad inlined from that file); the Vite dev server reads.code-context/dashboard.tokenitself (beside the db fromVITE_DASHBOARD_DB, defaultcontext.db, orCODE_CONTEXT_DASHBOARD_TOKEN) and adds theAuthorization: Bearerheader on its/apiproxy, so the page needs no token in dev.token.tsreadsVITE_DASHBOARD_TOKENonly behindimport.meta.env.DEV, so a production build drops it and uses the injectedwindow.__DASHBOARD_TOKEN__only.npm run buildfinishes withscripts/check-dist-token.mjs, which fails when adist/dashboard/assetsfile contains a 64-hex value within 200 chars of__DASHBOARD_TOKEN__/VITE_DASHBOARD_TOKENor the current token; the CItestjob runs it too (TM-07). (#247) - Bridge hook follow-up: the hook command written by setup shell-quotes both paths, and the cwd-relative
.mcp.jsonfallback is gone — the db comes from--db/CODE_CONTEXT_DBonly. (#245) - The dashboard bearer token can no longer be redirected through the database or replayed through a captured
/healthanswer. The MCP notify client reads the dashboard port from.code-context/dashboard.port(mode 0600, written by the dashboard besidedashboard.tokenat startup) and never from the_dashboard_portskills row, which any db writer (execute, an import, a restore) could change to receive theAuthorization: Bearerheader on the next notify (TM-05); the row is still written for legacy UI readers. Before the token is sent, the client GETshttp://127.0.0.1:<port>/health?nonce=<fresh 16-byte hex>— neverlocalhost, which can resolve to::1— and proceeds only when the unauthenticated JSON answersserver: "code-context-dashboard"and aproofequal tohex(HMAC-SHA256(DASHBOARD_TOKEN, nonce + "\n" + "code-context-dashboard")), compared in constant time. There is no static hash and no verification cache: every notify re-runs its own challenge over the same keep-alivehttp.Agent, so a listener that replayed a captured/healthvalue or that cannot compute the HMAC never receives the header. A failure stays silent apart from one WARN per process. On clean shutdown (SIGINT/SIGTERM and server close) the dashboard deletes the port file, so a client treats a missing file as "no dashboard" and sends nothing. (#246) - The indexer refuses secret-shaped files.
isDeniedFileNamenow denies the extensions.pem .key .p12 .pfx .jks .keystore .asc .gpg .tfstate .tfstate.backup .kdbx .ovpnand the namesid_rsa*id_dsa*id_ecdsa*id_ed25519*credentialscredentials.jsoncredentials.ymlcredentials.yamlsecrets.ymlsecrets.yamlsecrets.jsonservice-account*.json*-service-account*.jsonterraform.tfvars*.tfvarstoken.jsonoauth*.json.netrc.htpasswd.pgpass.npmrc.pypirc.git-credentialsknown_hostsauthorized_keys, matched case-insensitively on the basename, so a key, certificate or credential never reachescontext.dbor agent context. A reindex drops rows the new rule refuses, andCODE_CONTEXT_INDEX_ALLOW(comma-separated basenames or*.extglobs) opts a name back in. (#249) export_to_fileandimport_from_filestay inside the allowed roots. Both tools resolve the target path (realpath of the parent dir + basename, symlinks resolved) and refuse anything outside the allowed roots — the same sandbox asindex_directory(cwd plusCODE_CONTEXT_ALLOWED_ROOTS), so the dump cannot write to or read from arbitrary files (TM-10).export_to_filerefuses an existing target unless the newoverwrite: trueargument is passed. Setup addscode-context-dump*.jsonto the project.gitignorebesidecontext.db*, so the default dump is not committed by accident. (#250)- The dashboard fallback page escapes agent-written markdown, and every HTML response sends a CSP.
simpleMarkdown()in the legacydashboard.html(served when the built SPA is missing) escapes& < > " 'before it adds any tag, so vision or milestone text like<img src=x onerror=…>renders as text instead of running same-origin. Every HTML response now sendsContent-Security-Policy: default-src 'self'; script-src 'self' 'nonce-<random>'; object-src 'none'; frame-ancestors 'none'; base-uri 'self', plusX-Content-Type-Options: nosniffandReferrer-Policy: no-referrer; the inlinewindow.__DASHBOARD_TOKEN__script and the legacy page's inline script carry the per-response nonce. The SSE and JSON API routes are unchanged. (#252) - A repo-local
.git/configcannot run commands during indexing. Every git call the server makes passes-c core.fsmonitor=falseand-c core.hooksPath=/dev/null, and diff calls also pass--no-ext-diff --no-textconv, so a repo that arrives as an archive withcore.fsmonitor = <command>cannot run that command onls-files,diff,logorrev-parse. One helper (gitArgsinsrc/server/git.ts) builds the args; the indexer, the freshness guard,checkCommitFormatand the dashboard all use it. (#251) - The database, its sidecars, every backup and the export dump are created with mode 0600. The MCP server, the dashboard, setup and reindex open
context.dbthroughopenDbSecure(src/secure-file.ts), which creates the db and its-wal/-shmsiblings owner-only whatever the process umask is — SQLite creates the siblings lazily with 0644 & ~umask, so the helper pre-creates them as 0600 before the open. Earlier macOS defaults gave 0644, so every other OS user could read the full content of every indexed file and the scrum data (TM-08). The pre-migrationcontext.db.bak, the--forcebackupcontext.db.bak-<ts>and theexport_to_filedump are 0600 too. An existing db with group/other read bits is never chmod'ed silently: it gets one WARN line in the session log telling you to runchmod 600. (#248) - Docs site dependencies: vite >= 6.4.3 and esbuild >= 0.25.0 in
docs/package-lock.json, closing the four Dependabot alerts (GHSA-fx2h-pf6j-xcff, GHSA-v6wh-96g9-6wx3, GHSA-4w7w-66w2-5vf9, GHSA-67mh-4wv8-2f99). (#262) executecannot write the gate columns. The tool refuses anyINSERT,UPDATE,REPLACEor upsert that writestickets.status,tickets.qa_verified,tickets.verified_by, atickets.github_*column,tickets.deleted_at,sprints.status,sprints.deleted_at,sprints.archived_at, amilestones.github_*column,milestones.archived_at,epics.archived_ator aprojects.github_*column.DELETEonticketsandsprints,REPLACE/OR REPLACEon these tables and anINSERTinto them without a column list are refused too, because they reset the gate columns. The error names the column and the tool that runs the gates (update_ticket,update_sprint/advance_sprint,update_milestone,update_epic,set_project,sync_github, or the dashboard delete and archive actions). Earlier a freeUPDATEset a ticket toDONE, verified it or pointed it at any GitHub issue, which the dashboard and the gate tools refuse. The check (src/server/sql-gate-columns.ts) parses tokens, not text: quoted names ("x",`x`,[x],'x'), schema prefixes, aliases, row-valueSET (a, b) = …,UPDATE … FROM,IS [NOT] DISTINCT FROMandON CONFLICT … DO UPDATE SETresolve to the real column; a statement it cannot read on a gated table is refused, and so is anUPDATEor upsert with a top-level comma anywhere after itsSETlist, or aname =before itsWHERE. A hand-written parser cannot match SQLite's grammar exactly (RETURNING, for one, is also a valid column name), so unknown shapes are refused. Writes to other columns and everySELECTstay free. (#181)executealso gates the archive preconditions, thesync_githubtarget and the parent deletes. The tool now refuses writes tomilestones.statusandepics.status(the archive check ofupdate_milestone/update_epicreads them), toprojects.nameandprojects.repo(with the already gatedprojects.github_*columns,sync_githubresolves its owner/repo and board from them), andDELETEonprojects,milestonesandepics. A milestone or epicDELETEsettickets.milestone_id/tickets.epic_idtoNULL(ON DELETE SET NULL); a deleted and re-created project key skipped theset_projectcheck that keeps a synced project on its repo. The error namesupdate_milestone,update_epicorset_project. Theexecutetool description now lists the gated columns and these tools. (#263)- The bridge hook reads its database from configuration, never from a directory walk. The PreToolUse hook (
src/bridge/hook.ts) resolvescontext.dbfrom theCODE_CONTEXT_DBenvironment variable, the--dbargument, or thecode-contextentry in the project's.mcp.json— setup now writes the absolute db path into the hook command. With no configured db it exits silently, and it refuses a db owned by another user or reached through a symlink, so acontext.dbplanted in a subdirectory cannot inject itspending_actionsinto the agent context (TM-04). (#245)