@linchpinagency/skills 0.1.6 → 0.1.8

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 CHANGED
@@ -12,7 +12,7 @@ GitHub Copilot, and other compatible coding agents.
12
12
  ![Zero dependencies](https://img.shields.io/badge/Dependencies-0-brightgreen)
13
13
 
14
14
  <!-- x-release-please-start-version -->
15
- ### Latest release: 0.1.6
15
+ ### Latest release: 0.1.8
16
16
  <!-- x-release-please-end -->
17
17
 
18
18
  | Release | Skill standard | Install |
@@ -81,6 +81,7 @@ The fastest way to understand the library is to run one loop end to end:
81
81
  | Understand a repo you just cloned | "what am I working with here?" | `project-context` |
82
82
  | Find out why something's broken | "the hero image 404s on mobile" | `investigate` |
83
83
  | Test a site like a user, and fix what's found | "QA the checkout flow" | `web-qa` |
84
+ | File work for later | "create an issue for the broken footer link" | `task-tracking` |
84
85
  | Check it's ready to commit | "is this ready to commit?" | `quality-gates` |
85
86
  | Commit and open the PR properly | "commit this and open a PR" | `commit-and-release` + `task-tracking` |
86
87
  | Handle a client support ticket | "the client says their contact form isn't sending" | `support-triage` |
@@ -190,6 +191,33 @@ npx @linchpinagency/skills --skip-upstream
190
191
  **Updating:** re-run the same command. The installer overwrites each skill in place, so a
191
192
  fresh run always pulls the latest published version.
192
193
 
194
+ ### Keeping skills current
195
+
196
+ Installed skills are a snapshot — nothing about a copy in `.claude/skills/` knows a newer
197
+ release exists. So every install writes a stamp beside the skills, in
198
+ `<skills-dir>/.linchpin-skills/`: `version.json` (the version, the date, the agent, the exact
199
+ command that produced the install, and the upstream ref that was vendored) plus a
200
+ self-contained copy of the update checker.
201
+
202
+ ```bash
203
+ # Are these skills behind? One line if yes, nothing if no.
204
+ node .claude/skills/.linchpin-skills/update-check.mjs
205
+
206
+ # Print the Claude Code SessionStart hook that runs it for you
207
+ node .claude/skills/.linchpin-skills/update-check.mjs --hook
208
+ ```
209
+
210
+ With the hook installed, a stale install announces itself at the start of a session —
211
+ `SessionStart` stdout becomes context, so the agent sees it too and can offer to run the
212
+ update command the stamp recorded. Hooks are a Claude Code feature; under Copilot, Codex, or
213
+ Cursor the same check still runs, just when you ask it to.
214
+
215
+ The check is deliberately unobtrusive: it queries the npm registry at most once a day (a
216
+ known-newer version keeps surfacing from cache in between), stays silent when it can't reach
217
+ the network, and always exits 0 — a session never fails to start because of it. It reports;
218
+ it never upgrades anything. Set `LINCHPIN_SKILLS_UPDATE_CHECK=0` to switch it off, and it
219
+ skips itself whenever `CI` is set.
220
+
193
221
  ### Where skills land
194
222
 
195
223
  | Agent (`--agent`) | Project scope | Global scope (`--global`) |
@@ -232,7 +260,7 @@ A project that wants skills in more than one agent's directory should run
232
260
  | `support-triage` | Project mgmt | Run a client support request end to end — clarify the real need, reproduce, judge urgency and scope, fix in the right layer, verify, and close the loop with the requester. |
233
261
  | `dependency-updates` | Workflow | Handle the dependency work Renovate can't automerge — majors, breaking changes, failing or conflicted bot PRs, security advisories, `@wordpress/*` package sets. |
234
262
  | `commit-and-release` | Workflow | Write commits, branches, and PR titles that satisfy the repo's own commitlint rules, and stay out of release-please's way (it owns versions and `CHANGELOG.md`). |
235
- | `task-tracking` | Workflow | Tie every unit of work to a ClickUp task (or explicit `NO-TASK`) with minimal friction via the ClickUp MCP — resolve/search a task, offer to create one before committing, update it when the work lands, and carry the task key in the conventional-commit scope. |
263
+ | `task-tracking` | Workflow | Tie every unit of work to a ClickUp task (or explicit `NO-TASK`) with minimal friction via the ClickUp MCP — resolve/search a task, create one on request ("create an issue" means ClickUp, not GitHub), update it when the work lands, and carry the task key in the conventional-commit scope. |
236
264
  | `write-a-linchpin-skill` | Meta | The house standard for authoring skills in this library — placement test, tier model, required frontmatter, the section skeleton, and the four house rules. Enforced by `scripts/validate-skills.mjs`. |
237
265
 
238
266
  _(More WordPress, React, Cloudflare Workers, marketing, and design skills to come.)_
package/bin/install.mjs CHANGED
@@ -15,6 +15,13 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
15
  const PKG_ROOT = path.resolve(__dirname, '..');
16
16
  const SKILLS_ROOT = path.join(PKG_ROOT, 'skills');
17
17
  const UPSTREAM_MANIFEST = path.join(PKG_ROOT, 'upstream.json');
18
+ const PKG_MANIFEST = path.join(PKG_ROOT, 'package.json');
19
+
20
+ // Version stamp + a self-contained copy of the update checker, written into each install
21
+ // directory. Dot-prefixed and without a SKILL.md, so no agent mistakes it for a skill.
22
+ const STAMP_DIR = '.linchpin-skills';
23
+ const STAMP_FILE = 'version.json';
24
+ const CHECKER = 'update-check.mjs';
18
25
 
19
26
  // Per-agent install locations. `project` paths are relative to cwd, `global` to home.
20
27
  // These follow the Agent Skills conventions each tool reads from. An agent may read more
@@ -80,6 +87,50 @@ function readUpstreamManifest() {
80
87
  }
81
88
  }
