docket-agent 0.1.0
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/LICENSE +21 -0
- package/README.md +268 -0
- package/bin/docket.js +10 -0
- package/eval/REPORT.md +67 -0
- package/eval/run.js +114 -0
- package/eval/scenarios.js +111 -0
- package/package.json +45 -0
- package/spec/SPEC.md +259 -0
- package/src/cli.js +79 -0
- package/src/commands/check.js +41 -0
- package/src/commands/compile.js +45 -0
- package/src/commands/init.js +54 -0
- package/src/commands/list.js +53 -0
- package/src/commands/mcp.js +187 -0
- package/src/commands/new.js +229 -0
- package/src/commands/record.js +116 -0
- package/src/lib/args.js +36 -0
- package/src/lib/compile.js +140 -0
- package/src/lib/loop.js +198 -0
- package/src/lib/pkg.js +5 -0
- package/src/lib/record.js +177 -0
- package/src/lib/ui.js +20 -0
- package/src/lib/warrant.js +142 -0
- package/src/lib/yaml.js +132 -0
- package/templates/client-follow-up.loop.md +59 -0
- package/templates/cross-tool-memory.loop.md +55 -0
- package/templates/insurance-appeal.loop.md +59 -0
- package/templates/marketing-brain.loop.md +62 -0
- package/templates/ticket-handoff.loop.md +59 -0
- package/templates/travel-morning.loop.md +54 -0
- package/templates/weekly-planning.loop.md +55 -0
package/spec/SPEC.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# The Loop File Spec — v0.1
|
|
2
|
+
|
|
3
|
+
Status: draft. Breaking changes possible until v1.0; the `version` field in
|
|
4
|
+
frontmatter exists so files can survive them.
|
|
5
|
+
|
|
6
|
+
## Why a spec
|
|
7
|
+
|
|
8
|
+
An agent's permissions shouldn't live in a prompt someone improvised at 11pm.
|
|
9
|
+
A loop file is a small, human-writable document that answers, in order:
|
|
10
|
+
|
|
11
|
+
1. What must the agent **know** before it starts?
|
|
12
|
+
2. How is this work **supposed to be done**?
|
|
13
|
+
3. What may it do **without asking**?
|
|
14
|
+
4. Where does it have to **stop**?
|
|
15
|
+
5. What **evidence** must it leave behind?
|
|
16
|
+
|
|
17
|
+
Unwritten answers get guessed at. Written answers can be enforced — checked,
|
|
18
|
+
compiled, and audited by tools.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## File format
|
|
22
|
+
|
|
23
|
+
A loop is a single file, `<name>.loop.md`, stored in `.docket/loops/`. It is
|
|
24
|
+
Markdown with YAML frontmatter:
|
|
25
|
+
|
|
26
|
+
```markdown
|
|
27
|
+
---
|
|
28
|
+
name: insurance-appeal
|
|
29
|
+
description: Build the appeal, cite the policy — stop before send.
|
|
30
|
+
version: 1
|
|
31
|
+
warrant:
|
|
32
|
+
read:
|
|
33
|
+
- policy documents
|
|
34
|
+
draft:
|
|
35
|
+
- appeal letter
|
|
36
|
+
change: []
|
|
37
|
+
send: []
|
|
38
|
+
ask:
|
|
39
|
+
- anything addressed to the insurer
|
|
40
|
+
never:
|
|
41
|
+
- accepting or rejecting a settlement
|
|
42
|
+
reserved:
|
|
43
|
+
- signing and sending
|
|
44
|
+
record:
|
|
45
|
+
- every policy clause cited
|
|
46
|
+
- where the draft stopped
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
# Brief
|
|
50
|
+
|
|
51
|
+
Prose. What the agent must know before it starts.
|
|
52
|
+
|
|
53
|
+
# Procedure
|
|
54
|
+
|
|
55
|
+
Prose. How this job is done properly.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Prose layers (brief, procedure) live in the Markdown body because humans
|
|
59
|
+
write and diff prose well. Machine layers (warrant, record, reserved) live
|
|
60
|
+
in frontmatter because tools enforce structure well.
|
|
61
|
+
|
|
62
|
+
### Frontmatter fields
|
|
63
|
+
|
|
64
|
+
| Field | Type | Required | Meaning |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `name` | string | yes | `[a-z0-9][a-z0-9-]*`; must match the filename |
|
|
67
|
+
| `description` | string | no | one line, shown in listings and compiled context |
|
|
68
|
+
| `version` | number | no | spec version, default `1` |
|
|
69
|
+
| `warrant` | map | no | see below |
|
|
70
|
+
| `reserved` | list of strings | no | what stays with the human, always |
|
|
71
|
+
| `record` | list of strings | no | what the agent must report when it finishes or stops |
|
|
72
|
+
|
|
73
|
+
### Body sections
|
|
74
|
+
|
|
75
|
+
Headings (`#`, `##`, or `###`) named exactly `Brief` or `Procedure`
|
|
76
|
+
(case-insensitive) open the prose layers. Every other line — subheadings,
|
|
77
|
+
other headings, comments inside fenced code blocks — is **content** and
|
|
78
|
+
belongs to the open section. Prose the human wrote is never silently dropped
|
|
79
|
+
from the compiled context.
|
|
80
|
+
|
|
81
|
+
`name` must match the filename (`<name>.loop.md`) and parsers must reject a
|
|
82
|
+
mismatch — otherwise record entries get attributed to a loop that cannot be loaded.
|
|
83
|
+
Parsers must also reject a `version` they don't understand rather than
|
|
84
|
+
silently misreading a newer format.
|
|
85
|
+
|
|
86
|
+
### YAML subset
|
|
87
|
+
|
|
88
|
+
Frontmatter is parsed by a deliberately small YAML subset: nested maps, lists
|
|
89
|
+
of scalars, quoted/unquoted scalars, booleans, numbers, `null`, `[]`, and `#`
|
|
90
|
+
comments. No anchors, no multi-line scalars, no flow collections, no lists of
|
|
91
|
+
maps. A grammar small enough to audit is part of the security posture.
|
|
92
|
+
|
|
93
|
+
## The warrant
|
|
94
|
+
|
|
95
|
+
Four verbs, in escalating order of consequence:
|
|
96
|
+
|
|
97
|
+
| Verb | Meaning |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `read` | look at it |
|
|
100
|
+
| `draft` | produce it, but it goes nowhere on its own |
|
|
101
|
+
| `change` | mutate state that stays inside the sandbox |
|
|
102
|
+
| `send` | consequences leave the sandbox: email, publish, file, deploy, pay |
|
|
103
|
+
|
|
104
|
+
Plus two cross-cutting lists:
|
|
105
|
+
|
|
106
|
+
- `ask` — always requires human approval, whatever the verb.
|
|
107
|
+
- `never` — does not happen, even with approval. A `never` is the human
|
|
108
|
+
pre-deciding under calm conditions what no amount of in-the-moment
|
|
109
|
+
persuasion may undo.
|
|
110
|
+
|
|
111
|
+
### The verdict algorithm
|
|
112
|
+
|
|
113
|
+
Given a loop, an action (one of the four verbs), and a target (a plain-language
|
|
114
|
+
description of what the action touches), the verdict is the **first** rule
|
|
115
|
+
that matches:
|
|
116
|
+
|
|
117
|
+
1. target matches an entry in `never` → **deny**
|
|
118
|
+
2. target matches an entry in `ask` → **ask**
|
|
119
|
+
3. target matches an entry in the action's own list → **allow**
|
|
120
|
+
4. otherwise → **ask**
|
|
121
|
+
|
|
122
|
+
Rule 4 is the heart of the spec: **unlisted means ask. Silence is never
|
|
123
|
+
permission.** An agent that encounters something the loop's author didn't
|
|
124
|
+
anticipate has, by construction, encountered something that needs a human.
|
|
125
|
+
|
|
126
|
+
### Pattern matching
|
|
127
|
+
|
|
128
|
+
All matching is case-insensitive, and it is **asymmetric by design**:
|
|
129
|
+
`ask`/`never` patterns match fuzzily in both directions, allow patterns match
|
|
130
|
+
strictly. A phrasing difference may cause an unnecessary ask; it must never
|
|
131
|
+
cause an accidental allow.
|
|
132
|
+
|
|
133
|
+
Shared rules:
|
|
134
|
+
|
|
135
|
+
- A pattern containing `*` is a glob over the entire target (author-explicit,
|
|
136
|
+
both modes).
|
|
137
|
+
- Commas, ` or `, and ` and ` split a pattern into alternatives, each tried
|
|
138
|
+
separately — natural-language lists (`secrets, tokens, or passwords`) are
|
|
139
|
+
lists.
|
|
140
|
+
- *Content words* exclude filler (`a`, `the`, `anything`, `to`, `of`, …).
|
|
141
|
+
Words compare under light candidate-set stemming (possessive `'s`, and
|
|
142
|
+
`-s`/`-es`/`-ed`/`-ing` when enough of the word remains), so `quotes`
|
|
143
|
+
matches `quote` and `contacting` matches `contact`.
|
|
144
|
+
- An alternative whose words are all filler (`anything`) matches **every**
|
|
145
|
+
target — its plain meaning.
|
|
146
|
+
|
|
147
|
+
Per alternative:
|
|
148
|
+
|
|
149
|
+
- **Cautious mode** (`ask`, `never`): matches on substring in either
|
|
150
|
+
direction, or content-word subset in either direction. Ambiguity escalates
|
|
151
|
+
to the human.
|
|
152
|
+
- **Strict mode** (allow lists): the target must *cover* the pattern —
|
|
153
|
+
contain it as a substring, or contain every one of its content words. The
|
|
154
|
+
reverse never allows: the target `email` does not match the allow entry
|
|
155
|
+
`status email to the team`.
|
|
156
|
+
|
|
157
|
+
Known limitation: a target that embeds an allowed phrase plus extra intent
|
|
158
|
+
(`status email to the team and also wire funds`) still covers the pattern.
|
|
159
|
+
The check is a pre-action gate for cooperative agents, not a parser of
|
|
160
|
+
compound intent — `ask` and `never` lists screen every target first, so list
|
|
161
|
+
the consequences you fear there.
|
|
162
|
+
|
|
163
|
+
Write `ask`/`never` patterns short and broad (`contacting the insurer`), and
|
|
164
|
+
allow-list patterns as concrete nouns (`appeal letter`). When a check that
|
|
165
|
+
should have matched falls through, the verdict is still `ask` (rule 4) — the
|
|
166
|
+
system degrades toward the human, never away.
|
|
167
|
+
|
|
168
|
+
### Exit codes
|
|
169
|
+
|
|
170
|
+
`docket check` exits `0` for allow, `2` for ask, `3` for deny (and `1` for
|
|
171
|
+
usage errors), so shells, hooks, and CI can gate on the warrant directly.
|
|
172
|
+
|
|
173
|
+
## The record
|
|
174
|
+
|
|
175
|
+
The record is the audit half of the trust story: *what did the agent see,
|
|
176
|
+
do, leave alone, and where did it stop?*
|
|
177
|
+
|
|
178
|
+
Storage: `.docket/record.jsonl`, one JSON object per line, append-only.
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{"seq":3,"ts":"2026-07-03T10:15:00.000Z","loop":"insurance-appeal","kind":"check","action":"send","target":"appeal email to insurer","verdict":"ask","rule":"ask: anything addressed to the insurer","prev":"sha256:…","hash":"sha256:…"}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Two kinds today:
|
|
185
|
+
|
|
186
|
+
- `check` — a warrant check, recorded automatically with its verdict. The
|
|
187
|
+
question "did the agent even ask?" becomes answerable.
|
|
188
|
+
- `note` — a work entry with any of: `saw`, `did`, `skipped`, `stopped`,
|
|
189
|
+
`note`.
|
|
190
|
+
|
|
191
|
+
### The hash chain
|
|
192
|
+
|
|
193
|
+
Each entry commits to the previous one:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
hash = "sha256:" + SHA256( prev + "\n" + canonical(entry minus hash) )
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
where `canonical` is JSON with lexicographically sorted keys, and `prev` is the
|
|
200
|
+
previous entry's `hash` (the literal string `GENESIS` for entry 1). `seq` must
|
|
201
|
+
increase by exactly 1.
|
|
202
|
+
|
|
203
|
+
`docket record verify` recomputes the chain. Any edit, deletion, insertion,
|
|
204
|
+
or reordering **inside** the log breaks it at a specific entry. One case the
|
|
205
|
+
chain alone cannot see: truncating the tail leaves a valid shorter chain.
|
|
206
|
+
That's why `verify` prints the current head hash — pin it somewhere the log
|
|
207
|
+
can't reach (a password manager, a commit message, another machine) and check
|
|
208
|
+
with `docket record verify --head <hash>`, which reports likely truncation
|
|
209
|
+
when the heads disagree. The chain doesn't stop tampering — it's a plain
|
|
210
|
+
file — it makes tampering **visible**, which is what an audit trail is for.
|
|
211
|
+
(Cryptographically signing the head is on the roadmap.)
|
|
212
|
+
|
|
213
|
+
## Compiled context
|
|
214
|
+
|
|
215
|
+
Loops are the source of truth; assistant-specific files are build artifacts.
|
|
216
|
+
`docket compile` renders loops into a fenced block:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
<!-- docket:begin — generated by `docket compile`, do not edit by hand -->
|
|
220
|
+
…
|
|
221
|
+
<!-- docket:end -->
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
and (with `--write`) inserts or replaces that block in the target file:
|
|
225
|
+
|
|
226
|
+
| Target | File |
|
|
227
|
+
|---|---|
|
|
228
|
+
| `claude` | `CLAUDE.md` |
|
|
229
|
+
| `agents` | `AGENTS.md` (ChatGPT/Codex, Zed, …) |
|
|
230
|
+
| `gemini` | `GEMINI.md` (Gemini CLI) |
|
|
231
|
+
| `cursor` | `.cursor/rules/docket.mdc` |
|
|
232
|
+
| `raw` | stdout |
|
|
233
|
+
|
|
234
|
+
Content outside the markers is never touched. Because every target renders
|
|
235
|
+
from the same loops, moving to a new tool is a recompile, not a re-teach.
|
|
236
|
+
|
|
237
|
+
## MCP tools
|
|
238
|
+
|
|
239
|
+
`docket mcp` serves four tools over stdio (newline-delimited JSON-RPC,
|
|
240
|
+
protocol `2024-11-05`). MCP hosts often spawn servers with a cwd far from
|
|
241
|
+
your project, so the server resolves its project from `--dir <path>` (or
|
|
242
|
+
`DOCKET_DIR`), falling back to walking up from cwd — and it always answers
|
|
243
|
+
`initialize`, reporting a missing project as a tool error instead of dying
|
|
244
|
+
before the handshake.
|
|
245
|
+
|
|
246
|
+
| Tool | Purpose |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `docket_list_loops` | discover the loops |
|
|
249
|
+
| `docket_loop_context` | fetch a loop's five layers before starting work |
|
|
250
|
+
| `docket_warrant_check` | get an allow/ask/deny verdict **before** acting; auto-recorded as a `check` entry |
|
|
251
|
+
| `docket_record` | append a `note` entry to the record |
|
|
252
|
+
|
|
253
|
+
## What this spec refuses to do
|
|
254
|
+
|
|
255
|
+
- **No secrets in loop files.** Loops are meant to be committed and shared.
|
|
256
|
+
- **No execution.** A loop describes and constrains work; it is not a workflow
|
|
257
|
+
engine. The agent you already use does the work.
|
|
258
|
+
- **No lock-in.** Everything is plain text in your repo. Deleting docket loses
|
|
259
|
+
you nothing but the tooling.
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { bold, cyan, dim } from './lib/ui.js';
|
|
2
|
+
import { VERSION } from './lib/pkg.js';
|
|
3
|
+
import { cmdInit } from './commands/init.js';
|
|
4
|
+
import { cmdNew, cmdTemplates } from './commands/new.js';
|
|
5
|
+
import { cmdList, cmdShow } from './commands/list.js';
|
|
6
|
+
import { cmdCheck } from './commands/check.js';
|
|
7
|
+
import { cmdRecord } from './commands/record.js';
|
|
8
|
+
import { cmdCompile } from './commands/compile.js';
|
|
9
|
+
import { cmdMcp } from './commands/mcp.js';
|
|
10
|
+
|
|
11
|
+
const HELP = `
|
|
12
|
+
${bold('docket')} — brief the agent, warrant the actions, keep the record
|
|
13
|
+
|
|
14
|
+
${bold('Usage:')} docket <command> [args]
|
|
15
|
+
|
|
16
|
+
${bold('Getting started')}
|
|
17
|
+
${cyan('init')} create a .docket directory here
|
|
18
|
+
${cyan('new')} <name> create a loop (interactive, or --template <t>)
|
|
19
|
+
${cyan('templates')} list the starter loop templates
|
|
20
|
+
|
|
21
|
+
${bold('Working with loops')}
|
|
22
|
+
${cyan('list')} list your loops
|
|
23
|
+
${cyan('show')} <loop> print a loop's five layers
|
|
24
|
+
${cyan('check')} <loop> <action> <target>
|
|
25
|
+
ask the warrant: allow, ask, or deny?
|
|
26
|
+
(actions: read, draft, change, send)
|
|
27
|
+
|
|
28
|
+
${bold('The record')}
|
|
29
|
+
${cyan('record add')} <loop> [--did ..] [--saw ..] [--skipped ..] [--stopped ..] [--note ..]
|
|
30
|
+
${cyan('record log')} [loop] [--n 20]
|
|
31
|
+
${cyan('record verify')} verify the hash chain end to end
|
|
32
|
+
|
|
33
|
+
${bold('Portability')}
|
|
34
|
+
${cyan('compile')} [--target claude|agents|cursor|raw] [--loop <name>] [--write]
|
|
35
|
+
render loops into CLAUDE.md / AGENTS.md / Cursor rules
|
|
36
|
+
${cyan('mcp')} run the MCP server (stdio) for agent integration
|
|
37
|
+
|
|
38
|
+
${dim('Every loop answers five questions: what must it know, how is the work')}
|
|
39
|
+
${dim('done, what may it do without asking, where does it stop, and what')}
|
|
40
|
+
${dim('evidence must it leave. Unwritten answers get guessed at.')}
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
export async function main(argv) {
|
|
44
|
+
const [command, ...rest] = argv;
|
|
45
|
+
switch (command) {
|
|
46
|
+
case undefined:
|
|
47
|
+
case 'help':
|
|
48
|
+
case '--help':
|
|
49
|
+
case '-h':
|
|
50
|
+
console.log(HELP.trimEnd());
|
|
51
|
+
return 0;
|
|
52
|
+
case 'version':
|
|
53
|
+
case '--version':
|
|
54
|
+
case '-v':
|
|
55
|
+
console.log(VERSION);
|
|
56
|
+
return 0;
|
|
57
|
+
case 'init':
|
|
58
|
+
return cmdInit(rest);
|
|
59
|
+
case 'new':
|
|
60
|
+
return cmdNew(rest);
|
|
61
|
+
case 'templates':
|
|
62
|
+
return cmdTemplates(rest);
|
|
63
|
+
case 'list':
|
|
64
|
+
return cmdList(rest);
|
|
65
|
+
case 'show':
|
|
66
|
+
return cmdShow(rest);
|
|
67
|
+
case 'check':
|
|
68
|
+
return cmdCheck(rest);
|
|
69
|
+
case 'record':
|
|
70
|
+
return cmdRecord(rest);
|
|
71
|
+
case 'compile':
|
|
72
|
+
return cmdCompile(rest);
|
|
73
|
+
case 'mcp':
|
|
74
|
+
return cmdMcp(rest);
|
|
75
|
+
default:
|
|
76
|
+
console.error(`docket: unknown command "${command}" — try \`docket help\``);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parseArgs } from '../lib/args.js';
|
|
2
|
+
import { requireDocketDir, loadLoop, ACTIONS } from '../lib/loop.js';
|
|
3
|
+
import { checkWarrant } from '../lib/warrant.js';
|
|
4
|
+
import { recordCheck } from '../lib/record.js';
|
|
5
|
+
import { bold, dim, VERDICT_STYLE } from '../lib/ui.js';
|
|
6
|
+
|
|
7
|
+
// Exit codes are part of the contract, so scripts and hooks can gate on them:
|
|
8
|
+
// 0 = allow, 2 = ask, 3 = deny.
|
|
9
|
+
const EXIT = { allow: 0, ask: 2, deny: 3 };
|
|
10
|
+
|
|
11
|
+
export function cmdCheck(argv) {
|
|
12
|
+
const { flags, positional } = parseArgs(argv, { booleans: ['no-record', 'quiet'] });
|
|
13
|
+
const [loopName, action, ...targetParts] = positional;
|
|
14
|
+
const target = targetParts.join(' ');
|
|
15
|
+
if (!loopName || !action || !target) {
|
|
16
|
+
console.error('usage: docket check <loop> <read|draft|change|send> <target…>');
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
if (!ACTIONS.includes(action)) {
|
|
20
|
+
console.error(`docket: "${action}" is not an action — use one of: ${ACTIONS.join(', ')}`);
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const docketDir = requireDocketDir();
|
|
24
|
+
const loop = loadLoop(docketDir, loopName);
|
|
25
|
+
const result = checkWarrant(loop, action, target);
|
|
26
|
+
|
|
27
|
+
// The check itself is evidence: record what was asked and what was answered.
|
|
28
|
+
if (!flags['no-record']) {
|
|
29
|
+
recordCheck(docketDir, loop.name, action, target, result, { via: 'cli' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const style = VERDICT_STYLE[result.verdict];
|
|
33
|
+
if (!flags.quiet) {
|
|
34
|
+
console.log(`${style.color(bold(style.badge))} ${action} → "${target}"`);
|
|
35
|
+
console.log(` ${result.reason}`);
|
|
36
|
+
console.log(dim(` rule: ${result.rule} · loop: ${loop.name} · exit ${EXIT[result.verdict]}`));
|
|
37
|
+
} else {
|
|
38
|
+
console.log(result.verdict);
|
|
39
|
+
}
|
|
40
|
+
return EXIT[result.verdict];
|
|
41
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { parseArgs } from '../lib/args.js';
|
|
3
|
+
import { requireDocketDir, listLoops, loadLoop } from '../lib/loop.js';
|
|
4
|
+
import { renderBlock, compileToFile, TARGETS } from '../lib/compile.js';
|
|
5
|
+
import { dim, green } from '../lib/ui.js';
|
|
6
|
+
|
|
7
|
+
export function cmdCompile(argv) {
|
|
8
|
+
const { flags } = parseArgs(argv, { booleans: ['write'] });
|
|
9
|
+
const target = flags.target ?? 'raw';
|
|
10
|
+
if (!TARGETS[target]) {
|
|
11
|
+
console.error(`docket: unknown target "${target}" — targets: ${Object.keys(TARGETS).join(', ')}`);
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
const docketDir = requireDocketDir();
|
|
15
|
+
if (flags.loop && flags.write) {
|
|
16
|
+
// The managed block always holds every loop; writing just one would
|
|
17
|
+
// silently delete the rest from the agent's context file.
|
|
18
|
+
console.error(
|
|
19
|
+
'docket: --loop is for previewing one loop on stdout — --write always compiles all loops'
|
|
20
|
+
);
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const loops = flags.loop ? [loadLoop(docketDir, flags.loop)] : listLoops(docketDir);
|
|
24
|
+
if (!loops.length) {
|
|
25
|
+
console.error('docket: no loops to compile — create one with `docket new <name>`');
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!flags.write || target === 'raw') {
|
|
30
|
+
console.log(renderBlock(loops));
|
|
31
|
+
if (flags.write && target === 'raw') {
|
|
32
|
+
console.error(dim('(raw target always prints to stdout)'));
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const rootDir = path.dirname(docketDir);
|
|
38
|
+
const file = compileToFile(rootDir, target, loops);
|
|
39
|
+
console.log(
|
|
40
|
+
green('✓') +
|
|
41
|
+
` compiled ${loops.length} loop${loops.length === 1 ? '' : 's'} → ${path.relative(process.cwd(), file)} ${dim(`(${TARGETS[target].label})`)}`
|
|
42
|
+
);
|
|
43
|
+
console.log(dim(' re-run after editing loops; the docket block is replaced in place'));
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseArgs } from '../lib/args.js';
|
|
4
|
+
import { findDocketDir } from '../lib/loop.js';
|
|
5
|
+
import { bold, cyan, dim, green } from '../lib/ui.js';
|
|
6
|
+
|
|
7
|
+
const GITIGNORE_HINT = `# Docket keeps everything in plain files on purpose — commit them.
|
|
8
|
+
# If a loop's Memory section holds things you don't want in git, move the
|
|
9
|
+
# loop here and ignore it explicitly, e.g.:
|
|
10
|
+
# loops/private-*.loop.md
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
export function cmdInit(argv) {
|
|
14
|
+
const { flags } = parseArgs(argv, { booleans: ['quiet'] });
|
|
15
|
+
const root = path.resolve(flags.dir ?? process.cwd());
|
|
16
|
+
const dir = path.join(root, '.docket');
|
|
17
|
+
if (fs.existsSync(dir)) {
|
|
18
|
+
console.log(`already initialized: ${dir}`);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
// A .docket in an ancestor doesn't block a nested project — it would
|
|
22
|
+
// silently swallow this project's loops and record. Create locally, warn.
|
|
23
|
+
const ancestor = findDocketDir(root);
|
|
24
|
+
if (ancestor) {
|
|
25
|
+
console.log(
|
|
26
|
+
`note: ${ancestor} exists above this directory; commands run here will now use the new one`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
fs.mkdirSync(path.join(dir, 'loops'), { recursive: true });
|
|
30
|
+
fs.writeFileSync(path.join(dir, 'README.md'), GITIGNORE_HINT);
|
|
31
|
+
|
|
32
|
+
if (!flags.quiet) {
|
|
33
|
+
console.log(green('✓') + ` created ${path.relative(process.cwd(), dir) || dir}`);
|
|
34
|
+
console.log(`
|
|
35
|
+
${bold('A loop is one recurring task the agent does for you, wrapped in five layers.')}
|
|
36
|
+
Before an agent touches anything, a loop answers:
|
|
37
|
+
|
|
38
|
+
1. What must it ${bold('know')} before it starts?
|
|
39
|
+
2. How is this work ${bold('supposed to be done')}?
|
|
40
|
+
3. What may it do ${bold('without asking')}?
|
|
41
|
+
4. Where does it have to ${bold('stop')}?
|
|
42
|
+
5. What ${bold('evidence')} must it leave behind?
|
|
43
|
+
|
|
44
|
+
${dim('Unwritten answers get guessed at. Written answers get enforced.')}
|
|
45
|
+
|
|
46
|
+
Next:
|
|
47
|
+
${cyan('docket templates')} see the starter loops
|
|
48
|
+
${cyan('docket new my-loop')} answer the five questions interactively
|
|
49
|
+
${cyan('docket new appeal --template insurance-appeal')}
|
|
50
|
+
${cyan('docket compile --target claude --write')} put your loops in front of the agent
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { parseArgs } from '../lib/args.js';
|
|
2
|
+
import { requireDocketDir, listLoops, loadLoop } from '../lib/loop.js';
|
|
3
|
+
import { warrantLines } from '../lib/compile.js';
|
|
4
|
+
import { bold, cyan, dim, red, yellow } from '../lib/ui.js';
|
|
5
|
+
|
|
6
|
+
export function cmdList() {
|
|
7
|
+
const docketDir = requireDocketDir();
|
|
8
|
+
const loops = listLoops(docketDir);
|
|
9
|
+
if (!loops.length) {
|
|
10
|
+
console.log('no loops yet — create one with `docket new <name>`');
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
console.log(bold(`${loops.length} loop${loops.length === 1 ? '' : 's'}`) + '\n');
|
|
14
|
+
for (const loop of loops) {
|
|
15
|
+
console.log(` ${cyan(loop.name.padEnd(22))} ${loop.description}`);
|
|
16
|
+
}
|
|
17
|
+
console.log(dim('\ndetails: docket show <loop>'));
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function cmdShow(argv) {
|
|
22
|
+
const { positional } = parseArgs(argv);
|
|
23
|
+
const name = positional[0];
|
|
24
|
+
if (!name) {
|
|
25
|
+
console.error('usage: docket show <loop>');
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const docketDir = requireDocketDir();
|
|
29
|
+
const loop = loadLoop(docketDir, name);
|
|
30
|
+
|
|
31
|
+
const section = (title, body) => {
|
|
32
|
+
console.log(bold(title));
|
|
33
|
+
console.log(body ? body.replace(/^/gm, ' ') : dim(' (empty)'));
|
|
34
|
+
console.log();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
console.log(`${bold(cyan(loop.name))} — ${loop.description}\n${dim(loop.file)}\n`);
|
|
38
|
+
section('Brief — what it knows before it starts', loop.brief);
|
|
39
|
+
section('Procedure — how the work is done', loop.procedure);
|
|
40
|
+
|
|
41
|
+
console.log(bold('Warrant — what it may do on its own'));
|
|
42
|
+
for (const row of warrantLines(loop)) {
|
|
43
|
+
const label =
|
|
44
|
+
row.kind === 'ask' ? yellow('ask'.padEnd(7)) : row.kind === 'never' ? red('never'.padEnd(7)) : row.label.padEnd(7);
|
|
45
|
+
const text = row.text.startsWith('(') ? dim(row.text) : row.text;
|
|
46
|
+
console.log(` ${label} ${text}`);
|
|
47
|
+
}
|
|
48
|
+
console.log(dim(' unlisted = ask. Silence is never permission.\n'));
|
|
49
|
+
|
|
50
|
+
section('Reserved — stays human', loop.reserved.map((j) => `- ${j}`).join('\n'));
|
|
51
|
+
section('Record — evidence it owes', loop.record.map((r) => `- ${r}`).join('\n'));
|
|
52
|
+
return 0;
|
|
53
|
+
}
|