A per-instance running list of platform work, kept by the assistant: tasks, a backlog of requested and inferred work, and milestones followed through GitHub to release and install.
The punch list is one Markdown file per day in punchList.dir. People and the assistant edit it directly; two runnables keep it current:
punch-list-refresh-status (every 61 minutes once enabled) applies checkbox ticks, follows each milestone's GitHub issues to release and install, writes today's file and posts milestone transitions to Slack.punch-list-harvest (daily once enabled) finds platform work in the instance's own archive (email, Slack, meetings, Drive) and telemetry (runner, watcher, meta) through one worker session, and files it to the Backlog without anyone asking.Both ship disabled and skip with [skip] no punchList block in the instance config (exit 0) until the instance configures the punchList block. Both are dry runs unless --live.
| Script | Description |
|---|---|
status/job.ts |
Runnable punch-list-refresh-status. Without --live it computes and logs today's file and the post, and writes and posts nothing. |
harvest/job.ts |
Runnable punch-list-harvest. --print-task prints the worker's task and where it came from, and dispatches nothing; without --live it dispatches the worker but writes and posts nothing. |
flowchart TD
file["punchList.dir/YYYY-MM-DD.md<br/>(people and the assistant edit it)"]
status["punch-list-refresh-status"] -->|ticks, status lines| file
gh["GitHub: milestones, issues,<br/>closing PRs, releases"] --> status
inst["installed package.json versions,<br/>scripts clone HEAD"] --> status
status -->|transitions| slack["punchList.slack.channel"]
archive["archive + telemetry<br/>(punchList.inputRoots)"] --> worker["harvest worker session"]
harvest["punch-list-harvest"] -->|task| worker
worker -->|JSON report| harvest
harvest -->|record, then Backlog items| file
harvest -->|summary, pinned quick links| slack
Every write to the file holds a cross-process lock (lockFile) and goes through a temp file and a rename. If the file changed since it was read (a person or the assistant editing at the same time) the write re-reads and retries, up to three attempts. A file that doesn't parse fails the run and is never overwritten.
{dir}/YYYY-MM-DD.md, "today" being the date in punchList.timeZone. The first run of a day rolls the latest earlier file over: unfinished work carries forward, and finished entries older than retentionDays are left off (archiving is omission; the earlier files stay as they are).
# Punch List — 2026-10-18
## Tasks
- [ ] **Rotate the expiring token** — the token expires Friday · added 2026-10-18 · `t-0a1b2c3d`
## Backlog
- [ ] **Dropbox support** — files live there · requested by Ana · [thread](https://…) · added 2026-10-17 · `b-0e1f2a3b`
## TODO
### acme-dropbox — Dropbox support
Free prose: the plan, the integration steps.
- Issues: [acme/jeeves-scripts-acme#2](https://github.com/…) open
## Done
_None._
## Integrated
_None._
## Completed Tasks
_None._
## Processed Backlog Items
_None._
| Section | Holds | Moves |
|---|---|---|
| Tasks | Checkbox entries (t- ids): platform work to do. |
Ticked: to Completed Tasks, stamped done <date>. |
| Backlog | Checkbox entries (b- ids): requested or inferred work not yet planned. |
Ticked: to Processed Backlog Items, stamped processed <date> (promoted to a milestone, done or rejected). |
| TODO | Milestone blocks being worked. | All issues shipped: to Done. |
| Done | Milestones whose issues all shipped, with - [ ] Integrated. |
Integrated ticked: to Integrated, stamped with the date. A reopened or unreleased issue: back to TODO. |
| Integrated | Finished milestones, - [x] Integrated <date>. |
Unticked: back to Done for another look. |
| Completed Tasks, Processed Backlog Items | Finished entries. | Unticked: back to Tasks or Backlog. |
- [ ] **Title** — details · <markers> · \t-0a1b2c3d`. The text between the checkbox and the id is kept verbatim. A person may type an entry without an id; the next write gives it one. Markers (added, done, processed, each with a date) are separated by · `.### <slug> — Title, free prose, then the checker's status lines (- Issues: , - Releases: , - Installed: ), which it rewrites each run. A milestone's slug is the title of the GitHub milestone holding its issues, in the scripts repo or any install-map repo. Convention: prefix the instance's name (acme-dropbox); core treats the slug as an opaque string and enforces no form._None._.parsePunchList and renderPunchList (root exports) read and write this format; parsePunchList(renderPunchList(x)) equals x.
punchList block. Resolves today in timeZone and loads (or rolls over) today's file.Integrated unticked).installMap repo (gh, the instance's gh auth): its issues, each issue's closing PRs (GraphQL), and each repo's published releases (no drafts or prereleases). Lists are read 100 at a time with no further pages.component/version (or a bare version, component '') is installed when a package.json matching that component's installMap glob holds the same major.minor.patch or later; a scripts-repo PR is installed once its merge commit is an ancestor of scriptsRepo.clone's HEAD (git merge-base --is-ancestor).<day>: N milestone(s) checked; [ticks applied; ]file written|would change|unchanged.slack.channel with slack.account's bot token, linking today's file through jeeves-server. A post that fails is kept in pendingFile and retried, oldest first, before anything new on the next run.Without --live it logs the file it would write and [dry-run] would post to <channel>: with the message. A run against unchanged GitHub state and an already-ticked file writes nothing and posts nothing.
punchList block. Rolls today's file over if needed.--print-task: logs where the task came from and the task (with the saved window, or the last 24 hours on a first run), then stops: no dispatch, no writes.{dir}/harvest/*.json with delivered: false) left by an earlier run, oldest first, without dispatching again: merges its items, posts and marks it delivered.cursorFile); on the first run, from 24 hours before the run, saved at once so a failed first run retries the same window.inputRoots (each expanded for {npmGlobal} and {home}) changed since the window start, logs no new input since <time>; nothing dispatched and stops.{dir}/TASK.md when the instance has one, else core's template (task-templates/punch-list/harvest.task.md, read from the package; nothing is copied). The run logs which (task from the instance file … or task from core template punch-list/harvest.task.md (no instance file at …)). Then the run's section: the window, the input paths, the spec (when specPath is set), everything already on the list (so the worker proposes no duplicate), and the output contract, stamped with today's date in timeZone.dispatchSession with low thinking. It writes one JSON report to the file the task names (in inboxDir) and replies DONE; nothing reads its session history.{"items":[{"title","why","kind","requester","sources"}],"summary"}. title 3-120 characters; why up to 600; kind requested (with requester) or inferred; at least one source, each an absolute file path from a watcher hit or a telemetry reference (runner:<job>#<run>, watcher:<path>, meta:<path>). A report of HARVEST_FAILED: <reason> (a worker whose tools failed) fails the run with the cursor unmoved, so the window is retried.skipped "<title>": <reason>).{dir}/harvest/<run start>.json before the file is touched; then the items join the Backlog (requested by <name> or inferred, the sources, added <date>), the summary is posted and the record marked delivered. A crash anywhere after the record resumes at step 3.slack.pinnedTs set, edits that message to link today's file and the spec (the spec line is left out without specPath); without it, logs no pinned message ts configured; quick links not updated.Without --live the worker is still dispatched (the report is real) but no file, record or cursor is written, the pinned message is left alone and nothing is posted: the worker writes its report to a temporary directory that the run removes (even when the run fails), not to inboxDir, which a dry run never creates. Re-running with no new input and no undelivered record does nothing.
The file is an authored record, not a mirror: harvested items are the worker's paraphrase with attribution and links. Nothing here redacts; the watcher redacts secrets at index time (Decision 70).
Core's template tells the worker which tools it may use (watcher search and scan, reading files it found, runner, watcher and meta status; no session, memory, web, exec or messaging tools), a budget of 30 tool calls, what to look at, and what to report: platform work only (the instance, its integrations, jobs and assistant, not the business's own work), requested items with the requester named, inferred items with why they matter, one item per need, every item citing a source, and HARVEST_FAILED when a tool fails. An instance with different needs writes its own {dir}/TASK.md; the run section and output contract are always appended by the runnable.
The top-level punchList block (punchListSchema, config). Absent: both runnables skip. Present but invalid: config check and every run fail (Decision 76).
"punchList": {
"dir": "/base/content/admin/punch-list",
"timeZone": "America/Chicago",
"slack": { "channel": "C0123456789" },
"specPath": "/base/content/admin/punch-list-spec.md",
"scriptsRepo": { "repo": "acme/jeeves-scripts-acme", "clone": "/base/jeeves-scripts" },
"installMap": {
"karmaniverous/jeeves-server": [
{ "component": "service", "packageJson": "{npmGlobal}/@karmaniverous/jeeves-server/package.json" }
]
}
}
| Key | Default | Rules |
|---|---|---|
dir |
required | Absolute path. The daily files, harvest/ records and an optional TASK.md. |
timeZone |
required | IANA zone. Sets the day boundaries, not when the runner fires a job. |
retentionDays |
30 |
Positive integer. |
slack.channel |
required | A conversation id (C…, G…, D…), or a channel: target. A user id (U…, W…, user:) is refused, because chat.update of the pinned message needs the conversation id: configure a DM by its D… id. Channel names are refused, as everywhere in core. |
slack.account |
default |
The gateway Slack account whose bot token posts. |
slack.pinnedTs |
unset | ts of the pinned quick-links message; unset, the harvester leaves it alone. |
specPath |
unset | Absolute path of the instance's punch-list spec, linked from the pinned message and the harvest task. |
scriptsRepo.repo |
required | owner/name of this instance's scripts repo (unversioned: Done means a merged PR). |
scriptsRepo.clone |
required | Absolute path of its deployed clone. |
installMap |
{} |
owner/name → [{ component, packageJson }]: the versioned repos scanned for milestones, and the glob of each released component's installed package.json. {npmGlobal} expands to npm's global node_modules (<prefix>/lib/node_modules, <prefix>\node_modules on Windows), with the prefix from npm_config_prefix when set, else npm prefix -g (read once per run; it honours every npmrc, e.g. the standard Windows installer's %APPDATA%\npm), else npm's built-in default when npm can't be run; {home} expands to the home directory. Core ships no map. |
lockFile |
{stateDir}/punch-list/punch-list.lock |
Absolute path. |
pendingFile |
{stateDir}/punch-list/status-pending.json |
Absolute path. |
cursorFile |
{stateDir}/punch-list/harvest-cursor.json |
Absolute path. |
inboxDir |
{stateDir}/punch-list/inbox |
Absolute path. |
inputRoots |
the default silo's email, slack, meetings, google-drive, and {stateDir}/jeeves-watcher/issues.json |
Non-empty list of absolute paths, or paths starting with {home} or {npmGlobal} (expanded as for installMap when the config is resolved, so config check, the pre-check and the worker's task all agree); set it for a multi-silo instance. |
The jeeves-server config is read from {configDir}/jeeves-server/config.json (its publicUrl and roots), like every component config.
To turn it on: add the block, run each runnable without --live (and punch-list-harvest --print-task) and check the output, then enable the jobs with deltas that add --live (the manifests pass no arguments, so a job enabled without it is a scheduled dry run); set the harvest's schedule and zone in its delta if 05:47 UTC doesn't suit:
"jobs": {
"punch-list-refresh-status": { "enabled": true, "args": ["--live"] },
"punch-list-harvest": {
"enabled": true,
"args": ["--live"],
"schedule": { "freq": "daily", "byhour": 5, "byminute": 47, "timezone": "America/Chicago" }
}
}
The punch-list skill tells every session how to read and add to the file.
gh authenticated for every scanned repo (integrations.gh).slack.channel (and edit the pinned message).punchList.dir and the archive under its roots (links to the file and to sources).git on PATH (the scripts-repo install check).| Job | Schedule |
|---|---|
punch-list-refresh-status |
Every 61 minutes (UTC), shipped disabled |
punch-list-harvest |
Daily at 05:47 UTC, shipped disabled |
| File | Purpose |
|---|---|
config.ts |
The resolved punchList block (derived paths, bare channel id), scannedRepos, expandPath |
runtime.ts |
The skip, shared run dependencies and today's file |
store.ts, lock.ts |
Load, roll over and write the daily file under the lock |
model/ |
The file model: types, line grammar, parse, render, transitions |
github/ |
The GitHub port, facts per milestone, install checks |
status/ |
punch-list-refresh-status: refresh, pending posts, the run |
harvest/ |
punch-list-harvest: input pre-check, task, worker, report, record, merge, the run |
messages.ts, slack.ts, links.ts |
Slack messages, posting, jeeves-server links |
../config/punch-list-schema.ts |
The punchList block schema |
core task-templates/punch-list/harvest.task.md |
The bundled harvest task |
core jobs/punch-list.json |
The runner job manifests (shipped disabled) |