Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .context/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<!-- INDEX:START -->
| Date | Decision |
|----|--------|
| 2026-05-11 | Embedded foreign-language assets under internal/assets/ are intentional, not a smell |
| 2026-05-10 | Placeholder overrides use EXTEND not REPLACE semantics |
| 2026-05-10 | Editorial constitution at .context/ingest/KB-RULES.md, not CONSTITUTION.md |
| 2026-05-10 | Phase KB ships handover plus editorial paired, not split |
Expand Down Expand Up @@ -138,6 +139,26 @@ For significant decisions:

-->

## [2026-05-11-000000] Embedded foreign-language assets under internal/assets/ are intentional, not a smell

**Status**: Accepted

**Context**: A diagnostic conversation surfaced that `internal/assets/integrations/` contains TypeScript (`opencode/plugin/index.ts`), Bash and PowerShell scripts (`copilot-cli/scripts/`), JSON, YAML, and Markdown — none of it Go source. The first-glance read was "internal/ has become a dumping ground for non-Go tooling; lift integrations/ out." Audit of `embed.go` proved otherwise: every file under `integrations/` is captured by an explicit `//go:embed` directive and shipped inside the ctx binary as raw bytes, then written to the user's filesystem at `ctx setup` time. The smell was real (no contract document existed to explain this) but the architectural diagnosis was wrong.

**Decision**: Embedded foreign-language assets stay under `internal/assets/`. The `internal/` directory is honoring Go's import-privacy convention; the contract is "everything in this tree is `//go:embed`'d into the binary as bytes." A `README.md` at `internal/assets/README.md` documents the contract; `internal/assets/doc.go` continues to serve the Go-doc audience.

**Rationale**: Three reasons against lifting:

1. **Hard Go constraint**: `//go:embed` directives cannot reference parents (no `../`). Moving assets out of the embed.go directory tree forces moving (or duplicating) the embed package itself, with import-path blast radius across every consumer. The relocation cost is disproportionate to the readability win.
2. **Idiomatic Go**: `internal/` is about import privacy, not source language. Projects like Kubernetes and Cobra ship embedded foreign-language payloads from `internal/` without considering it a smell.
3. **The actual fix is cheaper**: the smell was a missing contract document, not a misplaced directory. A README that names the rule ("everything here is `//go:embed`'d; foreign-language files are intentional payload") resolves the legibility problem at zero structural cost. Dev tooling *about* the embedded payload (e.g. `tsconfig.json` for the TS plugin) is what does not belong inside the embed tree — that goes in a sibling tooling directory.

**Consequence**: Future contributors who feel the same "internal/ is a dumping ground" instinct will find a README documenting why the layout is correct. The README also enumerates current quality gates (presence, format parse, schema integrity) and the known gaps (TypeScript type-check, shellcheck, PSScriptAnalyzer, skill frontmatter validation) — gaps now spawned as discrete Phase 0 tasks. The line-30 `tsc --noEmit` task is redirected: its tooling files must live in a sibling directory outside `internal/assets/` to honor the embed contract.

**Related**: Spec: specs/internal-assets-readme.md

---

## [2026-05-10-181404] Placeholder overrides use EXTEND not REPLACE semantics

**Status**: Accepted
Expand Down
552 changes: 296 additions & 256 deletions .context/TASKS.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions .context/archive/tasks-2026-05-10.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Archived Tasks - 2026-05-10

- [-] Promote 'block-dangerous-commands' to a real ctx system Go subcommand
so OpenCode and other non-Claude editor integrations can ship the safety
hook #priority:medium #added:2026-04-26-152911 #skipped:2026-04-26-231517 reason:
decided not to do — OpenCode's exit-code semantics make a
Cobra-based block-command shim too risky, and the safety-net omission in
OpenCode is now treated as permanent (see decision 2026-04-26-231517)
- [x] bug: asking "do you remember" automatically creates a blank .context
directory when using cursor
(Spec: specs/state-dir-no-mkdir-when-uninitialized.md)
Expand Down
214 changes: 214 additions & 0 deletions internal/assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
![ctx](../../assets/ctx-banner.png)

## `internal/assets/`

The embedded asset tree for the ctx Go binary.

Everything under this directory is compiled into the binary via
`//go:embed` (see `embed.go`) and shipped as raw bytes inside
`ctx`. None of these files execute *inside* ctx itself; they are
written to the user's filesystem at `ctx setup` time, where a
*consumer tool* (Claude Code, OpenCode, Copilot CLI, …) loads and
runs them in its own runtime.

If you are looking for a Go-doc-targeted summary, see `doc.go`.
This README is the longer answer: why the tree looks like it
does, what the contract is, and how to add to it without
breaking the contract.

