@linchpinagency/skills 0.1.7 → 0.1.9
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 +28 -1
- package/bin/install.mjs +75 -6
- package/bin/update-check.mjs +199 -0
- package/package.json +1 -1
- package/skills/task-tracking/SKILL.md +12 -7
- package/skills/task-tracking/references/clickup-json.md +125 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ GitHub Copilot, and other compatible coding agents.
|
|
|
12
12
|

|
|
13
13
|
|
|
14
14
|
<!-- x-release-please-start-version -->
|
|
15
|
-
### Latest release: 0.1.
|
|
15
|
+
### Latest release: 0.1.9
|
|
16
16
|
<!-- x-release-please-end -->
|
|
17
17
|
|
|
18
18
|
| Release | Skill standard | Install |
|
|
@@ -191,6 +191,33 @@ npx @linchpinagency/skills --skip-upstream
|
|
|
191
191
|
**Updating:** re-run the same command. The installer overwrites each skill in place, so a
|
|
192
192
|
fresh run always pulls the latest published version.
|
|
193
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
|
+
|
|
194
221
|
### Where skills land
|
|
195
222
|
|
|
196
223
|
| Agent (`--agent`) | Project scope | Global scope (`--global`) |
|
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
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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)
|
|
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
|
@@ -102,9 +102,11 @@ need a commit or a branch behind it.
|
|
|
102
102
|
`clickup_create_task` requires a `list_id` and `name`. Resolve the list with the cheapest
|
|
103
103
|
path that works:
|
|
104
104
|
|
|
105
|
-
1. **
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
1. **Read `.clickup.json` first.** If the repo root has one, it pins the Space, a default
|
|
106
|
+
list, and often a routing map from source directory → list — confirm the destination in
|
|
107
|
+
one line ("Create in *Mantle › Modules › Security*?") rather than making the user
|
|
108
|
+
navigate. A ClickUp section in the project's `CLAUDE.md`/`AGENTS.md` counts too. Schema
|
|
109
|
+
and lookup order: [`references/clickup-json.md`](references/clickup-json.md).
|
|
108
110
|
2. **Otherwise present a picker** built from `clickup_get_workspace_hierarchy`:
|
|
109
111
|
- Call it with `max_depth: 2` **scoped to the likely Space** (pass `space_ids`) so you
|
|
110
112
|
return Folders + Lists for one space, not the whole workspace.
|
|
@@ -202,10 +204,13 @@ Work happens on a dedicated branch opened as a PR against the base branch (usual
|
|
|
202
204
|
|
|
203
205
|
## Reducing friction
|
|
204
206
|
|
|
205
|
-
- **Pin
|
|
206
|
-
the
|
|
207
|
-
confirm instead of navigation
|
|
208
|
-
`
|
|
207
|
+
- **Pin the routing per project in `.clickup.json`.** A small file at the repo root holding
|
|
208
|
+
the Space, a default list, and (where the board mirrors the code) a directory → list map,
|
|
209
|
+
so creation becomes a one-line confirm instead of navigation. Schema, worked example, and
|
|
210
|
+
packaging notes: [`references/clickup-json.md`](references/clickup-json.md). The ids belong
|
|
211
|
+
in that project's repo, not in this shared library.
|
|
212
|
+
- **Write the file when you had to look it up.** Resolving a list the slow way is the moment
|
|
213
|
+
to offer to pin it — otherwise the next agent pays the same cost.
|
|
209
214
|
- **Remember the last-used list** within a session and reuse it.
|
|
210
215
|
- **Infer the Space from the repo** to scope every search and hierarchy call.
|
|
211
216
|
- **Batch the questions**: when you must ask, resolve task-vs-NO-TASK and (if creating) the
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# `.clickup.json` — per-project ClickUp routing
|
|
2
|
+
|
|
3
|
+
A small file at a project's repo root that pins **where this project's tasks live**, so
|
|
4
|
+
creating one is a single confirmation instead of a workspace-hierarchy crawl across dozens of
|
|
5
|
+
spaces.
|
|
6
|
+
|
|
7
|
+
The **convention** is portable and lives here. The **IDs** are project-specific and live in
|
|
8
|
+
that project's repo — never in this library.
|
|
9
|
+
|
|
10
|
+
## Why it exists
|
|
11
|
+
|
|
12
|
+
Without it, every "create a task for X" starts the same way: call
|
|
13
|
+
`clickup_get_workspace_hierarchy`, guess the Space from the repo name, page through folders,
|
|
14
|
+
and rediscover the same handful of list IDs that were found last week. That's slow, it burns
|
|
15
|
+
context, and it produces inconsistent placement when the guess is wrong.
|
|
16
|
+
|
|
17
|
+
With it, the flow is: read the file, confirm the list in one line, create.
|
|
18
|
+
|
|
19
|
+
## Where to look
|
|
20
|
+
|
|
21
|
+
Check in this order, and stop at the first hit:
|
|
22
|
+
|
|
23
|
+
1. `.clickup.json` at the repo root
|
|
24
|
+
2. A ClickUp section in the project's `CLAUDE.md` / `AGENTS.md`
|
|
25
|
+
3. Nothing pinned → fall back to the hierarchy lookup in the main skill, and **offer to write
|
|
26
|
+
`.clickup.json`** once the list has been resolved, so the next agent doesn't repeat the work
|
|
27
|
+
|
|
28
|
+
## Schema
|
|
29
|
+
|
|
30
|
+
Every field is optional except `space` and `defaultList` — a two-key file is already useful.
|
|
31
|
+
|
|
32
|
+
| Key | Type | Purpose |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `space` | object | `id`, `name`, and `customIdPrefix` (e.g. `MANTLE` — the prefix on custom IDs, used to sanity-check a key before putting it in a commit scope) |
|
|
35
|
+
| `defaultList` | object | `id`, `name`, `path`, and a `use` string saying what belongs there. The fallback for anything that doesn't route elsewhere |
|
|
36
|
+
| `lists` | object | Human list name → list id. Flat map; use `›` in the name for nesting when two lists share a name |
|
|
37
|
+
| `folders` | object | Human folder name → folder id. Only needed when a tool call wants a folder rather than a list |
|
|
38
|
+
| `moduleRouting` | object | Source directory → list **name** (a key in `lists`). For repos whose board mirrors their code structure |
|
|
39
|
+
| `unmapped` | object | Deliberate code↔board mismatches, recorded so they read as intentional rather than as failed lookups |
|
|
40
|
+
|
|
41
|
+
Two rules that matter more than the shape:
|
|
42
|
+
|
|
43
|
+
- **IDs are the contract; names are for humans.** ClickUp list names get renamed freely and
|
|
44
|
+
the id survives it. Route on the id, show the name.
|
|
45
|
+
- **`moduleRouting` points at names, not ids** — so a renamed list is a one-line fix in
|
|
46
|
+
`lists` rather than a find-and-replace through the routing map.
|
|
47
|
+
|
|
48
|
+
## Example
|
|
49
|
+
|
|
50
|
+
Trimmed from a real plugin repo whose ClickUp board has one list per code module:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"$comment": "ClickUp routing for this repo. IDs are workspace-stable; names are for humans.",
|
|
55
|
+
"space": {
|
|
56
|
+
"id": "90140515528",
|
|
57
|
+
"name": "Mantle",
|
|
58
|
+
"customIdPrefix": "MANTLE"
|
|
59
|
+
},
|
|
60
|
+
"defaultList": {
|
|
61
|
+
"id": "901401607739",
|
|
62
|
+
"name": "Product Roadmap",
|
|
63
|
+
"path": "Mantle › Product Roadmap",
|
|
64
|
+
"use": "Cross-cutting work, new modules, and anything that doesn't map to a single existing module."
|
|
65
|
+
},
|
|
66
|
+
"lists": {
|
|
67
|
+
"Product Roadmap": "901401607739",
|
|
68
|
+
"Housekeeping": "901414301271",
|
|
69
|
+
"Optimizations": "901413938412",
|
|
70
|
+
"Security": "901413938417",
|
|
71
|
+
"Declutter": "901413954051"
|
|
72
|
+
},
|
|
73
|
+
"folders": {
|
|
74
|
+
"Modules": "90147467026"
|
|
75
|
+
},
|
|
76
|
+
"moduleRouting": {
|
|
77
|
+
"$comment": "includes/Modules/<Dir> → list name. Fall back to defaultList.",
|
|
78
|
+
"Optimizations": "Optimizations",
|
|
79
|
+
"Security": "Security"
|
|
80
|
+
},
|
|
81
|
+
"unmapped": {
|
|
82
|
+
"modulesWithoutList": [
|
|
83
|
+
"Maintenance — no dedicated list; use Product Roadmap"
|
|
84
|
+
],
|
|
85
|
+
"listsWithoutModule": [
|
|
86
|
+
"Declutter — intended home for admin-menu tidying; no module exists yet"
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`$comment` keys are ignored by every JSON parser and are the only way to annotate JSON —
|
|
93
|
+
use them, since this file is read by people as often as by agents.
|
|
94
|
+
|
|
95
|
+
## `unmapped` is the part people skip
|
|
96
|
+
|
|
97
|
+
A board and a codebase drift apart. A list gets created for work that was never built; a
|
|
98
|
+
module ships without anyone adding a list for it. An agent that finds no route for
|
|
99
|
+
`includes/Modules/Maintenance` can't tell "nobody added it" from "I looked in the wrong
|
|
100
|
+
place", so it either asks a pointless question or files the task somewhere wrong.
|
|
101
|
+
|
|
102
|
+
Recording the mismatch converts a lookup failure into a documented decision. Keep it honest
|
|
103
|
+
and prune it when the gap closes.
|
|
104
|
+
|
|
105
|
+
## Packaging
|
|
106
|
+
|
|
107
|
+
Projects that build a distributable — WordPress plugins and themes especially — should
|
|
108
|
+
exclude the file from the build:
|
|
109
|
+
|
|
110
|
+
- `.distignore` for `wp dist-archive` / plugin zips
|
|
111
|
+
- `.npmignore` or a `files` allowlist for npm packages
|
|
112
|
+
|
|
113
|
+
It's dev-time metadata with no runtime meaning. It contains no secrets — workspace, folder,
|
|
114
|
+
and list IDs are not credentials, and the ClickUp API still requires a token — so it can be
|
|
115
|
+
committed to a private repo without concern. Treat it the same as any other project config
|
|
116
|
+
in a public repo: harmless, but pointless to publish.
|
|
117
|
+
|
|
118
|
+
## Keeping it current
|
|
119
|
+
|
|
120
|
+
- A renamed list keeps its id — nothing to do.
|
|
121
|
+
- A **new** list, or one that gets retired, needs the map updated. Cheapest moment is when
|
|
122
|
+
you notice the drift while creating a task; fix it in the same PR.
|
|
123
|
+
- If a lookup by the pinned id fails, the list was deleted or moved out of the space. Re-run
|
|
124
|
+
the hierarchy lookup, correct the file, and say so — don't silently fall back to the
|
|
125
|
+
default list, or tasks quietly pile up in the wrong place.
|