@lab43/q 0.5.0 → 0.6.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.
Files changed (48) hide show
  1. package/README.md +66 -41
  2. package/package.json +7 -4
  3. package/{.claude-plugin → q-extension/.claude-plugin}/plugin.json +1 -1
  4. package/q-extension/agents/adversarial-reviewer.md +98 -0
  5. package/q-extension/conventions/conventions.md +49 -0
  6. package/q-extension/conventions/documentation.md +79 -0
  7. package/q-extension/conventions/extensions.md +70 -0
  8. package/q-extension/conventions/issue-tracking.md +56 -0
  9. package/{conventions → q-extension/conventions}/plans.md +7 -3
  10. package/{conventions → q-extension/conventions}/principles.md +2 -0
  11. package/{conventions → q-extension/conventions}/pull-requests.md +1 -1
  12. package/q-extension/conventions/specs.md +31 -0
  13. package/q-extension/hooks/locked-version.mjs +169 -0
  14. package/q-extension/hooks/session-start.mjs +155 -0
  15. package/{hooks → q-extension/hooks}/session-start.sh +9 -8
  16. package/{references → q-extension/references}/agent-briefing.md +20 -9
  17. package/q-extension/references/enforce-declarations.md +16 -0
  18. package/q-extension/references/q-state.md +31 -0
  19. package/{references → q-extension/references}/run-contract.md +8 -5
  20. package/{skills → q-extension/skills}/address-feedback/SKILL.md +15 -11
  21. package/q-extension/skills/clean-worktrees/SKILL.md +66 -0
  22. package/{skills → q-extension/skills}/create-plan/SKILL.md +3 -3
  23. package/q-extension/skills/groom-docs/SKILL.md +65 -0
  24. package/{skills → q-extension/skills}/implement/SKILL.md +3 -3
  25. package/{skills → q-extension/skills}/implement-plan/SKILL.md +5 -5
  26. package/q-extension/skills/install/SKILL.md +136 -0
  27. package/q-extension/skills/reconcile/SKILL.md +110 -0
  28. package/{skills → q-extension/skills}/review/SKILL.md +2 -1
  29. package/{skills → q-extension/skills}/update-docs/SKILL.md +15 -10
  30. package/{skills → q-extension/skills}/upstream/SKILL.md +4 -2
  31. package/.claude-plugin/marketplace.json +0 -6
  32. package/agents/adversarial-reviewer.md +0 -62
  33. package/conventions/documentation.md +0 -102
  34. package/conventions/extensions.md +0 -52
  35. package/conventions/issue-tracking.md +0 -28
  36. package/hooks/session-start.mjs +0 -128
  37. package/references/enforce-pins.md +0 -16
  38. package/references/q-state.md +0 -31
  39. package/skills/groom-docs/SKILL.md +0 -62
  40. package/skills/install/SKILL.md +0 -150
  41. package/skills/sync/SKILL.md +0 -45
  42. package/skills/uninstall-extension/SKILL.md +0 -52
  43. package/skills/update/SKILL.md +0 -76
  44. /package/{conventions → q-extension/conventions}/writing.md +0 -0
  45. /package/{hooks → q-extension/hooks}/hooks.json +0 -0
  46. /package/{skills → q-extension/skills}/drive/SKILL.md +0 -0
  47. /package/{skills → q-extension/skills}/parallelize/SKILL.md +0 -0
  48. /package/{skills → q-extension/skills}/triage/SKILL.md +0 -0
@@ -0,0 +1,56 @@
1
+ # Issue Tracking
2
+
3
+ Rules for working a project's issue tracker — the shared source of work items — from any session. Sign every item you file and every comment you post: "— Claude 🤖" (source: @lab43/q conventions/writing.md, Sign what you post). A project's own tracker conventions win over this doc (source: @lab43/q conventions/conventions.md, Three tiers of conventions), and so does an issue template the tracker carries.
4
+
5
+ ## Respect existing claims
6
+
7
+ An item assigned to someone else, marked in progress or blocked, or with a fix already in review is not available to pick up. A claim that looks stale — long untouched, its PR closed unmerged — may be wrong: surface it to the user rather than working the item or silently passing it by.
8
+
9
+ ## Claim what you work
10
+
11
+ Picking up a tracker item means assigning it to the user and moving it to the tracker's working status — on the user's agreement, asked once per session and carried forward.
12
+
13
+ ## Ask before filing
14
+
15
+ Filing a new tracker item — the follow-up work a session surfaces — happens on the user's agreement, given per item rather than once per session. Ask the session's candidates together, in one batch. Each candidate is one piece of work. A bundled item gets half fixed and stays open. Write each agreed item to the authoring rules (see: Write for whoever picks it up).
16
+
17
+ ## Write for whoever picks it up
18
+
19
+ Whoever reads an item holds none of the context that produced it. Hold its prose to the writing rules (see: @lab43/q conventions/writing.md).
20
+
21
+ Four readers act on an item, each deciding something different:
22
+
23
+ - Whoever ranks the tracker decides whether the work is worth doing, and when.
24
+ - Whoever picks it up decides what to build.
25
+ - Whoever reviews the result decides whether the item is done.
26
+ - Whoever hits the same symptom later decides whether this item is their problem.
27
+
28
+ Name the work the item asks for in the title. The first reader and the last meet the title alone, and decide from it whether to open the item at all.
29
+
30
+ ### Sections
31
+
32
+ Compose the body from these sections, in order. Most items need only Goal. Add another only when it has something a reader needs. A body carrying only a Goal needs no headings. Head each section once the body carries more than one.
33
+
34
+ - **Goal** — what the problem is and why it needs solving. For a bug, the behavior that should hold instead.
35
+ - **Evidence** — what a reader can check: the reproduction, the `file:line`, the rule the behavior contradicts, a link to the error log or dashboard showing it. Paste an error message verbatim as well as linking to it. Whoever hits the same symptom searches for that string. A log link ages out while the item stays open.
36
+ - **Constraints** — what the work must respect: the limits found, the dead ends walked, anything deliberately excluded, an approach already decided (see: State the goal, not the solution).
37
+ - **Callouts** — what the other sections don't hold and whoever picks it up still needs: an approach noticed but not settled, a question the run will have to answer, a link to a related item. Nothing here binds. The run that takes the item rules on all of it. Something nobody would ask about is noise here.
38
+ - **Closed when** — what is true once the item is closed. A closing condition left to guess gets the item closed wrong, or not at all.
39
+
40
+ Cut the story of the session that surfaced the item. It belongs to none of these sections.
41
+
42
+ ### State the goal, not the solution
43
+
44
+ Leave the fix to the run that takes the item. A spelled-out fix is the most perishable thing an item can carry. It was written without the current code in front of the author. The run that picks the item up then reads it as a premise instead of deriving its own, and the work starts from an assumption nobody checked.
45
+
46
+ What was found while writing the item still belongs, stated as a finding rather than a direction (see: Sections). Where the approach was already decided, state it and say what decided it.
47
+
48
+ ### The body is the work as intended
49
+
50
+ While an item sits unclaimed, keep the body currently true. Fold in whatever changes what the item asks for, so nobody reconstructs the ask by reading the thread in order. What someone reported seeing stays as they wrote it. Folding in changes the ask, never the account. Edit the body on the user's agreement, as with claiming and commenting.
51
+
52
+ Once the item is claimed (see: Claim what you work), the body freezes. Everything a run learns from then on goes in a comment. Planning is work like any other, so a `/q:create-plan` run is under the freeze too. The body is the rubric the work is checked against, and a run that edits it moves its own goalposts.
53
+
54
+ ## Work links back
55
+
56
+ The PR addressing an item links to it from the PR body; when the tracker doesn't pick that link up automatically, comment the PR's link on the item — on the user's agreement, asked once per session and carried forward.
@@ -8,7 +8,11 @@ The plan documents the work, not the conversation that produced it: the reader i
8
8
 