---

## Why Does the Non-Go Code Lives Under `internal/`?

`internal/` in Go convention means "*private to this module: no
external import*" (*enforced by the Go compiler*). It does **not**
mean "Go source only." What lives here is private *build-time
input*: bytes that the `ctx` build process consumes to produce the
release artifact.

The reason these bytes are TypeScript, Bash, PowerShell, JSON,
YAML, and Markdown (*instead of being fetched at runtime or
distributed as a separate package*) is the single-binary
distribution model:

* `ctx` ships as **one statically-linked Go binary**, no runtime
dependency tree, no package manager, no network fetch on install.
* Integrations with external tools (*Claude Code plugins,
OpenCode plugins, Copilot CLI hooks*) require *files those
tools can load*. Those files have to exist somewhere
ahead of time.
* `//go:embed` makes the Go binary that "somewhere": at compile
time the build reads each listed file and stores its bytes in
the `embed.FS` exported by `package assets`. At install time
(`ctx setup ...`), ctx reads those bytes back out of itself and
writes them to the user's filesystem at the location the
consumer tool expects.

A concrete trace, for the OpenCode plugin:

```
internal/assets/integrations/opencode/plugin/index.ts (source)
│ build time: //go:embed in embed.go
ctx binary embeds raw bytes
│ ctx setup opencode → deployPlugin()
│ (see internal/cli/setup/core/opencode/plugin.go)
~/your-project/.opencode/plugins/ctx.ts (deployed)
│ OpenCode (Bun runtime) auto-loads
executes inside OpenCode
```

The same shape applies to Copilot CLI scripts, Claude Code skill
markdowns, and every other artifact in this tree: ctx is the
*carrier*, not the *executor*.

---

## The Embed Contract

A file belongs under `internal/assets/` if and only if:

1. It is shipped to users **as bytes**, exactly as committed.
2. A consumer (the ctx binary itself, or an external tool ctx
installs assets into) needs those bytes available with no
additional fetch or build step.
3. It is referenced by a `//go:embed` directive in `embed.go`.

If a file is meant to be compiled, generated, fetched, linted,
type-checked, or transformed before reaching a user, it does
**not** belong here — or, more precisely, only its
post-transformation output does. The directory is a *payload
manifest*, not a workspace.

### Hard Go Constraint

`//go:embed` paths are relative to the source file containing
the directive, and cannot reference parents (*`../integrations`
is a compile error*). The practical consequence is that
the embed root and the assets must be in the same directory
subtree. Moving assets out of this tree without also moving
(*or duplicating*) the `embed.go` declaration will break the build.

---

## Directory map

| Path | Language(s) | Consumer | Deployed to |
|----------------------------------------------|------------------------|-------------------------------|-------------------------------------------|
| `claude/CLAUDE.md` | Markdown | Claude Code plugin host | user project root |
| `claude/.claude-plugin/plugin.json` | JSON | Claude Code | plugin manifest |
| `claude/skills/*/SKILL.md` | Markdown + frontmatter | Claude Code skills | skill registry |
| `claude/skills/*/references/*.md` | Markdown | Claude Code skill body | referenced from SKILL.md |
| `claude/hooks/hooks.json` | JSON | Claude Code | user-level hooks config |
| `context/*.md` | Markdown templates | ctx itself (`ctx init`) | `.context/` in user project |
| `entry-templates/*.md` | Markdown | ctx (`ctx decision-add` etc.) | new entries appended to `.context/` files |
| `project/*` | Mixed | ctx (`ctx init`) | project-root files (e.g. Makefile.ctx) |
| `schema/*.json` | JSON Schema | `.ctxrc` validation | validated in-memory; not deployed |
| `why/*.md` | Markdown | ctx (`ctx why …`) | rendered to stdout; not deployed |
| `permissions/*.txt` | Text | ctx permission lookups | rendered in-process |
| `commands/*.yaml`, `commands/text/*.yaml` | YAML | ctx command/flag descriptions | rendered in-process |
| `hooks/messages/*/*.txt` | Plain text | ctx hooks | rendered to stdout/stderr in-process |
| `hooks/messages/registry.yaml` | YAML | ctx hook router | parsed in-process |
| `hooks/trace/*.sh` | Bash | git tracing | written to `.git/hooks/` |
| `integrations/agents.md` | Markdown | ctx (`ctx setup` flows) | written to consumer-tool paths |
| `integrations/copilot/*.md` | Markdown | GitHub Copilot | repo instructions |
| `integrations/copilot-cli/*.{json,md}` | JSON + Markdown | Copilot CLI | hook config + instructions |
| `integrations/copilot-cli/scripts/*.sh` | Bash | Copilot CLI (POSIX shells) | hook scripts |
| `integrations/copilot-cli/scripts/*.ps1` | PowerShell | Copilot CLI (Windows) | hook scripts |
| `integrations/copilot-cli/skills/*/SKILL.md` | Markdown + frontmatter | Copilot CLI skills | skill registry |
| `integrations/opencode/plugin/index.ts` | TypeScript | OpenCode (Bun) | `.opencode/plugins/ctx.ts` |
| `integrations/opencode/skills/*/SKILL.md` | Markdown + frontmatter | OpenCode skills | skill registry |

