@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/package.json +27 -6
- package/src/bundle.ts +127 -0
- package/src/cache.ts +557 -0
- package/src/config.ts +96 -0
- package/src/engine-semantics.ts +26 -0
- package/src/filestore.ts +63 -0
- package/src/id-allocation.ts +288 -0
- package/src/index.ts +199 -0
- package/src/indexmd.ts +146 -0
- package/src/init.ts +338 -0
- package/src/intents.ts +262 -0
- package/src/lint.ts +240 -0
- package/src/ops.ts +320 -0
- package/src/orientation.ts +88 -0
- package/src/overview.ts +593 -0
- package/src/packet.ts +119 -0
- package/src/parse.ts +200 -0
- package/src/prompt-routing.ts +651 -0
- package/src/schema.ts +56 -0
- package/src/search.ts +147 -0
- package/src/shipped-history.json +53 -0
- package/src/shipped.ts +96 -0
- package/src/state-of-play.ts +370 -0
- package/src/states.ts +85 -0
- package/src/upgrade.ts +177 -0
- package/src/verify.ts +122 -0
- package/src/version.ts +6 -0
- package/src/workflows.ts +481 -0
- package/README.md +0 -5
package/src/verify.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Verification linkage: a `docket:verifies /specs/x.md`
|
|
2
|
+
// comment in a test file claims that file protects that spec. Linkage is
|
|
3
|
+
// derived, never declared — this module is pure text/graph logic; the CLI
|
|
4
|
+
// owns globbing and file IO, and Docket never runs the tests it maps.
|
|
5
|
+
|
|
6
|
+
import type { Bundle } from "./bundle";
|
|
7
|
+
|
|
8
|
+
export const VERIFY_TOKEN = "docket:verifies";
|
|
9
|
+
|
|
10
|
+
export interface VerifyMarker {
|
|
11
|
+
/** Repo-relative path of the file carrying the marker. */
|
|
12
|
+
source: string;
|
|
13
|
+
/** 1-indexed line the marker sits on. */
|
|
14
|
+
line: number;
|
|
15
|
+
/** Raw target as written: `/specs/x.md` or `/specs/x.md#anchor`. */
|
|
16
|
+
target: string;
|
|
17
|
+
/** Bundle path of the target concept — undefined when unresolvable. */
|
|
18
|
+
spec?: string;
|
|
19
|
+
/** Heading anchor, preserved for finer-than-file grain. */
|
|
20
|
+
anchor?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Any comment syntax matches: the token is distinctive enough that we scan
|
|
24
|
+
// raw text rather than parse per-language comments. Consequence: a string
|
|
25
|
+
// literal containing the token also matches (this bit our own test fixtures)
|
|
26
|
+
// — files that must *mention* the token split it, keeping coverage explicit.
|
|
27
|
+
const MARKER = new RegExp(`${VERIFY_TOKEN}\\s+(\\S+)`);
|
|
28
|
+
|
|
29
|
+
// Targets are path-shaped; cut at the first character that isn't. Sheds
|
|
30
|
+
// trailing comment closers (`-->`, `*/`) and quote/punctuation debris.
|
|
31
|
+
const trimTarget = (raw: string): string =>
|
|
32
|
+
raw.replace(/[^A-Za-z0-9._/#-].*$/, "");
|
|
33
|
+
|
|
34
|
+
/** Extract raw markers from one file's text. `spec` stays unset — resolve separately. */
|
|
35
|
+
export function scanVerifyMarkers(
|
|
36
|
+
source: string,
|
|
37
|
+
content: string,
|
|
38
|
+
): VerifyMarker[] {
|
|
39
|
+
const out: VerifyMarker[] = [];
|
|
40
|
+
content.split("\n").forEach((text, i) => {
|
|
41
|
+
const match = text.match(MARKER);
|
|
42
|
+
if (!match?.[1]) return;
|
|
43
|
+
const target = trimTarget(match[1]);
|
|
44
|
+
if (!target) return;
|
|
45
|
+
const anchor = target.split("#")[1];
|
|
46
|
+
out.push({
|
|
47
|
+
source,
|
|
48
|
+
line: i + 1,
|
|
49
|
+
target,
|
|
50
|
+
...(anchor ? { anchor } : {}),
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Fill `spec` on markers whose target is a bundle-absolute `.md` path naming
|
|
58
|
+
* an existing concept. Anything else (relative paths, missing files) stays
|
|
59
|
+
* unresolved — lint turns those into warnings.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveVerifyMarkers(
|
|
62
|
+
markers: VerifyMarker[],
|
|
63
|
+
conceptPaths: ReadonlySet<string>,
|
|
64
|
+
): VerifyMarker[] {
|
|
65
|
+
return markers.map((m) => {
|
|
66
|
+
const clean = m.target.split("#")[0] ?? "";
|
|
67
|
+
if (!clean.startsWith("/") || !clean.endsWith(".md")) return m;
|
|
68
|
+
const path = clean.slice(1);
|
|
69
|
+
return conceptPaths.has(path) ? { ...m, spec: path } : m;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface VerifySource {
|
|
74
|
+
source: string;
|
|
75
|
+
line: number;
|
|
76
|
+
anchor?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface VerifyStatusRow {
|
|
80
|
+
/** Bundle path of the spec (or other targeted concept). */
|
|
81
|
+
spec: string;
|
|
82
|
+
title?: string;
|
|
83
|
+
type: string;
|
|
84
|
+
sources: VerifySource[];
|
|
85
|
+
/** True only for Spec-type concepts nothing verifies — the signal is zero. */
|
|
86
|
+
unverified: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Presence per spec: every `type: Spec` concept, plus any other concept a
|
|
91
|
+
* marker targets. Presence marks only — no counts-as-scores.
|
|
92
|
+
*/
|
|
93
|
+
export function verifyStatus(
|
|
94
|
+
bundle: Bundle,
|
|
95
|
+
markers: VerifyMarker[],
|
|
96
|
+
): VerifyStatusRow[] {
|
|
97
|
+
const bySpec = new Map<string, VerifySource[]>();
|
|
98
|
+
for (const m of markers) {
|
|
99
|
+
if (!m.spec) continue;
|
|
100
|
+
const sources = bySpec.get(m.spec) ?? [];
|
|
101
|
+
sources.push({
|
|
102
|
+
source: m.source,
|
|
103
|
+
line: m.line,
|
|
104
|
+
...(m.anchor ? { anchor: m.anchor } : {}),
|
|
105
|
+
});
|
|
106
|
+
bySpec.set(m.spec, sources);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const rows: VerifyStatusRow[] = [];
|
|
110
|
+
for (const c of bundle.concepts) {
|
|
111
|
+
const targeted = bySpec.has(c.path);
|
|
112
|
+
if (c.fm.type !== "Spec" && !targeted) continue;
|
|
113
|
+
rows.push({
|
|
114
|
+
spec: c.path,
|
|
115
|
+
title: c.fm.title,
|
|
116
|
+
type: c.fm.type,
|
|
117
|
+
sources: bySpec.get(c.path) ?? [],
|
|
118
|
+
unverified: c.fm.type === "Spec" && !targeted,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return rows.sort((a, z) => a.spec.localeCompare(z.spec));
|
|
122
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The engine version — single source of truth for provenance stamps.
|
|
2
|
+
// Release bumps this constant together with the package.json versions; marker
|
|
3
|
+
// lines, workflow `origin:` frontmatter, and the CLI's --version all read it
|
|
4
|
+
// from here.
|
|
5
|
+
|
|
6
|
+
export const DOCKET_VERSION = "0.1.1";
|
package/src/workflows.ts
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
// Canonical docket workflows. The judgment procedures that operate a
|
|
2
|
+
// bundle live IN the bundle as agent-neutral `type: Workflow` concepts — they
|
|
3
|
+
// version, link, and lint like everything else and travel with the repo.
|
|
4
|
+
// Tool-specific surfaces (Claude skills, CLAUDE.md/AGENTS.md sections) are
|
|
5
|
+
// thin generated adapters that defer to the bundle file. Bundle = source of
|
|
6
|
+
// truth; adapters are regenerable and safe to gitignore.
|
|
7
|
+
|
|
8
|
+
import { ENGINE_SEMANTICS } from "./engine-semantics";
|
|
9
|
+
import type { InitResult } from "./init";
|
|
10
|
+
import { DOCKET_INTENTS, type DocketIntentId } from "./intents";
|
|
11
|
+
import { DOCKET_VERSION } from "./version";
|
|
12
|
+
|
|
13
|
+
export const WORKFLOWS_DIR = "workflows";
|
|
14
|
+
|
|
15
|
+
export interface WorkflowDef {
|
|
16
|
+
/** Kebab-case name; the bundle file is `workflows/<slug>.md`. */
|
|
17
|
+
slug: string;
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
/** Canonical user intent this workflow serves. */
|
|
21
|
+
intent: DocketIntentId;
|
|
22
|
+
/** Markdown body. Agent-neutral: imperative steps, `docket` CLI, no tool-specific framing. */
|
|
23
|
+
body: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const workflowPath = (w: WorkflowDef): string =>
|
|
27
|
+
`${WORKFLOWS_DIR}/${w.slug}.md`;
|
|
28
|
+
|
|
29
|
+
// Bodies address "you", the agent executing the workflow, whatever harness it
|
|
30
|
+
// runs in. Engine commands are spelled `docket …` — repos that run the engine
|
|
31
|
+
// through a package runner note that in their agent instructions.
|
|
32
|
+
export const DOCKET_WORKFLOWS: readonly WorkflowDef[] = [
|
|
33
|
+
{
|
|
34
|
+
slug: "docket-pickup",
|
|
35
|
+
title: "Pick up a task",
|
|
36
|
+
intent: "pickup",
|
|
37
|
+
description: DOCKET_INTENTS.pickup.discovery,
|
|
38
|
+
body: `Use this workflow only for authorized tracked Docket work. Pickup authority requires positive evidence: a Docket ID, an unambiguous reference to an existing tracked item, or an explicit request to select the next Docket or backlog item. Generic implementation language does not select pickup. A concrete direct request proceeds in the user's stated scope without creating, starting, or adopting Docket work; do not invoke this workflow for it.
|
|
39
|
+
|
|
40
|
+
1. **Resolve the target and command**: a Docket ID authorizes \`docket task start <ID> --json\`. Resolve an unambiguous tracked-item reference to its ID, then use the same named command. Only explicit next-Docket-task or backlog-selection language authorizes bare \`docket task start --json\`. If an apparent tracked reference remains ambiguous, perform only focused resolution or ask for clarification; never omit the ID, substitute the top ready item, or mutate \`.docket/active-task\`.
|
|
41
|
+
2. **Start through the engine**: run only the command authorized in step 1. If the command fails, stop; do not rename the session or begin tracked work.
|
|
42
|
+
3. **Use the returned title intent**: read \`suggestedSessionTitle\` from the successful structured result. Do not rebuild it from prompt text or separately queried task fields.
|
|
43
|
+
4. **Best-effort rename**: ask the current harness's native adapter to name the calling session with that exact value. If the host has no current-session naming capability, the capability is unavailable, or the rename fails, continue silently without retrying or treating pickup as failed.
|
|
44
|
+
5. **Hand off context**: use the returned task, epic, dependency, linked-concept, and commit fields as the context packet, then begin the requested tracked work.
|
|
45
|
+
|
|
46
|
+
${ENGINE_SEMANTICS.transitions}
|
|
47
|
+
|
|
48
|
+
${ENGINE_SEMANTICS.mutationOwnership.pickup}`,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
slug: "docket-epic",
|
|
52
|
+
title: "Supervise an epic",
|
|
53
|
+
intent: "epic-supervision",
|
|
54
|
+
description: DOCKET_INTENTS["epic-supervision"].discovery,
|
|
55
|
+
body: `Supervise the named epic until its acceptance criteria support explicit closure or one concrete blocker prevents safe progress. Docket files and task-linked Git history are the durable source of truth. Native worker, wait, follow-up, notification, and isolated-checkout capabilities are optional accelerators; they never change readiness or completion semantics.
|
|
56
|
+
|
|
57
|
+
## 1. Establish the authoritative graph
|
|
58
|
+
|
|
59
|
+
1. Confirm the user named an epic and authorized running it, not merely reviewing it. Read the epic file, verify that it is an Epic with an ID and title, then run \`docket task list --epic <EPIC-ID> --all --json\` and \`docket ready --json\`.
|
|
60
|
+
2. Derive one manager title from those authoritative epic fields, exactly \`Epic <ID> — <title>\`, and retain it for the entire supervision run. Ask the current harness's native adapter to apply it to the calling manager session. Unsupported, unavailable, or failed rename capability is a silent no-op; it never blocks supervision.
|
|
61
|
+
3. Record the manager baseline: current Git commit and branch, working-tree state, epic status and acceptance criteria, every child status and dependency, already-linked task commits, and the stopping condition. Preserve unrelated user changes; do not hide, overwrite, or move them into a worker checkout.
|
|
62
|
+
4. Use the engine's ready result as authoritative. ${ENGINE_SEMANTICS.readiness} ${ENGINE_SEMANTICS.readyOrdering} Filter that result to the named epic; never dispatch from a remembered or hand-derived ready list.
|
|
63
|
+
5. If no child is ready but unfinished children remain, inspect their dependency and blocked-state evidence. Continue only when Docket state identifies a resolvable in-scope next action; otherwise prepare the blocker receipt in section 6.
|
|
64
|
+
|
|
65
|
+
## 2. Preflight isolation and likely write overlap
|
|
66
|
+
|
|
67
|
+
Before creating any worker, inspect each ready child's context, acceptance criteria, linked concepts, and likely implementation/test/generated-document surfaces. Parallel writing is allowed only when every selected child is dependency-independent, likely write sets are materially distinct, each worker has a separate checkout at the exact accepted manager ref, and the manager can integrate and verify results one at a time. Treat shared workflow templates, generated adapters, dependency manifests, schemas, migrations, indexes, and central registries as likely overlap unless evidence shows otherwise.
|
|
68
|
+
|
|
69
|
+
If any condition is unknown or false—or if the host lacks a verified worker, wait/follow-up, notification, or isolated-checkout binding—use the mandatory serial fallback: run exactly one child at a time in the calling session or one isolated worker, integrate it fully, refresh Docket state, and only then choose the next child. Never run concurrent writers in one checkout. A shared \`.docket/active-task\` is single-checkout state, not a coordination mechanism.
|
|
70
|
+
|
|
71
|
+
## 3. Dispatch one bounded child contract
|
|
72
|
+
|
|
73
|
+
For each selected child, provide the exact task ID, accepted baseline commit, isolated checkout or serial location, permitted scope, acceptance criteria, relevant linked concepts, expected verification, and these constraints:
|
|
74
|
+
|
|
75
|
+
- follow [the pickup workflow](/workflows/docket-pickup.md) before implementation and [the close workflow](/workflows/docket-close.md) only after the task is actually complete;
|
|
76
|
+
- change only the named child and required reconciliation surfaces; do not start siblings, close the epic, or invent orchestration infrastructure;
|
|
77
|
+
- preserve unrelated changes, use task-linked commits, clear the checkout's active-task marker after close, and return commit hashes, verification results, interventions, and exact blockers;
|
|
78
|
+
- do not claim integration or readiness changes from the worker checkout—the manager re-establishes those facts after accepting the result.
|
|
79
|
+
|
|
80
|
+
When no native worker binding is available, execute this same contract serially in the calling session. The contract, not process count, defines supervision.
|
|
81
|
+
|
|
82
|
+
An isolated child session follows pickup normally and keeps its own \`<ID> — <title>\` task name; never apply the manager title to that child. In the serial fallback, child pickup can temporarily rename the shared calling session, so immediately after every successful child pickup reapply the retained \`Epic <ID> — <title>\` manager title before implementation continues. A failed or unsupported restoration remains a silent no-op and does not change task state or the child contract.
|
|
83
|
+
|
|
84
|
+
## 4. Inspect and integrate one result at a time
|
|
85
|
+
|
|
86
|
+
1. Treat a worker report as a lead, not authority. Inspect its checkout or ref, diff, task file, checked or explicitly waived criteria, Outcome, Log, commit trailers, verification output, and clean active-task state.
|
|
87
|
+
2. Reject or return incomplete, out-of-scope, unverified, or ambiguously based work. Keep the branch/worktree/ref recoverable and state the required correction. Never mark the child done merely because the worker said it finished.
|
|
88
|
+
3. Integrate one accepted commit series into the manager checkout. Resolve only understood in-scope conflicts; otherwise stop integration, preserve both refs and the conflict evidence, and produce a blocker receipt. Do not integrate a second result against unresolved or unverified state.
|
|
89
|
+
4. Run the verification proportionate to the accepted diff, regenerate derived state with \`docket index\`, then rerun \`docket task list --epic <EPIC-ID> --all --json\` and \`docket ready --json\`. Re-read the epic and Git history. Select further work only from this refreshed state.
|
|
90
|
+
5. At every accepted boundary, durable task files plus integrated Git commits must be sufficient for a replacement manager to resume. Native task IDs and wait cursors are useful transient handles, never the recovery source of truth.
|
|
91
|
+
|
|
92
|
+
## 5. Review and close the epic explicitly
|
|
93
|
+
|
|
94
|
+
All children being done is necessary evidence, not epic completion. When no unfinished child remains, review every epic acceptance criterion against integrated task Outcomes, diffs, tests, decisions, and reconciled docs. Run final repository verification. If any criterion lacks evidence, create or identify the smallest in-scope follow-on child and continue; do not check or waive a criterion silently.
|
|
95
|
+
|
|
96
|
+
When every criterion is satisfied or explicitly waived with a reason, apply the close workflow to the epic itself: write its Outcome with commit evidence, reconcile affected concepts, close through the engine, regenerate the index, update the log, and commit with the epic's task trailer. Verify the integrated epic status rather than inferring it from the close command's prose.
|
|
97
|
+
|
|
98
|
+
## 6. Return one consolidated receipt
|
|
99
|
+
|
|
100
|
+
Return only after verified epic closure or a concrete blocker. Before returning, ask the native adapter to reapply the retained manager title once so the calling session ends on the epic rather than incidental child work; unsupported or failed rename remains a silent no-op. A completion receipt names the epic, integrated child and epic commits, verification performed, serial-versus-parallel choice and why, interventions or conflicts, and any deliberately deferred follow-up. A blocker receipt names the exact failing child or epic criterion, dependency/decision/error, last accepted manager commit, preserved worker refs or worktrees, current Docket state, checks already attempted, and the single action needed to resume.
|
|
101
|
+
|
|
102
|
+
Do not create an orchestration database, scheduler, permanent runner, or synthetic epic status. On interruption, restart this workflow from section 1: Docket and Git reveal completed children and the next authoritative ready set; absent native lifecycle state simply selects the serial fallback.`,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
slug: "docket-task",
|
|
106
|
+
title: "Create a work item",
|
|
107
|
+
intent: "task-management",
|
|
108
|
+
description:
|
|
109
|
+
"Create a task, epic, or decision as a conformant OKF concept file — ID generation, template, links, index update.",
|
|
110
|
+
body: `Create a work item conformant with the OKF task profile (bundled at \`specs/okf-task-profile.md\` when the repo carries it). The request describes the item ("task: add X to Y, epic phase-1, depends on KEY-8").
|
|
111
|
+
|
|
112
|
+
**Prefer the engine**: \`docket task create --title "…" --epic /work/epics/… --deps KEY-x,KEY-y --priority p1 --description "…"\` handles ID assignment, file placement, and a conformant template. Then edit the created file to fill in real \`# Context\` links and \`# Acceptance Criteria\`, and run \`docket index\`. The manual steps below are the fallback when the engine is unavailable.
|
|
113
|
+
|
|
114
|
+
1. **Assign the ID**: work items (tasks AND epics) take the next number in the project sequence under the key from \`docket.yaml\` — \`grep -rh "^id: KEY-" <bundle>/\`, max + 1. Decisions likewise on their own prefix (default \`DEC-\`). Verify the result is unused.
|
|
115
|
+
2. **Write the file** at \`work/tasks/<ID>-<short-slug>.md\` (epics → \`work/epics/\`, decisions → \`decisions/\`) with frontmatter: \`type\`, \`title\`, \`description\` (one sentence), \`id\`, \`status: todo\`, \`epic\` (bundle-absolute link — ask or infer; a task without an epic is allowed but noted), \`depends_on\` (task IDs, omit if none), \`priority\` (default \`p2\`), \`assignee\`, \`tags\`, \`timestamp\` (current UTC ISO 8601).
|
|
116
|
+
3. **Body**: \`# Context\` — link the relevant specs/docs/decisions (bundle-absolute paths); \`# Acceptance Criteria\` — checkboxes, verifiable, few. Omit \`# Log\` until there's something to log.
|
|
117
|
+
4. **Regenerate the index** (\`docket index\`) and add a \`log.md\` entry when the item is notable.
|
|
118
|
+
5. If work starts now, follow [the pickup workflow](/workflows/docket-pickup.md). It delegates task state and context-packet mechanics to \`docket task start <ID> --json\`; never set the active task without the status move or vice versa. Pausing later is \`docket task stop\` (clears the active task, status stays).
|
|
119
|
+
|
|
120
|
+
Never skip or reuse numbers, never hand-maintain task lists inside epic files, never mark \`status\` beyond \`todo\` at creation.`,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
slug: "docket-groom",
|
|
124
|
+
title: "Groom the backlog",
|
|
125
|
+
intent: "backlog-hygiene",
|
|
126
|
+
description: DOCKET_INTENTS["backlog-hygiene"].discovery,
|
|
127
|
+
body: `Run this full backlog-hygiene audit only when the user explicitly asks to groom or audit the backlog, find stale or inconsistent work, or review task hygiene. Ordinary status, orientation, what-is-next, and review requests use \`docket overview --json\` instead and stop when that structured response is sufficient.
|
|
128
|
+
|
|
129
|
+
Read every file in \`work/\` and report, then apply agreed fixes. Start from the engine: \`docket ready --json\` and \`docket task list --json\`.
|
|
130
|
+
|
|
131
|
+
${ENGINE_SEMANTICS.mutationOwnership.grooming}
|
|
132
|
+
|
|
133
|
+
1. **Derive ready**: \`docket ready\` (never compute by hand). ${ENGINE_SEMANTICS.readiness} ${ENGINE_SEMANTICS.readyOrdering}
|
|
134
|
+
2. **Flag inconsistencies**:
|
|
135
|
+
- \`in-progress\` tasks with no commits trailer-matching their ID (\`git log --grep "Task: <ID>"\`) and no Log entry in 7+ days → probably stalled; propose \`blocked\` or \`todo\`.
|
|
136
|
+
- \`done\` tasks with unchecked acceptance criteria or missing \`# Outcome\`.
|
|
137
|
+
- \`closed\` tasks without a concrete \`# Disposition\` and replacement links when applicable.
|
|
138
|
+
- \`depends_on\` pointing at nonexistent or done-and-superseded IDs; broken bundle links (\`docket lint\`).
|
|
139
|
+
- Epics without a \`spec\` link; tasks without an \`epic\` link.
|
|
140
|
+
- \`index.md\` out of sync (\`docket index\` fixes; report if it changes anything).
|
|
141
|
+
3. **Propose, then apply**: present findings compactly; on confirmation (or when running autonomously, for mechanical fixes only) update files via \`docket task move\`/\`docket task log\`, regenerate the index, and add a \`**YYYY-MM-DD**\` line to affected \`# Log\` sections explaining status changes.
|
|
142
|
+
4. Commit as \`chore(docket): groom backlog\` (no task trailer — \`docket task stop\` first).
|
|
143
|
+
|
|
144
|
+
Never change priorities or close tasks without saying so; grooming narrates every mutation.`,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
slug: "docket-close",
|
|
148
|
+
title: "Conclude a task",
|
|
149
|
+
intent: "task-management",
|
|
150
|
+
description:
|
|
151
|
+
"Conclude a task as completed or explicitly closed without completion — narrative, doc reconciliation, index/log updates.",
|
|
152
|
+
body: `Conclude the given task (default: the ID in \`.docket/active-task\`). A terminal move is the moment the wiki gets paid — don't skip steps.
|
|
153
|
+
|
|
154
|
+
${ENGINE_SEMANTICS.transitions}
|
|
155
|
+
|
|
156
|
+
${ENGINE_SEMANTICS.mutationOwnership.close}
|
|
157
|
+
|
|
158
|
+
1. **Choose the terminal meaning explicitly**. Completion is the backward-compatible default: every acceptance criterion is checked (or explicitly waived in the Outcome with a reason), and the target state is \`done\`. Use non-completion only when the user explicitly intends to abandon, decline, supersede, or otherwise discontinue the work; leave unmet criteria unchecked, target \`closed\`, and require a concrete disposition reason. If neither meaning is supported, say so and stop.
|
|
159
|
+
2. **Write the terminal narrative**. For completion, write \`# Outcome\`: what actually shipped, citing commit hashes found via \`git log --grep "Task: <ID>" --oneline\` plus the task file's history, with anything descoped or discovered. For non-completion, write \`# Disposition\`: why the work ended, what remains unmet, and any replacement task or decision links; do not claim that work shipped.
|
|
160
|
+
3. **Reconcile the docs** (the LLM-first step): from the task diff and terminal narrative, identify wiki concepts (\`specs/\`, \`reference/\`, \`decisions/\`, plan documents) the conclusion invalidates or extends. Update them now. If a choice foreclosed alternatives, record it as a \`type: Decision\` concept and link it from the Outcome or Disposition.
|
|
161
|
+
4. **Update state**: for completion, run \`docket task close <ID> --note "…"\`; for non-completion, run \`docket task close <ID> --without-completion --note "<disposition>"\`. Then run \`docket index\`, add a \`log.md\` entry that says completed or closed, and check dependency and epic effects. Only \`done\` unblocks dependents or counts toward epic completion; a terminal epic may be \`closed\` without all children being done.
|
|
162
|
+
5. **Commit everything together** — task file + reconciled docs + index/log — with the \`Task: <ID>\` trailer (keep the task active so the hook injects it, or add it manually), then \`docket task stop\` to clear the active task.
|
|
163
|
+
|
|
164
|
+
The commit that concludes a task must contain the doc reconciliation — that's the product's core promise.`,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
slug: "docket-standup",
|
|
168
|
+
title: "Status report",
|
|
169
|
+
intent: "project-maintenance",
|
|
170
|
+
description:
|
|
171
|
+
"Read-only status report from the bundle and git history — done since last report, in flight, ready next, blocked.",
|
|
172
|
+
body: `Report project status from files + git. **Mutate nothing.** Pull state from the engine (\`docket task list --json\`, \`docket ready --json\`); use git for the activity window.
|
|
173
|
+
|
|
174
|
+
1. **Window**: since the last standup or the range given (default: 7 days).
|
|
175
|
+
2. **Done**: tasks whose status flipped to \`done\` in the window — from \`git log -p --since=<window> -- <bundle>/work/tasks/\` (status line changes) — one line each: ID, title, outcome gist.
|
|
176
|
+
3. **Closed without completion**: tasks whose status flipped to \`closed\` in the window — one line each: ID, title, and disposition; keep them separate from shipped work.
|
|
177
|
+
4. **In flight**: \`in-progress\` tasks with their latest Log entry and commit count from \`git log --grep "Task: <ID>" --since=<window>\`. Call out any with zero commits and no Log movement.
|
|
178
|
+
5. **Ready next**: derived ready list (\`docket ready\`), top 5. ${ENGINE_SEMANTICS.readiness} ${ENGINE_SEMANTICS.readyOrdering}
|
|
179
|
+
6. **Blocked**: \`blocked\` tasks with the blocking reason from their Log.
|
|
180
|
+
7. **Epic pulse**: one line per active epic — fraction of its tasks done, with closed children called out separately (derive by grep, don't trust hand-maintained lists).
|
|
181
|
+
|
|
182
|
+
Output: compact markdown suitable for pasting into a chat. Flag (don't fix) any inconsistencies noticed along the way — fixing belongs to [docket-groom](/workflows/docket-groom.md).`,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
slug: "docket-state-of-play",
|
|
186
|
+
title: "Refresh product context",
|
|
187
|
+
intent: "project-maintenance",
|
|
188
|
+
description:
|
|
189
|
+
"Refresh the linked project re-entry note — recent outcomes, the current frontier, and context worth remembering.",
|
|
190
|
+
body: `Refresh the optional bundle-root \`overview.md\` re-entry note. The engine parses, ages, and renders this authored summary but never writes it; live task status, readiness, progress, and activity stay in the derived overview.
|
|
191
|
+
|
|
192
|
+
1. **Read the evidence**: run \`docket overview --json\`; read the product spec, current epics and tasks, recent Outcomes, explicit Decision concepts, \`log.md\`, recent task-linked commits, and the existing \`overview.md\` when present. Treat the derived overview as execution truth and the product spec/decisions as direction truth.
|
|
193
|
+
2. **Write only the re-entry through-line**: summarize a few recent outcomes rather than commits, then name the one or few current/next epics or frontiers—including work already underway—with enough context to understand the move. Put the canonical resume target first when one exists; multiple real frontiers remain multiple authored links rather than an engine-selected winner. Add Worth knowing only for a decision, constraint, discovery, risk, parked thread, or useful wiki destination that materially helps re-entry. Use concrete nouns and consequences, link claims to bundle evidence, and omit empty material instead of writing filler. The preserved project preamble owns the recognizable full name, concise purpose, and other durable product introduction; do not repeat it here, and do not infer missing identity. Repeat a derived fact only when it explains why something matters, never to copy an inventory.
|
|
194
|
+
3. **Write the linked note**: use the full output of \`git rev-parse HEAD\` as \`as_of\` and the current UTC ISO-8601 time as \`reviewed_at\`. What we've done recently and What's up next are required and non-empty. Worth knowing is optional; omit the heading when it would be empty.
|
|
195
|
+
|
|
196
|
+
\`\`\`markdown
|
|
197
|
+
---
|
|
198
|
+
format: re-entry/v2
|
|
199
|
+
as_of: <full commit sha>
|
|
200
|
+
reviewed_at: <timestamp>
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
# Project re-entry
|
|
204
|
+
|
|
205
|
+
## What we've done recently
|
|
206
|
+
|
|
207
|
+
- <outcome and consequence with a link to evidence>
|
|
208
|
+
|
|
209
|
+
## What's up next
|
|
210
|
+
|
|
211
|
+
- <current or next frontier and why it matters, linked to its epic or task>
|
|
212
|
+
|
|
213
|
+
## Worth knowing
|
|
214
|
+
|
|
215
|
+
- <optional decision, constraint, discovery, risk, or parked thread with a useful link>
|
|
216
|
+
\`\`\`
|
|
217
|
+
|
|
218
|
+
4. **Apply freshness honestly**: five task-linked commits after \`as_of\` or fourteen days after \`reviewed_at\` makes the note need review. Renderers keep the visibly dated last-known context readable rather than hiding it or presenting it as fresh. Refresh when the re-entry through-line materially changes, not merely to reset a clock. After a task close that changes the note, stamp the close commit in a separate tracker-only refresh so it starts at zero task-linked commits behind.
|
|
219
|
+
5. **Verify and commit**: run \`docket overview\` and \`docket lint\`; confirm the linked sections and freshness are accurate. Commit as \`chore(docket): refresh product context\` with no Task trailer (\`docket task stop\` first).
|
|
220
|
+
|
|
221
|
+
A missing \`overview.md\` is valid and renders no placeholder. Earlier formats remain readable and unchanged, but renderers label legacy prose and \`re-entry/v1\` as needing review. Never migrate them automatically; the next meaningful refresh replaces the file with the linked form above.`,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
slug: "docket-freshness",
|
|
225
|
+
title: "Doc-freshness sweep",
|
|
226
|
+
intent: "project-maintenance",
|
|
227
|
+
description:
|
|
228
|
+
"Retrospective doc-freshness review — sweep commits since the last watermark, catch wiki drift that close-time reconciliation missed, stamp a new watermark.",
|
|
229
|
+
body: `Close-time reconciliation is prospective — it fires only when a task closes, and only for that task's diff. This workflow is the retrospective complement: periodically re-ask "what does this invalidate?" across everything that happened since the last sweep.
|
|
230
|
+
|
|
231
|
+
1. **Find the anchor**: the most recent \`**Freshness**\` entry in \`log.md\` holds the watermark sha. If none exists (first run), sweep the full history.
|
|
232
|
+
2. **Collect the range**: \`git log <sha>..HEAD --name-only\` (keep trailers). Partition the commits:
|
|
233
|
+
- **Trailerless** — the high-risk bucket: nobody ever asked the reconciliation question. Give each the full treatment: from its changed paths, which concepts (\`specs/\`, \`reference/\`, \`decisions/\`, plan documents) does it invalidate or extend?
|
|
234
|
+
- **Trailered** (\`Task: KEY-n\`) — reconciliation should have happened at close. Spot-check: did closes that plausibly invalidated docs actually touch them?
|
|
235
|
+
3. **Rotate a deep read**: pick the 1–2 concepts in \`specs/\` and \`reference/\` with the oldest last-modified commit and verify their content against current reality (code, plan). This catches drift that has no local commit at all — don't skip it just because the commit range is clean.
|
|
236
|
+
4. **Propose, then apply**: present findings compactly (per doc: what's stale, which commit made it so). On confirmation — or autonomously for unambiguous factual fixes only — update the docs.
|
|
237
|
+
5. **Stamp the watermark**: append to today's section of \`log.md\`:
|
|
238
|
+
|
|
239
|
+
\`\`\`
|
|
240
|
+
- **Freshness** — reviewed through \`<short-sha of HEAD>\` (<n> commits, <k> trailerless): <one-line findings summary, or "no drift found">.
|
|
241
|
+
\`\`\`
|
|
242
|
+
|
|
243
|
+
A "no drift found" stamp is a real result — record it; the recorded null finding is what makes the next sweep cheap.
|
|
244
|
+
6. Commit doc fixes and the watermark together as \`chore(docket): freshness review\` (\`docket task stop\` first — no task trailer).
|
|
245
|
+
|
|
246
|
+
Never end a sweep without stamping the watermark, even when nothing changed.`,
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
export type WorkflowSemantic =
|
|
251
|
+
| "readiness"
|
|
252
|
+
| "ready-ordering"
|
|
253
|
+
| "state-transitions"
|
|
254
|
+
| "mutation-ownership";
|
|
255
|
+
|
|
256
|
+
export interface WorkflowSemanticDiagnostic {
|
|
257
|
+
slug: string;
|
|
258
|
+
semantic: WorkflowSemantic;
|
|
259
|
+
message: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const SEMANTIC_REQUIREMENTS: Readonly<
|
|
263
|
+
Record<string, readonly { semantic: WorkflowSemantic; claim: string }[]>
|
|
264
|
+
> = {
|
|
265
|
+
"docket-pickup": [
|
|
266
|
+
{ semantic: "state-transitions", claim: ENGINE_SEMANTICS.transitions },
|
|
267
|
+
{
|
|
268
|
+
semantic: "mutation-ownership",
|
|
269
|
+
claim: ENGINE_SEMANTICS.mutationOwnership.pickup,
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
"docket-epic": [
|
|
273
|
+
{ semantic: "readiness", claim: ENGINE_SEMANTICS.readiness },
|
|
274
|
+
{ semantic: "ready-ordering", claim: ENGINE_SEMANTICS.readyOrdering },
|
|
275
|
+
],
|
|
276
|
+
"docket-groom": [
|
|
277
|
+
{ semantic: "readiness", claim: ENGINE_SEMANTICS.readiness },
|
|
278
|
+
{ semantic: "ready-ordering", claim: ENGINE_SEMANTICS.readyOrdering },
|
|
279
|
+
{
|
|
280
|
+
semantic: "mutation-ownership",
|
|
281
|
+
claim: ENGINE_SEMANTICS.mutationOwnership.grooming,
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
"docket-close": [
|
|
285
|
+
{ semantic: "state-transitions", claim: ENGINE_SEMANTICS.transitions },
|
|
286
|
+
{
|
|
287
|
+
semantic: "mutation-ownership",
|
|
288
|
+
claim: ENGINE_SEMANTICS.mutationOwnership.close,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
"docket-standup": [
|
|
292
|
+
{ semantic: "readiness", claim: ENGINE_SEMANTICS.readiness },
|
|
293
|
+
{ semantic: "ready-ordering", claim: ENGINE_SEMANTICS.readyOrdering },
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const CONTRADICTORY_CLAIMS: readonly {
|
|
298
|
+
semantic: WorkflowSemantic;
|
|
299
|
+
pattern: RegExp;
|
|
300
|
+
label: string;
|
|
301
|
+
}[] = [
|
|
302
|
+
{
|
|
303
|
+
semantic: "ready-ordering",
|
|
304
|
+
pattern: /dependency depth/i,
|
|
305
|
+
label: "dependency depth does not order the ready queue",
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
semantic: "ready-ordering",
|
|
309
|
+
pattern: /(?:ready list|ready queue)[^\n.]*priority[- ]ordered/i,
|
|
310
|
+
label: "priority alone does not order the ready queue",
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
semantic: "readiness",
|
|
314
|
+
pattern: /ready (?:is|means) (?:a )?stored status/i,
|
|
315
|
+
label: "ready is derived rather than stored",
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
semantic: "state-transitions",
|
|
319
|
+
pattern: /(?:workflow|adapter) owns (?:the )?status transition/i,
|
|
320
|
+
label: "the engine owns status transitions",
|
|
321
|
+
},
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Release guard for workflow claims that mirror engine behavior. Requirements
|
|
326
|
+
* make omission loud; contradiction checks catch the known classes of drift.
|
|
327
|
+
*/
|
|
328
|
+
export function validateWorkflowSemantics(
|
|
329
|
+
workflows: readonly Pick<WorkflowDef, "slug" | "body">[],
|
|
330
|
+
): WorkflowSemanticDiagnostic[] {
|
|
331
|
+
const diagnostics: WorkflowSemanticDiagnostic[] = [];
|
|
332
|
+
for (const workflow of workflows) {
|
|
333
|
+
for (const requirement of SEMANTIC_REQUIREMENTS[workflow.slug] ?? []) {
|
|
334
|
+
if (!workflow.body.includes(requirement.claim)) {
|
|
335
|
+
diagnostics.push({
|
|
336
|
+
slug: workflow.slug,
|
|
337
|
+
semantic: requirement.semantic,
|
|
338
|
+
message: `missing canonical ${requirement.semantic} claim`,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
for (const contradiction of CONTRADICTORY_CLAIMS) {
|
|
343
|
+
if (contradiction.pattern.test(workflow.body)) {
|
|
344
|
+
diagnostics.push({
|
|
345
|
+
slug: workflow.slug,
|
|
346
|
+
semantic: contradiction.semantic,
|
|
347
|
+
message: contradiction.label,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return diagnostics;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Render a workflow as a bundle concept file. `origin` records provenance
|
|
357
|
+
* which shipped text this copy descends from, so a later
|
|
358
|
+
* `docket upgrade` can 3-way merge against that base. Unknown field to OKF
|
|
359
|
+
* consumers — tolerated, never required.
|
|
360
|
+
*/
|
|
361
|
+
export function renderWorkflow(w: WorkflowDef, timestamp: string): string {
|
|
362
|
+
return `---
|
|
363
|
+
type: Workflow
|
|
364
|
+
title: ${w.title}
|
|
365
|
+
description: ${w.description}
|
|
366
|
+
origin: ${w.slug}@${DOCKET_VERSION}
|
|
367
|
+
tags: [docket, workflow]
|
|
368
|
+
timestamp: ${timestamp}
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
${w.body}
|
|
372
|
+
`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Marks a file as a generated adapter: init may overwrite anything carrying
|
|
377
|
+
* it, and skips (never clobbers) anything without it. Carries the engine
|
|
378
|
+
* version so upgrade can report current → available.
|
|
379
|
+
*/
|
|
380
|
+
export const ADAPTER_MARKER = `<!-- generated by docket init@${DOCKET_VERSION} — edits are overwritten; the workflow in the bundle is the source of truth -->`;
|
|
381
|
+
|
|
382
|
+
/** True when the text carries an adapter marker, versioned or in the legacy unversioned form. */
|
|
383
|
+
export const hasAdapterMarker = (text: string): boolean =>
|
|
384
|
+
text.includes("<!-- generated by docket init");
|
|
385
|
+
|
|
386
|
+
/** Harness skill stub: trigger surface plus any bounded native capability binding. */
|
|
387
|
+
export function renderAgentSkillStub(
|
|
388
|
+
w: WorkflowDef,
|
|
389
|
+
bundle: string,
|
|
390
|
+
nativeBinding?: string,
|
|
391
|
+
): string {
|
|
392
|
+
const dir = bundle.endsWith("/") ? bundle : `${bundle}/`;
|
|
393
|
+
const binding = (() => {
|
|
394
|
+
if (w.slug === "docket-pickup")
|
|
395
|
+
return `
|
|
396
|
+
|
|
397
|
+
## Native current-session rename binding
|
|
398
|
+
|
|
399
|
+
${nativeBinding ?? "This target declares current-session rename unsupported. Skip the optional rename without warning and continue the canonical workflow."}`;
|
|
400
|
+
if (w.slug === "docket-epic")
|
|
401
|
+
return `
|
|
402
|
+
|
|
403
|
+
## Native epic-supervision lifecycle binding
|
|
404
|
+
|
|
405
|
+
${nativeBinding ?? "This target declares current-session rename unsupported and has no verified native worker lifecycle binding. Skip manager-title application and restoration without warning, run the canonical workflow serially in the calling session, and do not infer worker creation, concurrent writing, waiting, follow-up, notification, or isolated-checkout support."}`;
|
|
406
|
+
return "";
|
|
407
|
+
})();
|
|
408
|
+
return `---
|
|
409
|
+
name: ${w.slug}
|
|
410
|
+
description: ${w.description}
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
${ADAPTER_MARKER}
|
|
414
|
+
|
|
415
|
+
Read \`${dir}${workflowPath(w)}\` and execute its steps against this repo's bundle. That file is the source of truth; this skill only routes to it.${binding}
|
|
416
|
+
`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Compatibility name for callers written before agent targets were generic. */
|
|
420
|
+
export const renderClaudeSkillStub = renderAgentSkillStub;
|
|
421
|
+
|
|
422
|
+
const SECTION_BEGIN = `<!-- >>> docket@${DOCKET_VERSION} >>> -->`;
|
|
423
|
+
const SECTION_END = "<!-- <<< docket <<< -->";
|
|
424
|
+
// Matches the begin marker at any version, and the legacy unversioned form.
|
|
425
|
+
const SECTION_BEGIN_RE = /<!-- >>> docket(@\S+)? >>> -->/;
|
|
426
|
+
|
|
427
|
+
/** True when the text carries a docket section span (any marker version). */
|
|
428
|
+
export const hasDocketSection = (text: string): boolean =>
|
|
429
|
+
SECTION_BEGIN_RE.test(text) && text.includes(SECTION_END);
|
|
430
|
+
|
|
431
|
+
/** The shared CLAUDE.md/AGENTS.md section pointing agents at engine + workflows. */
|
|
432
|
+
export function renderDocketSection(project: string, bundle: string): string {
|
|
433
|
+
const dir = bundle.endsWith("/") ? bundle : `${bundle}/`;
|
|
434
|
+
const orientation = DOCKET_INTENTS.orientation;
|
|
435
|
+
const list = DOCKET_WORKFLOWS.map(
|
|
436
|
+
(w) => `- \`${dir}${workflowPath(w)}\` — ${w.description}`,
|
|
437
|
+
).join("\n");
|
|
438
|
+
return `${SECTION_BEGIN}
|
|
439
|
+
## Docket
|
|
440
|
+
|
|
441
|
+
This repo tracks docs and work with Docket: every doc and work item is a markdown concept in \`${dir}\` (one link graph). Files are the source of truth; commits link to tasks via \`Task: ${project}-<n>\` trailers.
|
|
442
|
+
|
|
443
|
+
**Engine** — the \`docket\` CLI is the write path: \`ready\`, \`overview\`, \`search\`, \`task list|create|start|stop|move|edit|close|log\`, \`lint\`, \`index\`, \`upgrade\` (all support \`--json\`). Use it for mechanics; never hand-edit status fields or the generated \`index.md\` body.
|
|
444
|
+
|
|
445
|
+
**Orientation** — for “what's next,” status, orientation, or an ordinary review, run \`${orientation.defaultEntrypoint.value}\`. This path is read-only and bounded: start with its structured result, follow bundle links only when the requested explanation needs more evidence, and do not start a task, regenerate the index, invoke a mutating workflow, or search unrelated implementation and fixture content when the overview is sufficient. Native skills are optional: without one, run the CLI command directly; an MCP-only client calls the read-only \`overview\` tool, which returns the same model and selection.
|
|
446
|
+
|
|
447
|
+
**Workflows** — the judgment procedures live in the bundle; read the file and follow it:
|
|
448
|
+
|
|
449
|
+
${list}
|
|
450
|
+
|
|
451
|
+
**Direct and tracked work** — a concrete direct request proceeds in the user's stated scope without creating, starting, selecting, or adopting Docket work; generic implementation language is not pickup authority. Use the \`docket-pickup\` workflow only when the user supplies a Docket ID, an unambiguous reference to an existing tracked item, or an explicit request to select the next Docket/backlog item. Resolve a tracked reference to its ID before starting it. If the reference remains ambiguous, perform a focused resolution or ask for clarification; never fall back to an unrelated top-ready item. Bare \`docket task start --json\` is permitted only for explicit next-Docket-task or backlog selection. Once pickup is authorized, the engine sets \`.docket/active-task\`, moves the selected task to \`in-progress\`, and returns the context packet plus one canonical \`suggestedSessionTitle\`; an installed native adapter may apply that title to the calling session on a best-effort basis. Unsupported hosts continue normally. Pause tracked work with \`docket task stop\` (clears the active task, status stays); conclude through \`docket task close\` + the close workflow. The command completes to \`done\` by default; \`--without-completion --note "<reason>"\` explicitly records \`closed\` instead.
|
|
452
|
+
|
|
453
|
+
**Epic supervision** — when the user explicitly asks to run or supervise a named epic through completion, follow the \`docket-epic\` workflow. Native worker, wait/follow-up, notification, and isolated-checkout bindings are optional; without a verified binding, execute its mandatory serial path in the calling session. Docket files and task-linked Git history remain authoritative across interruption.
|
|
454
|
+
${SECTION_END}
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Compose the docket section into an agent-instructions file (CLAUDE.md,
|
|
460
|
+
* AGENTS.md). Missing file → create; no markers → append; markers present →
|
|
461
|
+
* regenerate just the marked span, preserving everything around it.
|
|
462
|
+
*/
|
|
463
|
+
export function composeManagedSection(
|
|
464
|
+
existing: string | undefined,
|
|
465
|
+
section: string,
|
|
466
|
+
): InitResult {
|
|
467
|
+
if (existing === undefined) return { action: "create", content: section };
|
|
468
|
+
const begin = existing.match(SECTION_BEGIN_RE)?.index ?? -1;
|
|
469
|
+
const end = existing.indexOf(SECTION_END);
|
|
470
|
+
if (begin >= 0 && end > begin) {
|
|
471
|
+
const next =
|
|
472
|
+
existing.slice(0, begin) +
|
|
473
|
+
section.trimEnd() +
|
|
474
|
+
existing.slice(end + SECTION_END.length);
|
|
475
|
+
return next === existing
|
|
476
|
+
? { action: "skip", content: existing, reason: "up to date" }
|
|
477
|
+
: { action: "update", content: next };
|
|
478
|
+
}
|
|
479
|
+
const base = existing.endsWith("\n") ? existing : `${existing}\n`;
|
|
480
|
+
return { action: "update", content: `${base}\n${section}` };
|
|
481
|
+
}
|