@lab43/q 0.4.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 (49) hide show
  1. package/README.md +80 -50
  2. package/package.json +21 -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 +4 -2
  11. package/{conventions → q-extension/conventions}/pull-requests.md +2 -2
  12. package/q-extension/conventions/specs.md +31 -0
  13. package/{conventions → q-extension/conventions}/writing.md +8 -0
  14. package/q-extension/hooks/locked-version.mjs +169 -0
  15. package/q-extension/hooks/session-start.mjs +155 -0
  16. package/{hooks → q-extension/hooks}/session-start.sh +9 -8
  17. package/q-extension/references/agent-briefing.md +66 -0
  18. package/q-extension/references/enforce-declarations.md +16 -0
  19. package/q-extension/references/q-state.md +31 -0
  20. package/{references → q-extension/references}/run-contract.md +18 -10
  21. package/{skills → q-extension/skills}/address-feedback/SKILL.md +19 -15
  22. package/q-extension/skills/clean-worktrees/SKILL.md +66 -0
  23. package/{skills → q-extension/skills}/create-plan/SKILL.md +5 -5
  24. package/{skills → q-extension/skills}/drive/SKILL.md +1 -1
  25. package/q-extension/skills/groom-docs/SKILL.md +65 -0
  26. package/{skills/tackle → q-extension/skills/implement}/SKILL.md +14 -14
  27. package/{skills → q-extension/skills}/implement-plan/SKILL.md +14 -14
  28. package/q-extension/skills/install/SKILL.md +136 -0
  29. package/{skills → q-extension/skills}/parallelize/SKILL.md +5 -5
  30. package/q-extension/skills/reconcile/SKILL.md +110 -0
  31. package/{skills → q-extension/skills}/review/SKILL.md +5 -4
  32. package/{skills → q-extension/skills}/triage/SKILL.md +11 -11
  33. package/{skills → q-extension/skills}/update-docs/SKILL.md +22 -17
  34. package/{skills → q-extension/skills}/upstream/SKILL.md +10 -8
  35. package/.claude-plugin/marketplace.json +0 -6
  36. package/agents/adversarial-reviewer.md +0 -62
  37. package/conventions/documentation.md +0 -99
  38. package/conventions/extensions.md +0 -48
  39. package/conventions/issue-tracking.md +0 -19
  40. package/hooks/session-start.mjs +0 -125
  41. package/references/agent-briefing.md +0 -54
  42. package/references/enforce-pins.md +0 -16
  43. package/references/q-state.md +0 -31
  44. package/skills/groom-docs/SKILL.md +0 -62
  45. package/skills/install/SKILL.md +0 -150
  46. package/skills/sync/SKILL.md +0 -45
  47. package/skills/uninstall-extension/SKILL.md +0 -52
  48. package/skills/update/SKILL.md +0 -75
  49. /package/{hooks → q-extension/hooks}/hooks.json +0 -0
