dotmd-cli 0.87.2 → 0.89.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -4
- package/assets/opencode/plugin.js +29 -14
- package/bin/dotmd.mjs +23 -6
- package/package.json +2 -1
- package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
- package/plugins/runlist-codex/bin/runlist-hook +86 -0
- package/plugins/runlist-codex/hooks/hooks.json +16 -0
- package/plugins/runlist-codex/skills/runlist/SKILL.md +21 -0
- package/runlist.config.example.mjs +5 -0
- package/src/atomic-mutation.mjs +10 -2
- package/src/baton.mjs +140 -49
- package/src/codex-integration.mjs +81 -0
- package/src/commands.mjs +1 -1
- package/src/config.mjs +9 -6
- package/src/export.mjs +1 -1
- package/src/guard.mjs +60 -7
- package/src/hub-membership.mjs +1 -1
- package/src/index-file.mjs +20 -5
- package/src/init.mjs +6 -6
- package/src/install.mjs +50 -2
- package/src/lifecycle.mjs +26 -7
- package/src/migrate-prompts.mjs +2 -2
- package/src/new.mjs +1 -1
- package/src/update.mjs +12 -1
package/README.md
CHANGED
|
@@ -37,13 +37,28 @@ integration for whichever host you run:
|
|
|
37
37
|
```bash
|
|
38
38
|
runlist install # what's installed for each host
|
|
39
39
|
runlist install claude # Claude Code plugin (marketplace + plugin)
|
|
40
|
+
runlist install codex # Codex skill and hooks (personal marketplace)
|
|
40
41
|
runlist install opencode # OpenCode plugin (one auto-discovered file)
|
|
41
42
|
runlist doctor --session # what identity runlist sees here, and from where
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
The integrations are one-time and global. Codex exports `CODEX_THREAD_ID` to
|
|
46
|
+
every tool shell, so its identity works without the plugin; the plugin adds a
|
|
47
|
+
workflow skill, session orientation, and the tool guard.
|
|
48
|
+
|
|
49
|
+
### Codex Plugin
|
|
50
|
+
|
|
51
|
+
`runlist install codex` copies the plugin included in the npm package into the
|
|
52
|
+
personal Codex marketplace and runs `codex plugin add runlist-codex@personal`.
|
|
53
|
+
Codex asks you to review and trust plugin hooks separately. Start a new thread
|
|
54
|
+
after installation so the skill and SessionStart primer load. The hooks use
|
|
55
|
+
the globally installed `runlist` CLI; a missing CLI produces one install hint
|
|
56
|
+
and otherwise leaves tool calls alone. `CODEX_THREAD_ID` stays the ownership
|
|
57
|
+
source. The CLI refuses to overwrite a plugin directory it did not generate.
|
|
58
|
+
|
|
59
|
+
Direct prompt reads warn by default in Codex and Claude. A repository can set
|
|
60
|
+
`export const guard = { promptReads: 'deny' };` to block reads of existing
|
|
61
|
+
pending prompts; `runlist prompts show` remains the read-only inspection verb.
|
|
47
62
|
|
|
48
63
|
### Claude Code Plugin
|
|
49
64
|
|
|
@@ -72,6 +87,9 @@ supplies the two things the CLI cannot get on its own:
|
|
|
72
87
|
- **A session-start briefing**, the equivalent of Claude Code's SessionStart
|
|
73
88
|
hook. OpenCode's Claude Code compatibility covers skills and the system
|
|
74
89
|
prompt, not hooks, so nothing else runs `runlist hud`.
|
|
90
|
+
- **Prompt-read guardrails.** Recognized reads are checked before execution so
|
|
91
|
+
the opt-in strict policy can stop them. Default warnings are attached to the
|
|
92
|
+
result because OpenCode's before hook cannot return model context.
|
|
75
93
|
|
|
76
94
|
Restart OpenCode after installing. The file is version-stamped (`runlist-generated:`, or
|
|
77
95
|
`dotmd-generated:` from older releases); a `dotmd.js` without either stamp is
|
|
@@ -131,7 +149,15 @@ runlist baton @/tmp/resume.md
|
|
|
131
149
|
```
|
|
132
150
|
|
|
133
151
|
Baton refuses when a handoff for the same work is already pending, so one piece
|
|
134
|
-
of work never has two resume prompts.
|
|
152
|
+
of work never has two resume prompts. Inspect the pending handoff with
|
|
153
|
+
`runlist prompts show <slug>`. Keep it if current; if stale, pass `--replace`
|
|
154
|
+
with a new draft. Baton archives the previous text and refreshes the pending
|
|
155
|
+
prompt in the same operation as any plan release.
|
|
156
|
+
|
|
157
|
+
Repositories with a commit wrapper can export `batonCommitCommand(message, paths)`
|
|
158
|
+
from `dotmd.config.mjs` and return an argv array such as
|
|
159
|
+
`['just', 'commit', message, ...paths]`. Baton quotes the printed command and
|
|
160
|
+
excludes session prompts from `paths`.
|
|
135
161
|
|
|
136
162
|
Saved prompts are local session state. Consume them with `runlist use`; inspect
|
|
137
163
|
without consuming via `runlist prompts show`. Consuming a baton prompt also claims
|
|
@@ -11,10 +11,8 @@
|
|
|
11
11
|
// that isn't one — so an exported helper wouldn't just be untidy, it would
|
|
12
12
|
// be *called* as a second plugin. Helpers stay module-local.
|
|
13
13
|
//
|
|
14
|
-
// 2.
|
|
15
|
-
//
|
|
16
|
-
// body is wrapped, and a failure degrades to "runlist does nothing here"
|
|
17
|
-
// rather than to a broken session.
|
|
14
|
+
// 2. Only a deliberate guard denial may throw. OpenCode awaits hooks inside
|
|
15
|
+
// the chat request, so every other failure degrades to a no-op.
|
|
18
16
|
//
|
|
19
17
|
// Runs under Bun inside the OpenCode process. Node builtins only, no deps.
|
|
20
18
|
|
|
@@ -64,12 +62,9 @@ function runCli(directory, args, input = null) {
|
|
|
64
62
|
});
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
// OpenCode runs no Claude Code hooks
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// warning, not a block, so it is applied after the call: the teaching text is
|
|
71
|
-
// appended to what the agent sees. Only calls that name a prompt file reach
|
|
72
|
-
// the CLI; everything else costs a regex.
|
|
65
|
+
// OpenCode runs no Claude Code hooks. Evaluate recognized prompt reads before
|
|
66
|
+
// the tool so opt-in strict mode can block them. Its V1 before hook cannot
|
|
67
|
+
// return model context, so ordinary warnings are appended to the result.
|
|
73
68
|
const PROMPT_FILE = /(^|[\\/])prompts[\\/]\S*\.md\b/;
|
|
74
69
|
|
|
75
70
|
function guardPayload(tool, args) {
|
|
@@ -86,6 +81,14 @@ export default async function dotmdOpencodePlugin({ directory }) {
|
|
|
86
81
|
// Keyed by session so a subagent session primes independently, the way
|
|
87
82
|
// SubagentStart does under Claude Code.
|
|
88
83
|
const primers = new Map();
|
|
84
|
+
const guardDecisions = new Map();
|
|
85
|
+
|
|
86
|
+
async function guardFor(tool, args) {
|
|
87
|
+
const payload = guardPayload(tool, args);
|
|
88
|
+
if (!payload) return null;
|
|
89
|
+
const raw = await runCli(directory, ['guard'], JSON.stringify(payload));
|
|
90
|
+
return raw ? JSON.parse(raw)?.hookSpecificOutput ?? null : null;
|
|
91
|
+
}
|
|
89
92
|
|
|
90
93
|
async function primerFor(sessionId) {
|
|
91
94
|
const key = sessionId ?? '';
|
|
@@ -101,6 +104,18 @@ export default async function dotmdOpencodePlugin({ directory }) {
|
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
return {
|
|
107
|
+
'tool.execute.before': async (input, output) => {
|
|
108
|
+
let refusal = null;
|
|
109
|
+
try {
|
|
110
|
+
const decision = await guardFor(input?.tool, output?.args);
|
|
111
|
+
if (input?.callID && decision) guardDecisions.set(input.callID, decision);
|
|
112
|
+
if (decision?.permissionDecision === 'deny') {
|
|
113
|
+
if (input?.callID) guardDecisions.delete(input.callID);
|
|
114
|
+
refusal = decision.permissionDecisionReason;
|
|
115
|
+
}
|
|
116
|
+
} catch { /* a missing/broken guard must not fail the chat turn */ }
|
|
117
|
+
if (refusal) throw new Error(`[runlist] ${refusal}`);
|
|
118
|
+
},
|
|
104
119
|
// Ownership identity. OpenCode sets no session-id variable of its own, and
|
|
105
120
|
// `OPENCODE_PID` — what runlist falls back to without this plugin — names the
|
|
106
121
|
// OpenCode *process*, so every session in one TUI shares it and can release
|
|
@@ -126,10 +141,10 @@ export default async function dotmdOpencodePlugin({ directory }) {
|
|
|
126
141
|
// nothing left to refuse.
|
|
127
142
|
'tool.execute.after': async (input, output) => {
|
|
128
143
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const note =
|
|
144
|
+
if (typeof output?.output !== 'string') return;
|
|
145
|
+
const decision = input?.callID ? guardDecisions.get(input.callID) : null;
|
|
146
|
+
if (input?.callID) guardDecisions.delete(input.callID);
|
|
147
|
+
const note = (decision ?? await guardFor(input?.tool, input?.args))?.additionalContext;
|
|
133
148
|
if (typeof note === 'string' && note) output.output += `\n\n${note}`;
|
|
134
149
|
} catch { /* a guard failure never touches the tool result */ }
|
|
135
150
|
},
|
package/bin/dotmd.mjs
CHANGED
|
@@ -147,8 +147,9 @@ with a one-line recap naming the habit to break.`,
|
|
|
147
147
|
|
|
148
148
|
runlist install report what is installed for each known host
|
|
149
149
|
runlist install claude install the Claude Code plugin (marketplace + plugin)
|
|
150
|
+
runlist install codex install/refresh the Codex plugin (personal marketplace)
|
|
150
151
|
runlist install opencode install/refresh the OpenCode plugin (global config dir)
|
|
151
|
-
runlist install
|
|
152
|
+
runlist install opencode --remove
|
|
152
153
|
runlist install opencode --path <dir> write to a specific plugin directory
|
|
153
154
|
runlist install opencode --force overwrite a runlist.js runlist did not write
|
|
154
155
|
|
|
@@ -168,6 +169,12 @@ host gets that a different way:
|
|
|
168
169
|
under extraKnownMarketplaces with a source that no longer matches;
|
|
169
170
|
runlist names the field but never edits that file.
|
|
170
171
|
|
|
172
|
+
codex Copies the npm-packaged runlist-codex plugin into the personal
|
|
173
|
+
marketplace and invokes \`codex plugin add\`. It supplies a compact
|
|
174
|
+
skill, SessionStart orientation, and a PreToolUse guard. Codex
|
|
175
|
+
requires a separate hook trust review; start a new thread after
|
|
176
|
+
installing. CODEX_THREAD_ID remains the ownership identity.
|
|
177
|
+
|
|
171
178
|
opencode Writes one auto-discovered plugin file. OpenCode has no plugin
|
|
172
179
|
registry but globs \`{plugin,plugins}/*.{ts,js}\` under its global
|
|
173
180
|
config dir, so no opencode.json edit is needed. It supplies two
|
|
@@ -302,7 +309,7 @@ Create & Export:
|
|
|
302
309
|
|
|
303
310
|
Setup:
|
|
304
311
|
init Create starter config + docs directory
|
|
305
|
-
install [opencode]
|
|
312
|
+
install [claude|codex|opencode] Install the agent-host integration (no arg = status)
|
|
306
313
|
update [--check|--cli-only|--plugin-only] Update the CLI + Claude Code plugin (--check reports skew, no network)
|
|
307
314
|
statuses [list|add|set|remove|migrate] Manage per-project status taxonomy
|
|
308
315
|
help statuses Full status vocabulary + unstuck-actions + transitions
|
|
@@ -849,7 +856,7 @@ Modes:
|
|
|
849
856
|
--migrate-prompts Retrofit pre-existing markdown files under any docs
|
|
850
857
|
root's prompts/ subdirectory with proper prompt
|
|
851
858
|
frontmatter (type, status, created from git
|
|
852
|
-
history,
|
|
859
|
+
history, runlist_version, context, related_plans).
|
|
853
860
|
Skips files that already have frontmatter.
|
|
854
861
|
--migrate-template <file> Migrate just one plan.
|
|
855
862
|
--migrate-template --include-archived
|
|
@@ -1271,13 +1278,21 @@ Options:
|
|
|
1271
1278
|
--note "why" Append the reason to ## Version History (plan mode only)
|
|
1272
1279
|
--message / --body Inline body (one-liners; prefer @path or stdin)
|
|
1273
1280
|
--force Recover another session's plan (explicit path required)
|
|
1281
|
+
--replace Replace exactly one pending handoff; archive its prior text
|
|
1274
1282
|
--json Structured repository/session/generated file result
|
|
1275
1283
|
--dry-run, -n Preview without writing
|
|
1276
1284
|
|
|
1285
|
+
Repo-specific commit hint (dotmd.config.mjs):
|
|
1286
|
+
export function batonCommitCommand(message, paths) {
|
|
1287
|
+
return ['just', 'commit', message, ...paths];
|
|
1288
|
+
}
|
|
1289
|
+
The function returns argv; baton shell-quotes each argument before printing.
|
|
1290
|
+
|
|
1277
1291
|
Examples:
|
|
1278
1292
|
runlist baton @/tmp/draft.md
|
|
1279
1293
|
runlist baton checkout-fixes @/tmp/draft.md
|
|
1280
1294
|
runlist baton docs/plans/auth.md @/tmp/draft.md
|
|
1295
|
+
runlist baton docs/plans/auth.md @/tmp/new-draft.md --replace
|
|
1281
1296
|
runlist baton --status paused --note "blocked on review" @/tmp/d.md
|
|
1282
1297
|
cat /tmp/draft.md | runlist baton
|
|
1283
1298
|
|
|
@@ -1296,11 +1311,13 @@ cooperating transaction:
|
|
|
1296
1311
|
grant ownership. A live pickup-hook delivery lease blocks release and force
|
|
1297
1312
|
takeover; hooks are at-least-once and deduplicate the stable operationId.
|
|
1298
1313
|
|
|
1299
|
-
Slug mode (no plan involved) saves resume-<slug>
|
|
1300
|
-
|
|
1314
|
+
Slug mode (no plan involved) saves resume-<slug> without a status change or
|
|
1315
|
+
commit. A bare word that names a plan is treated as that plan.
|
|
1301
1316
|
|
|
1302
1317
|
Baton saves nothing while a handoff for the same work is pending (resume-<name>,
|
|
1303
|
-
or a pending prompt linked to the plan):
|
|
1318
|
+
or a pending prompt linked to the plan): inspect it with \`runlist prompts show\`.
|
|
1319
|
+
Keep it if current; use \`--replace\` with a new draft if stale. Replacement
|
|
1320
|
+
preserves the prior text under archived/ and refuses multiple matches.
|
|
1304
1321
|
With no @file, \`-\` or --message, baton reads stdin only when something is piped
|
|
1305
1322
|
in; an open pipe that sends nothing is given up on after a moment.`,
|
|
1306
1323
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotmd-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.89.1",
|
|
4
4
|
"description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, lifecycle, and AI summaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"bin/",
|
|
17
17
|
"src/",
|
|
18
18
|
"assets/",
|
|
19
|
+
"plugins/runlist-codex/",
|
|
19
20
|
"scripts/postinstall.mjs",
|
|
20
21
|
"runlist.config.example.mjs"
|
|
21
22
|
],
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "runlist-codex",
|
|
3
|
+
"version": "0.89.1",
|
|
4
|
+
"description": "Runlist workflow skill, session orientation, and tool guard for Codex",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Reo Owens"
|
|
7
|
+
},
|
|
8
|
+
"skills": "./skills/",
|
|
9
|
+
"interface": {
|
|
10
|
+
"displayName": "Runlist Codex",
|
|
11
|
+
"shortDescription": "Plan, handoff, and document workflow for Codex.",
|
|
12
|
+
"longDescription": "Adds a concise runlist skill, session primer, and prompt/status guard hooks for managed Markdown repositories.",
|
|
13
|
+
"developerName": "Reo Owens",
|
|
14
|
+
"category": "Productivity",
|
|
15
|
+
"capabilities": [],
|
|
16
|
+
"defaultPrompt": "Show my runlist plans and pending handoffs."
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# runlist Codex plugin hook wrapper.
|
|
3
|
+
#
|
|
4
|
+
# The plugin's hooks drive the whole workflow through the PATH `runlist` binary
|
|
5
|
+
# (`runlist hud`, `runlist guard`), falling back to the legacy `dotmd` alias. If a user enables the plugin but hasn't run
|
|
6
|
+
# `npm i -g dotmd-cli`, the binary is missing, the hook errors `command not
|
|
7
|
+
# found`, and the session is primed with *silent nothing* — no signal that the
|
|
8
|
+
# whole workflow is one install away. runlist can't warn about its own absence
|
|
9
|
+
# (it's the thing that's missing), so the check lives here, outside the binary.
|
|
10
|
+
#
|
|
11
|
+
# Usage: runlist-hook [--hint] <runlist-subcommand> [args...]
|
|
12
|
+
# --hint emit a one-line install hint to stdout when no CLI is on PATH.
|
|
13
|
+
# Only SessionStart passes this, so the hint surfaces once per hook
|
|
14
|
+
# invocation (stdout becomes session context), never on a tool call.
|
|
15
|
+
#
|
|
16
|
+
# When the CLI is missing we always exit 0 with no further output: hooks must
|
|
17
|
+
# stay non-blocking, and the PreToolUse guard in particular must never deny a
|
|
18
|
+
# tool just because the optional CLI isn't installed.
|
|
19
|
+
hint=0
|
|
20
|
+
if [ "$1" = "--hint" ]; then
|
|
21
|
+
hint=1
|
|
22
|
+
shift
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
if command -v runlist >/dev/null 2>&1; then
|
|
26
|
+
CLI=runlist
|
|
27
|
+
elif command -v dotmd >/dev/null 2>&1; then
|
|
28
|
+
CLI=dotmd
|
|
29
|
+
else
|
|
30
|
+
if [ "$hint" = "1" ]; then
|
|
31
|
+
echo "runlist plugin: neither \`runlist\` nor \`dotmd\` is on PATH, so its hooks are no-ops. Enable the workflow with: npm i -g dotmd-cli"
|
|
32
|
+
fi
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# PreToolUse fast path: the guard fires on matched Bash, apply_patch, and MCP
|
|
37
|
+
# read calls, and
|
|
38
|
+
# a full Node boot (~100ms: CLI startup + config discovery) is pure overhead for
|
|
39
|
+
# the vast majority of payloads no guard rule can possibly match. Every rule
|
|
40
|
+
# needs a `.md` path AND either a prompts/ path (commit/cat/read-prompt) or the
|
|
41
|
+
# word `status` (edit-status) somewhere in the payload — a cheap shell glob
|
|
42
|
+
# decides that without leaving sh. Anything else gets the no-opinion reply
|
|
43
|
+
# (`{}`) directly. False positives here just pay the old cost (Node runs and
|
|
44
|
+
# returns {}); false negatives are impossible because the globs are strictly
|
|
45
|
+
# weaker than the rules they gate.
|
|
46
|
+
if [ "$1" = "guard" ]; then
|
|
47
|
+
payload=$(cat)
|
|
48
|
+
# Broad Git forms do not name a .md file in the payload; Node inspects the
|
|
49
|
+
# actual eligible/staged path set before deciding whether a live prompt is
|
|
50
|
+
# included. Always route those forms through the guard.
|
|
51
|
+
case "$payload" in
|
|
52
|
+
*git*add*|*git*stage*|*git*commit*)
|
|
53
|
+
printf '%s' "$payload" | "$CLI" "$@"
|
|
54
|
+
exit $?
|
|
55
|
+
;;
|
|
56
|
+
esac
|
|
57
|
+
case "$payload" in
|
|
58
|
+
*.md*)
|
|
59
|
+
case "$payload" in
|
|
60
|
+
*prompts/*|*prompts\\*|*status*)
|
|
61
|
+
printf '%s' "$payload" | "$CLI" "$@"
|
|
62
|
+
exit $?
|
|
63
|
+
;;
|
|
64
|
+
esac
|
|
65
|
+
;;
|
|
66
|
+
esac
|
|
67
|
+
printf '{}\n'
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# UserPromptSubmit fast path is retained for compatibility with the Claude
|
|
72
|
+
# wrapper; Codex does not register that hook here. It would fire on every
|
|
73
|
+
# message that asks for a handoff gets an answer. The glob is strictly weaker
|
|
74
|
+
# than the Node-side match, so a skipped payload could never have matched.
|
|
75
|
+
if [ "$1" = "hud" ] && [ "$2" = "--prompt-submit" ]; then
|
|
76
|
+
payload=$(cat)
|
|
77
|
+
case "$payload" in
|
|
78
|
+
*[Bb][Aa][Tt][Oo][Nn]*|*[Hh][Aa][Nn][Dd]*[Oo][Ff][Ff]*|*[Rr][Ee][Ss][Uu][Mm][Ee]*|*[Pp][Ii][Cc][Kk]*[Uu][Pp]*)
|
|
79
|
+
printf '%s' "$payload" | "$CLI" "$@"
|
|
80
|
+
exit 0
|
|
81
|
+
;;
|
|
82
|
+
esac
|
|
83
|
+
exit 0
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
exec "$CLI" "$@"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "startup|resume|clear|compact",
|
|
6
|
+
"hooks": [{ "type": "command", "command": "sh \"${PLUGIN_ROOT}/bin/runlist-hook\" --hint hud", "timeout": 5 }]
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"PreToolUse": [
|
|
10
|
+
{
|
|
11
|
+
"matcher": "Bash|apply_patch|mcp__.*__(read_file|read_text_file|read_media_file|read_multiple_files)",
|
|
12
|
+
"hooks": [{ "type": "command", "command": "sh \"${PLUGIN_ROOT}/bin/runlist-hook\" guard", "timeout": 5 }]
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: runlist
|
|
3
|
+
description: Manage a repository's plans, docs, decisions, and saved prompts with the runlist CLI. Use when starting or closing plan work, checking the plan queue, or creating or consuming a handoff.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Runlist workflow
|
|
7
|
+
|
|
8
|
+
Use this skill in repositories with `runlist.config.mjs` or `dotmd.config.mjs`. The CLI is `runlist` (from `dotmd-cli`; `dotmd` remains an alias). The session hook prints a short live orientation. `CODEX_THREAD_ID` is the session identity used for plan claims; do not replace it with a process ID.
|
|
9
|
+
|
|
10
|
+
- Orient with `runlist plans`. Use `runlist agent-context` for structured state; `runlist briefing` is the comprehensive view and can be large.
|
|
11
|
+
- Start plan work with `runlist use <plan-file>`. It claims the plan, marks it `in-session`, and prints the card.
|
|
12
|
+
- Change status with `runlist set <status> <file> [--note "why"]`; this runs validation, lifecycle hooks, ref repairs, and index updates. Never hand-edit a `status:` field.
|
|
13
|
+
- Close a shipped plan with `runlist archive <file>`; use `partial`, `active`, `awaiting`, or `blocked` when those match the actual state. Valid statuses are repository and type specific: `runlist statuses list --type plan`.
|
|
14
|
+
- For a handoff, write a concrete resume draft, then run `runlist baton [<plan-or-slug>] @<draft-file>`. An owned plan is released in the same operation. If a handoff for that work is pending, inspect it with `runlist prompts show <file>`; keep it if current or pass `--replace` with the new draft. Replacement archives the old text and refreshes exactly one pending prompt.
|
|
15
|
+
- Consume a saved prompt with `runlist use <prompt-file>` (or bare `runlist use` for the oldest). This archives and claims before printing the body. For read-only triage, use `runlist prompts show <file>` or `runlist prompts show --all`; never open pending prompts with a file reader or `cat`.
|
|
16
|
+
- Pending prompts are session-local. Commit the tracked plan or source files named by baton, not `docs/prompts/*.md`.
|
|
17
|
+
- Create a document with `runlist new <type> <name> @<draft-file>`. A draft starting with frontmatter can supply scaffold fields; `type` is fixed by the command.
|
|
18
|
+
- Record a decision with `runlist new decision <plan> --question "…" @<record-file>`; configured registers may also require `--answers`.
|
|
19
|
+
- If a lifecycle mutation reports an abandoned transaction, inspect `runlist doctor --transactions` before retrying. Use `--apply` only for cases the doctor identifies as recoverable.
|
|
20
|
+
|
|
21
|
+
The `PreToolUse` hook warns on direct pending-prompt reads and denies hand-edited status transitions or Git operations that would include a live prompt. A repo can opt into blocking direct reads of existing pending prompts with `guard: { promptReads: 'deny' }`. Archived prompts and `runlist prompts show` remain readable. Codex requires explicit trust of a plugin's hooks before they run.
|
|
@@ -13,6 +13,11 @@ export const archiveDir = 'archived';
|
|
|
13
13
|
// Directories to skip when scanning
|
|
14
14
|
export const excludeDirs = ['evidence'];
|
|
15
15
|
|
|
16
|
+
// Agent-host tool guard: direct reads of existing pending prompts warn by
|
|
17
|
+
// default. Set promptReads to 'deny' to block those reads before execution;
|
|
18
|
+
// `runlist prompts show` and archived prompt reads remain available.
|
|
19
|
+
// export const guard = { promptReads: 'deny' };
|
|
20
|
+
|
|
16
21
|
// Floor under the scan surface. `runlist check` fails when it scans fewer docs than
|
|
17
22
|
// this, so a broken root or an over-eager exclude can't read as a clean estate —
|
|
18
23
|
// zero errors and zero docs look identical otherwise. Off when unset. Set it well
|
package/src/atomic-mutation.mjs
CHANGED
|
@@ -1159,7 +1159,7 @@ function reserveExclusive(filePath, mode, content, testHooks = {}) {
|
|
|
1159
1159
|
fd = openSync(filePath, 'wx', mode);
|
|
1160
1160
|
const opened = fstatSync(fd, { bigint: true });
|
|
1161
1161
|
openedIdentity = { dev: opened.dev, ino: opened.ino };
|
|
1162
|
-
const reservationContent = content ?? JSON.stringify({
|
|
1162
|
+
const reservationContent = content ?? JSON.stringify({ runlistReservation: true, pid: process.pid, createdAt: new Date().toISOString() }) + '\n';
|
|
1163
1163
|
writeFileSync(fd, reservationContent, 'utf8');
|
|
1164
1164
|
testHooks.afterReserveWrite?.(filePath);
|
|
1165
1165
|
testHooks.beforeReserveFsync?.(filePath);
|
|
@@ -1310,6 +1310,10 @@ export function moveFileAtomic(sourcePath, targetPath, render, options) {
|
|
|
1310
1310
|
// transaction manifest exists, so a guard conflict can never leave a
|
|
1311
1311
|
// transaction to recover from.
|
|
1312
1312
|
for (const guard of guards) {
|
|
1313
|
+
if (guard.absent) {
|
|
1314
|
+
if (existsSync(guard.path)) throw new MutationConflictError(`File appeared while the move mutation set was being prepared: ${path.resolve(guard.path)}`);
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1313
1317
|
const snapshot = snapshotFile(guard.path);
|
|
1314
1318
|
if (guard.expectedContent !== undefined && snapshot.content !== guard.expectedContent) {
|
|
1315
1319
|
throw new MutationConflictError(`File changed while the move mutation set was being prepared: ${snapshot.path}`);
|
|
@@ -1353,7 +1357,7 @@ export function moveFileAtomic(sourcePath, targetPath, render, options) {
|
|
|
1353
1357
|
for (const item of creations) ensureTransactionDirectory(path.dirname(item.path), transaction, options, item.path);
|
|
1354
1358
|
try {
|
|
1355
1359
|
reservation = reserveExclusive(targetPath, source.identity.mode, JSON.stringify({
|
|
1356
|
-
|
|
1360
|
+
runlistReservation: true,
|
|
1357
1361
|
transactionId,
|
|
1358
1362
|
sourcePath,
|
|
1359
1363
|
backup,
|
|
@@ -1657,6 +1661,10 @@ export function mutateFileSet({ updates = [], creations = [], guards = [] }, opt
|
|
|
1657
1661
|
// is created — a guard conflict must leave the tree byte-identical, not even
|
|
1658
1662
|
// an empty directory behind.
|
|
1659
1663
|
for (const guard of guards) {
|
|
1664
|
+
if (guard.absent) {
|
|
1665
|
+
if (existsSync(guard.path)) throw new MutationConflictError(`File appeared while the mutation set was being prepared: ${path.resolve(guard.path)}`);
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1660
1668
|
const snapshot = snapshotFile(guard.path);
|
|
1661
1669
|
if (guard.expectedContent !== undefined && snapshot.content !== guard.expectedContent) {
|
|
1662
1670
|
throw new MutationConflictError(`File changed while the mutation set was being prepared: ${snapshot.path}`);
|