| name | assign-thread |
| description | Manage the assigned_to list on a thread. Supports --add (append handles), --remove (delete handles), --set (replace list), and --clear (remove field entirely). Appends an audit-trail line to the thread body recording who made the change, when, and why. Rebuilds thread-index.md and current-state.md. Use when the user says "assign this thread to <handles>", "add <name> to assigned_to", "unassign from <name>", "clear all assignments", or when managing thread ownership. |
| version | 1.0.0-rc4 |
| pack | project-brain |
| requires | ["git","read:~/.config/project-brain/projects.yaml","write:[brain-root]"] |
assign-thread
Teams use the assigned_to list to track thread ownership and collaboration scope. This skill is a thin, auditable way to mutate the list per CONVENTIONS § 3.2. It is intentionally policy-neutral — the pack does not enforce any semantics (single owner, multi-collaborator, role-based are all valid). Teams wire enforcement externally via CODEOWNERS, branch protection, or custom CI.
Every assignment change is recorded in an audit trail appended to the thread body (not just frontmatter) so the history is readable without git log. The skill stages the thread file, calls verify-tree --rebuild-index to regenerate the aggregate snapshots, and commits in one step.
When to invoke
- "Assign this thread to "
- "Add to assigned_to"
- "Remove from this thread"
- "Set assigned_to to "
- "Clear assignments — thread has no owner"
- "Reassign the thread from Alice to Bob"
Inputs
| Name | Source | Required | Description |
|---|
thread_slug | user prompt or cwd inference | yes | Slug of the thread to modify. Defaults to the thread whose directory contains cwd. |
operation | flag (--add, --remove, --set, --clear) | yes | Exactly one of the four mutation modes. Mutually exclusive. |
handles | user prompt (comma-separated) | cond. | List of handles to add/remove/set. Required for --add, --remove, --set; forbidden for --clear. |
note | user prompt | no | Optional free-form note explaining the assignment change (e.g., "handoff to Alice per 1:1"). |
actor | --actor <email> flag or $EMAIL env var | no | Override for "who performed this action" in the audit line. Resolution: --actor flag → literal TODO@example.com placeholder. No git config invocation (rc4 pre-promote skills are git-free). |
push | flag (--push) | no | Push to the default remote after commit. Default off. |
--brain=<path> | user prompt or cwd inference | no | Absolute path to the brain root. Defaults to the nearest ancestor project-brain/ directory. |
Prompt strategy: resolve thread_slug from cwd. Ask the user to pick which operation if not supplied as a flag. For each operation, prompt for the handles (none for --clear). Optionally prompt for a note. Infer actor from --actor <email> if supplied; otherwise use the literal placeholder TODO@example.com. Do NOT invoke git config — rc4 keeps pre-promote skills git-free.
| --dry-run | boolean | no | Print the plan (operation, assigned_to changes, audit trail, commit message) without performing any file writes, git mutations, or audit-log writes. See Process § Dry-run semantics. |
Preconditions
The skill refuses if any of these are not met.
- Current working directory is inside a brain root (a
project-brain/ directory containing CONVENTIONS.md) or an explicit --brain=<path> was given.
project-brain/threads/[thread_slug]/thread.md exists.
- Thread
status is active, parked, or in-review. Archived threads are terminal; refuse with guidance to use update-thread on the archive if a fix is needed.
- Exactly one of
--add, --remove, --set, --clear is supplied. Refuse if zero or more than one.
- For
--add and --set: handles is a non-empty comma-separated list.
- For
--remove: handles is a non-empty comma-separated list.
- For
--clear: no handles parameter is given.
- (rc4: this precondition is deferred.) Pre-rc4 this skill refused when the thread had uncommitted changes, to prevent mixing an in-flight edit into the assignment audit line. rc4 makes the skill pure-file — it only appends to the body's
## Assignment history section — so uncommitted thread edits are harmless. No git status invocation. The user commits whenever they want.
- Standard path-traversal guard:
thread_slug contains only [a-z0-9-], matches the slug in project-brain/threads/, and the resolved path is canonical (no .. escapes).
actor is resolvable. If --actor <email> is supplied, use it. Otherwise write the literal placeholder TODO@example.com into the audit line and append a TODO note to the thread body reminding the user to fix it. No git invocation and no env-var read — rc4 keeps assign-thread shell-free. Precondition always succeeds; never refuses.
Process
⛔️ HARD CONSTRAINT — ONE TOOL CALL
Call ${PROJECT_BRAIN_PACK_ROOT}/scripts/assign-thread.sh ONCE. No Read of thread.md, no pre-validation, no extra prompt about operation mode when the user's own sentence already reveals it. The script reads thread.md, validates preconditions, mutates frontmatter, appends the audit line, rebuilds indexes, and exits with a clear error if anything fails. You react to the exit code, not to checks you do first.
Derive the operation from language. "Assign X to bob" → --add bob. "Unassign bob from X" → --remove bob. "Who's on X?" → use review-thread, not this. Only prompt the user if the user's message leaves the mode genuinely ambiguous.
Strip ${PROJECT_BRAIN_PACK_ROOT} and the bare path resolves against the skill's own dir → "no such file". Keep it.
One call:
"${PROJECT_BRAIN_PACK_ROOT}/scripts/assign-thread.sh" \
--brain=<absolute brain path> \
--slug=<thread_slug> \
--add=<handles> \
[--actor=<email>] \
[--note='<note>']
Infer --slug from cwd (nearest threads/<slug>/ ancestor). Only ask the user for it if they're invoking assign-thread without any thread context. The script completes in ~110ms; it replaces 8 individual steps (load/compute/mutate/append/rebuild/report).
After success, report. Echo the script's stdout in your response message verbatim — don't summarize, transform, or rely on the Bash tool's result card to display it. The user should see the script's output as part of your reply.
Suggested follow-up commit (the user runs this themselves):
git add project-brain/threads/[slug]/ project-brain/thread-index.md project-brain/current-state.md
git commit -m "assign-thread: [slug] [operation] [handles]"
Dry-run semantics
When --dry-run is set:
- Run all preconditions, including thread existence, working-tree cleanliness, and validation of any new handles against an optional project-specific allow-list (if the project wires one). Exit 1 if any precondition fails.
- Compute the full plan: the old
assigned_to list, the new assigned_to list after applying the requested operation (--add, --remove, --set, --clear), the audit-trail line that would be appended to the thread body's ## Assignment history section (YYYY-MM-DD — <actor> — <op>: <handles>), whether the section would be created or amended, and the commit message.
- Invoke
verify-tree --rebuild-index --dry-run to surface any index-rebuild failures before committing. If that fails, print the rebuild error and exit 1.
- Write NOTHING to disk: no thread.md edits, no index-file edits, no audit-log writes.
- Invoke NO git mutations: no
git add, no git commit, no git push (even if --push was also supplied — dry-run dominates). Read-only git operations (git status, git config user.email) are allowed.
- Exit 0 if the plan would succeed end-to-end, exit 1 if any precondition or rebuild-dry-run check failed, exit 2 on unexpected error.
Print the plan to stdout including a clear "before" and "after" view of the assigned_to list and the verbatim audit line. Under future audit-log wiring (per AUDIT-LOG.md), a record with "dry_run": true is appended; the v0.9.0-alpha.4 stub is spec-only, so no audit write happens either way.
Side effects
Files written or modified
| Path (relative to brain root) | Operation | Condition |
|---|
threads/[slug]/thread.md | edit (frontmatter + body) | all operations |
thread-index.md | edit | rebuild step (all operations) |
current-state.md | edit | rebuild step (all operations) |
Git operations
None. rc4 pre-promote skills are pure file operations; the skill reports the files it modified and the user runs git add + git commit themselves when they want to checkpoint. A suggested commit command is included in the Report step for copy-paste.
External calls
verify-tree --rebuild-index in step 8 (via Bash or equivalent).
Outputs
User-facing summary. A short message with:
- The commit SHA.
- The operation performed (
add, remove, set, clear).
- Old and new
assigned_to lists (or note if field was absent).
- Handles that were skipped (if any) with a brief explanation.
- The audit-trail entry that was appended.
- A next-step suggestion (e.g., "Assigned to alice,bob. Run
list-threads --assigned=alice to find all threads assigned to Alice.").
State passed forward.
thread_slug — unchanged from input.
operation — the operation that ran (add, remove, set, or clear).
new_assigned_to — the resulting list (empty list if cleared or empty after removal, absent if --clear).
Verbosity contract
Reads verbosity from <brain>/config.yaml (env override: PROJECT_BRAIN_VERBOSITY). Defaults to terse.
- terse (default): one line: operation + handles changed, then
Done.
- Example:
Added alice to project-brain/threads/alpha/. Done.
- normal: old and new assigned lists side-by-side.
- verbose: full audit-trail line + rebuild output.
Frontmatter flips
| File | Field | Before | After | Operation |
|---|
threads/[slug]/thread.md | assigned_to | current list (or absent) | new list per op | add/remove/set |
threads/[slug]/thread.md | assigned_to | current list | (removed) | clear |
No other fields are touched. status, maturity, parked_*, archived_* are never written by this skill.
Postconditions
- Thread directory unchanged at
threads/[slug]/. Nothing moved, no content deleted.
- Thread
assigned_to frontmatter matches the new computed value (or field absent if --clear).
- Thread body has a new audit-trail line under
## Assignment history.
thread-index.md and current-state.md reflect the post-operation state of all threads (autogenerated).
- Exactly one commit was added.
verify-tree passes on the updated thread.
Failure modes
| Failure | Cause | Response |
|---|
| Brain root not found | No project-brain/CONVENTIONS.md up the tree; no --brain given | refuse — prompt user to init-project-brain |
| Thread slug does not resolve | Typo; wrong cwd | refuse — list nearby slugs (levenshtein) and re-prompt |
Thread in archived | Terminal state | refuse — suggest archiving was final or manual fix needed |
| Multiple mutation flags supplied | --add + --remove or similar | refuse — clarify that exactly one operation per invocation |
| No mutation flag supplied | User forgot to specify an operation | refuse — ask the user which operation |
--clear with non-empty handles | Conflicting args | refuse — --clear takes no handles |
Empty handles for --add, --remove, --set | Invalid input | refuse — ask for a non-empty handle list |
| Uncommitted edits to thread.md | Concurrent edit risk | refuse — ask user to commit or stash |
(retired in rc4 — actor defaults to TODO@example.com when --actor not supplied; user fixes later) | — | (never refuse — ask user to configure git or supply --actor |
| Rebuild source-validation failure | Thread frontmatter schema violations | refuse — report violating thread; user must repair before retrying |
| Rebuild write failure | Filesystem / permissions issue | refuse — live index files unchanged (atomic); report error |
Path traversal attempt in thread_slug | Malicious or malformed slug (e.g., ../../../etc/passwd) | refuse — reject slug, report invalid characters |
Related skills
list-threads --assigned=<handle> — find threads assigned to a given handle (not yet implemented; design sketch in pack roadmap).
update-thread — for other structured thread edits (maturity, candidates, soft_links).
park-thread, discard-thread — other lifecycle operations; coordinate with assignment (e.g., unassign on discard).
verify-tree — run anytime to validate the tree; called automatically by this skill.
Asset dependencies
None. The skill emits the audit-trail line inline (format defined in § Process step 6); it does not read any templates from the pack's assets/ directory.
| Asset path | Used at step | Purpose |
|---|
| (none) | (n/a) | (n/a) |
Security / Privacy
- Handle format is open. Handles are written as given; no validation that they match a particular format (GitHub username, email, Slack @-handle, etc.). If a team uses emails, the emails end up in git — this is an intentional design choice per CONVENTIONS § 3.2 note, not a bug. Teams with privacy concerns should set a clear policy on what formats are acceptable.
- Actor field in audit line. The
--actor parameter allows override of who is credited in the audit-trail line. An unscrupulous actor could set --actor=alice@example.com to impersonate. The git commit author (visible in git log) is the canonical record of who actually ran the skill; the audit line is convenience documentation for quick reading. Projects can validate the two match in CI if desired.
- No external validation. The skill does not check if handles are valid in any external system (no LDAP lookup, no GitHub user check, etc.). This keeps the skill lightweight and offline. Validation is a team policy matter.
Versioning
0.1.3 (unreleased — Stage 4 of v0.9.0 cut) — added the missing Asset dependencies section so the SKILL.md conforms to the full 12-section contract in skill-contract-template.md. No behavior change.
0.1.2 — Added --dry-run flag specification with full semantics contract.
0.1.0 — initial release (Stage 3 of v0.9.0, landed after v0.9.0-alpha.2 which added the assigned_to field). Single operation per invocation. Audit trail model. Stage 2 rebuild contract.