tracerkit 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -34
- package/dist/bin.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{uninstall-k_ZGY-Mk.js → uninstall-Ba_zfPOx.js} +125 -151
- package/package.json +2 -2
- package/skills/brief/SKILL.md +6 -6
- package/skills/check/SKILL.md +15 -15
- package/skills/plan/SKILL.md +47 -3
- package/skills/prd/SKILL.md +8 -6
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/tracerkit)
|
|
10
10
|
[](https://opensource.org/licenses/MIT)
|
|
11
11
|
|
|
12
|
-
Replace ad-hoc AI prompts with a repeatable spec-driven workflow: from idea to verified
|
|
12
|
+
Replace ad-hoc AI prompts with a repeatable spec-driven workflow: from idea to verified spec.
|
|
13
13
|
|
|
14
14
|
Named after the tracer-bullet technique from _The Pragmatic Programmer_: **Tracer** + **Kit**.
|
|
15
15
|
|
|
@@ -19,9 +19,9 @@ Named after the tracer-bullet technique from _The Pragmatic Programmer_: **Trace
|
|
|
19
19
|
|
|
20
20
|
## Why TracerKit?
|
|
21
21
|
|
|
22
|
-
AI assistants work best with small, well-scoped tasks
|
|
22
|
+
AI assistants work best with small, well-scoped tasks — not sprawling layers or flat task lists. TracerKit structures every feature as **tracer-bullet vertical slices**: each phase cuts through every layer (schema → service → API → UI → tests) and is demoable on its own. Integration problems surface early, not at the end.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Three skills drive the workflow: **define** (`/tk:prd`), **plan** (`/tk:plan`), **verify** (`/tk:check`). The AI reads your specs directly, counts progress, and marks completed work done. Pure Markdown, zero runtime deps.
|
|
25
25
|
|
|
26
26
|
## Get Started
|
|
27
27
|
|
|
@@ -75,10 +75,10 @@ You: # open the plan, implement each phase, write tests...
|
|
|
75
75
|
|
|
76
76
|
You: /tk:check dark-mode-support
|
|
77
77
|
AI: Status: done | Total: 5/5
|
|
78
|
-
|
|
78
|
+
Marked complete in .tracerkit/prds/dark-mode-support.md
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
Between sessions, `/tk:brief` shows active features and picks up where you left off:
|
|
82
82
|
|
|
83
83
|
```
|
|
84
84
|
You: /tk:brief
|
|
@@ -101,7 +101,7 @@ tracerkit config storage github # set current project to use GitHub
|
|
|
101
101
|
tracerkit config github.repo org/repo # set target repo
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
PRDs and plans become GitHub Issues with `tk:prd` and `tk:plan` labels. On `/tk:check` pass, issues are closed
|
|
104
|
+
PRDs and plans become GitHub Issues with `tk:prd` and `tk:plan` labels. On `/tk:check` pass, issues are closed with `completed` reason and any related PRs are linked automatically. Each project can use a different backend; local is the default. See [Configuration](docs/configuration.md) for details.
|
|
105
105
|
|
|
106
106
|
To migrate existing artifacts between backends:
|
|
107
107
|
|
|
@@ -109,39 +109,18 @@ To migrate existing artifacts between backends:
|
|
|
109
109
|
tracerkit migrate-storage # local→github or github→local (auto-detected)
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
Direction is inferred from the current `storage` config. All artifacts are migrated, existing duplicates are skipped, and the config is flipped to the target backend. Source artifacts are left intact as backup.
|
|
112
|
+
Direction is inferred from the current `storage` config. All artifacts are migrated, existing duplicates are skipped, and the config is flipped to the target backend. Source artifacts are left intact as backup.
|
|
113
113
|
|
|
114
114
|
</details>
|
|
115
115
|
|
|
116
116
|
## Skills
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
**Output:** `.tracerkit/prds/<slug>.md` (local) or GitHub Issue with `tk:prd` label
|
|
125
|
-
|
|
126
|
-
### `/tk:plan <slug>`: Create an implementation plan
|
|
127
|
-
|
|
128
|
-
Reads a PRD and breaks it into phased **tracer-bullet vertical slices**. Each phase is a thin but complete path through every layer (schema, service, API, UI, tests), demoable on its own.
|
|
129
|
-
|
|
130
|
-
**Output:** `.tracerkit/plans/<slug>.md` (local) or GitHub Issue with `tk:plan` label
|
|
131
|
-
|
|
132
|
-
### `/tk:brief`: Session briefing
|
|
133
|
-
|
|
134
|
-
Shows active features, their progress, and suggested focus. Use at the start of a session to orient.
|
|
135
|
-
|
|
136
|
-
**Output:** Feature dashboard in the terminal. No files written.
|
|
137
|
-
|
|
138
|
-
### `/tk:check [slug]`: Verify and archive
|
|
139
|
-
|
|
140
|
-
Verifies the codebase against the plan's done-when checkboxes. Runs tests, validates user stories, updates phase progress, and transitions the PRD status. On `done`, archives the PRD and plan to `.tracerkit/archives/<slug>/` automatically.
|
|
141
|
-
|
|
142
|
-
Without arguments, shows a feature dashboard with status and progress before asking which feature to check.
|
|
143
|
-
|
|
144
|
-
**Output:** Verdict block appended to the plan. On `done`: archives to `.tracerkit/archives/<slug>/` (local) or closes both issues (GitHub).
|
|
118
|
+
| Skill | What it does | Output |
|
|
119
|
+
| ------------------ | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
|
120
|
+
| `/tk:prd <idea>` | Interview → codebase scan → structured PRD | `.tracerkit/prds/<slug>.md` or GitHub Issue |
|
|
121
|
+
| `/tk:plan <slug>` | PRD → phased vertical slices, each demoable on its own | `.tracerkit/plans/<slug>.md` or GitHub Issue |
|
|
122
|
+
| `/tk:brief` | Feature dashboard with progress and suggested focus | Terminal only, no files |
|
|
123
|
+
| `/tk:check [slug]` | Verify done-when checkboxes against codebase and tests | Verdict block in plan. On `done`: status updated (local) or issues closed + PRs linked (GitHub) |
|
|
145
124
|
|
|
146
125
|
## Docs
|
|
147
126
|
|
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as e, f as t, i as n, l as r, n as i, o as a, p as o, r as s, s as c, t as l, u } from "./uninstall-
|
|
2
|
+
import { a as e, f as t, i as n, l as r, n as i, o as a, p as o, r as s, s as c, t as l, u } from "./uninstall-Ba_zfPOx.js";
|
|
3
3
|
import { existsSync as d, readFileSync as f, statSync as p } from "node:fs";
|
|
4
4
|
import { dirname as m, join as h, resolve as g } from "node:path";
|
|
5
5
|
import { fileURLToPath as _ } from "node:url";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { c as e, i as t, n, o as r, r as i, t as a, u as o } from "./uninstall-
|
|
1
|
+
import { c as e, i as t, n, o as r, r as i, t as a, u as o } from "./uninstall-Ba_zfPOx.js";
|
|
2
2
|
export { r as COMMANDS, e as DEPRECATED_SKILLS, o as SKILL_NAMES, i as init, n as migrateStorage, a as uninstall, t as update };
|
|
@@ -6,8 +6,7 @@ import { execSync as d } from "node:child_process";
|
|
|
6
6
|
//#region src/config.ts
|
|
7
7
|
var f = "local", p = "github", m = [f, p], h = {
|
|
8
8
|
prds: ".tracerkit/prds",
|
|
9
|
-
plans: ".tracerkit/plans"
|
|
10
|
-
archives: ".tracerkit/archives"
|
|
9
|
+
plans: ".tracerkit/plans"
|
|
11
10
|
}, g = { labels: {
|
|
12
11
|
prd: "tk:prd",
|
|
13
12
|
plan: "tk:plan"
|
|
@@ -26,24 +25,23 @@ function _(t) {
|
|
|
26
25
|
throw Error("Invalid .tracerkit/config.json — expected valid JSON");
|
|
27
26
|
}
|
|
28
27
|
return {
|
|
29
|
-
storage:
|
|
30
|
-
paths:
|
|
31
|
-
github:
|
|
28
|
+
storage: v(i.storage),
|
|
29
|
+
paths: y(i.paths),
|
|
30
|
+
github: b(i.github)
|
|
32
31
|
};
|
|
33
32
|
}
|
|
34
|
-
function
|
|
33
|
+
function v(e) {
|
|
35
34
|
return typeof e == "string" && m.includes(e) ? e : f;
|
|
36
35
|
}
|
|
37
|
-
function
|
|
38
|
-
let t =
|
|
36
|
+
function y(e) {
|
|
37
|
+
let t = C(e) ? e : {};
|
|
39
38
|
return {
|
|
40
39
|
prds: typeof t.prds == "string" ? t.prds : h.prds,
|
|
41
|
-
plans: typeof t.plans == "string" ? t.plans : h.plans
|
|
42
|
-
archives: typeof t.archives == "string" ? t.archives : h.archives
|
|
40
|
+
plans: typeof t.plans == "string" ? t.plans : h.plans
|
|
43
41
|
};
|
|
44
42
|
}
|
|
45
|
-
function
|
|
46
|
-
let t =
|
|
43
|
+
function b(e) {
|
|
44
|
+
let t = C(e) ? e : {}, n = C(t.labels) ? t.labels : {};
|
|
47
45
|
return {
|
|
48
46
|
...typeof t.repo == "string" ? { repo: t.repo } : {},
|
|
49
47
|
labels: {
|
|
@@ -52,7 +50,7 @@ function y(e) {
|
|
|
52
50
|
}
|
|
53
51
|
};
|
|
54
52
|
}
|
|
55
|
-
function
|
|
53
|
+
function x(r, i) {
|
|
56
54
|
let o = c(r, ".tracerkit", "config.json");
|
|
57
55
|
t(s(o), { recursive: !0 });
|
|
58
56
|
let l = {};
|
|
@@ -61,31 +59,31 @@ function b(r, i) {
|
|
|
61
59
|
} catch {
|
|
62
60
|
l = {};
|
|
63
61
|
}
|
|
64
|
-
let u =
|
|
62
|
+
let u = S(l, i);
|
|
65
63
|
a(o, JSON.stringify(u, null, 2) + "\n");
|
|
66
64
|
}
|
|
67
|
-
function
|
|
65
|
+
function S(e, t) {
|
|
68
66
|
let n = { ...e };
|
|
69
|
-
for (let e of Object.keys(t))
|
|
67
|
+
for (let e of Object.keys(t)) C(n[e]) && C(t[e]) ? n[e] = S(n[e], t[e]) : n[e] = t[e];
|
|
70
68
|
return n;
|
|
71
69
|
}
|
|
72
|
-
function
|
|
70
|
+
function C(e) {
|
|
73
71
|
return typeof e == "object" && !!e && !Array.isArray(e);
|
|
74
72
|
}
|
|
75
|
-
var
|
|
73
|
+
var w = [
|
|
76
74
|
"tk:brief",
|
|
77
75
|
"tk:prd",
|
|
78
76
|
"tk:plan",
|
|
79
77
|
"tk:check"
|
|
80
|
-
],
|
|
78
|
+
], T = ["tk:verify"], E = {
|
|
81
79
|
force: "--force",
|
|
82
80
|
help: "--help",
|
|
83
81
|
version: "--version"
|
|
84
|
-
},
|
|
82
|
+
}, D = [
|
|
85
83
|
"brief",
|
|
86
84
|
"progress",
|
|
87
85
|
"archive"
|
|
88
|
-
],
|
|
86
|
+
], O = [
|
|
89
87
|
{
|
|
90
88
|
name: "init",
|
|
91
89
|
args: "[path]",
|
|
@@ -111,44 +109,44 @@ var C = [
|
|
|
111
109
|
args: "[path]",
|
|
112
110
|
desc: "Migrate artifacts between local and GitHub storage backends"
|
|
113
111
|
}
|
|
114
|
-
],
|
|
115
|
-
function
|
|
112
|
+
], k = c(s(u(import.meta.url)), "..", "skills");
|
|
113
|
+
function A(e, t = "") {
|
|
116
114
|
let n = r(e, { withFileTypes: !0 }), i = [];
|
|
117
115
|
for (let r of n) {
|
|
118
116
|
let n = t ? `${t}/${r.name}` : r.name;
|
|
119
|
-
r.isDirectory() ? i.push(...
|
|
117
|
+
r.isDirectory() ? i.push(...A(c(e, r.name), n)) : i.push(n);
|
|
120
118
|
}
|
|
121
119
|
return i.sort();
|
|
122
120
|
}
|
|
123
|
-
function
|
|
121
|
+
function j(e) {
|
|
124
122
|
return `.claude/skills/tk:${e}`;
|
|
125
123
|
}
|
|
126
|
-
function
|
|
124
|
+
function M(e) {
|
|
127
125
|
return e.slice(18);
|
|
128
126
|
}
|
|
129
|
-
function
|
|
127
|
+
function N(e, t) {
|
|
130
128
|
let n = e;
|
|
131
|
-
return t.paths.prds !== h.prds && (n = n.replaceAll(h.prds, t.paths.prds)), t.paths.plans !== h.plans && (n = n.replaceAll(h.plans, t.paths.plans)), t.
|
|
129
|
+
return t.paths.prds !== h.prds && (n = n.replaceAll(h.prds, t.paths.prds)), t.paths.plans !== h.plans && (n = n.replaceAll(h.plans, t.paths.plans)), t.github?.repo && (n = n.replaceAll("{{github.repo}}", t.github.repo)), t.github?.labels?.prd && (n = n.replaceAll("{{github.labels.prd}}", t.github.labels.prd)), t.github?.labels?.plan && (n = n.replaceAll("{{github.labels.plan}}", t.github.labels.plan)), n;
|
|
132
130
|
}
|
|
133
|
-
function
|
|
134
|
-
let o = i ?? k
|
|
131
|
+
function P(e, r, i) {
|
|
132
|
+
let o = i ?? A(k).map(j);
|
|
135
133
|
for (let i of o) {
|
|
136
|
-
let o = c(
|
|
137
|
-
t(s(l), { recursive: !0 }), a(l,
|
|
134
|
+
let o = c(k, M(i)), l = c(e, i);
|
|
135
|
+
t(s(l), { recursive: !0 }), a(l, N(n(o, "utf8"), r));
|
|
138
136
|
}
|
|
139
137
|
return { copied: o };
|
|
140
138
|
}
|
|
141
|
-
function
|
|
139
|
+
function F(e) {
|
|
142
140
|
return l("sha256").update(e).digest("hex");
|
|
143
141
|
}
|
|
144
|
-
function
|
|
145
|
-
let i = k
|
|
142
|
+
function I(t, r) {
|
|
143
|
+
let i = A(k).map(j), a = [], o = [], s = [];
|
|
146
144
|
for (let l of i) {
|
|
147
145
|
let i = c(t, l);
|
|
148
146
|
if (!e(i)) s.push(l);
|
|
149
147
|
else {
|
|
150
|
-
let e =
|
|
151
|
-
|
|
148
|
+
let e = N(n(c(k, M(l)), "utf8"), r);
|
|
149
|
+
F(Buffer.from(e)) === F(n(i)) ? a.push(l) : o.push(l);
|
|
152
150
|
}
|
|
153
151
|
}
|
|
154
152
|
return {
|
|
@@ -159,10 +157,10 @@ function F(t, r) {
|
|
|
159
157
|
}
|
|
160
158
|
//#endregion
|
|
161
159
|
//#region src/commands/update.ts
|
|
162
|
-
function
|
|
163
|
-
if (!
|
|
164
|
-
let r = _(t), { unchanged: a, modified: o, missing: s } =
|
|
165
|
-
for (let n of
|
|
160
|
+
function L(t, n) {
|
|
161
|
+
if (!w.some((n) => e(c(t, ".claude", "skills", n)))) throw Error("TracerKit not initialized — run `tracerkit init` first");
|
|
162
|
+
let r = _(t), { unchanged: a, modified: o, missing: s } = I(t, r), l = [];
|
|
163
|
+
for (let n of T) {
|
|
166
164
|
let r = c(t, ".claude", "skills", n);
|
|
167
165
|
e(r) && (i(r, {
|
|
168
166
|
recursive: !0,
|
|
@@ -175,7 +173,7 @@ function I(t, n) {
|
|
|
175
173
|
...u ? o : []
|
|
176
174
|
];
|
|
177
175
|
if (d.length > 0) {
|
|
178
|
-
|
|
176
|
+
P(t, r, d);
|
|
179
177
|
for (let e of a) l.push(`✓ ${e}`);
|
|
180
178
|
for (let e of s) l.push(`✓ ${e} (added)`);
|
|
181
179
|
if (u) for (let e of o) l.push(`✓ ${e} (replaced)`);
|
|
@@ -188,14 +186,14 @@ function I(t, n) {
|
|
|
188
186
|
}
|
|
189
187
|
//#endregion
|
|
190
188
|
//#region src/commands/init.ts
|
|
191
|
-
function
|
|
192
|
-
if (
|
|
193
|
-
let { copied: n } =
|
|
189
|
+
function ee(t) {
|
|
190
|
+
if (w.some((n) => e(c(t, ".claude", "skills", n)))) return L(t, { force: !1 });
|
|
191
|
+
let { copied: n } = P(t, _(t));
|
|
194
192
|
return n.map((e) => `✓ ${e}`);
|
|
195
193
|
}
|
|
196
194
|
//#endregion
|
|
197
195
|
//#region src/commands/migrate-storage.ts
|
|
198
|
-
var
|
|
196
|
+
var te = {
|
|
199
197
|
created: "tk:created",
|
|
200
198
|
in_progress: "tk:in-progress",
|
|
201
199
|
done: "tk:done"
|
|
@@ -227,7 +225,7 @@ function B(e) {
|
|
|
227
225
|
return t.length === 0 ? "<!-- tk:metadata\n-->" : `<!-- tk:metadata\n${t.map(([e, t]) => `${e}: ${t}`).join("\n")}\n-->`;
|
|
228
226
|
}
|
|
229
227
|
function V(e) {
|
|
230
|
-
return
|
|
228
|
+
return te[e] ?? "tk:created";
|
|
231
229
|
}
|
|
232
230
|
function H(e) {
|
|
233
231
|
let t = e.match(/<!--\s*tk:metadata\n([\s\S]*?)-->\n*([\s\S]*)$/);
|
|
@@ -269,66 +267,44 @@ function K(t, i) {
|
|
|
269
267
|
type: "prd",
|
|
270
268
|
metadata: r,
|
|
271
269
|
body: i,
|
|
272
|
-
title: G(i)
|
|
273
|
-
archived: !1
|
|
270
|
+
title: G(i)
|
|
274
271
|
});
|
|
275
272
|
}
|
|
276
273
|
let l = c(t, i.paths.plans);
|
|
277
274
|
if (e(l)) for (let e of r(l)) {
|
|
278
275
|
if (!e.endsWith(".md")) continue;
|
|
279
|
-
let t = o(e, ".md"), r = n(c(l, e), "utf8");
|
|
276
|
+
let t = o(e, ".md"), r = n(c(l, e), "utf8"), { metadata: i, body: s } = z(r);
|
|
280
277
|
a.push({
|
|
281
278
|
slug: t,
|
|
282
279
|
type: "plan",
|
|
283
|
-
metadata:
|
|
284
|
-
body:
|
|
285
|
-
title: G(r)
|
|
286
|
-
archived: !1
|
|
280
|
+
metadata: i,
|
|
281
|
+
body: s,
|
|
282
|
+
title: G(s || r)
|
|
287
283
|
});
|
|
288
284
|
}
|
|
289
|
-
let u = c(t, i.paths.archives);
|
|
290
|
-
if (e(u)) for (let t of r(u, { withFileTypes: !0 })) {
|
|
291
|
-
if (!t.isDirectory()) continue;
|
|
292
|
-
let r = t.name, i = c(u, r, "prd.md"), o = c(u, r, "plan.md");
|
|
293
|
-
if (e(i)) {
|
|
294
|
-
let { metadata: e, body: t } = z(n(i, "utf8"));
|
|
295
|
-
a.push({
|
|
296
|
-
slug: r,
|
|
297
|
-
type: "prd",
|
|
298
|
-
metadata: {
|
|
299
|
-
...e,
|
|
300
|
-
status: "done"
|
|
301
|
-
},
|
|
302
|
-
body: t,
|
|
303
|
-
title: G(t),
|
|
304
|
-
archived: !0
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
if (e(o)) {
|
|
308
|
-
let e = n(o, "utf8");
|
|
309
|
-
a.push({
|
|
310
|
-
slug: r,
|
|
311
|
-
type: "plan",
|
|
312
|
-
metadata: { status: "done" },
|
|
313
|
-
body: e,
|
|
314
|
-
title: G(e),
|
|
315
|
-
archived: !0
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
285
|
return a;
|
|
320
286
|
}
|
|
321
287
|
function q(e) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
288
|
+
let t = e;
|
|
289
|
+
if (t.code === "ENOENT") return /* @__PURE__ */ Error("gh CLI not found — install it: https://cli.github.com");
|
|
290
|
+
let n = (t.stderr ?? t.message ?? "").toLowerCase();
|
|
291
|
+
return n.includes("not logged in") || n.includes("authentication") ? /* @__PURE__ */ Error("Not authenticated with GitHub. Run: gh auth login") : n.includes("rate limit") || n.includes("403") ? /* @__PURE__ */ Error("GitHub rate limit exceeded. Wait and retry.") : n.includes("not found") || n.includes("404") ? /* @__PURE__ */ Error("Repository not found. Check github.repo in .tracerkit/config.json") : e instanceof Error ? e : Error(String(e));
|
|
292
|
+
}
|
|
293
|
+
function J(e) {
|
|
294
|
+
try {
|
|
295
|
+
return d(`gh ${e.map((e) => `'${e.replace(/'/g, "'\\''")}'`).join(" ")}`, {
|
|
296
|
+
encoding: "utf8",
|
|
297
|
+
stdio: [
|
|
298
|
+
"pipe",
|
|
299
|
+
"pipe",
|
|
300
|
+
"pipe"
|
|
301
|
+
]
|
|
302
|
+
}).trim();
|
|
303
|
+
} catch (e) {
|
|
304
|
+
throw q(e);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function Y(e, t) {
|
|
332
308
|
return e.github.repo ? e.github.repo : t([
|
|
333
309
|
"repo",
|
|
334
310
|
"view",
|
|
@@ -338,7 +314,7 @@ function J(e, t) {
|
|
|
338
314
|
".nameWithOwner"
|
|
339
315
|
]);
|
|
340
316
|
}
|
|
341
|
-
function
|
|
317
|
+
function X(e, t, n) {
|
|
342
318
|
let r = n([
|
|
343
319
|
"issue",
|
|
344
320
|
"list",
|
|
@@ -355,10 +331,16 @@ function Y(e, t, n) {
|
|
|
355
331
|
]);
|
|
356
332
|
return JSON.parse(r || "[]");
|
|
357
333
|
}
|
|
358
|
-
function
|
|
359
|
-
return t.some((t) =>
|
|
334
|
+
function Z(e, t) {
|
|
335
|
+
return t.some((t) => {
|
|
336
|
+
if (t.body) {
|
|
337
|
+
let { metadata: n } = H(t.body);
|
|
338
|
+
if (n.slug) return n.slug === e;
|
|
339
|
+
}
|
|
340
|
+
return W(t.title) === e;
|
|
341
|
+
});
|
|
360
342
|
}
|
|
361
|
-
function
|
|
343
|
+
function ne(e, t, n) {
|
|
362
344
|
for (let r of t) n([
|
|
363
345
|
"label",
|
|
364
346
|
"create",
|
|
@@ -368,7 +350,7 @@ function Z(e, t, n) {
|
|
|
368
350
|
"--force"
|
|
369
351
|
]);
|
|
370
352
|
}
|
|
371
|
-
function
|
|
353
|
+
function re(e, t, n) {
|
|
372
354
|
let r = [
|
|
373
355
|
"issue",
|
|
374
356
|
"create",
|
|
@@ -383,7 +365,7 @@ function ne(e, t, n) {
|
|
|
383
365
|
let i = n(r).match(/\/(\d+)\s*$/);
|
|
384
366
|
return i ? parseInt(i[1], 10) : 0;
|
|
385
367
|
}
|
|
386
|
-
function
|
|
368
|
+
function ie(e, t, n) {
|
|
387
369
|
n([
|
|
388
370
|
"issue",
|
|
389
371
|
"close",
|
|
@@ -392,7 +374,7 @@ function re(e, t, n) {
|
|
|
392
374
|
e
|
|
393
375
|
]);
|
|
394
376
|
}
|
|
395
|
-
function
|
|
377
|
+
function ae(e, t, n) {
|
|
396
378
|
let r = n([
|
|
397
379
|
"pr",
|
|
398
380
|
"list",
|
|
@@ -409,7 +391,7 @@ function ie(e, t, n) {
|
|
|
409
391
|
]);
|
|
410
392
|
return JSON.parse(r || "[]");
|
|
411
393
|
}
|
|
412
|
-
function
|
|
394
|
+
function oe(e, t, n, r) {
|
|
413
395
|
r([
|
|
414
396
|
"issue",
|
|
415
397
|
"comment",
|
|
@@ -420,7 +402,7 @@ function ae(e, t, n, r) {
|
|
|
420
402
|
n
|
|
421
403
|
]);
|
|
422
404
|
}
|
|
423
|
-
function
|
|
405
|
+
function se(e) {
|
|
424
406
|
for (let t of e) {
|
|
425
407
|
let e = R[t];
|
|
426
408
|
if (e) return e;
|
|
@@ -430,82 +412,74 @@ function oe(e) {
|
|
|
430
412
|
function Q(e, n) {
|
|
431
413
|
t(s(e), { recursive: !0 }), a(e, n);
|
|
432
414
|
}
|
|
433
|
-
function
|
|
434
|
-
let n = t?.runGh ??
|
|
435
|
-
return r.storage === "local" ?
|
|
415
|
+
function ce(e, t) {
|
|
416
|
+
let n = t?.runGh ?? J, r = _(e);
|
|
417
|
+
return r.storage === "local" ? le(e, r, n) : ue(e, r, n);
|
|
436
418
|
}
|
|
437
|
-
function
|
|
438
|
-
let r = [], i =
|
|
439
|
-
if (a.length === 0) return
|
|
419
|
+
function le(e, t, n) {
|
|
420
|
+
let r = [], i = Y(t, n), a = K(e, t);
|
|
421
|
+
if (a.length === 0) return x(e, { storage: p }), r.push("No artifacts found — nothing to migrate."), r.push(`✓ Storage switched to "${p}".`), r;
|
|
440
422
|
let o = t.github.labels?.prd ?? "tk:prd", s = t.github.labels?.plan ?? "tk:plan";
|
|
441
|
-
|
|
423
|
+
ne(i, [
|
|
442
424
|
o,
|
|
443
425
|
s,
|
|
444
426
|
...[...new Set(a.map((e) => V(e.metadata.status ?? "created")))]
|
|
445
427
|
], n);
|
|
446
|
-
let c =
|
|
428
|
+
let c = X(i, o, n), l = X(i, s, n);
|
|
447
429
|
for (let e of a) {
|
|
448
430
|
let t = e.type === "prd" ? o : s, a = e.type === "prd" ? c : l;
|
|
449
|
-
if (
|
|
431
|
+
if (Z(e.slug, a)) {
|
|
450
432
|
r.push(`⚠ skip ${e.type} "${e.slug}" — already exists on GitHub`);
|
|
451
433
|
continue;
|
|
452
434
|
}
|
|
453
|
-
let u =
|
|
435
|
+
let u = e.metadata.status ?? "created", d = V(u), f = `${B(e.metadata)}\n\n${e.body.replace(/^\n/, "")}`, p = re(i, {
|
|
454
436
|
title: `[${t}] ${e.slug}: ${e.title}`,
|
|
455
|
-
body:
|
|
456
|
-
labels: [t,
|
|
437
|
+
body: f,
|
|
438
|
+
labels: [t, d]
|
|
457
439
|
}, n);
|
|
458
|
-
|
|
440
|
+
u === "done" ? (ie(i, p, n), $(i, e.slug, p, n), r.push(`✓ ${e.type} "${e.slug}" → issue #${p} (closed)`)) : r.push(`✓ ${e.type} "${e.slug}" → issue #${p}`);
|
|
459
441
|
}
|
|
460
|
-
return
|
|
442
|
+
return x(e, { storage: p }), r.push(`✓ Storage switched to "${p}".`), r;
|
|
461
443
|
}
|
|
462
|
-
function
|
|
463
|
-
let i =
|
|
464
|
-
i.length !== 0 &&
|
|
444
|
+
function $(e, t, n, r) {
|
|
445
|
+
let i = ae(e, t, r);
|
|
446
|
+
i.length !== 0 && oe(e, n, `Linked PR: ${i.map((e) => `#${e.number}`).join(", ")}`, r);
|
|
465
447
|
}
|
|
466
|
-
function
|
|
467
|
-
let i = [], a =
|
|
448
|
+
function ue(t, n, r) {
|
|
449
|
+
let i = [], a = Y(n, r), o = n.github.labels?.prd ?? "tk:prd", s = n.github.labels?.plan ?? "tk:plan", l = X(a, o, r), u = X(a, s, r), d = [...l.map((e) => ({
|
|
468
450
|
...e,
|
|
469
451
|
type: "prd"
|
|
470
452
|
})), ...u.map((e) => ({
|
|
471
453
|
...e,
|
|
472
454
|
type: "plan"
|
|
473
455
|
}))];
|
|
474
|
-
if (d.length === 0) return
|
|
456
|
+
if (d.length === 0) return x(t, { storage: f }), i.push("No GitHub issues found — nothing to migrate."), i.push(`✓ Storage switched to "${f}".`), i;
|
|
475
457
|
for (let r of d) {
|
|
476
|
-
let a = W(r.title);
|
|
477
|
-
if (!
|
|
478
|
-
let
|
|
479
|
-
if (
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
let o = c(t, r.type === "prd" ? n.paths.prds : n.paths.plans, `${a}.md`);
|
|
491
|
-
if (e(o)) {
|
|
492
|
-
i.push(`⚠ skip ${r.type} "${a}" — local file already exists`);
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
r.type === "prd" ? Q(o, `${U({
|
|
496
|
-
...l,
|
|
497
|
-
status: d
|
|
498
|
-
})}\n${u}`) : Q(o, u), i.push(`✓ ${r.type} "${a}" → ${o}`);
|
|
458
|
+
let { metadata: a, body: o } = H(r.body ?? ""), s = a.slug ?? W(r.title);
|
|
459
|
+
if (!s) continue;
|
|
460
|
+
let l = r.labels.map((e) => e.name), u = a.status ?? se(l), d = c(t, r.type === "prd" ? n.paths.prds : n.paths.plans, `${s}.md`);
|
|
461
|
+
if (e(d)) {
|
|
462
|
+
i.push(`⚠ skip ${r.type} "${s}" — local file already exists`);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (r.type === "prd") Q(d, `${U({
|
|
466
|
+
...a,
|
|
467
|
+
status: u
|
|
468
|
+
})}\n${o}`);
|
|
469
|
+
else {
|
|
470
|
+
let e = {};
|
|
471
|
+
a.source_prd && (e.source_prd = a.source_prd), (a.slug || s) && (e.slug = a.slug ?? s), u && (e.status = u), a.completed && (e.completed = a.completed), Q(d, `${U(e)}\n${o}`);
|
|
499
472
|
}
|
|
473
|
+
i.push(`✓ ${r.type} "${s}" → ${d}`);
|
|
500
474
|
}
|
|
501
|
-
return
|
|
475
|
+
return x(t, { storage: f }), i.push(`✓ Storage switched to "${f}".`), i;
|
|
502
476
|
}
|
|
503
477
|
//#endregion
|
|
504
478
|
//#region src/commands/uninstall.ts
|
|
505
|
-
function
|
|
506
|
-
if (!
|
|
479
|
+
function de(t) {
|
|
480
|
+
if (!w.some((n) => e(c(t, ".claude", "skills", n)))) throw Error("TracerKit not initialized — nothing to uninstall");
|
|
507
481
|
let n = [];
|
|
508
|
-
for (let r of
|
|
482
|
+
for (let r of w) {
|
|
509
483
|
let a = c(t, ".claude", "skills", r);
|
|
510
484
|
e(a) && (i(a, {
|
|
511
485
|
recursive: !0,
|
|
@@ -515,4 +489,4 @@ function ue(t) {
|
|
|
515
489
|
return n;
|
|
516
490
|
}
|
|
517
491
|
//#endregion
|
|
518
|
-
export {
|
|
492
|
+
export { P as a, T as c, f as d, _ as f, L as i, E as l, ce as n, O as o, x as p, ee as r, D as s, de as t, w as u };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tracerkit",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Spec-driven workflow for
|
|
3
|
+
"version": "1.16.0",
|
|
4
|
+
"description": "Spec-driven workflow for AI coding agents — PRD → plan → verify. Pure Markdown skills, zero runtime deps.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "helderberto",
|
package/skills/brief/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ Overview of active features, progress, and suggested focus.
|
|
|
12
12
|
|
|
13
13
|
<!-- if:local -->
|
|
14
14
|
|
|
15
|
-
- Available PRDs: !`ls .tracerkit/prds/
|
|
15
|
+
- Available PRDs: !`ls .tracerkit/prds/*.md 2>/dev/null || echo "(none)"`
|
|
16
16
|
<!-- end:local -->
|
|
17
17
|
<!-- if:github -->
|
|
18
18
|
- Available PRDs: list open GitHub Issues with label `{{github.labels.prd}}`
|
|
@@ -31,11 +31,11 @@ For each `.md` file in `.tracerkit/prds/`: parse frontmatter, extract `status` a
|
|
|
31
31
|
|
|
32
32
|
List open GitHub Issues with label `{{github.labels.prd}}`:
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
1. For each issue, parse the `<!-- tk:metadata -->` comment in the body
|
|
35
|
+
2. Extract `status` and `created` fields from the metadata
|
|
36
|
+
3. Also check labels: `tk:created`, `tk:in-progress`
|
|
37
|
+
4. Skip issues with `tk:done` label
|
|
38
|
+
5. The slug is extracted from the title: `[{{github.labels.prd}}] <slug>: ...`
|
|
39
39
|
<!-- end:github -->
|
|
40
40
|
|
|
41
41
|
### 2. Count progress from plans
|
package/skills/check/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Verify implementation against plan. Shows progress, finds blockers, and
|
|
2
|
+
description: Verify implementation against plan. Shows progress, finds blockers, and marks complete when done. Use after implementing a plan, or without arguments to see a feature dashboard.
|
|
3
3
|
argument-hint: '[slug]'
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -7,13 +7,13 @@ argument-hint: '[slug]'
|
|
|
7
7
|
|
|
8
8
|
# Check Implementation
|
|
9
9
|
|
|
10
|
-
Check implementation against a plan. Update checks, stamp findings, transition status, and
|
|
10
|
+
Check implementation against a plan. Update checks, stamp findings, transition status, and mark complete when done.
|
|
11
11
|
|
|
12
12
|
## Pre-loaded context
|
|
13
13
|
|
|
14
14
|
<!-- if:local -->
|
|
15
15
|
|
|
16
|
-
- Available plans: !`ls .tracerkit/plans/
|
|
16
|
+
- Available plans: !`ls .tracerkit/plans/*.md 2>/dev/null || echo "(none)"`
|
|
17
17
|
<!-- end:local -->
|
|
18
18
|
<!-- if:github -->
|
|
19
19
|
- Available plans: list open GitHub Issues with label `{{github.labels.plan}}`
|
|
@@ -175,25 +175,25 @@ Append a verdict block at the bottom of the plan issue body by editing the issue
|
|
|
175
175
|
|
|
176
176
|
If a previous verdict block exists, replace it with the new one.
|
|
177
177
|
|
|
178
|
-
### 7. On `done` —
|
|
178
|
+
### 7. On `done` — mark complete
|
|
179
179
|
|
|
180
180
|
If all checks pass and zero BLOCKERS:
|
|
181
181
|
|
|
182
182
|
<!-- if:local -->
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
1. Copy PRD → `prd.md` (set `status: done`, add `completed` timestamp in frontmatter)
|
|
187
|
-
2. Copy plan → `plan.md` (append `## Archived` with date)
|
|
188
|
-
3. Delete originals
|
|
184
|
+
1. Update PRD frontmatter: `status: done`, add `completed: <UTC ISO 8601>`
|
|
185
|
+
2. Update plan frontmatter: `status: done`, add `completed: <UTC ISO 8601>`
|
|
189
186
|
|
|
190
187
|
<!-- end:local -->
|
|
191
188
|
<!-- if:github -->
|
|
192
189
|
|
|
193
|
-
1.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
190
|
+
1. If on a feature branch with commits ahead of default:
|
|
191
|
+
a. Push branch if not pushed
|
|
192
|
+
b. Open PR with `Closes #<prd-number>, Closes #<plan-number>` (or update existing PR body)
|
|
193
|
+
2. Search merged PRs matching slug: `gh pr list --search <slug> --state merged` — add comment on PRD issue linking found PRs
|
|
194
|
+
3. PRD issue: add `tk:done`, remove `tk:in-progress`, update metadata `status: done` + `completed` timestamp
|
|
195
|
+
4. Close PRD issue (reason: `completed`)
|
|
196
|
+
5. Close plan issue (reason: `completed`)
|
|
197
197
|
|
|
198
198
|
<!-- end:github -->
|
|
199
199
|
|
|
@@ -208,6 +208,6 @@ List the blockers to fix, then re-run `/tk:check <slug>`.
|
|
|
208
208
|
## Rules
|
|
209
209
|
|
|
210
210
|
- The review subagent must be **read-only** — it must not create, edit, or delete any files
|
|
211
|
-
- The only file writes this skill makes are: checkboxes + verdict block in the plan, and the
|
|
211
|
+
- The only file writes this skill makes are: checkboxes + verdict block in the plan, and the status updates on `done`
|
|
212
212
|
- Never modify implementation code — only observe and report
|
|
213
|
-
- If the PRD file is missing but all checks pass, warn and proceed —
|
|
213
|
+
- If the PRD file is missing but all checks pass, warn and proceed — mark the plan complete only (skip PRD status update)
|
package/skills/plan/SKILL.md
CHANGED
|
@@ -24,7 +24,7 @@ Output: a GitHub Issue with label `{{github.labels.plan}}`.
|
|
|
24
24
|
|
|
25
25
|
<!-- if:local -->
|
|
26
26
|
|
|
27
|
-
- Available PRDs: !`ls .tracerkit/prds/
|
|
27
|
+
- Available PRDs: !`ls .tracerkit/prds/*.md 2>/dev/null || echo "(none)"`
|
|
28
28
|
<!-- end:local -->
|
|
29
29
|
<!-- if:github -->
|
|
30
30
|
- Available PRDs: list open GitHub Issues with label `{{github.labels.prd}}`
|
|
@@ -102,7 +102,7 @@ Each phase: thin vertical slice through all layers (schema → service → API
|
|
|
102
102
|
|
|
103
103
|
**Phase naming:** use a goal phrase answering "what can we demo when this is done?" (e.g., "Phase 1 — Revenue visible end-to-end"), not a layer name.
|
|
104
104
|
|
|
105
|
-
**Done when:** checkbox list of atomic, verifiable conditions
|
|
105
|
+
**Done when:** checkbox list of atomic, verifiable conditions. Each must name a test file/name, a shell command, or a file+content to verify. No prose-only conditions. Test: "Can an agent verify by reading files, running a command, or checking a test?"
|
|
106
106
|
|
|
107
107
|
**Layer-by-layer exception:** if complex schema changes underpin all modules and no story stands alone, build data foundation first, then slice vertically.
|
|
108
108
|
|
|
@@ -131,15 +131,23 @@ Present breakdown (title, user stories covered, done-when per phase). Ask: granu
|
|
|
131
131
|
Save to `.tracerkit/plans/<slug>.md` (create dir if missing).
|
|
132
132
|
|
|
133
133
|
```markdown
|
|
134
|
+
---
|
|
135
|
+
source_prd: .tracerkit/prds/<slug>.md
|
|
136
|
+
slug: <slug>
|
|
137
|
+
status: in_progress
|
|
138
|
+
---
|
|
139
|
+
|
|
134
140
|
# Plan: <Feature Name>
|
|
135
141
|
|
|
136
142
|
> Source PRD: `.tracerkit/prds/<slug>.md`
|
|
137
143
|
```
|
|
138
144
|
|
|
145
|
+
Then update PRD frontmatter: add `plan: .tracerkit/plans/<slug>.md` field.
|
|
146
|
+
|
|
139
147
|
<!-- end:local -->
|
|
140
148
|
<!-- if:github -->
|
|
141
149
|
|
|
142
|
-
Ensure labels exist: `{{github.labels.plan}}`, `tk:in-progress
|
|
150
|
+
Ensure labels exist: `gh label create {{github.labels.plan}} --repo {{github.repo}} --force`, `gh label create tk:in-progress --repo {{github.repo}} --force`.
|
|
143
151
|
|
|
144
152
|
Create GitHub Issue — title: `[{{github.labels.plan}}] <slug>: Plan: <Feature Title>`, labels: `{{github.labels.plan}}`, `tk:in-progress`.
|
|
145
153
|
|
|
@@ -147,6 +155,7 @@ Create GitHub Issue — title: `[{{github.labels.plan}}] <slug>: Plan: <Feature
|
|
|
147
155
|
<!-- tk:metadata
|
|
148
156
|
source_prd: #<PRD issue number>
|
|
149
157
|
slug: <slug>
|
|
158
|
+
status: in_progress
|
|
150
159
|
-->
|
|
151
160
|
|
|
152
161
|
# Plan: <Feature Name>
|
|
@@ -156,6 +165,19 @@ slug: <slug>
|
|
|
156
165
|
|
|
157
166
|
<!-- end:github -->
|
|
158
167
|
|
|
168
|
+
### 6b. Backlink PRD
|
|
169
|
+
|
|
170
|
+
<!-- if:local -->
|
|
171
|
+
|
|
172
|
+
Already linked via PRD frontmatter `plan:` field (set in step 6).
|
|
173
|
+
|
|
174
|
+
<!-- end:local -->
|
|
175
|
+
<!-- if:github -->
|
|
176
|
+
|
|
177
|
+
Add comment on PRD issue: "Plan: #<plan-issue-number>" (creates cross-reference).
|
|
178
|
+
|
|
179
|
+
<!-- end:github -->
|
|
180
|
+
|
|
159
181
|
Use this structure for the plan body:
|
|
160
182
|
|
|
161
183
|
```markdown
|
|
@@ -195,6 +217,28 @@ Gaps found in the PRD needing resolution. Blank if none.
|
|
|
195
217
|
|
|
196
218
|
Print one line per phase: `Phase N — <title> (<condition summary>)`. Then ask: "Run `/tk:check <slug>` when ready?"
|
|
197
219
|
|
|
220
|
+
## Execution guidance
|
|
221
|
+
|
|
222
|
+
When implementing this plan, **always offer to create a feature branch** before writing any code:
|
|
223
|
+
|
|
224
|
+
> "Create branch `feat/<slug>` for this work? (Y/n)"
|
|
225
|
+
|
|
226
|
+
If accepted, create the branch from the default branch.
|
|
227
|
+
|
|
228
|
+
### During implementation
|
|
229
|
+
|
|
230
|
+
Mark each "Done when" checkbox `[x]` **immediately after verifying** the condition.
|
|
231
|
+
|
|
232
|
+
Always update the local plan file (`.tracerkit/plans/<slug>.md`): change `- [ ]` → `- [x]`. This file is the working copy for both local and GitHub modes.
|
|
233
|
+
|
|
234
|
+
<!-- if:github -->
|
|
235
|
+
|
|
236
|
+
**Sync to GitHub at phase boundaries**: after completing all items in a phase, update the plan issue body with `gh issue edit` to reflect the local state. This avoids per-item API calls.
|
|
237
|
+
|
|
238
|
+
After all phases, open a PR with body containing `Closes #<prd-issue>, Closes #<plan-issue>`.
|
|
239
|
+
|
|
240
|
+
<!-- end:github -->
|
|
241
|
+
|
|
198
242
|
## Rules
|
|
199
243
|
|
|
200
244
|
- Phases derive from PRD user stories — never invented
|
package/skills/prd/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ Skip satisfied steps. If argument provided, skip to Step 2.
|
|
|
13
13
|
|
|
14
14
|
<!-- if:local -->
|
|
15
15
|
|
|
16
|
-
- Existing PRDs: !`ls .tracerkit/prds/
|
|
16
|
+
- Existing PRDs: !`ls .tracerkit/prds/*.md 2>/dev/null || echo "(none)"`
|
|
17
17
|
<!-- end:local -->
|
|
18
18
|
<!-- if:github -->
|
|
19
19
|
- Existing PRDs: list open GitHub Issues with label `{{github.labels.prd}}`
|
|
@@ -26,10 +26,11 @@ The argument is: $ARGUMENTS
|
|
|
26
26
|
If empty, go to Step 1; derive slug after gathering the idea. If provided, derive slug:
|
|
27
27
|
|
|
28
28
|
1. Take only the text before the first `—` or `–` (if present)
|
|
29
|
-
2.
|
|
30
|
-
3.
|
|
31
|
-
4.
|
|
32
|
-
5.
|
|
29
|
+
2. Strip leading command verbs: create, build, implement, add, update, fix, make, write, plan, get, show, support
|
|
30
|
+
3. Lowercase the text
|
|
31
|
+
4. Remove filler words: a, an, the, for, of, to, in, on, with, and, or, but, is, be
|
|
32
|
+
5. Take the first 4 remaining words (or fewer if less exist)
|
|
33
|
+
6. Join with hyphens → `<slug>`
|
|
33
34
|
|
|
34
35
|
<!-- if:local -->
|
|
35
36
|
|
|
@@ -98,12 +99,13 @@ status: created
|
|
|
98
99
|
<!-- end:local -->
|
|
99
100
|
<!-- if:github -->
|
|
100
101
|
|
|
101
|
-
Ensure labels exist: `{{github.labels.prd}}`, `tk:created
|
|
102
|
+
Ensure labels exist: `gh label create {{github.labels.prd}} --repo {{github.repo}} --force`, `gh label create tk:created --repo {{github.repo}} --force`.
|
|
102
103
|
|
|
103
104
|
Create GitHub Issue — title: `[{{github.labels.prd}}] <slug>: <Feature Title>`, labels: `{{github.labels.prd}}`, `tk:created`.
|
|
104
105
|
|
|
105
106
|
```markdown
|
|
106
107
|
<!-- tk:metadata
|
|
108
|
+
slug: <slug>
|
|
107
109
|
created: <UTC ISO 8601>
|
|
108
110
|
status: created
|
|
109
111
|
-->
|