@@ -0,0 +1,70 @@
1
+ # Extensions
2
+
3
+ The format behind the extension conventions tier — rules for authoring and publishing a q extension.
4
+
5
+ ## Identity
6
+
7
+ A q extension is an npm package that carries the `q-extension` keyword in its `package.json` and ships a `q-extension/` payload directory (see: Layout). Both halves identify it. The keyword is how tooling tells extensions from a project's other dependencies and how they are found on the registry. The payload is what the keyword promises. A package carrying the keyword and shipping nothing is not an extension, and q's machinery passes over it in silence rather than reporting a state no skill can repair.
8
+
9
+ Look for extensions across a project's direct `dependencies` as well as its `devDependencies`. A package can ship rules and code together — a component library carrying the conventions for using it — and a project that imports such a package holds it as a regular dependency. Meeting both halves means q's machinery:
10
+
11
+ - discovers the package
12
+ - indexes what it documents
13
+ - registers the plugin it ships, for the project's sessions to load
14
+ - reconciles the project against its releases
15
+
16
+ q is not an extension but what extensions extend: the framework whose rules they add to and whose format they follow (source: @lab43/q conventions/conventions.md, Three tiers of conventions). `@lab43/q` carries no keyword, and is named outright wherever a rule reaches it.
17
+
18
+ ## Which rules ship
19
+
20
+ A repo that publishes rules has two homes for a rule it writes, and every rule goes to one of them. A repo that publishes none has one home, `docs/conventions/`, and becoming a publisher is the user's call rather than a session's.
21
+
22
+ - **The payload** — everything under the package's `q-extension/` directory, which is what binds its consumers (see: Layout).
23
+ - **`docs/conventions/`** — rules for developing the repo itself: its own code, its docs, its repo mechanics. Consumers are never bound by these and never routed to them.
24
+
25
+ The test is one question: would this change what a session in a consuming repo writes or flags? Yes puts it in the payload. No keeps it local.
26
+
27
+ A rule can pass that test later, as its audience outgrows one project — org-wide rules, or rules for code that uses a product. Move it then: out of `docs/conventions/` into the payload, never copied into both, its index line crossing from the project's group to the payload's rather than disappearing (see: Authoring).
28
+
29
+ **Payload is addressed solely to consumers.** Payload docs never name the repo that authors them, its development practices, or its internal layout. Even acknowledging that they might be read from inside that repo muddies them for the audience they speak to. A repo still follows the payload it ships, as a consuming project of its own rules.
30
+
31
+ Rejected: shipping the payload's conventions at the contract path, `docs/conventions/`, so that one path means conventions everywhere. It conflates what the package ships with the repo's own working rules, so a consumer reading the shipped docs would meet repo-local rulings as law.
32
+
33
+ ## Description
34
+
35
+ `package.json`'s `q.description` states what the extension's rules govern. A consuming project's briefing heads the extension's group of index lines with it (source: @lab43/q conventions/documentation.md, Taxonomy). Name the territory the rules cover, not the package's shape.
36
+
37
+ ## Layout
38
+
39
+ A package's payload is a `q-extension/` directory at its package root. Everything the package ships for q lives inside it. Name `q-extension` in the `files` whitelist: without it the package ships no rules at all, announces itself as an extension anyway, and nothing warns.
40
+
41
+ What it holds depends on what the extension carries:
42
+
43
+ - `conventions/` when it ships conventions docs, each written to the documentation policy (see: @lab43/q conventions/documentation.md).
44
+ - `.claude-plugin/` when it ships a plugin, beside whatever that plugin loads — its skills, agents and hooks. `q-extension/` is then the plugin root, and `${CLAUDE_PLUGIN_ROOT}` resolves to it. A consuming project registers the plugin in its own marketplace under the `name` in the plugin's `plugin.json`, so that name is the namespace its sessions type: `/<name>:<skill>`. Choose one no other extension is likely to carry. `q` is taken. What else a plugin may hold is Claude Code's to decide; q fixes only where its root sits.
45
+
46
+ Every extension ships at least one of the two, which is what identifies a payload (see: Identity). Anything else the package ships for q sits beside them.
47
+
48
+ An extension shipping no conventions docs is watermarked but never indexed, having nothing for a briefing's docs index to carry. Watermarking and indexing are separate for that reason.
49
+
50
+ Rejected: a `q` metadata key in `package.json` naming paths or listing docs — the payload directory and the keyword already answer where everything is, and an enumeration of docs rots against its own contents. The key carries the description and nothing else (see: Description).
51
+
52
+ ## Pinning
53
+
54
+ Declare `@lab43/q` as a devDependency in the `package.json` at the repo root. That is the only place anything looks for it, whatever else the repo holds — a monorepo declares q at the root, never in a workspace manifest. Keep it a devDependency: q is tooling rather than code a project imports, and a project holding it in `dependencies` goes unvalidated at session start, silently.
55
+
56
+ Declare each extension in that same root manifest. `dependencies` and `devDependencies` both serve (see: Identity), but no other manifest does — a declaration in a workspace package is one nothing reads. The specifier's form is the project's own choice: q reads versions from the lockfile and `node_modules`, never from the manifest.
57
+
58
+ A repo that publishes rules already declares q at its root, like any q project, with one requirement of its own: the pin is exact. With the payload at that root, the published package is the repo itself — its `package.json` goes into the tarball, and the pin in it tells consumers which q version the rules were written against. That is a fact only an exact version can state, and the manifest is the author's only channel for it: the lockfile that would resolve a range never ships. The declaration stays inert in consumers — npm installs no dependency's devDependencies — so extensions declaring different q versions never collide; the gap is surfaced at reconciliation and closes through releases.
59
+
60
+ ## Authoring
61
+
62
+ In its authoring project, an extension's conventions docs and its `q.description` join the documentation surface (see: Description). Both are groomed and reconciled against q's updates like the project's own docs.
63
+
64
+ They are indexed there too. The authoring repo's briefing gives them a group of their own, shaped like a consumed extension's and headed the same way. The group carries no pin, because a repo does not depend on itself, and no watermark, because a watermark certifies that changed rules have been absorbed and there is nothing to absorb in rules you are editing.
65
+
66
+ An extension's doc may deviate from a q rule the same way a project doc does, stating the deviation with an overrides marker (see: @lab43/q conventions/documentation.md, Markers); the project's own rulings still win over any extension's (source: @lab43/q conventions/conventions.md, Three tiers of conventions).
67
+
68
+ The plugin the extension ships loads from the working tree too. Add its entry to the project's own marketplace by hand, beside q's: sourced at `./q-extension` and named by the `name` in its `plugin.json`. Enable it in `.claude/settings.json` the way q's entry is enabled there.
69
+
70
+ All of this assumes the payload sits at the repo's own root. Publish from a sub-package of a larger repo and consumers install it exactly the same way, but the authoring repo gets none of the above: no group in its briefing, and a root declaration that is not the pin the package ships (see: Pinning).
@@ -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: 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).
@@ -19,7 +19,7 @@ Two guards:
19
19
  - Extract sameness, not resemblance: extract only mechanical duplicates that must change together. Keep code that merely looks similar but diverges in semantics separate (a link styled to mimic a button's visuals is still a link — two components, not one).
20
20
  - Don't treat imitating an exemplar as copying: the rule targets verbatim plumbing and scaffolding, not structural similarity to the file a convention doc holds up as the model.
21
21
 
22
- When tooling genuinely forces a copy (an environment that can't import shared modules), mark both copies with a keep-in-sync note so the duplication reads as intent, not oversight. A plain note, not a `(source:)` marker (see: q conventions/documentation.md, Markers): neither copy is the authority it would name.
22
+ When tooling genuinely forces a copy (an environment that can't import shared modules), mark both copies with a keep-in-sync note so the duplication reads as intent, not oversight. A plain note, not a `(source:)` marker (see: @lab43/q conventions/documentation.md, Markers): neither copy is the authority it would name.
23
23
 
24
24
  ## Colocate knowledge with its next reader
25
25
 
@@ -31,10 +31,12 @@ 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.
37
- - **Pointers to unmaintained artifacts** — plan docs (frozen after merge) (source: q conventions/plans.md, Lifecycle), tickets, PRs, external trackers. State the constraint in the comment itself, or point at the convention doc that owns it. The one sanctioned link: a ticket tracking work that will change this code when it lands — a workaround awaiting an upstream fix, a shim awaiting a migration. The dependency is stated inline so the comment stands alone; the link exists only to check the pending work's status.
39
+ - **Pointers to unmaintained artifacts** — plan docs (frozen after merge) (source: @lab43/q conventions/plans.md, Lifecycle), tickets, PRs, external trackers. State the constraint in the comment itself, or point at the convention doc that owns it. The one sanctioned link: a ticket tracking work that will change this code when it lands — a workaround awaiting an upstream fix, a shim awaiting a migration. The dependency is stated inline so the comment stands alone; the link exists only to check the pending work's status.
38
40
 
39
41
  ## Prefer the strongest enforcement rung
40
42
 
@@ -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: q conventions/writing.md). Sign the body and every comment or reply on it: "— Claude 🤖" (source: q conventions/writing.md, Sign what you post). A project's own PR conventions win over this doc (source: 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
 
@@ -19,7 +19,7 @@ Describe the change, never the run that produced it. Review rounds, rulings, and
19
19
 
20
20
  Compose the body from these sections, in order. Most PRs need only Summary and Testing. Add another section only when it has something the reviewer needs.
21
21
 
22
- - **Summary** — the rubric the reviewer checks the diff against: what is true after merge that wasn't before, and why. State outcomes, not edits: one claim per deliverable, each something the reader can now rely on. The diff is the catalog of changes. When the diff is large, say where the substance lives and which files are mechanical fallout. The Summary links the work's source: the tracker item when one exists (source: q conventions/issue-tracking.md, Work links back), and the plan doc when the PR implements a plan. Where the tracker closes an item from the link's wording, word it to close only in the PR whose merge finishes the item — the last layer of a stack, the only PR otherwise. Every other layer links the item without closing it, or the first merge closes work the rest of the stack hasn't delivered.
22
+ - **Summary** — the rubric the reviewer checks the diff against: what is true after merge that wasn't before, and why. State outcomes, not edits: one claim per deliverable, each something the reader can now rely on. The diff is the catalog of changes. When the diff is large, say where the substance lives and which files are mechanical fallout. The Summary links the work's source: the tracker item when one exists (source: @lab43/q conventions/issue-tracking.md, Work links back), and the plan doc when the PR implements a plan. Where the tracker closes an item from the link's wording, word it to close only in the PR whose merge finishes the item — the last layer of a stack, the only PR otherwise. Every other layer links the item without closing it, or the first merge closes work the rest of the stack hasn't delivered.
23
23
  - **Callouts** — answers to the questions the diff will raise:
24
24
  - Choices that look wrong but are deliberate.
25
25
  - Expected changes deliberately not made.
@@ -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.
@@ -20,6 +20,14 @@ Never regulate prose with a length target — "keep it short", "a few sentences"
20
20
 
21
21
  Use the words a reader would use to describe the thing. Write "errors now show a message instead of crashing", not "error handling was hardened with graceful degradation". Jargon and category labels make the reader translate before they can follow.
22
22
 
23
+ ## One name per concept
24
+
25
+ Once a concept has an established name, use exactly that name everywhere. A synonym reads as a second concept.
26
+
27
+ Check whether a plainer word would do before a name becomes established (see: Plain words).
28
+
29
+ Rejected: a glossary — it either duplicates the definitions the home docs already carry, or becomes the central registry of shared facts the documentation policy rejects (source: @lab43/q conventions/documentation.md, Single source of truth).
30
+
23
31
  ## Instructions are commands
24
32
 
25
33
  Write an instruction — to a human or an agent — as a command: "commit the fixes", never "the fixes are committed" or "the run commits the fixes". Passives and narration hide who acts. Keep declarative sentences for facts a reader relies on.
@@ -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