82
89
 
90
+ function packageVersion() {
91
+ try {
92
+ return JSON.parse(fs.readFileSync(PKG_MANIFEST, 'utf8')).version || '0.0.0';
93
+ } catch {
94
+ return '0.0.0';
95
+ }
96
+ }
97
+
98
+ // The exact command that reproduces this install, recorded in the stamp so the update
99
+ // checker can tell someone how to re-run it with the flags they actually used.
100
+ function updateCommand(opts) {
101
+ const parts = ['npx @linchpinagency/skills'];
102
+ if (opts.skills.length) parts.push(...opts.skills);
103
+ if (opts.agent !== 'claude-code') parts.push('--agent', opts.agent);
104
+ if (opts.global) parts.push('--global');
105
+ if (opts.skipUpstream) parts.push('--skip-upstream');
106
+ return parts.join(' ');
107
+ }
108
+
109
+ // Record what landed here and leave the checker beside it. Best-effort: a stamp we can't
110
+ // write costs an upgrade nudge, not the install.
111
+ function writeStamp(target, { version, opts, skills, upstream }) {
112
+ const dir = path.join(target.dir, STAMP_DIR);
113
+ try {
114
+ fs.mkdirSync(dir, { recursive: true });
115
+ const stamp = {
116
+ package: '@linchpinagency/skills',
117
+ version,
118
+ installedAt: new Date().toISOString(),
119
+ agent: target.agent,
120
+ scope: opts.global ? 'global' : 'project',
121
+ updateCommand: updateCommand(opts),
122
+ skills,
123
+ upstream,
124
+ };
125
+ fs.writeFileSync(path.join(dir, STAMP_FILE), JSON.stringify(stamp, null, 2) + '\n');
126
+ fs.copyFileSync(path.join(__dirname, CHECKER), path.join(dir, CHECKER));
127
+ return true;
128
+ } catch (err) {
129
+ console.warn(` ! Could not write the version stamp in ${dir}: ${err.message}`);
130
+ return false;
131
+ }
132
+ }
133
+
83
134
  // Fetch a repo tarball at a pinned ref, extract it once, and copy the requested skill
84
135
  // dirs into every directory in `bases`. Best-effort: any failure (offline, no `tar`,
85
136
  // missing skill) warns and returns false rather than aborting the Linchpin install.
@@ -193,12 +244,15 @@ async function main() {
193
244
  process.exit(1);
194
245
  }
195
246
 
