---
name: log
description: Writes one day's work log as a file from the tracker, calendar and chat, and fills the days an earlier run missed. Triggers - "/pm:log", "write the log for today", "fill in the days that were missed", "일지 써줘", "업무 일지 작성", "빠진 날 채워줘".
disable-model-invocation: true
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, mcp__claude_ai_Notion__notion-fetch, mcp__claude_ai_Notion__notion-query-data-sources, mcp__claude_ai_Notion__notion-search, mcp__claude_ai_Notion__notion-get-users, mcp__claude_ai_Slack__slack_read_channel, mcp__claude_ai_Slack__slack_read_thread, mcp__claude_ai_Slack__slack_search_public_and_private, mcp__claude_ai_Google_Calendar__list_events, mcp__claude_ai_Slack__slack_read_user_profile
---

# log — one day, one file

**Part of a plugin.** The scripts this skill runs ship beside it under `${CLAUDE_PLUGIN_ROOT}`. If that path does not resolve, this file was installed on its own — stop and say the plugin itself is needed (`claude plugin install pm@byjunyoung`), rather than improvising what the scripts do.

Assembles a day's work log from the tracker the task skills already use, plus a calendar and chat where you name them, and writes it as a single markdown file. Reads everywhere, writes one place: a file under `log.out.dir`. Nothing is written back to the tracker.

Built for a scheduler. It runs with nobody watching, so the rules below lean hard on *say what you could not do* rather than *fill it in anyway*.

**It records; it does not appraise.** These files are the raw material a later review turns into accomplishment statements — that is `/pm:log-review`. This skill's job is to capture, on the day, the things that cannot be reconstructed months later: who said what about your work, what was decided and why, and the link that proves a piece of work existed. It never rates the importance of anything, and it never writes a sentence a source did not supply.

## Principles

- **Never ask.** There is no one to answer. A missing value is skipped and named in the closing summary.
- **Never invent.** Every line traces to something a source returned. An empty section gets `log.empty_line`, not a plausible sentence.
- **Quote, do not characterise.** Praise, criticism and decisions are recorded in the words they were said in, with a link. Do not summarise someone's opinion into your own phrasing — a later review needs the original.
- **Read the tracker's schema before querying it.** Property names drift and differ per workspace. Fetch the data source and use the names it currently has. Never trust names pinned in this document or in config.
- **A source that fails does not stop the run.** Write the log from what did come back, and record what did not.
- **The file is the output.** No preview gate — the target is a file in the user's own log directory, and every other system is read-only here.
- **Yesterday is still fixable.** Every run re-checks the recent past and repairs what an earlier run missed.

## Configuration

```bash
python3 ${CLAUDE_PLUGIN_ROOT}/_common/scripts/lib/resolve-config.py --name pm-conventions.yaml --authored task.record.type,task.record.ref
```

**Exit 3 stops the run** with the message naming the line to add to the settings — this is the one skill that runs unattended and it runs no machine check (preflight), so a tracker setting written half-way is caught here rather than read from the wrong place.

`log.enabled` is on unless it says `false`; `false` means stop immediately and say so — that is a correct outcome, not an error. This skill runs only when called by name, so a scheduler calls `/pm:log` itself. `task.record` gives the tracker; `log` gives everything else. A `null` source is skipped and named in the summary.

## 1. Settle the date

Get today in the local timezone from the shell, not from memory. Everything downstream keys off this one value.

```bash
date +%Y-%m-%d
```

## 2. Learn the tracker's shape

**The calls live per tracker, not in this document.** Read the record's adapter before querying anything; a `type` of `none` means there is no tracker — skip section 3 and say so in the summary.

```bash
python3 ${CLAUDE_PLUGIN_ROOT}/_common/scripts/lib/adapter.py --name pm-conventions.yaml --kind trackers --type {task.record.type} --role record
```

`--type` is required; pass `none` where `task.record.type` is `none`. It prints the file to read — the bundled one, or yours from `adapters.dirs` where you drafted one. **Exit 3 means no adapter exists for that type.** Stop and say so; `/pm:setup` drafts one from the tools connected on this machine. Do not improvise the calls, and do not fall back to a tracker the config did not name. **Exit 4 means the adapter exists but answers for the other side** — a mirror-only file asked for the record, say. Stop the same way; `/pm:setup` 4c drafts the side that is missing. Say what it means in the person's words — "this tool isn't supported yet; `/pm:setup` can add it from what's connected here" — never the exit code or the file path.

Where the type is `markdown`, the adapter's `moved` and `list` commands are the read, and there is no schema to fetch beyond the front matter the files carry. Otherwise, whatever the tracker, read its **current** schema or field names immediately before querying, and use what it has now — the title, status, assignee, project, grouping, priority, due-date and progress equivalents, and the exact spellings a query needs. Names drift and differ per workspace. A name pinned in a document or in config returns nothing, and nothing looks exactly like a quiet day.

If that read fails, skip section 3 and say so. Do not fall back to guessed names.

## 3. Collect what moved

Query rows the day actually touched, split by whether they are yours.

- **Mine** — assignee contains `log.me.tracker_user_id`, last-edited on the date.
- **The team's** — assignee is anyone else or empty, last-edited on the date.

**Where `log.me.tracker_user_id` is null, nothing says whose a row is.** Put every row under *My tasks*, leave *Team tasks (reference)* to items discussed that have no row, and say in the summary that rows were not split between yours and the team's, naming `log.me.tracker_user_id` as the key that would split them. Never guess ownership from a name or from who last edited.