9
9
  - Every sentence either directs work or prevents a wrong decision — cut anything else.
10
10
  - State each fact in one section only; other sections reference it, never restate it.
11
- - A reference may run forward when the acting section comes later. Treat each forward reference as evidence to weigh: the plan may be presenting information in the wrong order.
11
+ - A reference may run forward when the acting section comes later. Treat a forward reference from any section but Context as evidence to weigh: the plan may be presenting information in the wrong order.
12
+
13
+ ## Filename
14
+
15
+ A plan is `docs/plans/<date>-<plan-name>.md`. The date is the day the plan was created, as `YYYY-MM-DD`, so the directory lists plans in the order they were written. The plan name is the part after the date. It names the plan everywhere else: the branch that implements it, the invocation that runs it.
12
16
 
13
17
  ## Frontmatter
14
18
 
@@ -21,7 +25,7 @@ The plan documents the work, not the conversation that produced it: the reader i
21
25
  In order. A section with nothing load-bearing to say for this plan is omitted, never padded — and what already binds every session (the conventions docs, the project's standing tooling) goes without saying:
22
26
 
23
27
  - **Goal** — what the plan delivers and why it's worth doing.
24
- - **Context** — the givens the plan inherits rather than chooses: what is true today, with `file:line` references, and any constraints the solution must respect — invariants, compatibility, external realities. A fact the code can't witness states how it was verified — the source read, or the check run — as evidence reviewers audit instead of repeating the verification. A given needs no defending rationale; anything that could have gone another way is a Decision instead.
28
+ - **Context** — the givens the plan inherits rather than chooses: what is true today, with `file:line` references, and any constraints the solution must respect — invariants, compatibility, external realities. The spec sections governing the plan's territory are givens of this kind: cite each by doc and section. Head each group of givens by the tension it holds, never by where the facts were read from. A fact the code can't witness states how it was verified — the source read, or the check run — as evidence reviewers audit instead of repeating the verification. Every given says what it bears on: the consequence it has for this plan, or the Decision or step it constrains. A given needs no defending rationale; anything that could have gone another way is a Decision instead.
25
29
  - **Decisions** — numbered, each carrying its rationale from the discussion (the why, not just the what) and its rejected alternatives inline. Rationale is the shortest argument that keeps a decision from being reopened. Rejected alternatives appear only when a reviewer would plausibly re-propose them.
26
30
  - **Out of scope** — only work a reader would otherwise assume was included, each item marked deferred or declined, with why. Not a parking lot: adjacent ideas the discussion surfaced but the plan doesn't need stay out entirely.
27
31
  - **Phases** — the implementation sequence, per Phases below.
@@ -55,6 +59,6 @@ Deviations discovered while the implementation runs are recorded as amendments
55
59
  - *(deviation: …)* — reality diverged from the text: a step done differently, a decision or given that shifted mid-run. States what happened instead, and what forced it.
56
60
  - *(result: …)* — the outcome of a step the plan could only pose as a question — a check, a measurement, a re-verified fact.
57
61
 
58
- After merge, a plan's body is frozen history: it describes the world at planning time, and readers treat plan age (git history) as the recency signal. How grooming treats plans is the documentation policy's rule (see: @lab43/q conventions/documentation.md, Taxonomy).
62
+ After merge, a plan's body is frozen history: it describes the world at planning time, and readers treat plan age (see: Filename) as the recency signal. How grooming treats plans is the documentation policy's rule (see: @lab43/q conventions/documentation.md, Taxonomy).
59
63
 
60
64
  Rejected: an `archive/` directory for terminal plans (moves break links; status is machine-readable in place) and a `superseded` status (chain-tracking costs more than it returns; a plan implemented then reversed stays `completed` — both plans are accurate history).
@@ -31,6 +31,8 @@ A private note only the agent reads — a memory file, a stored preference — i
31
31
 
32
32
  A comment, in code or in a doc's markup, states what its file cannot show: the constraint, the gotcha, the reason a workaround must stay. The test: does the comment change how the next reader edits this site? If not, cut it.
33
33
 
34
+ An exception marker's reason passes the test (see: @lab43/q conventions/documentation.md, Markers). It stops the next reader pulling the site back into line with a rule it is excused from. A spec marker passes it too: it tells the next reader the site enforces a commitment, which they would otherwise remove as an ordinary check.
35
+
34
36
  What fails the test is the author addressing someone other than that reader, in two shapes:
35
37
 
36
38
  - **Justification** — defending the chosen design, weighing alternatives, narrating how the solution was arrived at. That addresses the reviewer, and it's noise once merged; git history and the PR carry it.
@@ -1,6 +1,6 @@
1
1
  # Pull Requests
2
2
 
3
- Rules for authoring a pull request. Hold its prose to the writing rules (see: @lab43/q conventions/writing.md). Sign the body and every comment or reply on it: "— Claude 🤖" (source: @lab43/q conventions/writing.md, Sign what you post). A project's own PR conventions win over this doc (source: @lab43/q conventions/documentation.md, Three tiers of conventions), and so does a PR template in the repo.
3
+ Rules for authoring a pull request. Hold its prose to the writing rules (see: @lab43/q conventions/writing.md). Sign the body and every comment or reply on it: "— Claude 🤖" (source: @lab43/q conventions/writing.md, Sign what you post). A project's own PR conventions win over this doc (source: @lab43/q conventions/conventions.md, Three tiers of conventions), and so does a PR template in the repo.
4
4
 
5
5
  ## Title
6
6
 
@@ -0,0 +1,31 @@
1
+ # Specs
2
+
3
+ Rules for `docs/specs/` documents: what a spec holds, how it is written, and how the code is held to it. A spec states what the product commits to. Whether a rule is a spec's or a convention's is the taxonomy's call (see: @lab43/q conventions/documentation.md, Taxonomy).
4
+
5
+ ## Format
6
+
7
+ One feature or domain per doc. Every spec opens with a title and an intro stating the feature's purpose, which is the territory the doc commits to. Sections group commitments. A section heading is what a marker cites, so it names the commitment's subject and stays stable across edits. Refine a spec in place. Delete the spec of a retired feature.
8
+
9
+ Specs are the project's own. An extension ships conventions and a plugin, never specs. A library's commitments to the code using it are conventions, and the conventions tier already carries them.
10
+
11
+ ## What a spec holds
12
+
13
+ Every statement is a commitment: what the product must do or must never do. A statement belongs when a future change breaking it should stop for the user's ruling. Cut what fails that test. Rationale sits inline wherever a decision would otherwise be reopened. What does not belong is a description of how the implementation works today, the screens, fields, and internals the code already shows.
14
+
15
+ ## Enforcement
16
+
17
+ Mark the code that enforces a commitment: the validation that rejects the bad input, a guard in the UI, a database constraint, the test that proves the rule. Each carries a spec marker naming the spec and the section, in that file's own comment syntax (see: @lab43/q conventions/documentation.md, Markers). Code that merely relies on the commitment carries nothing. Almost everything downstream relies on a commitment in some way, so marking relying code would put markers everywhere and the count would stop meaning anything. When enforcing and relying are hard to tell apart, ask what happens without the code. Without enforcing code the product can break the commitment. Without relying code it cannot.
18
+
19
+ Rejected: a marker on every site a spec binds. "Bound by" has no edge a reviewer could hold, so the boundary would be litigated on every diff.
20
+
21
+ The marker makes discovery a grep in both directions. To find the specs a change is bound by, grep the changed files and their tests for markers. To find the code a spec amendment must visit, grep the repo for markers naming the amended section. Every marked site moves with the amendment. That grep is the floor of the search, not the whole of it. Enforcing code that missed its marker, and code that relied on the old commitment, are found only by reading the territory. Mark the enforcing code the reading finds.
22
+
23
+ Rejected: a spec naming the files or tests that enforce it. That enumeration rots when code moves. The marker carries the same fact in the one place that moves with the code.
24
+
25
+ A statement stays in the spec after a test holds it. That is a carve-out from the last-rung rule, which deletes prose once a stronger rung holds it (see: @lab43/q conventions/conventions.md, Documentation is the last rung). The spec records the intent behind the test, so the test cannot drift from the commitment without the drift being visible.
26
+
27
+ ## Disagreement
28
+
29
+ A change that contradicts a spec ships with the amendment or does not ship. It has two exits: revert the change, or amend the spec in the same change. Only the user picks which, because amending a spec is a product decision. A run that finds its agreed work contradicts a spec has stepped outside its agreement, and stops for the user's ruling.
30
+
31
+ A plan that changes committed behavior edits the spec in the phase that ships the behavior.
@@ -0,0 +1,169 @@
1
+ // Resolve the version a project's lockfile records for a direct dependency —
2
+ // the anchor of the session-start hook's version checks.
3
+ //
4
+ // Dependency-free scanning, no YAML library: each reader knows just enough of
5
+ // its format to find a direct dependency's version. Malformed input of any
6
+ // kind — a missing file, unparseable text, an absent package, an unexpected
7
+ // shape — returns undefined rather than throwing; the caller decides what
8
+ // silence means, and the hook importing this must never exit on a stack trace.
9
+
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+
13
+ const read = (file) => {
14
+ try {
15
+ return fs.readFileSync(file, "utf8");
16
+ } catch {
17
+ return null;
18
+ }
19
+ };
20
+
21
+ const unquote = (text) => text.replace(/^["']|["']$/g, "");
22
+
23
+ // npm: package-lock.json (lockfileVersion 3) names a direct dependency's
24
+ // version at packages["node_modules/<name>"].version.
25
+ const npmVersion = (text, name) => {
26
+ let lock;
27
+ try {
28
+ lock = JSON.parse(text);
29
+ } catch {
30
+ return undefined;
31
+ }
32
+ const version = lock?.packages?.[`node_modules/${name}`]?.version;
33
+ return typeof version === "string" ? version : undefined;
34
+ };
35
+
36
+ // pnpm: pnpm-lock.yaml (lockfileVersion '9.0') records the root importer's
37
+ // direct dependencies as
38
+ // importers: → .: → dependencies:/devDependencies: → <name>: → version:
39
+ // at two spaces per level. A peer-dependency suffix rides the version, as
40
+ // 7.5.0(peer@1.0.0), and is not part of it.
41
+ const pnpmVersion = (text, name) => {
42
+ let inImporters = false;
43
+ let inRoot = false;
44
+ let inDeps = false;
45
+ let inName = false;
46
+ for (const raw of text.split(/\r?\n/)) {
47
+ const entry = raw.trim();
48
+ if (!entry) continue;
49
+ const indent = raw.length - raw.trimStart().length;
50
+ if (indent === 0) {
51
+ inImporters = entry === "importers:";
52
+ inRoot = inDeps = inName = false;
53
+ } else if (!inImporters) {
54
+ continue;
55
+ } else if (indent === 2) {
56
+ inRoot = entry === ".:";
57
+ inDeps = inName = false;
58
+ } else if (!inRoot) {
59
+ continue;
60
+ } else if (indent === 4) {
61
+ inDeps = entry === "dependencies:" || entry === "devDependencies:";
62
+ inName = false;
63
+ } else if (!inDeps) {
64
+ continue;
65
+ } else if (indent === 6) {
66
+ inName = unquote(entry.replace(/:$/, "")) === name;
67
+ } else if (inName && indent === 8) {
68
+ const match = entry.match(/^version:\s*(.+)$/);
69
+ if (match) return unquote(match[1]).replace(/\(.*/, "");
70
+ }
71
+ }
72
+ return undefined;
73
+ };
74
+
75
+ // yarn, both formats: blocks keyed by comma-separated dependency descriptors.
76
+ // A key quoted whole (berry) and keys quoted per descriptor (classic) both
77
+ // come apart the same way: split on the commas, strip the quote characters
78
+ // each piece kept.
79
+ const yarnBlocks = (text) => {
80
+ const blocks = [];
81
+ let current = null;
82
+ for (const line of text.split(/\r?\n/)) {
83
+ if (/^[^\s#]/.test(line)) {
84
+ current = {
85
+ descriptors: line
86
+ .replace(/:\s*$/, "")
87
+ .split(",")
88
+ .map((descriptor) => unquote(descriptor.trim())),
89
+ lines: [],
90
+ };
91
+ blocks.push(current);
92
+ } else if (line.trim() && current) {
93
+ current.lines.push(line);
94
+ }
95
+ }
96
+ return blocks;
97
+ };
98
+
99
+ const blockVersion = (block, form) => {
100
+ for (const line of block.lines) {
101
+ if (line.length - line.trimStart().length !== 2) continue;
102
+ const match = line.trim().match(form);
103
+ if (match) return unquote(match[1]);
104
+ }
105
+ return undefined;
106
+ };
107
+
108
+ // berry: the block keyed "<root>@workspace:." maps each direct dependency to
109
+ // its specifier, npm:-prefixed; the block keyed "<name>@npm:<specifier>"
110
+ // carries the resolved version.
111
+ const berryVersion = (blocks, name) => {
112
+ const workspace = blocks.find((block) =>
113
+ block.descriptors.some((descriptor) => descriptor.endsWith("@workspace:.")),
114
+ );
115
+ if (!workspace) return undefined;
116
+ let specifier;
117
+ let inDeps = false;
118
+ for (const line of workspace.lines) {
119
+ const entry = line.trim();
120
+ const indent = line.length - line.trimStart().length;
121
+ if (indent === 2) {
122
+ inDeps = entry === "dependencies:" || entry === "devDependencies:";
123
+ } else if (inDeps && indent === 4) {
124
+ const match = entry.match(/^(.+?):\s+(.+)$/);
125
+ if (match && unquote(match[1]) === name) specifier = unquote(match[2]);
126
+ }
127
+ }
128
+ if (typeof specifier !== "string") return undefined;
129
+ const block = blocks.find((b) => b.descriptors.includes(`${name}@${specifier}`));
130
+ return block && blockVersion(block, /^version:\s*(.+)$/);
131
+ };
132
+
133
+ // classic: nothing marks a direct dependency, so the block is found by the
134
+ // specifier the project's package.json records — one descriptor among the
135
+ // comma-separated key.
136
+ const classicVersion = (blocks, name, specifier) => {
137
+ if (typeof specifier !== "string") return undefined;
138
+ const block = blocks.find((b) => b.descriptors.includes(`${name}@${specifier}`));
139
+ return block && blockVersion(block, /^version\s+"([^"]*)"$/);
140
+ };
141
+
142
+ /**
143
+ * The version the lockfile in `projDir` resolves for the direct dependency
144
+ * `name`, or undefined when no lockfile is readable or the package can't be
145
+ * found in it. `specifier` is the dependency's own entry in the project's
146
+ * package.json — only yarn classic needs it (see above). Dispatches on which
147
+ * lockfile exists; a file that exists but misleads settles the answer as
148
+ * undefined rather than falling through to the next format.
149
+ */
150
+ export const lockedVersion = (projDir, name, specifier) => {
151
+ try {
152
+ const npmText = read(path.join(projDir, "package-lock.json"));
153
+ if (npmText !== null) return npmVersion(npmText, name);
154
+ const pnpmText = read(path.join(projDir, "pnpm-lock.yaml"));
155
+ if (pnpmText !== null) return pnpmVersion(pnpmText, name);
156
+ const yarnText = read(path.join(projDir, "yarn.lock"));
157
+ if (yarnText !== null) {
158
+ const blocks = yarnBlocks(yarnText);
159
+ // __metadata's presence is what tells berry from classic — its value
160
+ // moves between yarn releases.
161
+ return blocks.some((block) => block.descriptors.includes("__metadata"))
162
+ ? berryVersion(blocks, name)
163
+ : classicVersion(blocks, name, specifier);
164
+ }
165
+ return undefined;
166
+ } catch {
167
+ return undefined;
168
+ }
169
+ };
@@ -0,0 +1,155 @@
1
+ // Validate the project's q setup and, when it doesn't validate, tell the
2
+ // session to run /q:reconcile. Claude Code loads whatever plugin version is on
3
+ // disk, so drift surfaces only if something checks at session start — no
4
+ // other channel runs every session. The checks anchor on the lockfile and on
5
+ // node_modules — the lockfile is exact whatever the manifest's specifier
6
+ // looks like, and npm moves it even when the specifier stays put: the q copy
7
+ // this session loaded vs the lockfile's @lab43/q, each watermarked package's
8
+ // lockfile version vs its watermark vs its installed version, and the
9
+ // reverse direction — an installed extension with no watermark entry
10
+ // (installed by hand, never indexed).
11
+ //
12
+ // The remedy is uniform — /q:reconcile re-derives the specifics and routes
13
+ // each finding to its remedy — so every failure emits the same message and
14
+ // the script stops at the first one. Two silences are designed. A project
15
+ // that declares no @lab43/q devDependency is not a q project. A dependency
16
+ // with no watermark entry that cannot be read as an extension is not one to
17
+ // report. Fail every other missing or unreadable input like any other
18
+ // invalid state.
19
+
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { lockedVersion } from "./locked-version.mjs";
24
+
25
+ const proj = process.env.CLAUDE_PROJECT_DIR || ".";
26
+ const root =
27
+ process.env.CLAUDE_PLUGIN_ROOT ||
28
+ path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
29
+
30
+ // Keep in sync with the message in session-start.sh (bash can't import it).
31
+ const MESSAGE =
32
+ "The q plugin could not validate this project's q setup, so its conventions and tooling may be stale or broken. Run /q:reconcile to repair it.";
33
+
34
+ const fail = () => {
35
+ console.log(
36
+ JSON.stringify({
37
+ hookSpecificOutput: {
38
+ hookEventName: "SessionStart",
39
+ additionalContext: MESSAGE,
40
+ },
41
+ }),
42
+ );
43
+ process.exit(0);
44
+ };
45
+ const read = (file) => {
46
+ try {
47
+ return fs.readFileSync(file, "utf8");
48
+ } catch {
49
+ return null;
50
+ }
51
+ };
52
+ const parse = (text) => {
53
+ try {
54
+ return JSON.parse(text);
55
+ } catch {
56
+ return undefined;
57
+ }
58
+ };
59
+ const installedVersion = (dir) => parse(read(path.join(dir, "package.json")) ?? "")?.version;
60
+
61
+ // The manifest is read for membership and specifiers, never for versions:
62
+ // @lab43/q in devDependencies is what makes this a q project, and a
63
+ // watermarked package missing from both dependency maps was removed out of
64
+ // band. A missing manifest means not a q project. One that exists but can't
65
+ // be read or parsed fails like any other invalid state.
66
+ let pkgText;
67
+ try {
68
+ pkgText = fs.readFileSync(path.join(proj, "package.json"), "utf8");
69
+ } catch (e) {
70
+ if (e.code === "ENOENT" || e.code === "ENOTDIR") process.exit(0);
71
+ fail();
72
+ }
73
+ const pkg = parse(pkgText);
74
+ if (pkg === undefined) fail();
75
+
76
+ const depMap = (field) =>
77
+ pkg && typeof pkg[field] === "object" && pkg[field] !== null ? pkg[field] : {};
78
+
79
+ const devDeps = depMap("devDependencies");
80
+ // An extension may be a regular dependency: a package shipping rules can also
81
+ // be one the project builds on. q never is — it is tooling, so it stays a
82
+ // devDependency check below.
83
+ const deps = { ...depMap("dependencies"), ...devDeps };
84
+
85
+ // Declaring no @lab43/q devDependency is a designed silence.
86
+ if (!Object.hasOwn(devDeps, "@lab43/q")) process.exit(0);
87
+
88
+ // The q this session actually loaded. CLAUDE_PLUGIN_ROOT is the directory it
89
+ // was resolved from, and npm wrote that copy's version, so comparing it
90
+ // against the lockfile also catches a session running some other checkout's q.
91
+ //
92
+ // The version sits one level above that directory. The plugin root is the
93
+ // payload directory, and npm requires package.json at the package root holding
94
+ // it, so the two are never the same directory.
95
+ const loaded = installedVersion(path.join(root, ".."));
96
+ if (typeof loaded !== "string") fail();
97
+ // A missing lockfile, or one that cannot resolve q, reads as undefined and
98
+ // fails this comparison like any other drift.
99
+ const lockedQ = lockedVersion(proj, "@lab43/q", devDeps["@lab43/q"]);
100
+ if (loaded !== lockedQ) fail();
101
+
102
+ // Watermarks. A project with q declared but no state file is unrecorded drift.
103
+ const stateText = read(path.join(proj, ".claude/q-state.json"));
104
+ if (stateText === null) fail();
105
+
106
+ const state = parse(stateText);
107
+ if (state === undefined || typeof state !== "object" || state === null) fail();
108
+
109
+ const recon = state.reconciledAgainst ?? {};
110
+ if (typeof recon !== "object" || recon === null || Array.isArray(recon)) fail();
111
+ // q is the framework rather than an extension, so it is checked by name: a
112
+ // declared project with no entry for it is caught here. The reverse-direction
113
+ // loop below cannot stand in. It identifies extensions by a keyword read from
114
+ // node_modules, which q does not carry and which an uninstalled or stale copy
115
+ // doesn't supply either.
116
+ if (!Object.hasOwn(recon, "@lab43/q")) fail();
117
+
118
+ for (const [ext, mark] of Object.entries(recon)) {
119
+ if (typeof mark !== "string") fail();
120
+ if (!Object.hasOwn(deps, ext)) fail(); // removed, never reconciled
121
+ if (typeof deps[ext] !== "string") fail(); // not a specifier — invalid state
122
+ const locked = lockedVersion(proj, ext, deps[ext]);
123
+ if (locked !== mark) fail(); // moved or unresolvable, never reconciled
124
+ const inst = installedVersion(path.join(proj, "node_modules", ext));
125
+ if (typeof inst !== "string") fail();
126
+ if (inst !== locked) fail(); // node_modules stale against the lockfile
127
+ }
128
+
129
+ // Reverse direction: a dependency whose installed copy is an extension but
130
+ // that has no watermark entry was installed by hand and never indexed.
131
+ // Identity is both halves — the q-extension keyword, and a payload directory
132
+ // holding conventions/ or .claude-plugin/. Either alone describes a package
133
+ // that is not an extension and has no watermark to be missing.
134
+ //
135
+ // Both are readable only from the package's own copy under node_modules, so a
136
+ // dependency whose manifest is absent or unparseable is skipped rather than
137
+ // reported.
138
+ // throwIfNoEntry covers a missing path, but stat still throws on an
139
+ // unreadable one, and this hook must never exit on a stack trace.
140
+ const isDir = (p) => {
141
+ try {
142
+ return fs.statSync(p).isDirectory();
143
+ } catch {
144
+ return false;
145
+ }
146
+ };
147
+ const hasPayload = (dir) =>
148
+ ["conventions", ".claude-plugin"].some((sub) => isDir(path.join(dir, "q-extension", sub)));
149
+
150
+ for (const dep of Object.keys(deps)) {
151
+ if (Object.hasOwn(recon, dep)) continue;
152
+ const installed = path.join(proj, "node_modules", dep);
153
+ const keywords = parse(read(path.join(installed, "package.json")) ?? "")?.keywords;
154
+ if (Array.isArray(keywords) && keywords.includes("q-extension") && hasPayload(installed)) fail();
155
+ }
@@ -1,22 +1,23 @@
1
1
  #!/bin/bash
2
2
  # SessionStart hook entry point. The checks live in session-start.mjs; this
3
3
  # wrapper exists so a missing node fails loud with a useful message instead of
4
- # a cryptic exec error. Node is a given on a project that pins q, which arrives
5
- # as an npm dependency. The gate below keeps the checks off projects that pin no
6
- # q at all.
4
+ # a cryptic exec error. Node is a given on a project that declares q, which
5
+ # arrives as an npm dependency. The gate below keeps the checks off projects
6
+ # that declare no q at all.
7
7
 
8
8
  proj="${CLAUDE_PROJECT_DIR:-.}"
9
9
  pkg="$proj/package.json"
10
10
  [ -f "$pkg" ] || exit 0
11
11
  # Match "@lab43/q" in key position only. As a value it is the name of q's own
12
- # manifest, which pins nothing. This stays a loose pre-filter: it also matches
12
+ # manifest, which declares no q. This stays a loose pre-filter: it also matches
13
13
  # the key under dependencies. session-start.mjs exits silently unless the key
14
14
  # is a devDependency.
15
15
  grep -qE '"@lab43/q"[[:space:]]*:' "$pkg" 2>/dev/null
16
16
  status=$?
17
- # Exit only on a clean miss: the manifest was read and pins no q. Any other
18
- # failure means the manifest could not be read, and a project that may well pin
19
- # q must not be skipped silently — fall through and let the checks report it.
17
+ # Exit only on a clean miss: the manifest was read and declares no q. Any other
18
+ # failure means the manifest could not be read, and a project that may well
19
+ # declare q must not be skipped silently — fall through and let the checks
20
+ # report it.
20
21
  [ "$status" -eq 1 ] && exit 0
21
22
 
22
23
  root="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
@@ -24,7 +25,7 @@ root="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
24
25
  if ! command -v node >/dev/null; then
25
26
  # No node means the checks can't run — same remedy as any other failure.
26
27
  # Keep the message in sync with MESSAGE in session-start.mjs.
27
- printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"The q plugin could not validate this project'"'"'s q setup, so its conventions and tooling may be stale or broken. Run /q:sync to repair it."}}\n'
28
+ printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"The q plugin could not validate this project'"'"'s q setup, so its conventions and tooling may be stale or broken. Run /q:reconcile to repair it."}}\n'
28
29
  exit 0
29
30
  fi
30
31
 
@@ -11,33 +11,43 @@ The documentation policy decides what the briefing must carry (see: @lab43/q con
11
11
 
12
12
  This project follows q, an agentic coding workflow. Its rules live in the project's own documentation. Those rules are conventions: binding decisions about how this project's code and docs get written, recorded as they are made. Guides sit alongside them — how to operate the project, rather than rules for writing it.
13
13
 
14
- If the session's skill list has no `/q:` skills, this machine is missing the q plugin — ask the user to install the project's dependencies (`npm install`, or the project's package manager's equivalent), then run `/q:sync`.
14
+ If the session's skill list has no `/q:` skills, this machine is missing the q plugin — ask the user to install the project's dependencies (`npm install`, or the project's package manager's equivalent), then run `/q:reconcile`.
15
15
 
16
16
  When another session is already working this repo, take a worktree rather than sharing the checkout.
17
17
 
18
- Conventions come in three tiers: q's own, the conventions of any installed extensions, and this project's own `docs/conventions/` (source: @lab43/q conventions/documentation.md, Three tiers of conventions). q and the extensions are pinned in `package.json`. Project rules win over an extension's rule, and an extension's rule wins over q's. Check all three tiers before writing code, before design decisions and reviews, and before changing docs. Doc changes — the README and this briefing itself included — go through `/q:update-docs`.
18
+ Conventions come in three tiers: q's own, the conventions of any installed extensions, and this project's own `docs/conventions/` (source: @lab43/q conventions/conventions.md, Three tiers of conventions). q and the extensions are dependencies in `package.json`. Project rules win over an extension's rule, and an extension's rule wins over q's. Check all three tiers before writing code, before design decisions and reviews, and before changing docs. Doc changes — the README and this briefing itself included — go through `/q:update-docs`.
19
19
 
20
- Package doc paths are package name plus path from the package root, resolved under `node_modules/`: `@lab43/q conventions/principles.md` is `node_modules/@lab43/q/conventions/principles.md` (source: @lab43/q conventions/documentation.md, Package doc paths).
20
+ Package doc paths are package name plus path from the package's `q-extension/` payload directory, resolved under `node_modules/`: `@lab43/q conventions/principles.md` is `node_modules/@lab43/q/q-extension/conventions/principles.md` (source: @lab43/q conventions/documentation.md, Package doc paths).
21
21
 
22
- `@lab43/q` — the rules of the q workflow, governing how a project's work gets planned, decided, documented, and shipped:
22
+ `@lab43/q` — <its `q.description`>
23
23
 
24
24
  - `@lab43/q conventions/principles.md` — cross-cutting rules for any design decision, plan, or review
25
25
  - `@lab43/q conventions/documentation.md` — what belongs in a project's documentation, where it lives, and how it stays accurate
26
+ - `@lab43/q conventions/conventions.md` — rules for a project's conventions docs: the tiers they come in and which tier wins, what a conventions doc holds, and when a rule leaves prose for a stronger enforcement rung
26
27
  - `@lab43/q conventions/extensions.md` — the extension format: rules for authoring and publishing a q extension
27
28
  - `@lab43/q conventions/plans.md` — format, sequencing, and lifecycle rules for `docs/plans/` documents
29
+ - `@lab43/q conventions/specs.md` — rules for `docs/specs/` documents: what a spec holds, how it is written, and how the code is held to it
28
30
  - `@lab43/q conventions/issue-tracking.md` — rules for working a project's issue tracker from any session
29
31
  - `@lab43/q conventions/pull-requests.md` — rules for authoring a pull request
30
32
  - `@lab43/q conventions/writing.md` — rules for writing prose: docs, plans, PR bodies, anything a human or agent will read
31
33
 
32
- `<package>` — the extension's description, restated:
34
+ `<package>` — <its `q.description`>
33
35
 
34
36
  - `<package> conventions/<name>.md` — one line per doc, restating its intro
35
37
 
38
+ `<this package>` — <its `q.description`>
39
+
40
+ - `<this package> conventions/<name>.md` — one line per doc, restating its intro
41
+
36
42
  This project's own:
37
43
 
38
44
  - `docs/conventions/principles.md` — cross-cutting rules, including deviations from q's
39
45
  - `docs/conventions/documentation.md` — documentation rulings and deviations
40
46
 
47
+ Specs:
48
+
49
+ - `docs/specs/<name>.md` — one line per spec, restating its intro
50
+
41
51
  Guides:
42
52
 
43
53
  - `docs/guides/<name>.md` — one line per guide, restating its intro
@@ -47,9 +57,10 @@ Guides:
47
57
 
48
58
  - **Write into `CLAUDE.md`**, creating it when it doesn't exist. Leave any `AGENTS.md` the project keeps for other tools alone. Linking the two is the project's call, not q's.
49
59
  - **Conform to the structure**: the section heading, the groups in the order they run here, one line per doc. Name each group for what it actually holds.
50
- - **Head each group with what its docs govern** (source: @lab43/q conventions/documentation.md, Taxonomy). q's group takes the heading spelled out above. Each installed extension gets a group of its own, headed by its package name and the `description` from its `package.json` (see: @lab43/q conventions/extensions.md, Description). Restate that description as written. Shortening an unwieldy one is the extension author's job. An extension shipping no description gets a heading of its name alone.
60
+ - **Head each group with what its docs govern** (source: @lab43/q conventions/documentation.md, Taxonomy). A package's group is headed by its name, an em dash, and the `q.description` from its `package.json`, q's own group included (see: @lab43/q conventions/extensions.md, Description). A package shipping no `q.description` gets a heading of its name alone, never its `description` that field answers the registry's readers.
61
+ - **Give the payload this repo ships its own group.** When the repo's own `q-extension/` holds `conventions/`, it ships those rules and consumes them too: index them in the same form as an installed extension's, read from the working tree rather than `node_modules/` (see: @lab43/q conventions/extensions.md, Authoring).
51
62
  - **Treat the prose as a floor, not a script.** Carry at least what the template's prose carries. Leave the project's own wording where it says the same thing. Where a statement isn't true of the project — it authors an extension rather than installing one, or loads the plugin some other way — say what is true instead.
52
63
  - **Keep what the project put there** — its own standing instructions, notes, and index entries beyond the required ones. Work missing information into what is already written rather than bolting a sentence alongside it. Rewrite freely to do that, but drop nothing the project said.
53
- - **Drop a group with no entries.** A fresh project has no extensions and no guides, so those groups arrive with the first one of each. The q group is always there.
54
- - **Index every doc the policy requires, and nothing stale** — every conventions doc, from q, from an installed extension, or the project's own, and every guide (source: @lab43/q conventions/documentation.md, Taxonomy). Drop the line for a doc that is gone. An extension the project no longer installs loses every line and its heading with them. The installed extensions are the direct dependencies whose own `package.json` carries the `q-extension` keyword (source: @lab43/q conventions/extensions.md). An extension shipping no `conventions/` contributes no lines, and so no group.
55
- - **Write each line as a path plus a blurb restating the doc's intro** — q's docs and an extension's by package name plus path from the package root (see: @lab43/q conventions/documentation.md, Package doc paths), the project's own by repo-relative path.
64
+ - **Drop a group with no entries.** A fresh project has no extensions, no specs, and no guides, so those groups arrive with the first one of each. The q group is always there.
65
+ - **Index every doc the policy requires, and nothing stale** — every conventions doc, from q, from an installed extension, from this repo's own payload, or the project's own, every spec, and every guide (source: @lab43/q conventions/documentation.md, Taxonomy). Drop the line for a doc that is gone. An extension the project no longer installs loses every line and its heading with them. The installed extensions are the direct dependencies `dependencies` and `devDependencies` alike — whose installed copy carries both the `q-extension` keyword and a payload directory (source: @lab43/q conventions/extensions.md, Identity). An extension shipping no conventions docs contributes no lines, and so no group.
66
+ - **Write each line as a path plus a blurb restating the doc's intro** — q's docs, an installed extension's, and this repo's own payload by their path form (see: @lab43/q conventions/documentation.md, Package doc paths), the project's own by repo-relative path.
@@ -0,0 +1,16 @@
1
+ # Enforce Declarations
2
+
3
+ The machine-local enforcement procedure: make this machine match what the project declares. Enforce without asking — the declarations are the project's recorded decisions, and this merely applies them.
4
+
5
+ 1. Check that `node` and `npm` resolve. When either is missing, Node.js is not installed — report that fix. Nothing below runs without it.
6
+ 2. When `node_modules/` is missing a declared dependency, or holds a version other than the lockfile's, run the project's package-manager install — `npm install`, or the pnpm or yarn equivalent its lockfile indicates. That install is the whole update mechanism: a session reads q from `node_modules/@lab43/q` as it stands, so nothing else has to reach it.
7
+ 3. Register the project's marketplace: `claude plugin marketplace add --scope local ./`. The tracked settings only declare the marketplace — sessions resolve it against a machine-global registry the declaration does not update: an interactive session offers the registration behind a trust prompt, and a headless one never registers it at all. The manifest at the project root supplies the name, so nothing has to read or pass one. Skip this step when the project has no `.claude-plugin/marketplace.json`. The add fails outright against a directory holding no manifest. A project without one has not been scaffolded yet. Otherwise run it unconditionally, since the add changes nothing when the registry already points here. Local scope records the registration in the project's own `.claude/settings.local.json`, which does not keep it private.
8
+
9
+ Three situations need it:
10
+
11
+ - A fresh clone has never registered the marketplace.
12
+ - A declined trust prompt left it unregistered.
13
+ - Another checkout of this repo has repointed the project's name at itself. The machine-global registry holds one entry per marketplace name. The add repoints it back, because an entry whose name matches but whose path differs is updated to the new path.
14
+ 4. After any change above, run `/reload-plugins`.
15
+
16
+ Name any tracked file the enforcement rewrote (a lockfile). That change stays in the tree as the user's.
@@ -0,0 +1,31 @@
1
+ # The q State File
2
+
3
+ Format and writer rules for `.claude/q-state.json`, the consumer-side record of the versions a project was last reconciled against. The skills that read or write the file follow this doc; nothing else edits it.
4
+
5
+ ## What the file is
6
+
7
+ The file holds machine-written version watermarks — never rules, never doc enumerations. Reconciliation is the work of folding a version change into the project — holding its docs against an extension release's changed rules, or its scaffolded surfaces against a new q version; `/q:reconcile` performs it. A watermark records the version the project was last reconciled against. Drift is the lockfile or the installed version disagreeing with it, however the move arrived — a hand-run npm install, a teammate's merge, a Dependabot bump. The skills and the session-start hook compare its versions against the lockfile and `node_modules`; nothing consults it for how to behave.
8
+
9
+ ## Format
10
+
11
+ The file lives at `.claude/q-state.json`, committed. JSON, one key per line, so a watermark move reads as a one-line diff:
12
+
13
+ ```json
14
+ {
15
+ "note": "Machine state written by q's skills. Never edit by hand; /q:reconcile repairs drift.",
16
+ "reconciledAgainst": {
17
+ "@lab43/q": "0.3.0"
18
+ }
19
+ }
20
+ ```
21
+
22
+ `reconciledAgainst` holds one entry for `@lab43/q` and one per installed extension: the version the project was last reconciled against. q's entry has the same shape as the rest, carrying no privilege and no separate field.
23
+
24
+ ## Writer rules
25
+
26
+ - `/q:install` writes a missing `@lab43/q` entry when it scaffolds, from the version installed, which has no reconciliation debt. It never touches a present entry — a stale one is reconciliation's to move.
27
+ - `/q:reconcile` writes watermarks as reconciliation's record: it moves a package's entry after reconciling its version move, writes an arrival's first entry from the version installed, and drops an entry as part of reconciling a departure. It writes nothing before a reconciliation — watermarks certify one.
28
+
29
+ An absent file means no record — create it on the first watermark write.
30
+
31
+ What divides this file from `package.json` is who writes it. The manifest is hand-authored and npm's to rewrite, so it carries what a person sets and a consumer reads — the q declaration, `q.description`. Watermarks are machine-written, so they live here, where the format stays q's to guarantee and nothing edits them by hand.