@gitdocket/core 0.0.0 → 0.1.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/src/init.ts ADDED
@@ -0,0 +1,338 @@
1
+ // docket init — pure text transforms for adopt-in-place. Adoption is
2
+ // additive, never a migration: every function here takes what already
3
+ // exists and returns a composed result, so callers can guarantee "never
4
+ // clobbers". Filesystem and git orchestration live in the CLI; this module
5
+ // stays runtime-portable.
6
+
7
+ import { INDEX_MARKER } from "./indexmd";
8
+ import { findFreshnessWatermark } from "./lint";
9
+ import { DOCKET_VERSION } from "./version";
10
+
11
+ export type InitAction = "create" | "update" | "skip";
12
+
13
+ export interface InitResult {
14
+ action: InitAction;
15
+ content: string;
16
+ /** Present when action is "skip" for a reason worth surfacing. */
17
+ reason?: string;
18
+ }
19
+
20
+ /** docket.yaml template — mirrors this repo's reference copy. */
21
+ export function defaultConfigYaml(project: string, bundle: string): string {
22
+ return `# Docket configuration — consumed by the docket CLI and by agents.
23
+
24
+ bundle: ${bundle}
25
+
26
+ project: ${project} # ID key for work items: ${project}-1, ${project}-2, … (epics + tasks share one sequence)
27
+
28
+ ids:
29
+ scheme: sequential # next = max existing + 1
30
+ decision_prefix: DEC # decisions number independently: DEC-1, DEC-2, …
31
+
32
+ workflow:
33
+ states: [todo, in-progress, blocked, in-review, done, closed]
34
+ # \`ready\` is never written to a file — it is derived:
35
+ # status == todo AND every task in depends_on has status == done.
36
+
37
+ git:
38
+ trailer: "Task" # commit trailer key linking commits to tasks
39
+ branch_prefix: "task/" # branch naming: task/${project}-12-short-slug
40
+ `;
41
+ }
42
+
43
+ /** Project key from a directory name: letters/digits, uppercased, max 3. */
44
+ export function deriveProjectKey(dirname: string): string {
45
+ const key = dirname
46
+ .replace(/[^a-zA-Z0-9]/g, "")
47
+ .toUpperCase()
48
+ .slice(0, 3);
49
+ return key || "DKT";
50
+ }
51
+
52
+ /** Bundle files scaffolded when missing. Paths are bundle-relative. */
53
+ export function scaffoldFiles(
54
+ project: string,
55
+ today: string,
56
+ ): { path: string; content: string }[] {
57
+ return [
58
+ {
59
+ path: "index.md",
60
+ content: `# ${project} — docs & work\n\n<!-- Replace the heading above with the project's full name and add one concise sentence explaining its purpose. Docket preserves this preamble when regenerating the index. -->\n\n${INDEX_MARKER}\n`,
61
+ },
62
+ {
63
+ path: "log.md",
64
+ content: `# Log\n\n## ${today}\n\n- **Create** — Adopted Docket (\`docket init\`).\n`,
65
+ },
66
+ ];
67
+ }
68
+
69
+ /**
70
+ * Stamp the freshness baseline watermark into log.md. Adoption is
71
+ * the baseline: a fresh bundle has nothing to retrospect, so init seeds the
72
+ * watermark the docket-freshness workflow advances from — instead of lint
73
+ * nagging for a ritual with nothing to sweep. The entry lands under today's
74
+ * section (log.md is newest-first). "create" refers to the entry — the file
75
+ * itself always pre-exists (scaffolded or brownfield).
76
+ */
77
+ export function composeFreshnessBaseline(
78
+ log: string,
79
+ sha: string | undefined,
80
+ today: string,
81
+ ): InitResult {
82
+ if (findFreshnessWatermark(log))
83
+ return { action: "skip", content: log, reason: "already stamped" };
84
+ if (!sha) return { action: "skip", content: log, reason: "no commits yet" };
85
+ const entry = `- **Freshness** — baseline at adoption; reviewed through \`${sha}\` (nothing to review before Docket).`;
86
+ const heading = `## ${today}`;
87
+ const lines = log.split("\n");
88
+ const at = lines.indexOf(heading);
89
+ if (at >= 0) {
90
+ lines.splice(at + 1, 0, "", entry);
91
+ return { action: "create", content: lines.join("\n") };
92
+ }
93
+ // No section for today — open one. The log is newest-first, so it goes
94
+ // right after the title (or at the very top of a title-less log).
95
+ const title = lines.findIndex((line) => line.startsWith("# "));
96
+ lines.splice(title + 1, 0, "", heading, "", entry);
97
+ return { action: "create", content: lines.join("\n") };
98
+ }
99
+
100
+ const HOOK_BEGIN = `# >>> docket prepare-commit-msg@${DOCKET_VERSION} >>>`;
101
+ const HOOK_END = "# <<< docket prepare-commit-msg <<<";
102
+ // Matches the begin marker at any version, and the legacy unversioned form.
103
+ const HOOK_MARKER_PREFIX = "# >>> docket prepare-commit-msg";
104
+
105
+ // Self-contained sh block; variable names are prefixed so appending into an
106
+ // existing hook can't collide, and every failure path exits 0 so a broken
107
+ // docket state never blocks a commit.
108
+ const HOOK_BLOCK = `${HOOK_BEGIN}
109
+ # Inject the active task's trailer into the commit message. Active task is
110
+ # per-checkout state in .docket/active-task (untracked), so git worktrees
111
+ # each carry their own active task.
112
+ docket_msg_file="$1"
113
+ docket_top="$(git rev-parse --show-toplevel 2>/dev/null)" || docket_top=""
114
+ if [ -n "$docket_top" ] && [ -f "$docket_top/.docket/active-task" ]; then
115
+ docket_task_id="$(head -n1 "$docket_top/.docket/active-task" | tr -d '[:space:]')"
116
+ if [ -n "$docket_task_id" ] && ! grep -qi "^Task:" "$docket_msg_file"; then
117
+ git interpret-trailers --in-place --trailer "Task: $docket_task_id" "$docket_msg_file"
118
+ fi
119
+ fi
120
+ ${HOOK_END}
121
+ `;
122
+
123
+ /**
124
+ * Compose the trailer-injecting block into a prepare-commit-msg hook.
125
+ * Missing hook → create; existing without our block → append; already
126
+ * installed → skip. Never rewrites what's there.
127
+ */
128
+ export function composeHook(existing: string | undefined): InitResult {
129
+ if (existing === undefined) {
130
+ return { action: "create", content: `#!/bin/sh\n${HOOK_BLOCK}` };
131
+ }
132
+ // Our marker (any version), or any hand-rolled hook already reading the
133
+ // active-task file (this repo's Phase 0 hook predates the marker).
134
+ if (
135
+ existing.includes(HOOK_MARKER_PREFIX) ||
136
+ existing.includes(".docket/active-task")
137
+ ) {
138
+ return { action: "skip", content: existing, reason: "already installed" };
139
+ }
140
+ const base = existing.endsWith("\n") ? existing : `${existing}\n`;
141
+ return { action: "update", content: `${base}\n${HOOK_BLOCK}` };
142
+ }
143
+
144
+ // Full block span, any marker version — for upgrade's regenerate-in-place.
145
+ const HOOK_BLOCK_RE =
146
+ /# >>> docket prepare-commit-msg(@\S+)? >>>[\s\S]*?# <<< docket prepare-commit-msg <<<\n?/;
147
+
148
+ /**
149
+ * Upgrade the hook block in place: the marked span (any version) is replaced
150
+ * with the current block. Hand-rolled hooks — active-task readers without our
151
+ * marker — are never touched (same rule as composeHook, inverted: compose
152
+ * skips what upgrade regenerates).
153
+ */
154
+ export function upgradeHookBlock(existing: string): InitResult {
155
+ if (!HOOK_BLOCK_RE.test(existing)) {
156
+ return {
157
+ action: "skip",
158
+ content: existing,
159
+ reason: existing.includes(".docket/active-task")
160
+ ? "hand-rolled hook — never touched"
161
+ : "no docket block",
162
+ };
163
+ }
164
+ // Replacer fn: the block contains a literal `$1`, which a string
165
+ // replacement would eat as a capture-group reference.
166
+ const content = existing.replace(HOOK_BLOCK_RE, () => HOOK_BLOCK);
167
+ return content === existing
168
+ ? { action: "skip", content: existing, reason: "up to date" }
169
+ : { action: "update", content };
170
+ }
171
+
172
+ const MCP_SERVER = { command: "docket-mcp" };
173
+
174
+ const CODEX_MCP_BEGIN = `# >>> docket mcp@${DOCKET_VERSION} >>>`;
175
+ const CODEX_MCP_END = "# <<< docket mcp <<<";
176
+ const CODEX_MCP_BEGIN_RE = /# >>> docket mcp(@\S+)? >>>/;
177
+ const CODEX_MCP_BLOCK = `${CODEX_MCP_BEGIN}
178
+ [mcp_servers.docket]
179
+ command = "docket-mcp"
180
+ ${CODEX_MCP_END}
181
+ `;
182
+ const CODEX_MCP_BLOCK_RE =
183
+ /# >>> docket mcp(@\S+)? >>>[\s\S]*?# <<< docket mcp <<<\n?/;
184
+
185
+ /** Register the docket MCP server in .mcp.json without touching other entries. */
186
+ export function mergeMcpJson(existing: string | undefined): InitResult {
187
+ if (existing === undefined) {
188
+ return {
189
+ action: "create",
190
+ content: `${JSON.stringify({ mcpServers: { docket: MCP_SERVER } }, null, 2)}\n`,
191
+ };
192
+ }
193
+ let parsed: unknown;
194
+ try {
195
+ parsed = JSON.parse(existing);
196
+ } catch {
197
+ return { action: "skip", content: existing, reason: "not valid JSON" };
198
+ }
199
+ if (typeof parsed !== "object" || parsed === null) {
200
+ return { action: "skip", content: existing, reason: "not a JSON object" };
201
+ }
202
+ const root = parsed as Record<string, unknown>;
203
+ const servers =
204
+ typeof root.mcpServers === "object" && root.mcpServers !== null
205
+ ? (root.mcpServers as Record<string, unknown>)
206
+ : {};
207
+ if (servers.docket) {
208
+ return { action: "skip", content: existing, reason: "already registered" };
209
+ }
210
+ root.mcpServers = { ...servers, docket: MCP_SERVER };
211
+ return { action: "update", content: `${JSON.stringify(root, null, 2)}\n` };
212
+ }
213
+
214
+ /**
215
+ * Add Docket's marker-managed MCP table to a valid Codex project config.
216
+ * TOML validation belongs to the runtime caller; this pure transform only
217
+ * composes text and refuses to replace an existing docket registration.
218
+ */
219
+ export function mergeCodexConfig(existing: string | undefined): InitResult {
220
+ if (existing === undefined)
221
+ return { action: "create", content: CODEX_MCP_BLOCK };
222
+ if (
223
+ CODEX_MCP_BEGIN_RE.test(existing) ||
224
+ /^\s*\[\s*mcp_servers(?:\.|\s*\.\s*)["']?docket["']?\s*\]\s*$/m.test(
225
+ existing,
226
+ )
227
+ ) {
228
+ return { action: "skip", content: existing, reason: "already registered" };
229
+ }
230
+ const base = existing.endsWith("\n") ? existing : `${existing}\n`;
231
+ return { action: "update", content: `${base}\n${CODEX_MCP_BLOCK}` };
232
+ }
233
+
234
+ /** Regenerate only Docket's marked Codex MCP block; never add one. */
235
+ export function upgradeCodexConfig(existing: string): InitResult {
236
+ if (!CODEX_MCP_BLOCK_RE.test(existing)) {
237
+ return {
238
+ action: "skip",
239
+ content: existing,
240
+ reason: "no docket block",
241
+ };
242
+ }
243
+ const content = existing.replace(CODEX_MCP_BLOCK_RE, () => CODEX_MCP_BLOCK);
244
+ return content === existing
245
+ ? { action: "skip", content: existing, reason: "up to date" }
246
+ : { action: "update", content };
247
+ }
248
+
249
+ export const ALLOW_RULES = ["mcp__docket", "Bash(docket:*)"] as const;
250
+
251
+ /**
252
+ * Ensure .claude/settings.json pre-approves the docket surface:
253
+ * enableAllProjectMcpServers plus the allow rules. Preserves everything else.
254
+ */
255
+ export function mergeClaudeSettings(existing: string | undefined): InitResult {
256
+ let root: Record<string, unknown> = {};
257
+ if (existing !== undefined) {
258
+ let parsed: unknown;
259
+ try {
260
+ parsed = JSON.parse(existing);
261
+ } catch {
262
+ return { action: "skip", content: existing, reason: "not valid JSON" };
263
+ }
264
+ if (typeof parsed !== "object" || parsed === null) {
265
+ return { action: "skip", content: existing, reason: "not a JSON object" };
266
+ }
267
+ root = parsed as Record<string, unknown>;
268
+ }
269
+
270
+ const permissions =
271
+ typeof root.permissions === "object" && root.permissions !== null
272
+ ? (root.permissions as Record<string, unknown>)
273
+ : {};
274
+ const allow = Array.isArray(permissions.allow)
275
+ ? permissions.allow.filter((r): r is string => typeof r === "string")
276
+ : [];
277
+ const missing = ALLOW_RULES.filter((rule) => !allow.includes(rule));
278
+
279
+ if (
280
+ existing !== undefined &&
281
+ missing.length === 0 &&
282
+ root.enableAllProjectMcpServers === true
283
+ ) {
284
+ return { action: "skip", content: existing, reason: "already configured" };
285
+ }
286
+
287
+ root.enableAllProjectMcpServers = true;
288
+ root.permissions = { ...permissions, allow: [...allow, ...missing] };
289
+ return {
290
+ action: existing === undefined ? "create" : "update",
291
+ content: `${JSON.stringify(root, null, 2)}\n`,
292
+ };
293
+ }
294
+
295
+ const GITIGNORE_BLOCK = `# docket per-checkout state (active task, cache)
296
+ .docket/
297
+ `;
298
+
299
+ /**
300
+ * Ensure `.docket/` is ignored. Additive: appends a commented rule unless some
301
+ * line already covers the directory; never reorders or rewrites existing rules.
302
+ */
303
+ export function ensureGitignore(existing: string | undefined): InitResult {
304
+ if (existing === undefined)
305
+ return { action: "create", content: GITIGNORE_BLOCK };
306
+ const covered = existing
307
+ .split("\n")
308
+ .map((line) => line.trim())
309
+ .some(
310
+ (line) =>
311
+ line === ".docket" ||
312
+ line === ".docket/" ||
313
+ line === "/.docket" ||
314
+ line === "/.docket/",
315
+ );
316
+ if (covered)
317
+ return { action: "skip", content: existing, reason: "already ignored" };
318
+ const base = existing.endsWith("\n") ? existing : `${existing}\n`;
319
+ return { action: "update", content: `${base}\n${GITIGNORE_BLOCK}` };
320
+ }
321
+
322
+ /** True when a markdown source lacks a frontmatter block with a `type` key. */
323
+ export function needsFrontmatter(source: string): boolean {
324
+ const match = source.match(/^---\n([\s\S]*?)\n---/);
325
+ if (!match) return true;
326
+ return !/^type:/m.test(match[1] ?? "");
327
+ }
328
+
329
+ /** Mechanical type proposal from a bundle-relative path — the agent refines it. */
330
+ export function proposeType(path: string): string {
331
+ if (path.startsWith("work/epics/")) return "Epic";
332
+ if (path.startsWith("work/tasks/")) return "Task";
333
+ if (path.startsWith("decisions/")) return "Decision";
334
+ if (path.startsWith("specs/")) return "Spec";
335
+ if (path.startsWith("reference/")) return "Reference";
336
+ if (path.startsWith("workflows/")) return "Workflow";
337
+ return "Doc";
338
+ }
package/src/intents.ts ADDED
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Canonical agent authority and intent contract.
3
+ *
4
+ * These examples are product/evaluation fixtures, not substring-matching
5
+ * rules. Agent surfaces may derive discovery text from this registry or
6
+ * validate their own text against it, but routing remains harness-owned.
7
+ */
8
+
9
+ export const DOCKET_INTENT_IDS = [
10
+ "orientation",
11
+ "backlog-hygiene",
12
+ "pickup",
13
+ "epic-supervision",
14
+ "task-management",
15
+ "project-maintenance",
16
+ ] as const;
17
+
18
+ export type DocketIntentId = (typeof DOCKET_INTENT_IDS)[number];
19
+
20
+ export const DIRECT_WORK_INTENT_ID = "direct-work" as const;
21
+
22
+ export const AGENT_INTENT_IDS = [
23
+ DIRECT_WORK_INTENT_ID,
24
+ ...DOCKET_INTENT_IDS,
25
+ ] as const;
26
+
27
+ export type AgentIntentId = (typeof AGENT_INTENT_IDS)[number];
28
+
29
+ export type IntentMode =
30
+ | "pass-through"
31
+ | "read-only"
32
+ | "proposal-first"
33
+ | "state-changing"
34
+ | "operation-scoped";
35
+
36
+ export type IntentEntrypoint =
37
+ | { kind: "direct"; value: string }
38
+ | { kind: "command"; value: string }
39
+ | { kind: "workflow"; value: string }
40
+ | { kind: "named-operation"; value: string };
41
+
42
+ export interface AgentIntentContract {
43
+ id: AgentIntentId;
44
+ title: string;
45
+ /** Mutually exclusive discovery summary for generated agent surfaces. */
46
+ discovery: string;
47
+ defaultEntrypoint: IntentEntrypoint;
48
+ mode: IntentMode;
49
+ authority: string;
50
+ inspectionScope: string;
51
+ positiveExamples: readonly string[];
52
+ exclusions: readonly string[];
53
+ }
54
+
55
+ export type DocketIntentContract = AgentIntentContract & {
56
+ id: DocketIntentId;
57
+ };
58
+
59
+ export const PICKUP_AUTHORITY_EVIDENCE = [
60
+ "a Docket ID",
61
+ "an unambiguous reference to an existing tracked item",
62
+ "an explicit request for Docket or backlog selection",
63
+ ] as const;
64
+
65
+ export const DIRECT_WORK_INTENT = {
66
+ id: DIRECT_WORK_INTENT_ID,
67
+ title: "Carry out direct user work",
68
+ discovery:
69
+ "Direct user work — execute a concrete product or repository request in the user's stated scope without Docket coordination.",
70
+ defaultEntrypoint: {
71
+ kind: "direct",
72
+ value: "the user's concrete requested work",
73
+ },
74
+ mode: "pass-through",
75
+ authority:
76
+ "The concrete request authorizes only its stated product or repository scope; generic implementation language such as work, task, fix, or implement does not authorize Docket pickup or any tracker mutation.",
77
+ inspectionScope:
78
+ "Inspect and change only the product or repository surfaces needed for the user's concrete request, subject to the ordinary safety and approval policy of the harness.",
79
+ positiveExamples: [
80
+ "fix the mobile navigation overflow",
81
+ "implement validation for this form",
82
+ "update this documentation example",
83
+ ],
84
+ exclusions: [
85
+ "a Docket ID or an unambiguous reference to an existing tracked item",
86
+ "an explicit request for Docket or backlog selection",
87
+ "a named Docket operation such as create, move, or close",
88
+ ],
89
+ } as const satisfies AgentIntentContract;
90
+
91
+ export const DOCKET_INTENTS = {
92
+ orientation: {
93
+ id: "orientation",
94
+ title: "Orient and review",
95
+ discovery:
96
+ "Read-only orientation — answer what is happening or what comes next from the shared Docket overview.",
97
+ defaultEntrypoint: { kind: "command", value: "docket overview --json" },
98
+ mode: "read-only",
99
+ authority:
100
+ "No confirmation is needed because the path may not mutate task, bundle, cache, index, or Git state.",
101
+ inspectionScope:
102
+ "Start with the structured overview; follow bundle links only when the requested explanation needs more evidence.",
103
+ positiveExamples: [
104
+ "what's next?",
105
+ "where are we?",
106
+ "let's review",
107
+ "give me a status update",
108
+ ],
109
+ exclusions: [
110
+ "an explicit request to groom or audit backlog hygiene",
111
+ "an explicit request to start or continue implementation",
112
+ "a named task mutation such as create, move, or close",
113
+ ],
114
+ },
115
+ "backlog-hygiene": {
116
+ id: "backlog-hygiene",
117
+ title: "Audit backlog hygiene",
118
+ discovery:
119
+ "Full backlog hygiene audit — inspect stale or inconsistent work state and propose fixes before applying any mutation.",
120
+ defaultEntrypoint: { kind: "workflow", value: "docket-groom" },
121
+ mode: "proposal-first",
122
+ authority:
123
+ "The audit is read-only until the user confirms proposed fixes or has explicitly granted autonomous mechanical cleanup authority.",
124
+ inspectionScope:
125
+ "Inspect the Docket bundle and task-linked Git evidence required by the groom workflow, not unrelated product implementation files.",
126
+ positiveExamples: [
127
+ "groom the backlog",
128
+ "audit our task hygiene",
129
+ "find stale or inconsistent tickets",
130
+ ],
131
+ exclusions: [
132
+ "ordinary review or what-is-next questions",
133
+ "starting the highest-priority ready task",
134
+ "a single named task operation",
135
+ ],
136
+ },
137
+ pickup: {
138
+ id: "pickup",
139
+ title: "Pick up or continue work",
140
+ discovery:
141
+ "Start or resume explicitly tracked Docket work only — use a Docket ID, an unambiguous existing item, or explicit next/backlog selection; direct work bypasses Docket and ambiguous references require resolution before active-task state changes.",
142
+ defaultEntrypoint: { kind: "workflow", value: "docket-pickup" },
143
+ mode: "state-changing",
144
+ authority:
145
+ "Pickup requires positive tracked-work evidence: a Docket ID, an unambiguous reference to an existing tracked item, or an explicit request for Docket or backlog selection. Generic action language alone does not authorize pickup.",
146
+ inspectionScope:
147
+ "Use the engine-returned task, epic, dependencies, linked concepts, and commits before inspecting implementation files needed for the task.",
148
+ positiveExamples: [
149
+ "start DKT-12",
150
+ "pick up the next Docket task",
151
+ "continue work on DKT-12",
152
+ ],
153
+ exclusions: [
154
+ "what-is-next questions without action language",
155
+ "requests that explicitly say not to start or change anything",
156
+ "generic implementation requests with no tracked-work evidence",
157
+ "an unresolved or ambiguous tracked-item reference",
158
+ "creating or closing a task as tracker administration",
159
+ ],
160
+ },
161
+ "epic-supervision": {
162
+ id: "epic-supervision",
163
+ title: "Supervise an epic",
164
+ discovery:
165
+ "Run a named epic to completion — supervise ready child work through isolated workers or the mandatory serial fallback, verify integration, and return one completion or blocker receipt.",
166
+ defaultEntrypoint: { kind: "workflow", value: "docket-epic" },
167
+ mode: "state-changing",
168
+ authority:
169
+ "Explicit run, start, or supervise language applied to a named epic authorizes its ready child work and final epic review, but no unrelated task or speculative scheduler work.",
170
+ inspectionScope:
171
+ "Inspect the named epic, its child dependency graph, likely write overlap, task-linked Git evidence, and verification surfaces required to integrate and review that epic.",
172
+ positiveExamples: [
173
+ "run epic DKT-42 and come back when it is done",
174
+ "start the DKT-42 epic",
175
+ "supervise all ready work under DKT-42",
176
+ ],
177
+ exclusions: [
178
+ "starting one named task",
179
+ "ordinary epic status or review without action language",
180
+ "building an orchestration service or changing unrelated work",
181
+ ],
182
+ },
183
+ "task-management": {
184
+ id: "task-management",
185
+ title: "Manage a named work item",
186
+ discovery:
187
+ "Perform an explicit task operation — create, inspect, edit, move, log, stop, or close only the work item and derived surfaces in scope.",
188
+ defaultEntrypoint: {
189
+ kind: "named-operation",
190
+ value: "the corresponding docket task command or workflow",
191
+ },
192
+ mode: "operation-scoped",
193
+ authority:
194
+ "The named operation supplies authority only for its documented mutations; read operations remain read-only and close follows its reconciliation workflow.",
195
+ inspectionScope:
196
+ "Inspect the named item and the linked concepts or derived surfaces required by that operation; do not broaden into backlog grooming.",
197
+ positiveExamples: [
198
+ "create an epic with these tickets",
199
+ "move DKT-12 to blocked",
200
+ "close DKT-12",
201
+ "show me DKT-12",
202
+ ],
203
+ exclusions: [
204
+ "general status or what-is-next questions",
205
+ "a full backlog hygiene audit",
206
+ "starting implementation unless pickup is also explicit",
207
+ ],
208
+ },
209
+ "project-maintenance": {
210
+ id: "project-maintenance",
211
+ title: "Run named Docket maintenance",
212
+ discovery:
213
+ "Run an explicitly named Docket maintenance procedure such as freshness review or product-context refresh, using that workflow's own mutation contract.",
214
+ defaultEntrypoint: {
215
+ kind: "named-operation",
216
+ value: "the explicitly requested maintenance workflow",
217
+ },
218
+ mode: "operation-scoped",
219
+ authority:
220
+ "Maintenance never acts as a fallback for orientation; the user must request the procedure or its concrete maintenance outcome.",
221
+ inspectionScope:
222
+ "Use only the evidence and repository writes named by the selected maintenance workflow.",
223
+ positiveExamples: [
224
+ "run a freshness review",
225
+ "refresh the product context",
226
+ "prepare the weekly standup report",
227
+ ],
228
+ exclusions: [
229
+ "ordinary status or review requests",
230
+ "backlog hygiene unless grooming is explicit",
231
+ "task implementation or tracker mutation outside the named procedure",
232
+ ],
233
+ },
234
+ } as const satisfies Record<DocketIntentId, DocketIntentContract>;
235
+
236
+ export const AGENT_INTENTS = {
237
+ [DIRECT_WORK_INTENT_ID]: DIRECT_WORK_INTENT,
238
+ ...DOCKET_INTENTS,
239
+ } as const satisfies Record<AgentIntentId, AgentIntentContract>;
240
+
241
+ export const AGENT_INTENT_DISAMBIGUATION = [
242
+ "A concrete product or repository request is direct work unless positive tracked-work evidence is present. Generic words such as work, task, fix, implement, or UX never supply pickup authority.",
243
+ "Pickup is authorized only by a Docket ID, an unambiguous reference to an existing tracked item, or an explicit request for Docket or backlog selection.",
244
+ "If a tracked-item reference cannot be resolved unambiguously, resolve or clarify that reference; never degrade to bare pickup or top-ready selection.",
245
+ "Direct work does not create, start, stop, adopt, clear, or otherwise mutate .docket/active-task or any tracked item. Existing active or ready work does not change the direct request's scope.",
246
+ "Ordinary review, status, and what-is-next language defaults to read-only orientation.",
247
+ "Specific Docket action language beats a generic word such as review: groom or audit selects backlog hygiene; tracked start, pick up, resume, or continue selects pickup; run, start, or supervise a named epic selects epic supervision; a named tracker operation selects task management.",
248
+ "A negative constraint such as do not start narrows permitted actions but never selects a broader workflow by itself.",
249
+ "Combined operations retain separate authority: creating a task does not start it unless pickup is also explicit, while track this and start it authorizes both bounded operations in sequence.",
250
+ "No intent may mutate outside its declared authority, and every Docket workflow is opt-in rather than a fallback for direct work.",
251
+ ] as const;
252
+
253
+ /** @deprecated Use AGENT_INTENT_DISAMBIGUATION for the complete boundary. */
254
+ export const DOCKET_INTENT_DISAMBIGUATION = AGENT_INTENT_DISAMBIGUATION;
255
+
256
+ export function agentIntent(id: AgentIntentId): AgentIntentContract {
257
+ return AGENT_INTENTS[id];
258
+ }
259
+
260
+ export function docketIntent(id: DocketIntentId): DocketIntentContract {
261
+ return DOCKET_INTENTS[id];
262
+ }