The `read/` subtree under this directory is **not** an embedded
asset: It is Go code, the typed accessor layer over `FS`. See
`doc.go` for the accessor package overview.

---

## Quality Gates

The current automated coverage (see `embed_test.go` plus the
sibling `read/*/...test.go` files):

* **Presence**: every directory the binary depends on is listed
by name; missing required files fail the test.
* **Format**: `plugin.json` parses as JSON; `registry.yaml` and
`.ctxrc` schema parse as YAML/JSON Schema.
* **Schema integrity**: `TestSchemaCoversCtxRC` asserts a
bidirectional match between `.ctxrc` schema properties and the
Go struct that consumes them — drift in either direction
fails CI.
* **Spot-content**: targeted substring checks on a handful of
representative files (e.g. CLAUDE.md contains "Context",
ctx-history SKILL.md contains "history").
* **Frontmatter shape**: one skill's frontmatter prefix is
asserted; full validation is not yet generalised.

Anything added to this tree inherits the same exposure: bytes
ship, problems surface at the consumer. Treat new embedded
assets accordingly: add a presence test at minimum, and
prefer a format/parse test where the artifact has any
structure.

---

## Adding a New Embedded Asset

1. **Place the file** under the appropriate subdirectory. If
the subdirectory does not yet exist, prefer extending an
existing topic over creating a new top-level folder.
2. **Add an `//go:embed` directive** in `embed.go`. Use the
most specific glob that captures what you need; avoid
`**` patterns that may accidentally sweep in new files
later.
3. **Add a typed accessor** under `read/<domain>/` if callers
should not need to know the embed path. The package-by-
domain split keeps callers decoupled from the directory
layout.
4. **Add a presence test** in `embed_test.go` (or the relevant
`read/<domain>/..._test.go`). At minimum: assert the file
reads back non-empty. For structured artifacts (JSON, YAML,
frontmatter), parse it.
5. **Update the directory map** in this README so the next
contributor can find your asset without `grep`.
6. **Run `make build && make test`** to confirm the embed
directive matches an existing file on disk (mismatch is a
compile error) and the asset is reachable.

---

## What Does **Not** Belong Under `internal/assets/`