196
- // One flat list of destination dirs across every selected agent.
197
- const bases = agentIds.flatMap((id) =>
198
- (opts.global ? AGENTS[id].global : AGENTS[id].project).map((rel) =>
199
- opts.global ? path.join(os.homedir(), rel) : path.join(process.cwd(), rel)
200
- )
247
+ // Every destination across every selected agent, each tagged with the agent it belongs to
248
+ // (the stamp records it; the copy loops only need the directory).
249
+ const targets = agentIds.flatMap((id) =>
250
+ (opts.global ? AGENTS[id].global : AGENTS[id].project).map((rel) => ({
251
+ agent: id,
252
+ dir: opts.global ? path.join(os.homedir(), rel) : path.join(process.cwd(), rel),
253
+ }))
201
254
  );
255
+ const bases = targets.map((t) => t.dir);
202
256
 
203
257
  const wanted = opts.skills.length ? opts.skills : all;
204
258
  const unknown = wanted.filter((s) => !all.includes(s));
@@ -221,11 +275,26 @@ async function main() {
221
275
  const labels = agentIds.map((id) => AGENTS[id].label).join(', ');
222
276
  console.log(`\nInstalled ${wanted.length} Linchpin skill(s) for ${labels}.`);
223
277
 
278
+ const upstream = [];
224
279
  if (!opts.skipUpstream && sources.length) {
225
280
  console.log('\nVendoring pinned base layer (upstream WordPress/agent-skills):');
226
- for (const s of sources) await installUpstreamSource(s, bases);
281
+ for (const s of sources) {
282
+ const installed = await installUpstreamSource(s, bases);
283
+ upstream.push({ repo: s.repo, ref: s.ref, installed });
284
+ }
227
285
  console.log('\nTip: --skip-upstream installs Linchpin skills only.');
228
286
  }
287
+
288
+ // Stamp last, so `upstream` reflects what actually landed rather than what was intended.
289
+ const version = packageVersion();
290
+ const stamped = targets.filter((t) => writeStamp(t, { version, opts, skills: wanted, upstream }));
291
+ if (stamped.length && agentIds.includes('claude-code')) {
292
+ const rel = path.join(STAMP_DIR, CHECKER);
293
+ console.log(
294
+ `\nStamped v${version}. To be told when these skills go stale, add a SessionStart hook:` +
295
+ `\n node ${path.join(opts.global ? '~/.claude/skills' : '.claude/skills', rel)} --hook`
296
+ );
297
+ }
229
298
  }
230
299
 
231
300
  main();
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+ // @linchpinagency/skills — installed-version check.
3
+ //
4
+ // Prints ONE line when a newer release is published, and nothing at all otherwise. Exits 0
5
+ // on every path — offline, unwritable cache, missing stamp, malformed JSON — because this is
6
+ // meant to be wired into a Claude Code `SessionStart` hook, where stdout becomes session
7
+ // context. A failure here must never be the first thing an agent reads.
8
+ //
9
+ // node .claude/skills/.linchpin-skills/update-check.mjs
10
+ // node .claude/skills/.linchpin-skills/update-check.mjs --force # ignore the throttle
11
+ // node .claude/skills/.linchpin-skills/update-check.mjs --json # always emit a status
12
+ // node .claude/skills/.linchpin-skills/update-check.mjs --hook # print the hook snippet
13
+ //
14
+ // Off switch: LINCHPIN_SKILLS_UPDATE_CHECK=0. Also silent when CI is set.
15
+ //
16
+ // `bin/install.mjs` copies this file next to the `version.json` stamp it writes, so the
17
+ // installed copy is self-contained — it reads the stamp as a sibling, not from the package.
18
+
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import os from 'node:os';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
25
+ const PKG = '@linchpinagency/skills';
26
+ // Overridable so this is testable offline, and usable behind a private registry mirror.
27
+ const REGISTRY = process.env.LINCHPIN_SKILLS_REGISTRY || `https://registry.npmjs.org/${PKG}/latest`;
28
+ const THROTTLE_MS = 24 * 60 * 60 * 1000;
29
+ const FETCH_TIMEOUT_MS = 3000;
30
+ const OFF = new Set(['0', 'false', 'off', 'no']);
31
+
32
+ function readJson(file) {
33
+ try {
34
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * The installed version, from the stamp `install.mjs` wrote alongside this file. Falls back
42
+ * to the package's own version so the script is still exercisable from a checkout, where no
43
+ * install stamp exists.
44
+ */
45
+ function readStamp() {
46
+ const stamp = readJson(path.join(HERE, 'version.json'));
47
+ if (stamp?.version) return stamp;
48
+ const pkg = readJson(path.join(HERE, '..', 'package.json'));
49
+ if (pkg?.version) return { version: pkg.version, updateCommand: `npx ${PKG}`, source: 'package' };
50
+ return null;
51
+ }
52
+
53
+ function cacheFile() {
54
+ const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
55
+ return path.join(base, 'linchpin-skills', 'update-check.json');
56
+ }
57
+
58
+ function readCache() {
59
+ const c = readJson(cacheFile());
60
+ return typeof c?.checkedAt === 'number' && c.latest ? c : null;
61
+ }
62
+
63
+ function writeCache(entry) {
64
+ try {
65
+ const file = cacheFile();
66
+ fs.mkdirSync(path.dirname(file), { recursive: true });
67
+ fs.writeFileSync(file, JSON.stringify(entry) + '\n');
68
+ } catch {
69
+ // A read-only or missing HOME just means we check again next time.
70
+ }
71
+ }
72
+
73
+ async function fetchLatest() {
74
+ try {
75
+ const res = await fetch(REGISTRY, {
76
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
77
+ headers: { accept: 'application/json' },
78
+ });
79
+ if (!res.ok) return null;
80
+ const { version } = await res.json();
81
+ return typeof version === 'string' ? version : null;
82
+ } catch {
83
+ return null; // offline, DNS, timeout, private registry — all the same answer here.
84
+ }
85
+ }
86
+
87
+ /** -1 / 0 / 1. Release beats prerelease at the same core version; build metadata ignored. */
88
+ function compareVersions(a, b) {
89
+ const parse = (v) => {
90
+ const [core, pre = ''] = String(v).trim().replace(/^v/, '').split('+')[0].split('-');
91
+ const parts = core.split('.').map((n) => Number.parseInt(n, 10));
92
+ return { nums: [0, 1, 2].map((i) => (Number.isFinite(parts[i]) ? parts[i] : 0)), pre };
93
+ };
94
+ const A = parse(a);
95
+ const B = parse(b);
96
+ for (let i = 0; i < 3; i++) {
97
+ if (A.nums[i] !== B.nums[i]) return A.nums[i] < B.nums[i] ? -1 : 1;
98
+ }
99
+ if (A.pre === B.pre) return 0;
100
+ if (!A.pre) return 1;
101
+ if (!B.pre) return -1;
102
+ return A.pre < B.pre ? -1 : 1;
103
+ }
104
+
105
+ async function evaluate({ force }) {
106
+ if (OFF.has(String(process.env.LINCHPIN_SKILLS_UPDATE_CHECK ?? '').toLowerCase())) {
107
+ return { status: 'disabled', reason: 'LINCHPIN_SKILLS_UPDATE_CHECK' };
108
+ }
109
+ // No human reads a CI log for upgrade nudges, and headless runs shouldn't reach the network.
110
+ if (process.env.CI && !force) return { status: 'disabled', reason: 'CI' };
111
+
112
+ const stamp = readStamp();
113
+ if (!stamp) return { status: 'unknown', reason: 'no-version-stamp' };
114
+
115
+ // Throttle the network call, not the message: a known-newer version keeps surfacing on
116
+ // later sessions from cache, so the nudge survives without re-hitting the registry.
117
+ const cache = force ? null : readCache();
118
+ const cached = cache && Date.now() - cache.checkedAt < THROTTLE_MS;
119
+ const latest = cached ? cache.latest : await fetchLatest();
120
+ if (!latest) return { status: 'unknown', reason: 'registry-unreachable', installed: stamp.version };
121
+ if (!cached) writeCache({ checkedAt: Date.now(), latest });
122
+
123
+ const command = stamp.updateCommand || `npx ${PKG}`;
124
+ const result = { installed: stamp.version, latest, command, installedAt: stamp.installedAt ?? null };
125
+ return compareVersions(latest, stamp.version) > 0
126
+ ? { status: 'update-available', ...result }
127
+ : { status: 'up-to-date', ...result };
128
+ }
129
+
130
+ function hookSnippet() {
131
+ const run =
132
+ 'f=.claude/skills/.linchpin-skills/update-check.mjs; ' +
133
+ '[ -f "$f" ] || f=$HOME/.claude/skills/.linchpin-skills/update-check.mjs; ' +
134
+ '[ -f "$f" ] && node "$f" || true';
135
+ return `{
136
+ "hooks": {
137
+ "SessionStart": [
138
+ {
139
+ "matcher": "*",
140
+ "hooks": [{
141
+ "type": "command",
142
+ "command": ${JSON.stringify(`bash -c '${run}'`)},
143
+ "timeout": 10
144
+ }]
145
+ }
146
+ ]
147
+ }
148
+ }`;
149
+ }
150
+
151
+ function printHook() {
152
+ console.log(`Add to .claude/settings.json (project) or ~/.claude/settings.json (global):\n`);
153
+ console.log(hookSnippet());
154
+ console.log(`
155
+ The command prefers the project install and falls back to the global one, and swallows its
156
+ own failures — a session never fails to start because of this check.`);
157
+ }
158
+
159
+ function help() {
160
+ console.log(
161
+ `
162
+ ${PKG} — report when the installed skills are behind the published release.
163
+
164
+ Usage:
165
+ node update-check.mjs [options]
166
+
167
+ Options:
168
+ --force Ignore the 24h throttle (and the CI opt-out) and query the registry now
169
+ --json Always print a status object, even when up to date
170
+ --hook Print the Claude Code SessionStart hook snippet that runs this check
171
+ -h, --help Show this help
172
+
173
+ Environment:
174
+ LINCHPIN_SKILLS_UPDATE_CHECK=0 Disable the check entirely
175
+ LINCHPIN_SKILLS_REGISTRY=<url> Query a different registry endpoint
176
+
177
+ Prints one line when an update is available, nothing otherwise. Always exits 0.
178
+ `.trimStart()
179
+ );
180
+ }
181
+
182
+ async function main() {
183
+ const args = process.argv.slice(2);
184
+ if (args.includes('--help') || args.includes('-h')) return help();
185
+ if (args.includes('--hook')) return printHook();
186
+
187
+ const asJson = args.includes('--json');
188
+ const result = await evaluate({ force: args.includes('--force') });
189
+
190
+ if (asJson) return console.log(JSON.stringify(result));
191
+ if (result.status !== 'update-available') return;
192
+
193
+ const when = result.installedAt ? ` (installed ${String(result.installedAt).slice(0, 10)})` : '';
194
+ console.log(
195
+ `Linchpin skills ${result.installed} → ${result.latest} available${when}. Update with: ${result.command}`
196
+ );
197
+ }
198
+
199
+ main().catch(() => {}); // Never let this be the reason a session start fails.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linchpinagency/skills",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Linchpin's library of reusable AI agent skills for WordPress projects.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: task-tracking
3
- description: Associate every unit of work with a task in Linchpin's task platform (currently ClickUp, via the ClickUp MCP) with the least possible friction, update that task when the work lands, and leave a handoff on it when stopping mid-flight. Use whenever starting work, creating a TODO, preparing to commit, finishing a change, or pausing work someone else may pick up. Resolve a ClickUp task from what the user gave you (ID/custom-ID/URL) or by searching; if none exists, confirm NO-TASK and keep working. The conventional-commit scope carries the task key (e.g. LINCHPIN-5113) or NO-TASK.
4
- version: 1.2.0
3
+ description: Associate every unit of work with a task in Linchpin's task platform (currently ClickUp, via the ClickUp MCP) with the least friction, update it when the work lands, and leave a handoff when stopping mid-flight. Use whenever starting work, creating a TODO, preparing to commit, finishing a change, or pausing work someone else may pick up and whenever anyone says "create an issue", "create a task", "file a ticket", or "log a bug", all of which mean a ClickUp task unless they name GitHub. Resolve a task from an ID/custom-ID/URL or by searching; if none exists, confirm NO-TASK and keep working. The conventional-commit scope carries the task key (e.g. LINCHPIN-5113) or NO-TASK.
4
+ version: 1.3.0
5
5
  ---
6
6
 
7
7
  # Task tracking (ClickUp)
@@ -18,6 +18,8 @@ platform change.
18
18
  ## When to use
19
19
 
20
20
  - Starting any unit of work, before cutting a branch.
21
+ - **Anyone asking for an issue, task, ticket, bug, or backlog item to be created** — in any
22
+ wording. See *"Create an issue" means ClickUp* below.
21
23
  - Opening a local TODO that should exist in the task system too.
22
24
  - Preparing to commit and needing the scope key.
23
25
  - Finishing work — the task needs its status and a pointer to the PR.
@@ -32,6 +34,21 @@ Canonical for: resolving, creating, and updating the task; the **scope key** tha
32
34
  commits; branch naming; and the PR ↔ task link. Everything about the commit message *other
33
35
  than the scope* belongs to [`commit-and-release`](../commit-and-release/SKILL.md).
34
36
 
37
+ ## "Create an issue" means ClickUp
38
+
39
+ **"Issue", "task", "ticket", "bug", "backlog item" — all of them mean a ClickUp task here.**
40
+ Asked to create one, run the creation flow in step 3. Being in a GitHub repo, reviewing a
41
+ PR, or reading `gh` output does not make "create an issue" mean a GitHub issue. Which space,
42
+ folder, and (for multi-site clients) which site it lands in is
43
+ [`engagement-types`](../engagement-types/SKILL.md)'s call.
44
+
45
+ **A GitHub issue only when GitHub is named** — "open a *GitHub* issue", "file it in the
46
+ repo's issues", "`gh issue create`". Open it with `gh issue create`; if it's work Linchpin
47
+ will do, create the ClickUp task too and cross-link them (issue body →
48
+ `app.clickup.com/t/<KEY>`; `clickup_create_comment` → issue URL). ClickUp stays the system
49
+ of record. For the genuinely ambiguous — a public repo where issues *are* the tracker — ask
50
+ once with `AskUserQuestion`, recommending ClickUp.
51
+
35
52
  ## Vocabulary
36
53
 
37
54
  - **Task key / issue key** — ClickUp's *custom ID*, e.g. `LINCHPIN-5113`. Space-scoped, so
@@ -78,6 +95,10 @@ Don't ask repeatedly and don't nag — one prompt at commit time.
78
95
 
79
96
  ### 3. Creation flow (least friction)
80
97
 
98
+ Reached two ways: from step 2 (a NO-TASK change about to be committed), or directly, when
99
+ someone just says *"create an issue/task for X"* — that's a standalone request and doesn't
100
+ need a commit or a branch behind it.
101
+
81
102
  `clickup_create_task` requires a `list_id` and `name`. Resolve the list with the cheapest
82
103
  path that works:
83
104
 
@@ -192,6 +213,7 @@ Work happens on a dedicated branch opened as a PR against the base branch (usual
192
213
 
193
214
  ## Gotchas
194
215
 
216
+ - **"Issue" is not a GitHub word here.** Route it to ClickUp unless GitHub was named.
195
217
  - **Search before creating** — avoid duplicate tasks; an open task often already exists.
196
218
  - **Don't dump the hierarchy.** 36 spaces is overwhelming; always scope `space_ids` and go
197
219
  only as deep as you need (`max_depth`).
@@ -216,11 +238,10 @@ Work happens on a dedicated branch opened as a PR against the base branch (usual
216
238
  | Move the status | `clickup_update_task` (valid statuses come from the List) |
217
239
  | Hand off mid-flight | `clickup_create_comment` with the five-line handoff block |
218
240
 
219
- Where a *new* task belongs — which space, folder, and (on multi-site clients) which site —
220
- is decided by [`engagement-types`](../engagement-types/SKILL.md).
221
-
222
241
  ## Guardrails
223
242
 
243
+ - **Never open a GitHub issue in place of a ClickUp task.** "Create an issue" means ClickUp;
244
+ `gh issue create` needs the user to have said *GitHub*.
224
245
  - **Never invent a task key.** If you can't resolve one, `NO-TASK` is the correct answer.
225
246
  - **Never block the user** waiting for a task decision — NO-TASK is always available.
226
247
  - **Never mark a task complete** on your own judgment. An open PR is at most "in review";
@@ -233,6 +254,8 @@ is decided by [`engagement-types`](../engagement-types/SKILL.md).
233
254
 
234
255
  ## Done
235
256
 
257
+ - [ ] Any "create an issue/task/ticket" request produced a **ClickUp** task — or a GitHub
258
+ issue only because the user named GitHub, in which case the two are cross-linked.
236
259
  - [ ] The unit of work has a resolved task key or an explicit, user-accepted `NO-TASK`.
237
260
  - [ ] The branch name matches the key (`issue/<KEY>` or `no-task/<slug>`).
238
261
  - [ ] Every commit on the branch carries the same scope.