/artifact-path
Internal skill for commands. Resolve a unique, task-scoped, collision-safe path for an artifact written under .lets/ (plans, reviews, session snapshots, hand-off briefs, per-run agent report directories). Do not trigger on user conversation - only when a command is about to
Claude Code
$ npx -y skills add restarter/lets-workflow --skill artifact-path --agent claude-code
Codex
$ npx skills add restarter/lets-workflow --skill artifact-path -a codex -y
Cursor
$ npx skills add restarter/lets-workflow --skill artifact-path -a cursor -y
Gemini CLI
$ npx skills add restarter/lets-workflow --skill artifact-path -a gemini-cli -y
GitHub Copilot
$ npx skills add restarter/lets-workflow --skill artifact-path -a github-copilot -y
OpenCode
$ npx skills add restarter/lets-workflow --skill artifact-path -a opencode -y
Windsurf
$ npx skills add restarter/lets-workflow --skill artifact-path -a windsurf -y
SKILL.md
artifact-path.SKILL.mdname: artifact-path
description: Internal skill for commands. Resolve a unique, task-scoped, collision-safe path for an artifact written under .lets/ (plans, reviews, session snapshots, hand-off briefs, per-run agent report directories). Do not trigger on user conversation - only when a command is about to write an artifact.
user-invocable: false
Artifact Path
`.lets/` is ONE directory shared by every worktree of the repo (whole-dir symlink). Two sessions writing the same artifact kind on the same day used to overwrite each other (lets-05c4s - a plan review was lost). This skill is the single place that names an artifact and guarantees the path is free.
**Contract:** args `kind=<kind> ext=<md|json|dir> [task=<task-id>]`. Output: ONE echoed line `ARTIFACT_FILE=<absolute path>`. The caller writes to that path VERBATIM - never recomputes the stamp, never strips the `-vN` suffix, never writes anywhere else. `ext=dir` CREATES the directory (the `mkdir` is the claim) and echoes its path; a file kind only names a free path.
Name shape
`{YYYY-MM-DD}-{HHMM}-{ID}-{KIND}[-vN].{EXT}` under the kind's directory:
| kind | directory | |------|-----------| | `plan` | `.lets/plans/` | | `idea` | `.lets/plans/` | | `review-local`, `review-branch`, `review-pr-<n>`, `review-plan` | `.lets/reviews/` | | `snapshot` | `.lets/sessions/` | | `handoff` | `.lets/handoffs/` | | `reports-<command>` (with `ext=dir`) | `.lets/reports/` - one directory per dispatching run, filled by the `agent-report` skill |
`ID` is the task id when one is active (MANDATORY whenever a task exists - same rule as the branch named by the active tracker convention). With no task: `{branch-slug}-{6hex}`, 6hex = first 6 chars of `$CLAUDE_CODE_SESSION_ID`, so two taskless sessions on `main` still get distinct names. `-vN` (v2, v3, ...) is appended whenever the path already exists - for task and taskless alike.
Step 1: Resolve the task id
If the caller passed `task=`, use it. Otherwise invoke `Skill(skill: "lets:detect-task", args: "fallback=no")` and use its result (may be empty). `fallback=no` is mandatory: the searched fallback asks a question, and a taskless snapshot on `/lets:end` must stay silent.
Step 2: Compute the path
LETS_PROJECT_ROOT=$(git rev-parse --show-toplevel)
KIND="{kind}"; EXT="{ext}"; TASK_ID="{task-id or empty}"
case "$KIND" in
plan|idea) DIR=plans ;;
review-*) DIR=reviews ;;
snapshot*) DIR=sessions ;;
handoff) DIR=handoffs ;;
reports-*) DIR=reports ;;
*) echo "artifact-path: unknown kind '$KIND'"; exit 1 ;;
esac
mkdir -p "$LETS_PROJECT_ROOT/.lets/$DIR"
STAMP=$(date +%Y-%m-%d-%H%M)
if [ -n "$TASK_ID" ]; then
ID="$TASK_ID"
else
HEX=$(printf '%s' "${CLAUDE_CODE_SESSION_ID:-$$}" | cut -c1-6) # session-id prefix; PID outside Claude Code
ID="$(git branch --show-current | tr '/' '-')-${HEX}"
fi
BASE="$LETS_PROJECT_ROOT/.lets/$DIR/${STAMP}-${ID}-${KIND}"
N=2
if [ "$EXT" = dir ]; then
# mkdir without -p is atomic: two sessions in the same minute get two directories, never one shared
ARTIFACT_FILE="$BASE"
until mkdir "$ARTIFACT_FILE" 2>/dev/null; do
[ -e "$ARTIFACT_FILE" ] || { echo "artifact-path: cannot create $ARTIFACT_FILE"; exit 1; }
ARTIFACT_FILE="${BASE}-v${N}"; N=$((N+1))
done
else
ARTIFACT_FILE="${BASE}.${EXT}"
while [ -e "$ARTIFACT_FILE" ]; do ARTIFACT_FILE="${BASE}-v${N}.${EXT}"; N=$((N+1)); done
fi
echo "ARTIFACT_FILE=$ARTIFACT_FILE"Return
Return the echoed `ARTIFACT_FILE` to the caller. If the echo is missing or `exit 1` fired, the caller MUST NOT write anything - surface the error.
Rules
- NEVER overwrite: the loop is the guard; the caller never "fixes" a path by hand.
- `ext=dir`: the directory exists when the echo returns; the caller never creates or renames it.
- NEVER compute a second `date` in the caller - the stamp is captured here once.
- Task id in the name is mandatory when a task is active.
- No tracker calls here beyond `detect-task`.
- Go-owned state files are NOT artifacts and never come through here: `.lets/sessions/.task-*` (`lets worktree task-state`), `.lets/sessions/peers/last/*.last` (`lets peers who` / `orchestrator`; a legacy `.lets/sessions/peers/*.role` is never read or written), `.lets/cache/released-*` (`lets worktree release`), and `.lets/cache/ask-ro-mcp-empty.json` - their owner writes them.
Read more
name: artifact-path description: Internal skill for commands. Resolve a unique, task-scoped, collision-safe path for an artifact written under .lets/ (plans, reviews, session snapshots, hand-off briefs, per-run agent report directories). Do not trigger on user conversation - only when a command is about to write an artifact. user-invocable: false
Artifact Path
`.lets/` is ONE directory shared by every worktree of the repo (whole-dir symlink). Two sessions writing the same artifact kind on the same day used to overwrite each other (lets-05c4s - a plan review was lost). This skill is the single place that names an artifact and guarantees the path is free.
**Contract:** args `kind=<kind> ext=<md|json|dir> [task=<task-id>]`. Output: ONE echoed line `ARTIFACT_FILE=<absolute path>`. The caller writes to that path VERBATIM - never recomputes the stamp, never strips the `-vN` suffix, never writes anywhere else. `ext=dir` CREATES the directory (the `mkdir` is the claim) and echoes its path; a file kind only names a free path.
Name shape
`{YYYY-MM-DD}-{HHMM}-{ID}-{KIND}[-vN].{EXT}` under the kind's directory:
| kind | directory | |------|-----------| | `plan` | `.lets/plans/` | | `idea` | `.lets/plans/` | | `review-local`, `review-branch`, `review-pr-<n>`, `review-plan` | `.lets/reviews/` | | `snapshot` | `.lets/sessions/` | | `handoff` | `.lets/handoffs/` | | `reports-<command>` (with `ext=dir`) | `.lets/reports/` - one directory per dispatching run, filled by the `agent-report` skill |
`ID` is the task id when one is active (MANDATORY whenever a task exists - same rule as the branch named by the active tracker convention). With no task: `{branch-slug}-{6hex}`, 6hex = first 6 chars of `$CLAUDE_CODE_SESSION_ID`, so two taskless sessions on `main` still get distinct names. `-vN` (v2, v3, ...) is appended whenever the path already exists - for task and taskless alike.
Step 1: Resolve the task id
If the caller passed `task=`, use it. Otherwise invoke `Skill(skill: "lets:detect-task", args: "fallback=no")` and use its result (may be empty). `fallback=no` is mandatory: the searched fallback asks a question, and a taskless snapshot on `/lets:end` must stay silent.
Step 2: Compute the path
LETS_PROJECT_ROOT=$(git rev-parse --show-toplevel)
KIND="{kind}"; EXT="{ext}"; TASK_ID="{task-id or empty}"
case "$KIND" in
plan|idea) DIR=plans ;;
review-*) DIR=reviews ;;
snapshot*) DIR=sessions ;;
handoff) DIR=handoffs ;;
reports-*) DIR=reports ;;
*) echo "artifact-path: unknown kind '$KIND'"; exit 1 ;;
esac
mkdir -p "$LETS_PROJECT_ROOT/.lets/$DIR"
STAMP=$(date +%Y-%m-%d-%H%M)
if [ -n "$TASK_ID" ]; then
ID="$TASK_ID"
else
HEX=$(printf '%s' "${CLAUDE_CODE_SESSION_ID:-$$}" | cut -c1-6) # session-id prefix; PID outside Claude Code
ID="$(git branch --show-current | tr '/' '-')-${HEX}"
fi
BASE="$LETS_PROJECT_ROOT/.lets/$DIR/${STAMP}-${ID}-${KIND}"
N=2
if [ "$EXT" = dir ]; then
# mkdir without -p is atomic: two sessions in the same minute get two directories, never one shared
ARTIFACT_FILE="$BASE"
until mkdir "$ARTIFACT_FILE" 2>/dev/null; do
[ -e "$ARTIFACT_FILE" ] || { echo "artifact-path: cannot create $ARTIFACT_FILE"; exit 1; }
ARTIFACT_FILE="${BASE}-v${N}"; N=$((N+1))
done
else
ARTIFACT_FILE="${BASE}.${EXT}"
while [ -e "$ARTIFACT_FILE" ]; do ARTIFACT_FILE="${BASE}-v${N}.${EXT}"; N=$((N+1)); done
fi
echo "ARTIFACT_FILE=$ARTIFACT_FILE"Return
Return the echoed `ARTIFACT_FILE` to the caller. If the echo is missing or `exit 1` fired, the caller MUST NOT write anything - surface the error.
Rules
- NEVER overwrite: the loop is the guard; the caller never "fixes" a path by hand.
- `ext=dir`: the directory exists when the echo returns; the caller never creates or renames it.
- NEVER compute a second `date` in the caller - the stamp is captured here once.
- Task id in the name is mandatory when a task is active.
- No tracker calls here beyond `detect-task`.
- Go-owned state files are NOT artifacts and never come through here: `.lets/sessions/.task-*` (`lets worktree task-state`), `.lets/sessions/peers/last/*.last` (`lets peers who` / `orchestrator`; a legacy `.lets/sessions/peers/*.role` is never read or written), `.lets/cache/released-*` (`lets worktree release`), and `.lets/cache/ask-ro-mcp-empty.json` - their owner writes them.
A development workflow plugin for Claude Code Stop babysitting your AI. Start shipping with it.
Other skills on lets-workflow.
goal-test
Local sandbox for experimenting with Claude Code's /goal command (stop-hook condition + LLM-evaluator driving autonomous work…
loop-bb-pr
Compose a /loop that monitors a Bitbucket PR for activity (new comments, review state changes, merge, decline) via the bb-api…
loop-gh-pr
Compose a /loop that watches all open GitHub PRs for activity (new PRs, new commits, new comments) and auto-prepares a READ-ONLY…
loop-test
Local sandbox for experimenting with Claude Code's /loop command (interval + dynamic/self-paced scheduling) applied to LETS…
actor-fetch-personalit…
Internal skill for commands. Fetch and validate personality from URL or file path for Actor agent. Do not trigger on user…
More skills from other plugins.
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
ask-matt
Ask which skill or flow fits your situation. A router over the skills in this repo.
Questions about artifact-path.
- Which coding agents can use artifact-path?
- artifact-path installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