**A markdown record** (`task.record.type: markdown`) is read with the adapter's `moved --dir {task.record.ref} --date {date}`, not a last-edited filter. It returns `{"paths": [...], "method": ...}`: the files committed on that date, plus, for today, files edited and not yet committed (`git+uncommitted`). Read each path's front matter for its row. A path that no longer exists was removed or renamed since — `moved` cannot tell which; report it under that wording in the summary and never open it as a record. The summary names the `method` `moved` returned; for a backfilled day that is "commits only", because the work tree of a past day is not recoverable. An `mtime` day came from modification times, which a clone or checkout rewrites — say so. A task folder that does not exist (`method` `none`) is "no task records yet ({dir})" in the summary: it is not a failure, and the section reads `log.empty_line`.

Query on **last edited or last updated**, never on created. Work in progress was created weeks ago; a created-date filter returns a nearly empty day and looks like a correct answer.

How the day filter is applied is the tracker's business. A store that can be queried exhaustively takes it as a condition; one that can only be searched needs the filter applied after the fetch, and its adapter says so. **Where the exhaustive read is gated or missing, say so in the summary** — an under-covered day must never read as a quiet one.

Carry each row's **stable url** alongside its title. For a markdown record the url is its `path`, exactly as `list` and `moved` print it. Titles get rewritten; urls do not, and a later review stitches months of entries together by url. A row recorded without its url cannot be stitched.

Where the previous entry for the same url shows a different status, note the transition rather than the state alone — `in progress → done` says something `done` does not. Rows that reached a terminal status today go into the front matter's `completed` list as title and url.

Resolve assignee ids to names through `log.people`. An id that is not there is looked up once against the tracker's user directory and used as found. Never guess a name from an id, and never infer one from an adjacent mapping — say the id was unresolved instead.

## 4. Collect the day around it

- **Calendar** — `log.sources.calendar`, the day in local time, ordered by start. Drop working-location, birthday and out-of-office entries. Each event becomes time, title, and one or two key attendees. The calls are in the adapter for `sources.calendar_type` (`adapter.py --kind sources --type {sources.calendar_type} --role calendar`); `none` skips the calendar and says so.
- **Chat** — `log.sources.chat_channels` for the day, plus messages the user sent or was mentioned in. Keep decisions, questions and answers, feedback, shared material, and open threads. Drop chatter, bots and reactions. The calls are in the adapter for `sources.chat_type` (`adapter.py --kind sources --type {sources.chat_type} --role chat`); `none` skips chat and says so, and exit 3 means the tool has no adapter yet — skip the section, and name that in the summary rather than reading a tool the config did not name.

From the same pass, pull out two things that are cheap now and unrecoverable later.

- **What others said** — messages *about* the user's work: thanks, praise, criticism, an accepted proposal, a request that names them as the one to do it. Record the sender, the quote as written, and the permalink. Do not include something merely because the user was mentioned in it, and do not soften or sharpen the wording.
- **Decisions** — a decision the user made or took part in, with the reason **as the source states it**. No reason in the source means no reason in the file. Do not reconstruct rationale.

- **Your own notes** — `log.sources.notes_channel` is a channel only you write in, so everything
  there is yours by definition and nothing needs tagging. Read the day's messages and drop the
  ones an app posted on your behalf: the workspace attributes those to the app, and the ids to
  ignore are in `log.sources.notes_exclude_apps`. Nothing you did not type belongs here.

  **Carry the text across, not a link to it.** Chat retention expires; this file does not, and
  a log that points at a message nobody can open years later has not recorded anything. Keep the
  time, the body as written, and the permalink alongside it.

Chat search tools cap their response size. Ask for concise output without surrounding context, and page rather than widening a single call — an over-large request fails outright and returns nothing, which reads as silence.

## 5. Assemble

Write `log.out.dir/YYYY/MM/YYYY-MM-DD.md`. Front matter carries `log.out.front_matter`; counts are counts and `completed` is an index of title and url, never a second copy of the body. Sections follow `log.sections` in order, and a section with nothing in it gets `log.empty_line`.

What belongs where, given the default section names:

| Section | What goes in |
|---|---|
| What I did | Three to six lines. Your own work and the discussions you personally drove |
| Meetings | One line per event, with the decision or the point of it |
| My tasks | Rows from section 3, mine. Title with url, priority, status transition, progress, project, grouping, due |
| Team tasks (reference) | Rows from section 3, the team's, each with its owner. Plus items discussed today that have no row yet |
| Decisions | What was decided, and the stated reason. One line each |
| What others said | Sender, quote, permalink. Verbatim |
| Evidence | Links to what the day produced — a change request, a design node, a document, a release announcement |
| Notes | What you typed to yourself that day — meeting notes, memos, things to look into. Body as written, with time and permalink |
| Follow-ups | Threads still open, each with the action it is waiting on |
| Tomorrow | One to three unchecked boxes, drawn from what is unresolved |

Where the file already exists with a body, update it rather than replacing it — preserve anything a human added by hand.

Prose follows whatever writing standard applies in `meta.language`.

## 6. Fill the gaps behind you

Look back `log.backfill.business_days` business days. For each one, check the file exists and its sections are not all empty. A day whose file is missing is written now, from the same sources scoped to that date. A day whose file exists but has an empty section the sources can now fill gets that section filled — nothing else on the page is touched.

Limit the look-back to the configured window so a long absence does not turn one run into a backfill of the whole quarter. Say in the summary how far back you went and what you repaired.

## 7. Summarise

Close with a few lines: the file path, the counts that went in, what the gap check repaired or that it found nothing, and any source that was skipped along with why. This summary is the only thing a scheduled run leaves in its own log, so it has to be enough to diagnose a bad day from.

## When NOT to invoke

- Turning these files into accomplishment statements → `/pm:log-review`
- Writing a task record's context table → `/pm:task draft`
- Filing a task in an engineering tracker → `/pm:task publish`
- Reconciling two trackers → `/pm:task sync`
- Writing requirements → `/pm:prd`
