internOS Workstreams framework for Claude Code. Use this skill whenever the user is operating inside an internOS workstream (their working directory is under `<workspace>/projects/<project>/workstreams/<name>/`), or when they mention workstreams, BRIEF.md, STATUS.md, MEMORY.md, tick.md tasks, or activating/discovering a project. Resolves the active thread from the working directory, loads the right context files in the right order, and enforces the resolution, runtime, recovery, and isolation doctrines so context survives across sessions and `/resume`. Trigger this even when the user doesn't say "internOS" or "workstream" by name — if pwd is inside a `projects/*/workstreams/*` subtree, this skill applies.
Instrucciones de origen · Vista previa de solo lectura
name
intern-os
description
internOS Workstreams framework for Claude Code. Use this skill whenever the user is operating inside an internOS workstream (their working directory is under `<workspace>/projects/<project>/workstreams/<name>/`), or when they mention workstreams, BRIEF.md, STATUS.md, MEMORY.md, tick.md tasks, or activating/discovering a project. Resolves the active thread from the working directory, loads the right context files in the right order, and enforces the resolution, runtime, recovery, and isolation doctrines so context survives across sessions and `/resume`. Trigger this even when the user doesn't say "internOS" or "workstream" by name — if pwd is inside a `projects/*/workstreams/*` subtree, this skill applies.
internOS — Workstreams (Claude Code)
internOS is a framework for humans and agents to collaborate on long-running workstreams without losing context between sessions. Each workstream exists in three places at once: a tick.md task (management), a communication thread (collaboration), and a filesystem directory (source of truth).
This skill is the Claude Code adapter. The framework-agnostic doctrine lives in intern-os/SKILL.md — read it when you need the full FRAMEWORK / PLAYBOOK / COMMUNICATION reference. This file covers what's specific to Claude Code: how a thread is resolved, how sessions relate to threads, and the operating protocol.
What "thread" means in Claude Code
On Discord and Slack, a thread is a platform-native conversation thread. In Claude Code, a thread is the persistent conversation about a workstream — surfaced as one or more sessions over time:
Each /resume continues the same thread.
Opening a fresh conversation in the same working directory continues the same thread.
Sessions are instances of the thread; the thread is the unit of work.
The thread_id in BRIEF.md uses the canonical form:
This is the working-directory-relative path under the configured workspace. It's the cheapest, most deterministic binding Claude Code can offer: no inference, no fuzzy matching.
Resolution: pwd → workstream
Claude Code thread resolution is fully delegated to a small script. Always use it — never resolve a workstream by reading directory listings or guessing from path fragments.
# Returns workstream absolute path on stdout (exit 0), or:# exit 1 — no active workstream (pwd not inside a workstream subtree). Silent, expected.# exit 2 — workstream dir found but BRIEF.md missing or thread_id mismatched. STOP and ask.
~/.claude/skills/intern-os/scripts/resolve-thread.sh
The script:
Walks up from $PWD looking for <workspace>/projects/<path-to-project>/workstreams/<name>.
Reads BRIEF.md and verifies thread_id exactly equals claude-code:projects/<path-to-project>/workstreams/<name>.
Prints the workstream path on success, or fails loudly on mismatch.
It also resolves from inside a code worktree: when $PWD is under projects/<path-to-project>/code/.worktrees/<name>/, it resolves to the workstream whose BRIEF.md worktrees: block declares that worktree (exact match on the project-relative dir path). Exactly one declarer → that workstream; more than one → exit 2 (ambiguous — fix the duplicate declaration); none → exit 1.
<workspace> is set via the INTERNOS_WORKSPACE environment variable — required, no implicit default. Point it at a single workspace (a directory that directly contains projects/) or at a workspaces container (see below).
Multiple workspaces.INTERNOS_WORKSPACE is PATH-style: colon-separate multiple workspace roots when each represents a distinct org / operating-system surface. Resolution walks the list in order and picks the first workspace that is an ancestor of $PWD. Example:
Workspaces container. Any entry may instead be a container — a directory whose immediate children are each workspaces (canonically named workspaces, e.g. ~/workspaces, ~/.hermes/workspaces). Detection is structural, not name-based: an entry that directly contains projects/ is a single workspace; one whose children each contain projects/ is a container and expands to those children. So a whole multi-workspace setup can be one entry:
export INTERNOS_WORKSPACE="$HOME/workspaces" # resolves across every child workspace
This is the Claude Code analogue of the Hermes adapter's container support — the same <any-path>/workspaces model, the same structural detection. Mixed lists work too ("$HOME/workspaces:$HOME/other/single-ws"). The canonical thread_id stays relative to the matched workspace, never the container.
Each workspace is independent: its own projects/ tree, its own project-level AGENTS.md, its own workstream directories. There is no cross-workspace resolution — $PWD belongs to exactly one workspace at a time, and the isolation doctrine applies across workspaces just as it does across projects. The thread_id is canonical relative to its workspace, so a workstream at <frutero>/projects/foo/workstreams/bar and one at <poktalabs>/projects/foo/workstreams/bar are different threads despite identical canonical thread_ids — resolution is anchored to $PWD, not the thread_id alone.
Dual binding (cross-platform workstreams). The framework's COMMUNICATION.md only specifies a single thread_id per workstream, but a workstream often spans two surfaces: a human-comms thread (Slack/Discord/Telegram) and an agent-ops adapter (Claude Code). When that happens, keep the primary thread_id as whichever platform the humans collaborate on, and add a sibling thread_id_claude_code: field carrying the canonical claude-code:projects/<project>/workstreams/<name> value:
The resolver accepts either field — whichever matches the canonical form for the current directory binds the thread. Use thread_id_claude_code only when the primary thread_id belongs to another platform; when Claude Code is the primary surface, put the canonical value directly in thread_id and omit the override.
Mismatch handling. If the script exits 2, do not proceed and do not patch the file silently. Tell the human what was expected vs. found, and ask whether the workstream was moved, copied, or scaffolded by hand. Quietly fixing thread_id values is exactly the kind of "helpful guess" that corrupts the binding model.
Operating protocol
When resolve-thread.sh returns a workstream path, follow the same protocol as any other internOS adapter:
Read BRIEF.md in full (workstream identity + thread_id).
Read STATUS.md in full (operational heartbeat, ≤10 lines by design — see STATUS.md hygiene below; session-start.sh caps this load automatically if the file has bloated).
Read project-level AGENTS.md from <workspace>/projects/<project>/AGENTS.md if it exists.
Escalate to the on-demand files only when the task actually needs them:
MEMORY.md — last 80 lines (search on demand for older context).
DECISIONS.md — when the task touches a prior decision.
STAKEHOLDERS.md — when the task involves people.
RESOURCES.md — when the task involves artifacts or deployments.
Check tasks: tick list --tag <workstream-name>.
Claim before working: tick claim TASK-X @claude-code.
Before ending the session:
Complete or release the task: tick done TASK-X @claude-code or tick release TASK-X @claude-code.
Update STATUS.mdin place — replace the current-state/next fields, never append a dated ## YYYY-MM-DD section (see STATUS.md hygiene below). A blank STATUS.md makes the workstream invisible to the next session; a growing one makes every future session more expensive.
If MEMORY.md exceeds 80 lines, consolidate — summary, not log. Target ≤50 lines.
Append a one-line summary entry to SESSIONS.md (see below).
This is required even if nothing changed. STATUS.md and SESSIONS.md being current is what makes /resume actually useful.
Worktrees (code work)
When a workstream does real code work, operate in a git worktree, not the code repo's primary checkout. internOS worktrees live at projects/<project>/code/.worktrees/<name>/ — a linked worktree of a code repo, parked in one shared directory beside the repos and already ignored by the project repo's code/* rule.
Create with the helper, not the native flag.~/.claude/skills/intern-os/scripts/worktree.sh create <name> --repo <code-repo> makes the worktree in the right code repo. Claude Code's native --worktree / EnterWorktree fork the project repo — where code/* is gitignored — so they yield a worktree with no code in it. If you want --worktree to Just Work, install the WorktreeCreate hook (below): it redirects native creation into code/.worktrees/ of the intended code repo (set INTERNOS_WORKTREE_REPO when the project has more than one code repo).
Declare it in BRIEF.md. Add a worktrees: block (repo, dir, branch) to the workstream's BRIEF.md. This is the authoritative link and is what lets resolve-thread.sh bind a worktree cwd back to its workstream.
List / prune.worktree.sh list shows every worktree with its owning repo, branch, state, and declaring workstream; worktree.sh ledger refreshes code/WORKTREES.md; worktree.sh prune --dry-run shows what's safe to remove. Prune never removes a worktree with a dirty tree or unpushed commits — merging and removal stay a human call.
Fresh checkout. A new worktree has tracked files only. Install deps in it, and add a .worktreeinclude file (.gitignore syntax) at the code repo root to auto-copy gitignored config (.env.local, secrets) into each new worktree.
Full spec: docs/specs/git-tracking.md.
Sessions vs. threads (SESSIONS.md)
Sessions are individual Claude Code conversations. The thread is the workstream. SESSIONS.md lives inside the workstream directory and is an append-only log of which sessions touched this thread:
2026-05-07 09:14 · 8c1a4d9e-... · resolved STATUS.md staleness, drafted plan for resource-binding bug
2026-05-07 14:03 · 1f2b9e77-... · implemented resource-binding fix and updated DECISIONS.md
Entries are written automatically when the lifecycle hooks are installed (see below). Manual writes are still possible via log-session.sh "<session-id>" "<summary>", but you usually won't need them.
If the user /resumes a session and continues work, no new entry is needed unless meaningful new work happened — judgment call. Don't pad SESSIONS.md with empty resume markers.
STATUS.md hygiene (hard rule)
STATUS.md is read in full on every session start by default — its size is a direct, recurring tax on every future session in this workstream, not a one-time cost. This has burned real budget: one workstream reached 1,050 lines / 143 KB before anyone noticed (see context-hygiene-audit-2026-08-06.md).
Never append a dated ## YYYY-MM-DD section to STATUS.md. Per-session narrative belongs in SESSIONS.md (one line) or, for content-worthy sessions, the workstream's journals/ layer — not STATUS.
Always REPLACE the current-state / next fields in place. Editing, not accumulating, is the only supported pattern.
If STATUS.md is already over ~40 lines when you open it, that's inherited bloat — move the dated/narrative content to JOURNAL.md (verbatim, in the workstream directory) before writing your update. Don't compound someone else's drift.
session-start.sh enforces a load-time backstop: above ~40 lines it loads only the first 15 lines plus an oversized-file flag, instead of the whole file. sync-check.sh separately warns at >15 content lines (the target signal) — that warning surfaces at the next SessionStart via .internos-warnings, so it can't prevent the read that already happened; the load cap is what actually bounds the cost.
Canonical homes for history, so it stops leaking back into STATUS:
STATUS.md — current state only, target ≤10 lines, default-loaded every session.
JOURNAL.md — overflow archive of old STATUS narrative, verbatim, on-demand only (never auto-loaded).
DECISIONS.md — load-bearing decisions, escalate on demand — watch its size too.
gbrain timeline — dated session summaries for semantic recall.
Lifecycle hooks (strongly recommended)
The adapter ships two hooks that do work the doctrine previously relied on Claude remembering. Install them once and the workstream is kept honest automatically.
SessionStart — pre-load context before the first response token
When a session starts inside a workstream, session-start.sh injects a system reminder containing:
STATUS.md in full — capped: above ~40 lines it loads only the first 15 lines plus an oversized-file flag (see STATUS.md hygiene above)
The last 3 SESSIONS.md entries
Open tick.md tasks tagged with this workstream
Any .internos-warnings written by the previous SessionEnd
This means: by the time you read your first user message inside a workstream, the operating context is already in your conversation. Do not re-read STATUS.md or SESSIONS.md just because you'd "normally" load them at the start of a workstream session — they're already there. Read BRIEF.md fully, MEMORY.md, DECISIONS.md, etc. only when a specific turn requires them.
If session-start.sh exited 2 (binding broken), the system reminder will say so explicitly. Stop and ask the human; don't start work blind.
SessionEnd — stamp, append, check
When the session ends, session-end.sh:
Stamps last_updated: <today> in BRIEF.md (creating the field if missing). This makes staleness visible to sync-check.sh and to humans skimming the file.
Appends a <timestamp> · <session-id> line to SESSIONS.md. The summary is your job — append it during the end-of-session protocol before the hook fires (the hook only adds the timestamp + id; you add the summary text on the same line).
Runs sync-check.sh --workstream <path> and writes findings to <workstream>/.internos-warnings. The next SessionStart will surface them.
Does not auto-release tick tasks. Claimed-but-not-completed tasks just persist into the next session — that's usually what the human wants ("I'll come back tomorrow"). The findings file will note the unreleased claim.
The hook returns immediately on workstreams it can't resolve, so it costs ~50ms in non-workstream sessions.
Doctrines (recap)
These come from the framework-agnostic intern-os/SKILL.md. They apply unchanged in Claude Code:
Resolution doctrine. Exact, deterministic, non-heuristic. If resolve-thread.sh says no thread, there is no thread. Don't infer one from a similar path.
Runtime doctrine. Load only what the current turn needs. Tier 1 = BRIEF + STATUS. Tier 2 = DECISIONS, STAKEHOLDERS. Tier 3 = MEMORY, RESOURCES, docs/. No cross-workstream reads by default. No broad scans across projects/.
Recovery doctrine. If the session is degraded, bloated, or reset, reconstruct from workstream files — not from transcript continuity. BRIEF + STATUS must be sufficient to restart.
Isolation doctrine. Don't read another workstream's files. Don't search across projects. Cross-workstream synthesis must be explicit and human-requested.
Discovering a project / activating a workstream
When the human says "discover project: X" or "activate workstream: X in project Y", follow the discovery / activation flows in intern-os/SKILL.md. The Claude Code specifics:
The new workstream directory must live at <workspace>/projects/<path-to-project>/workstreams/<name>/.
BRIEF.md thread_id must be claude-code:projects/<path-to-project>/workstreams/<name> — anything else will fail resolution.
After scaffolding, verify by cding into the workstream and running resolve-thread.sh — it should print the path on the first try. If not, the binding is wrong; fix it before claiming any task.
When NOT to load this skill
The user is doing general coding outside any workstream subtree. resolve-thread.sh exits 1 silently — that's the signal to stand down.
The user explicitly asks for cross-project synthesis or a workspace-wide overview. That's a sync-check.sh / generate-registry.sh job, not workstream operation.
The user is troubleshooting tick.md, the resolver script, or the workspace structure itself. That's tooling work, not workstream work — read this file for context but don't enter the operating protocol.
Pointers into the framework-agnostic skill
For deeper reference, read these from the canonical skill (one repo up from this adapter, or wherever it's installed):
intern-os/SKILL.md — full framework, doctrine, project lifecycle
intern-os/references/en/FRAMEWORK.md — what's validated by tooling vs. what's doctrine
intern-os/references/en/COMMUNICATION.md — thread format, message conventions
intern-os/references/en/TICK-INTEGRATION.md — tick.md commands and tags
intern-os/references/en/ROLLOUT.md — rolling out internOS to an existing workspace
Load these only when the current turn actually needs them. Defaulting to "read everything" is exactly the runtime-doctrine violation this skill is designed to prevent.