@gitdocket/core 0.0.0 → 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/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
+ }
package/src/lint.ts ADDED
@@ -0,0 +1,240 @@
1
+ // PM 101 encoded as lint, not UI. Errors are profile conformance
2
+ // per the spec's Conformance section; warnings are practice. Core stays
3
+ // git-free: callers with a repo (the CLI) compute git-derived inputs like
4
+ // the trailerless-commit count and pass them in.
5
+
6
+ import type { Bundle } from "./bundle";
7
+ import type { FileStore } from "./filestore";
8
+ import { type Diagnostic, isReserved } from "./parse";
9
+ import {
10
+ parseStateOfPlay,
11
+ presentStateOfPlay,
12
+ STATE_OF_PLAY_PATH,
13
+ } from "./state-of-play";
14
+ import type { VerifyMarker } from "./verify";
15
+
16
+ export interface LintOptions {
17
+ now?: Date;
18
+ /** Staleness threshold for in-flight statuses and the Freshness watermark. */
19
+ maxAgeDays?: number;
20
+ /** Commits without a Task trailer since the watermark — computed by callers with git. */
21
+ trailerlessCommits?: number;
22
+ /** Task-linked commits after overview.md's as_of watermark, computed by Git-aware callers. */
23
+ stateOfPlayCommitsAgo?: number;
24
+ /**
25
+ * docket:verifies markers scanned by callers with repo access.
26
+ * The epic's only lint rule: an unresolvable target warns; "spec is
27
+ * unverified" deliberately does not — that lives in `verify status`.
28
+ */
29
+ verifyMarkers?: VerifyMarker[];
30
+ }
31
+
32
+ export interface FreshnessWatermark {
33
+ sha: string;
34
+ /** The `## YYYY-MM-DD` section the watermark entry sits under. */
35
+ date?: string;
36
+ }
37
+
38
+ /**
39
+ * Latest `**Freshness** … reviewed through <sha>` entry in log.md
40
+ * (newest-first, so first match wins). Prose may sit between the marker and
41
+ * the sha — init's baseline stamp reads "baseline at adoption; reviewed
42
+ * through `<sha>`".
43
+ */
44
+ export function findFreshnessWatermark(
45
+ logSource: string,
46
+ ): FreshnessWatermark | undefined {
47
+ let date: string | undefined;
48
+ for (const line of logSource.split("\n")) {
49
+ const heading = line.match(/^## (\d{4}-\d{2}-\d{2})/);
50
+ if (heading) {
51
+ date = heading[1];
52
+ continue;
53
+ }
54
+ const mark = line.match(
55
+ /\*\*Freshness\*\*.*reviewed through `?([0-9a-f]{7,40})`?/,
56
+ );
57
+ if (mark?.[1]) return { sha: mark[1], date };
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ /** Resolve an internal link against the bundle root; undefined = not checkable (non-md, escapes bundle). */
63
+ export function resolveLink(
64
+ fromPath: string,
65
+ target: string,
66
+ ): string | undefined {
67
+ const clean = target.split("#")[0] ?? "";
68
+ if (!clean.endsWith(".md")) return undefined;
69
+ const parts = clean.startsWith("/")
70
+ ? clean.slice(1).split("/")
71
+ : [...fromPath.split("/").slice(0, -1), ...clean.split("/")];
72
+ const out: string[] = [];
73
+ for (const part of parts) {
74
+ if (part === "" || part === ".") continue;
75
+ if (part === "..") {
76
+ if (out.length === 0) return undefined;
77
+ out.pop();
78
+ } else out.push(part);
79
+ }
80
+ return out.join("/");
81
+ }
82
+
83
+ export async function lintBundle(
84
+ store: FileStore,
85
+ bundle: Bundle,
86
+ opts: LintOptions = {},
87
+ ): Promise<Diagnostic[]> {
88
+ const now = opts.now ?? new Date();
89
+ const maxAgeDays = opts.maxAgeDays ?? 14;
90
+ const out: Diagnostic[] = [...bundle.diagnostics];
91
+ const files = new Set(await store.list());
92
+ const report = (severity: Diagnostic["severity"]) => {
93
+ return (path: string, message: string) =>
94
+ out.push({ path, message, severity });
95
+ };
96
+ const error = report("error");
97
+ const warn = report("warning");
98
+ const ageDays = (iso: string): number =>
99
+ (now.getTime() - new Date(iso).getTime()) / 86_400_000;
100
+
101
+ for (const item of bundle.workItems) {
102
+ // Conformance: every depends_on entry resolves to an existing task id.
103
+ for (const dep of item.fm.depends_on) {
104
+ if (bundle.byId(dep)?.kind !== "work")
105
+ error(item.path, `depends_on ${dep} does not resolve to a work item`);
106
+ }
107
+
108
+ const filename = item.path.split("/").at(-1) ?? item.path;
109
+ if (!filename.startsWith(`${item.fm.id}-`))
110
+ warn(
111
+ item.path,
112
+ `filename does not start with ${item.fm.id}- (slug drift?)`,
113
+ );
114
+
115
+ if (item.fm.type === "Epic" && !item.fm.spec)
116
+ warn(item.path, "epic has no spec link");
117
+
118
+ if (
119
+ item.fm.status === "done" &&
120
+ /^\s*- \[ \]/m.test(await store.read(item.path))
121
+ )
122
+ warn(item.path, "done but has unchecked criteria");
123
+
124
+ if (item.fm.status === "in-progress" || item.fm.status === "in-review") {
125
+ const ts = item.fm.timestamp;
126
+ if (typeof ts === "string" && ageDays(ts) > maxAgeDays)
127
+ warn(
128
+ item.path,
129
+ `${item.fm.status} but untouched for ${Math.floor(ageDays(ts))} days — stale?`,
130
+ );
131
+ }
132
+ }
133
+
134
+ for (const concept of bundle.concepts) {
135
+ for (const link of concept.links) {
136
+ if (!link.internal) continue;
137
+ const resolved = resolveLink(concept.path, link.target);
138
+ if (resolved && !files.has(resolved))
139
+ warn(concept.path, `broken link: ${link.target}`);
140
+ }
141
+ }
142
+
143
+ // overview.md is optional. The linked re-entry note expires on either
144
+ // substantial task-linked movement or review age. Superseded formats stay
145
+ // readable and are accepted non-destructively but ask for a linked refresh.
146
+ if (files.has(STATE_OF_PLAY_PATH)) {
147
+ const parsed = parseStateOfPlay(await store.read(STATE_OF_PLAY_PATH));
148
+ out.push(...parsed.diagnostics);
149
+ if (parsed.note) {
150
+ const view = presentStateOfPlay(parsed.note, opts.stateOfPlayCommitsAgo, {
151
+ now,
152
+ });
153
+ if (view.review.status === "needs-review") {
154
+ const why = [
155
+ ...(view.review.reasons.includes("legacy-format")
156
+ ? ["legacy prose format"]
157
+ : []),
158
+ ...(view.review.reasons.includes("superseded-format")
159
+ ? ["superseded re-entry/v1 format"]
160
+ : []),
161
+ ...(view.review.reasons.includes("evidence-moved")
162
+ ? [`${view.taskCommitsAgo} task-linked commits behind ${view.asOf}`]
163
+ : []),
164
+ ...(view.review.reasons.includes("review-expired")
165
+ ? [`reviewed ${view.review.reviewedDaysAgo} days ago`]
166
+ : []),
167
+ ].join(", ");
168
+ warn(
169
+ STATE_OF_PLAY_PATH,
170
+ `product context needs review (${why}) — run the docket-state-of-play workflow`,
171
+ );
172
+ }
173
+ }
174
+ }
175
+
176
+ // Reserved files (index.md, log.md) aren't concepts, but the hand-maintained
177
+ // index is exactly where links rot — check them with a light regex pass.
178
+ for (const path of files) {
179
+ if (!isReserved(path)) continue;
180
+ const source = await store.read(path);
181
+ for (const match of source.matchAll(/\]\(([^)\s]+)\)/g)) {
182
+ const target = match[1] ?? "";
183
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target)) continue; // external
184
+ const resolved = resolveLink(path, target);
185
+ if (resolved && !files.has(resolved))
186
+ warn(path, `broken link: ${target}`);
187
+ }
188
+ }
189
+
190
+ // Stray merge-conflict markers: an ordinary git merge — or a
191
+ // docket upgrade conflict left unresolved — can land markers in committed
192
+ // files, and upgrade re-runs report up-to-date once origin is bumped, so
193
+ // lint is the recurring nag. Requires all three marker lines at line
194
+ // starts; a fenced code block quoting a complete conflict still trips this
195
+ // (accepted — quote partial markers instead).
196
+ for (const path of files) {
197
+ const source = await store.read(path);
198
+ if (
199
+ /^<{7} /m.test(source) &&
200
+ /^={7}$/m.test(source) &&
201
+ /^>{7} /m.test(source)
202
+ )
203
+ warn(path, "merge-conflict markers present — resolve and remove them");
204
+ }
205
+
206
+ // Verification markers: the target must name an existing concept
207
+ // by bundle-absolute path. Marker sources live outside the bundle, so the
208
+ // diagnostic path is repo-relative.
209
+ for (const m of opts.verifyMarkers ?? []) {
210
+ if (!m.spec || !files.has(m.spec))
211
+ warn(
212
+ m.source,
213
+ `docket:verifies target does not resolve: ${m.target} (line ${m.line})`,
214
+ );
215
+ }
216
+
217
+ // Freshness nag — only for bundles that keep a log.md at all.
218
+ if (files.has("log.md")) {
219
+ const watermark = findFreshnessWatermark(await store.read("log.md"));
220
+ if (!watermark) {
221
+ warn(
222
+ "log.md",
223
+ "no **Freshness** watermark — run the docket-freshness workflow",
224
+ );
225
+ } else {
226
+ if (watermark.date && ageDays(watermark.date) > maxAgeDays)
227
+ warn(
228
+ "log.md",
229
+ `Freshness watermark is ${Math.floor(ageDays(watermark.date))} days old — run the docket-freshness workflow`,
230
+ );
231
+ if ((opts.trailerlessCommits ?? 0) > 0)
232
+ warn(
233
+ "log.md",
234
+ `${opts.trailerlessCommits} trailerless work commit(s) since Freshness watermark ${watermark.sha} (tracker-only chore(docket) commits exempt) — run /docket-freshness`,
235
+ );
236
+ }
237
+ }
238
+
239
+ return out;
240
+ }