@lab43/q 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -41
- package/package.json +7 -4
- package/{.claude-plugin → q-extension/.claude-plugin}/plugin.json +1 -1
- package/q-extension/agents/adversarial-reviewer.md +98 -0
- package/q-extension/conventions/conventions.md +49 -0
- package/q-extension/conventions/documentation.md +79 -0
- package/q-extension/conventions/extensions.md +70 -0
- package/q-extension/conventions/issue-tracking.md +56 -0
- package/{conventions → q-extension/conventions}/plans.md +8 -4
- package/{conventions → q-extension/conventions}/principles.md +2 -0
- package/{conventions → q-extension/conventions}/pull-requests.md +1 -1
- package/q-extension/conventions/specs.md +31 -0
- package/q-extension/hooks/locked-version.mjs +169 -0
- package/q-extension/hooks/session-start.mjs +155 -0
- package/{hooks → q-extension/hooks}/session-start.sh +9 -8
- package/{references → q-extension/references}/agent-briefing.md +22 -11
- package/q-extension/references/enforce-declarations.md +16 -0
- package/q-extension/references/q-state.md +31 -0
- package/{references → q-extension/references}/run-contract.md +8 -5
- package/{skills → q-extension/skills}/address-feedback/SKILL.md +15 -11
- package/q-extension/skills/clean-worktrees/SKILL.md +66 -0
- package/{skills → q-extension/skills}/create-plan/SKILL.md +3 -3
- package/q-extension/skills/groom-docs/SKILL.md +65 -0
- package/{skills → q-extension/skills}/implement/SKILL.md +3 -3
- package/{skills → q-extension/skills}/implement-plan/SKILL.md +5 -5
- package/q-extension/skills/install/SKILL.md +136 -0
- package/q-extension/skills/reconcile/SKILL.md +110 -0
- package/{skills → q-extension/skills}/review/SKILL.md +2 -1
- package/{skills → q-extension/skills}/update-docs/SKILL.md +15 -10
- package/{skills → q-extension/skills}/upstream/SKILL.md +4 -2
- package/.claude-plugin/marketplace.json +0 -6
- package/agents/adversarial-reviewer.md +0 -62
- package/conventions/documentation.md +0 -102
- package/conventions/extensions.md +0 -52
- package/conventions/issue-tracking.md +0 -28
- package/hooks/session-start.mjs +0 -128
- package/references/enforce-pins.md +0 -16
- package/references/q-state.md +0 -31
- package/skills/groom-docs/SKILL.md +0 -62
- package/skills/install/SKILL.md +0 -150
- package/skills/sync/SKILL.md +0 -45
- package/skills/uninstall-extension/SKILL.md +0 -52
- package/skills/update/SKILL.md +0 -76
- /package/{conventions → q-extension/conventions}/writing.md +0 -0
- /package/{hooks → q-extension/hooks}/hooks.json +0 -0
- /package/{skills → q-extension/skills}/drive/SKILL.md +0 -0
- /package/{skills → q-extension/skills}/parallelize/SKILL.md +0 -0
- /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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Plans
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Rules for how a project's plans are written, sequenced, and carried to completion. `/q:create-plan` writes to this rubric and its adversarial plan reviews enforce it. `/q:implement-plan` executes the delivery shape the plan records.
|
|
4
4
|
|
|
5
5
|
## Audience
|
|
6
6
|
|
|
@@ -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
|
|
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 (
|
|
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/
|
|
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 how a project's specs are written and how the code is held to them. 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
|
|
5
|
-
# as an npm dependency. The gate below keeps the checks off projects
|
|
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
|
|
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
|
|
18
|
-
# failure means the manifest could not be read, and a project that may well
|
|
19
|
-
# q must not be skipped silently — fall through and let the checks
|
|
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:
|
|
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:
|
|
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
|
-
When another session is already working this repo, take a worktree rather than sharing the checkout.
|
|
16
|
+
When another session is already working this repo, take a worktree rather than sharing the checkout (source: @lab43/q references/run-contract.md, The delivery branch).
|
|
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/
|
|
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
|
|
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` —
|
|
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` — how a project's conventions are tiered, written, and enforced
|
|
26
27
|
- `@lab43/q conventions/extensions.md` — the extension format: rules for authoring and publishing a q extension
|
|
27
|
-
- `@lab43/q conventions/plans.md` —
|
|
28
|
+
- `@lab43/q conventions/plans.md` — how a project's plans are written, sequenced, and carried to completion
|
|
29
|
+
- `@lab43/q conventions/specs.md` — how a project's specs are written and how the code is held to them
|
|
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>` —
|
|
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).
|
|
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
|
|
55
|
-
- **Write each line as a path plus a blurb restating the doc's intro** — q's docs
|
|
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.
|