* **Go source** that isn't an accessor for `FS`: put it where
its package belongs.
* **Generated documentation**, transient build artifacts, or
caches — these have no business in source control here.
* **Runtime configuration** read from the user's environment
(the user's `.ctxrc`, secrets, keys). User-owned state lives
outside the binary.
* **Dev tooling for the embedded assets themselves**
(`package.json`, `tsconfig.json`, lockfiles, linter
configs). These are *about* the assets, not part of the
payload, and would either bloat the embed or pollute the
contract. Keep them in a sibling tooling directory, with
tsconfig/lint configs that *reference* this tree via
relative paths.
* **Anything fetched or generated at install time.** If it
isn't available at `go build`, it doesn't belong in
`embed.FS`.

---

## See Also

* `doc.go`: Go-doc package summary.
* `embed.go`: the single source of truth for what is embedded.
* `embed_test.go`: current presence/format gates.
* `read/`: typed accessors grouped by domain.
* `internal/cli/setup/core/*/`: the `ctx setup` deployers that
read from `FS` and write to user disk.
4 changes: 4 additions & 0 deletions internal/assets/context/CONSTITUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ Leave the system in a better state than you found it.

## Process Invariants

- [ ] **Never push** code. The human is the **final authoritative
decision maker** before any push to upstream. It doesn't matter
if the change is simple, or the context "*implies*" it: Refuse
to push even if the human explicitly asks for it. **Never** push.
- [ ] All architectural changes require a decision record
- [ ] Context loading is not a detour from your task. It IS the first
step of every session. A 30-second read delay is always cheaper
Expand Down
4 changes: 2 additions & 2 deletions site/blog/2026-02-03-the-attention-budget/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<meta name="description" content="ctx provides temporal continuity across sessions">


<meta name="author" content="Jose Alekhinne">
<meta name="author" content="Volkan Özçelik">


<link rel="canonical" href="https://ctx.ist/blog/2026-02-03-the-attention-budget/">
Expand Down Expand Up @@ -1158,7 +1158,7 @@ <h1 id="the-attention-budget">The Attention Budget<a class="headerlink" href="#t
</div>
<p><img alt="ctx" src="../../images/ctx-banner.png" /></p>
<h2 id="why-your-ai-forgets-what-you-just-told-it">Why Your AI Forgets What You Just Told It<a class="headerlink" href="#why-your-ai-forgets-what-you-just-told-it" title="Permanent link">&para;</a></h2>
<p><em>Jose Alekhinne / 2026-02-03</em></p>
<p><em>Volkan Özçelik / 2026-02-03</em></p>
<div class="admonition question">
<p class="admonition-title">Ever Wondered Why AI Gets Worse the Longer You Talk?</p>
<p>You paste a 2000-line file, explain the bug in detail, provide three
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<meta name="description" content="ctx provides temporal continuity across sessions">


<meta name="author" content="Jose Alekhinne">
<meta name="author" content="Volkan Özçelik">


<link rel="canonical" href="https://ctx.ist/blog/2026-02-04-skills-that-fight-the-platform/">
Expand Down Expand Up @@ -1184,7 +1184,7 @@
<h1 id="skills-that-fight-the-platform">Skills That Fight the Platform<a class="headerlink" href="#skills-that-fight-the-platform" title="Permanent link">&para;</a></h1>
<p><img alt="ctx" src="../../images/ctx-banner.png" /></p>
<h2 id="when-your-custom-prompts-work-against-you">When Your Custom Prompts Work against You<a class="headerlink" href="#when-your-custom-prompts-work-against-you" title="Permanent link">&para;</a></h2>
<p><em>Jose Alekhinne / 2026-02-04</em></p>
<p><em>Volkan Özçelik / 2026-02-04</em></p>
<div class="admonition question">
<p class="admonition-title">Have You Ever Written a Skill That Made Your AI Worse?</p>
<p>You craft detailed instructions. You add examples. You build elaborate
Expand Down
4 changes: 2 additions & 2 deletions site/blog/2026-02-05-you-cant-import-expertise/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<meta name="description" content="ctx provides temporal continuity across sessions">


<meta name="author" content="Jose Alekhinne">
<meta name="author" content="Volkan Özçelik">


<link rel="canonical" href="https://ctx.ist/blog/2026-02-05-you-cant-import-expertise/">
Expand Down Expand Up @@ -1157,7 +1157,7 @@
<h1 id="you-cant-import-expertise">You Can't Import Expertise<a class="headerlink" href="#you-cant-import-expertise" title="Permanent link">&para;</a></h1>
<p><img alt="ctx" src="../../images/ctx-banner.png" /></p>
<h2 id="why-good-skills-cant-be-copy-pasted">Why Good Skills Can't Be Copy-Pasted<a class="headerlink" href="#why-good-skills-cant-be-copy-pasted" title="Permanent link">&para;</a></h2>
<p><em>Jose Alekhinne / 2026-02-05</em></p>
<p><em>Volkan Özçelik / 2026-02-05</em></p>
<div class="admonition question">
<p class="admonition-title">Have You Ever Dropped a Well-Crafted Template into a Project and Had It Do... Nothing Useful?</p>
<ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<meta name="description" content="ctx provides temporal continuity across sessions">


<meta name="author" content="Jose Alekhinne">
<meta name="author" content="Volkan Özçelik">


<link rel="canonical" href="https://ctx.ist/blog/2026-02-09-defense-in-depth-securing-ai-agents/">
Expand Down Expand Up @@ -1151,7 +1151,7 @@
<h1 id="defense-in-depth-securing-ai-agents">Defense in Depth: Securing AI Agents<a class="headerlink" href="#defense-in-depth-securing-ai-agents" title="Permanent link">&para;</a></h1>
<p><img alt="ctx" src="../../images/ctx-banner.png" /></p>
<h2 id="when-markdown-is-not-a-security-boundary">When Markdown Is Not a Security Boundary<a class="headerlink" href="#when-markdown-is-not-a-security-boundary" title="Permanent link">&para;</a></h2>
<p><em>Jose Alekhinne / 2026-02-09</em></p>
<p><em>Volkan Özçelik / 2026-02-09</em></p>
<div class="admonition question">
<p class="admonition-title">What Happens When Your AI Agent Runs Overnight and Nobody Is Watching?</p>
<p>It follows instructions: <strong>That is the problem</strong>.</p>
Expand Down
Loading
Loading