ystack 0.1.0 → 0.2.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/CHANGELOG.md +36 -1
- package/LINTING.md +36 -71
- package/PHILOSOPHY.md +11 -11
- package/README.md +103 -43
- package/RUNTIMES.md +10 -10
- package/bin/cli.js +84 -62
- package/hooks/docs-match-progress.js +100 -0
- package/hooks/no-undocumented-check.js +91 -0
- package/hooks/progress-before-ship.js +74 -0
- package/hooks/session-start.sh +11 -6
- package/hooks/workflow-nudge.js +2 -2
- package/package.json +3 -5
- package/skills/address-review/SKILL.md +1 -1
- package/skills/build/SKILL.md +3 -3
- package/skills/docs/SKILL.md +4 -2
- package/skills/go/SKILL.md +11 -9
- package/skills/import/SKILL.md +182 -81
- package/skills/pr/SKILL.md +15 -10
- package/skills/quick/SKILL.md +110 -0
- package/skills/scaffold/SKILL.md +77 -65
- package/PLAN.md +0 -515
package/bin/cli.js
CHANGED
|
@@ -160,6 +160,51 @@ function installHooks(projectRoot, ystackRoot) {
|
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// PreToolUse — progress-before-ship (on Bash for git push / gh pr create)
|
|
164
|
+
const hasProgressBeforeShip = settings.hooks.PreToolUse.some(
|
|
165
|
+
(h) => h.hooks?.some((hh) => hh.command?.includes("progress-before-ship")),
|
|
166
|
+
);
|
|
167
|
+
if (!hasProgressBeforeShip) {
|
|
168
|
+
settings.hooks.PreToolUse.push({
|
|
169
|
+
matcher: "Bash",
|
|
170
|
+
hooks: [{
|
|
171
|
+
type: "command",
|
|
172
|
+
command: `node "${join(projectRoot, ".claude", "hooks", "progress-before-ship.js")}"`,
|
|
173
|
+
timeout: 5,
|
|
174
|
+
}],
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// PreToolUse — no-undocumented-check (on Edit of progress files)
|
|
179
|
+
const hasNoUndocumentedCheck = settings.hooks.PreToolUse.some(
|
|
180
|
+
(h) => h.hooks?.some((hh) => hh.command?.includes("no-undocumented-check")),
|
|
181
|
+
);
|
|
182
|
+
if (!hasNoUndocumentedCheck) {
|
|
183
|
+
settings.hooks.PreToolUse.push({
|
|
184
|
+
matcher: "Edit",
|
|
185
|
+
hooks: [{
|
|
186
|
+
type: "command",
|
|
187
|
+
command: `node "${join(projectRoot, ".claude", "hooks", "no-undocumented-check.js")}"`,
|
|
188
|
+
timeout: 5,
|
|
189
|
+
}],
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// PostToolUse — docs-match-progress (after editing doc files)
|
|
194
|
+
const hasDocsMatchProgress = settings.hooks.PostToolUse.some(
|
|
195
|
+
(h) => h.hooks?.some((hh) => hh.command?.includes("docs-match-progress")),
|
|
196
|
+
);
|
|
197
|
+
if (!hasDocsMatchProgress) {
|
|
198
|
+
settings.hooks.PostToolUse.push({
|
|
199
|
+
matcher: "Edit|Write",
|
|
200
|
+
hooks: [{
|
|
201
|
+
type: "command",
|
|
202
|
+
command: `node "${join(projectRoot, ".claude", "hooks", "docs-match-progress.js")}"`,
|
|
203
|
+
timeout: 5,
|
|
204
|
+
}],
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
163
208
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
164
209
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
165
210
|
|
|
@@ -197,19 +242,26 @@ function removeHooks(projectRoot) {
|
|
|
197
242
|
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
198
243
|
if (settings.hooks?.PostToolUse) {
|
|
199
244
|
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(
|
|
200
|
-
(h) => !h.hooks?.some((hh) =>
|
|
245
|
+
(h) => !h.hooks?.some((hh) =>
|
|
246
|
+
hh.command?.includes("context-monitor") ||
|
|
247
|
+
hh.command?.includes("docs-match-progress")
|
|
248
|
+
),
|
|
201
249
|
);
|
|
202
250
|
}
|
|
203
251
|
if (settings.hooks?.PreToolUse) {
|
|
204
252
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
205
|
-
(h) => !h.hooks?.some((hh) =>
|
|
253
|
+
(h) => !h.hooks?.some((hh) =>
|
|
254
|
+
hh.command?.includes("workflow-nudge") ||
|
|
255
|
+
hh.command?.includes("progress-before-ship") ||
|
|
256
|
+
hh.command?.includes("no-undocumented-check")
|
|
257
|
+
),
|
|
206
258
|
);
|
|
207
259
|
}
|
|
208
260
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
209
261
|
} catch { /* ignore */ }
|
|
210
262
|
|
|
211
263
|
const hooksDir = join(projectRoot, ".claude", "hooks");
|
|
212
|
-
for (const file of ["context-monitor.js", "session-start.sh", "workflow-nudge.js"]) {
|
|
264
|
+
for (const file of ["context-monitor.js", "session-start.sh", "workflow-nudge.js", "progress-before-ship.js", "docs-match-progress.js", "no-undocumented-check.js"]) {
|
|
213
265
|
const target = join(hooksDir, file);
|
|
214
266
|
if (existsSync(target)) rmSync(target);
|
|
215
267
|
}
|
|
@@ -284,42 +336,7 @@ async function cmdInit() {
|
|
|
284
336
|
}
|
|
285
337
|
}
|
|
286
338
|
|
|
287
|
-
// --- Step 3:
|
|
288
|
-
let initBeads = false;
|
|
289
|
-
if (!skillsOnly) {
|
|
290
|
-
const hasBeads = existsSync(join(projectRoot, ".beads"));
|
|
291
|
-
const hasBdCli = commandExists("bd");
|
|
292
|
-
|
|
293
|
-
if (hasBeads) {
|
|
294
|
-
p.log.info("Beads already initialized");
|
|
295
|
-
} else if (hasBdCli) {
|
|
296
|
-
initBeads = handleCancel(await p.confirm({
|
|
297
|
-
message: "Initialize Beads for persistent memory?",
|
|
298
|
-
}));
|
|
299
|
-
} else {
|
|
300
|
-
const installBeads = handleCancel(await p.select({
|
|
301
|
-
message: "Beads (persistent memory for agents):",
|
|
302
|
-
options: [
|
|
303
|
-
{ label: "Install Beads (brew install beads)", value: "install" },
|
|
304
|
-
{ label: "Skip — I'll set up Beads later", value: "skip" },
|
|
305
|
-
],
|
|
306
|
-
}));
|
|
307
|
-
|
|
308
|
-
if (installBeads === "install") {
|
|
309
|
-
p.log.step("Installing Beads...");
|
|
310
|
-
try {
|
|
311
|
-
execSync("brew install beads", { stdio: "inherit" });
|
|
312
|
-
p.log.success("Beads installed");
|
|
313
|
-
initBeads = true;
|
|
314
|
-
} catch {
|
|
315
|
-
p.log.warn("Install failed. Try manually: brew install beads");
|
|
316
|
-
p.log.warn("or: npm install -g @beads/bd");
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// --- Step 4: Runtime ---
|
|
339
|
+
// --- Step 3: Runtime ---
|
|
323
340
|
const runtime = handleCancel(await p.select({
|
|
324
341
|
message: "Runtime:",
|
|
325
342
|
options: [
|
|
@@ -328,7 +345,7 @@ async function cmdInit() {
|
|
|
328
345
|
],
|
|
329
346
|
}));
|
|
330
347
|
|
|
331
|
-
// --- Step
|
|
348
|
+
// --- Step 4: Hooks ---
|
|
332
349
|
let installHooksFlag = true;
|
|
333
350
|
if (!skillsOnly && runtime === "claude-code") {
|
|
334
351
|
installHooksFlag = handleCancel(await p.confirm({
|
|
@@ -352,7 +369,9 @@ async function cmdInit() {
|
|
|
352
369
|
}
|
|
353
370
|
|
|
354
371
|
// Config
|
|
355
|
-
const
|
|
372
|
+
const ystackDir = join(projectRoot, ".ystack");
|
|
373
|
+
mkdirSync(ystackDir, { recursive: true });
|
|
374
|
+
const configPath = join(ystackDir, "config.json");
|
|
356
375
|
const configExisted = existsSync(configPath);
|
|
357
376
|
if (!configExisted) {
|
|
358
377
|
const monorepo = detectMonorepo();
|
|
@@ -376,15 +395,15 @@ async function cmdInit() {
|
|
|
376
395
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
377
396
|
}
|
|
378
397
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
execSync("bd init", { stdio: "pipe", cwd: projectRoot });
|
|
385
|
-
} catch { /* handled below */ }
|
|
398
|
+
// Progress directory
|
|
399
|
+
const progressDir = join(ystackDir, "progress");
|
|
400
|
+
mkdirSync(progressDir, { recursive: true });
|
|
401
|
+
if (!existsSync(join(progressDir, "_overview.md"))) {
|
|
402
|
+
writeFileSync(join(progressDir, "_overview.md"), `# Project Progress\n\n## Module Status\n\n| Module | Done | Total | Status |\n|--------|------|-------|--------|\n\n## Ready Front\n\n_No modules registered yet. Run \`/import\` or \`/scaffold\` to get started._\n`);
|
|
386
403
|
}
|
|
387
404
|
|
|
405
|
+
ensureGitignore(projectRoot);
|
|
406
|
+
|
|
388
407
|
s.stop("Setup complete");
|
|
389
408
|
|
|
390
409
|
// Summary
|
|
@@ -393,13 +412,11 @@ async function cmdInit() {
|
|
|
393
412
|
p.log.success("Hooks: context-monitor, workflow-nudge");
|
|
394
413
|
}
|
|
395
414
|
if (configExisted) {
|
|
396
|
-
p.log.info("Config: ystack
|
|
415
|
+
p.log.info("Config: .ystack/config.json preserved");
|
|
397
416
|
} else {
|
|
398
|
-
p.log.success("Config: created ystack
|
|
399
|
-
}
|
|
400
|
-
if (initBeads) {
|
|
401
|
-
p.log.success("Beads initialized");
|
|
417
|
+
p.log.success("Config: created .ystack/config.json");
|
|
402
418
|
}
|
|
419
|
+
p.log.success("Progress: .ystack/progress/ ready");
|
|
403
420
|
|
|
404
421
|
p.note(
|
|
405
422
|
[
|
|
@@ -443,7 +460,7 @@ async function cmdRemove() {
|
|
|
443
460
|
p.intro("ystack remove");
|
|
444
461
|
|
|
445
462
|
const proceed = handleCancel(await p.confirm({
|
|
446
|
-
message: "Remove ystack skills and hooks? (keeps
|
|
463
|
+
message: "Remove ystack skills and hooks? (keeps .ystack/, docs)",
|
|
447
464
|
}));
|
|
448
465
|
if (!proceed) {
|
|
449
466
|
p.outro("Cancelled.");
|
|
@@ -456,7 +473,7 @@ async function cmdRemove() {
|
|
|
456
473
|
removeHooks(projectRoot);
|
|
457
474
|
s.stop("Removed skills and hooks");
|
|
458
475
|
|
|
459
|
-
p.log.info("Kept: ystack
|
|
476
|
+
p.log.info("Kept: .ystack/, docs/");
|
|
460
477
|
p.outro("Done!");
|
|
461
478
|
}
|
|
462
479
|
|
|
@@ -504,11 +521,12 @@ async function cmdCreate() {
|
|
|
504
521
|
"docs/src/content",
|
|
505
522
|
".claude/skills",
|
|
506
523
|
".context",
|
|
524
|
+
".ystack/progress",
|
|
507
525
|
];
|
|
508
526
|
for (const dir of dirs) {
|
|
509
527
|
mkdirSync(join(projectDir, dir), { recursive: true });
|
|
510
528
|
}
|
|
511
|
-
console.log(green(" ✓ apps/, packages/, docs/, .claude/, .context/\n"));
|
|
529
|
+
console.log(green(" ✓ apps/, packages/, docs/, .claude/, .context/, .ystack/\n"));
|
|
512
530
|
|
|
513
531
|
// --- Generate files ---
|
|
514
532
|
console.log(bold("Generating config files..."));
|
|
@@ -589,7 +607,7 @@ async function cmdCreate() {
|
|
|
589
607
|
writeFileSync(join(projectDir, "biome.json"), JSON.stringify(biomeConfig, null, 2) + "\n");
|
|
590
608
|
console.log(green(" ✓ biome.json"));
|
|
591
609
|
|
|
592
|
-
// ystack
|
|
610
|
+
// .ystack/config.json
|
|
593
611
|
const docsRoot = docsFramework === "fumadocs" ? "content/docs" : "docs/src/content";
|
|
594
612
|
const ystackConfig = {
|
|
595
613
|
project: name,
|
|
@@ -608,8 +626,12 @@ async function cmdCreate() {
|
|
|
608
626
|
auto_docs_check: true,
|
|
609
627
|
},
|
|
610
628
|
};
|
|
611
|
-
writeFileSync(join(projectDir, "ystack
|
|
612
|
-
console.log(green(" ✓ ystack
|
|
629
|
+
writeFileSync(join(projectDir, ".ystack/config.json"), JSON.stringify(ystackConfig, null, 2) + "\n");
|
|
630
|
+
console.log(green(" ✓ .ystack/config.json"));
|
|
631
|
+
|
|
632
|
+
// .ystack/progress/_overview.md
|
|
633
|
+
writeFileSync(join(projectDir, ".ystack/progress/_overview.md"), `# Project Progress\n\n## Module Status\n\n| Module | Done | Total | Status |\n|--------|------|-------|--------|\n\n## Ready Front\n\n_No modules registered yet. Run \`/import\` or \`/scaffold\` to get started._\n`);
|
|
634
|
+
console.log(green(" ✓ .ystack/progress/_overview.md"));
|
|
613
635
|
|
|
614
636
|
// CLAUDE.md
|
|
615
637
|
const claudeMd = `# ${name}
|
|
@@ -624,7 +646,7 @@ This project uses [ystack](https://github.com/yulonghe97/ystack) for doc-driven
|
|
|
624
646
|
|
|
625
647
|
## Module Registry
|
|
626
648
|
|
|
627
|
-
Modules are defined in
|
|
649
|
+
Modules are defined in \`.ystack/config.json\`. Each module maps code directories to documentation pages.
|
|
628
650
|
|
|
629
651
|
## Available Commands
|
|
630
652
|
|
|
@@ -662,7 +684,7 @@ This project uses [ystack](https://github.com/yulonghe97/ystack) for doc-driven
|
|
|
662
684
|
|
|
663
685
|
## Module Registry
|
|
664
686
|
|
|
665
|
-
Modules are defined in
|
|
687
|
+
Modules are defined in \`.ystack/config.json\`. Each module maps code directories to documentation pages.
|
|
666
688
|
|
|
667
689
|
## Workflow
|
|
668
690
|
|
|
@@ -932,7 +954,7 @@ function usage() {
|
|
|
932
954
|
${bold("ystack")} — agent harness for doc-driven development
|
|
933
955
|
|
|
934
956
|
${bold("Commands:")}
|
|
935
|
-
${green("init")} Interactive setup — configure docs,
|
|
957
|
+
${green("init")} Interactive setup — configure docs, runtime, hooks
|
|
936
958
|
${green("init --skills-only")} Install skills only, skip everything else
|
|
937
959
|
${green("update")} Update skills and hooks to latest version
|
|
938
960
|
${green("remove")} Remove ystack skills and hooks (keeps data)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ystack docs-match-progress — PostToolUse hook
|
|
3
|
+
*
|
|
4
|
+
* After writing/editing doc files, checks that any [x] features in the
|
|
5
|
+
* module's progress file have real content in docs (not <!-- ystack:stub -->).
|
|
6
|
+
*
|
|
7
|
+
* Fires after Edit or Write on files under the docs root.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
11
|
+
import { join, relative } from "node:path";
|
|
12
|
+
|
|
13
|
+
// Only check on doc file edits
|
|
14
|
+
const filePath = process.env.CLAUDE_TOOL_INPUT_FILE_PATH || "";
|
|
15
|
+
if (!filePath) {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Must be a ystack project
|
|
20
|
+
if (!existsSync(".ystack/config.json")) {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Read config to find docs root
|
|
25
|
+
let config;
|
|
26
|
+
try {
|
|
27
|
+
config = JSON.parse(readFileSync(".ystack/config.json", "utf-8"));
|
|
28
|
+
} catch {
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const docsRoot = config.docs?.root;
|
|
33
|
+
if (!docsRoot || !filePath.includes(docsRoot)) {
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Find which module this doc file belongs to
|
|
38
|
+
const modules = config.modules || {};
|
|
39
|
+
let matchedModule = null;
|
|
40
|
+
|
|
41
|
+
for (const [key, mod] of Object.entries(modules)) {
|
|
42
|
+
if (mod.doc && filePath.includes(mod.doc)) {
|
|
43
|
+
matchedModule = key;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!matchedModule) {
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Read the module's progress file
|
|
53
|
+
const progressPath = `.ystack/progress/${matchedModule}.md`;
|
|
54
|
+
if (!existsSync(progressPath)) {
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const progressContent = readFileSync(progressPath, "utf-8");
|
|
59
|
+
|
|
60
|
+
// Find checked items with doc anchors
|
|
61
|
+
const checkedPattern = /^- \[x\] .+→\s*(.+)/gm;
|
|
62
|
+
const checkedAnchors = [];
|
|
63
|
+
let match;
|
|
64
|
+
while ((match = checkedPattern.exec(progressContent)) !== null) {
|
|
65
|
+
checkedAnchors.push(match[1].trim());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (checkedAnchors.length === 0) {
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Read the doc file being edited
|
|
73
|
+
let docContent;
|
|
74
|
+
try {
|
|
75
|
+
docContent = readFileSync(filePath, "utf-8");
|
|
76
|
+
} catch {
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check for stubs that should have been filled
|
|
81
|
+
const stubs = [];
|
|
82
|
+
for (const anchor of checkedAnchors) {
|
|
83
|
+
// Extract the anchor part (after #)
|
|
84
|
+
const anchorId = anchor.includes("#") ? anchor.split("#")[1] : null;
|
|
85
|
+
if (!anchorId) continue;
|
|
86
|
+
|
|
87
|
+
// Check if this anchor's section still has a stub marker
|
|
88
|
+
const anchorRegex = new RegExp(
|
|
89
|
+
`\\{#${anchorId}\\}[\\s\\S]*?<!-- ystack:stub -->`,
|
|
90
|
+
);
|
|
91
|
+
if (anchorRegex.test(docContent)) {
|
|
92
|
+
stubs.push(anchorId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (stubs.length > 0) {
|
|
97
|
+
console.log(
|
|
98
|
+
`[ystack] ${stubs.length} feature(s) marked done in progress but still stubbed in docs: ${stubs.join(", ")}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ystack no-undocumented-check — PreToolUse hook on Edit
|
|
3
|
+
*
|
|
4
|
+
* When someone checks a box in a progress file ([ ] → [x]),
|
|
5
|
+
* verifies the linked doc section doesn't still have <!-- ystack:stub -->.
|
|
6
|
+
*
|
|
7
|
+
* Prevents marking features as "done" when docs haven't been written.
|
|
8
|
+
* This is a warn, not a block — /go checks the box before /docs runs,
|
|
9
|
+
* so the stub is expected at that point. The warning reminds you to
|
|
10
|
+
* run /docs before /pr.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
// Only check edits to progress files
|
|
16
|
+
const filePath = process.env.CLAUDE_TOOL_INPUT_FILE_PATH || "";
|
|
17
|
+
if (!filePath.includes(".ystack/progress/") || filePath.includes("_overview")) {
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if the edit is checking a box
|
|
22
|
+
const newContent = process.env.CLAUDE_TOOL_INPUT_NEW_STRING || "";
|
|
23
|
+
if (!newContent.includes("- [x]")) {
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Must be a ystack project with config
|
|
28
|
+
if (!existsSync(".ystack/config.json")) {
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let config;
|
|
33
|
+
try {
|
|
34
|
+
config = JSON.parse(readFileSync(".ystack/config.json", "utf-8"));
|
|
35
|
+
} catch {
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const docsRoot = config.docs?.root;
|
|
40
|
+
if (!docsRoot) {
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Extract anchor references from the lines being checked
|
|
45
|
+
const anchorPattern = /- \[x\] .+→\s*(.+)/g;
|
|
46
|
+
let match;
|
|
47
|
+
const anchors = [];
|
|
48
|
+
while ((match = anchorPattern.exec(newContent)) !== null) {
|
|
49
|
+
anchors.push(match[1].trim());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (anchors.length === 0) {
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check each anchor for stubs in the doc files
|
|
57
|
+
const warnings = [];
|
|
58
|
+
for (const anchor of anchors) {
|
|
59
|
+
const parts = anchor.split("#");
|
|
60
|
+
const docPath = parts[0];
|
|
61
|
+
const anchorId = parts[1];
|
|
62
|
+
|
|
63
|
+
if (!docPath || !anchorId) continue;
|
|
64
|
+
|
|
65
|
+
// Try to find the doc file
|
|
66
|
+
const candidates = [
|
|
67
|
+
`${docsRoot}/${docPath}/index.mdx`,
|
|
68
|
+
`${docsRoot}/${docPath}/index.md`,
|
|
69
|
+
`${docsRoot}/${docPath}.mdx`,
|
|
70
|
+
`${docsRoot}/${docPath}.md`,
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
for (const candidate of candidates) {
|
|
74
|
+
if (existsSync(candidate)) {
|
|
75
|
+
const content = readFileSync(candidate, "utf-8");
|
|
76
|
+
const stubCheck = new RegExp(
|
|
77
|
+
`\\{#${anchorId}\\}[\\s\\S]*?<!-- ystack:stub -->`,
|
|
78
|
+
);
|
|
79
|
+
if (stubCheck.test(content)) {
|
|
80
|
+
warnings.push(`${docPath}#${anchorId}`);
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (warnings.length > 0) {
|
|
88
|
+
console.log(
|
|
89
|
+
`[ystack] Checking off feature(s) with stubbed doc sections: ${warnings.join(", ")}. Run /docs before /pr to fill them in.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ystack progress-before-ship — PreToolUse hook on Bash
|
|
3
|
+
*
|
|
4
|
+
* Warns when creating a PR or pushing code if the branch has code
|
|
5
|
+
* changes in module scopes but no corresponding .ystack/progress/ updates.
|
|
6
|
+
*
|
|
7
|
+
* Only fires on git push or gh pr create commands.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
|
|
13
|
+
// Only check on git push or gh pr create
|
|
14
|
+
const toolInput = process.env.CLAUDE_TOOL_INPUT || "";
|
|
15
|
+
if (!toolInput.includes("git push") && !toolInput.includes("gh pr create")) {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Must be a ystack project
|
|
20
|
+
if (!existsSync(".ystack/config.json")) {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Quick mode — skip progress checks for bug fixes and chores
|
|
25
|
+
if (existsSync(".context/.quick")) {
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Get changed files on this branch vs main
|
|
30
|
+
let changedFiles;
|
|
31
|
+
try {
|
|
32
|
+
changedFiles = execSync("git diff main...HEAD --name-only 2>/dev/null", {
|
|
33
|
+
encoding: "utf-8",
|
|
34
|
+
}).trim().split("\n").filter(Boolean);
|
|
35
|
+
} catch {
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (changedFiles.length === 0) {
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if any code files changed (not docs, not config, not progress)
|
|
44
|
+
const codeChanges = changedFiles.filter(
|
|
45
|
+
(f) =>
|
|
46
|
+
!f.startsWith("docs/") &&
|
|
47
|
+
!f.startsWith(".ystack/") &&
|
|
48
|
+
!f.startsWith(".claude/") &&
|
|
49
|
+
!f.startsWith(".context/") &&
|
|
50
|
+
!f.endsWith(".md") &&
|
|
51
|
+
!f.endsWith(".json") &&
|
|
52
|
+
!f.endsWith(".yaml") &&
|
|
53
|
+
!f.endsWith(".yml"),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (codeChanges.length === 0) {
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Small diffs (≤ 5 code files) are likely bug fixes or chores — don't warn
|
|
61
|
+
if (codeChanges.length <= 5) {
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if progress files were updated
|
|
66
|
+
const progressChanges = changedFiles.filter((f) =>
|
|
67
|
+
f.startsWith(".ystack/progress/"),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (progressChanges.length === 0) {
|
|
71
|
+
console.log(
|
|
72
|
+
`[ystack] ${codeChanges.length} code files changed but no .ystack/progress/ updates. If this is a feature, did /go check the boxes? If it's a bug fix, ignore this.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
package/hooks/session-start.sh
CHANGED
|
@@ -2,19 +2,24 @@
|
|
|
2
2
|
# ystack session start — shows project status on session start
|
|
3
3
|
|
|
4
4
|
# Check for ystack project
|
|
5
|
-
if [ ! -f "ystack
|
|
5
|
+
if [ ! -f ".ystack/config.json" ]; then
|
|
6
6
|
exit 0
|
|
7
7
|
fi
|
|
8
8
|
|
|
9
9
|
echo "[ystack] Project detected"
|
|
10
10
|
|
|
11
|
-
# Show
|
|
12
|
-
if [ -d ".
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
# Show ready front from progress files
|
|
12
|
+
if [ -d ".ystack/progress" ]; then
|
|
13
|
+
# Count unchecked items across all progress files
|
|
14
|
+
UNCHECKED=$(grep -r '^\- \[ \]' .ystack/progress/*.md 2>/dev/null | head -10)
|
|
15
|
+
if [ -n "$UNCHECKED" ]; then
|
|
15
16
|
echo ""
|
|
16
17
|
echo "Ready to work on:"
|
|
17
|
-
|
|
18
|
+
grep -r '^\- \[ \]' .ystack/progress/*.md 2>/dev/null | head -10 | while read -r line; do
|
|
19
|
+
FILE=$(echo "$line" | cut -d: -f1 | xargs basename .md)
|
|
20
|
+
FEATURE=$(echo "$line" | sed 's/.*\- \[ \] //' | sed 's/ *→.*//')
|
|
21
|
+
echo " $FILE — $FEATURE"
|
|
22
|
+
done
|
|
18
23
|
fi
|
|
19
24
|
fi
|
|
20
25
|
|
package/hooks/workflow-nudge.js
CHANGED
|
@@ -31,8 +31,8 @@ if (state.nudged) {
|
|
|
31
31
|
process.exit(0);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// Developer dismissed nudges
|
|
35
|
-
if (existsSync(".context/.no-nudge")) {
|
|
34
|
+
// Developer dismissed nudges or is in quick mode
|
|
35
|
+
if (existsSync(".context/.no-nudge") || existsSync(".context/.quick")) {
|
|
36
36
|
process.exit(0);
|
|
37
37
|
}
|
|
38
38
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ystack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "An agent harness for doc-driven development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"ystack": "
|
|
7
|
+
"ystack": "bin/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"skills/",
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"PHILOSOPHY.md",
|
|
15
15
|
"LINTING.md",
|
|
16
16
|
"RUNTIMES.md",
|
|
17
|
-
"PLAN.md",
|
|
18
17
|
"CHANGELOG.md",
|
|
19
18
|
"LICENSE"
|
|
20
19
|
],
|
|
@@ -24,14 +23,13 @@
|
|
|
24
23
|
"harness",
|
|
25
24
|
"documentation",
|
|
26
25
|
"claude-code",
|
|
27
|
-
"beads",
|
|
28
26
|
"workflow"
|
|
29
27
|
],
|
|
30
28
|
"author": "Yulong He",
|
|
31
29
|
"license": "MIT",
|
|
32
30
|
"repository": {
|
|
33
31
|
"type": "git",
|
|
34
|
-
"url": "https://github.com/yulonghe97/ystack.git"
|
|
32
|
+
"url": "git+https://github.com/yulonghe97/ystack.git"
|
|
35
33
|
},
|
|
36
34
|
"dependencies": {
|
|
37
35
|
"@clack/prompts": "^1.2.0"
|
|
@@ -185,7 +185,7 @@ For items marked WON'T FIX or FALSE POSITIVE, draft reply comments:
|
|
|
185
185
|
### Suggested Replies
|
|
186
186
|
|
|
187
187
|
**Comment #5** (@sarah — refundRequestedAt timestamp):
|
|
188
|
-
> Good idea — we've deferred this to a follow-up. Tracked in [
|
|
188
|
+
> Good idea — we've deferred this to a follow-up. Tracked in [progress file / issue reference].
|
|
189
189
|
|
|
190
190
|
**Comment #6** (CI: e2e-tests):
|
|
191
191
|
> Flaky test unrelated to this PR — `auth/login.spec.ts` timed out. Re-running CI.
|
package/skills/build/SKILL.md
CHANGED
|
@@ -19,9 +19,9 @@ You are the planning phase of the ystack agent harness. Your job is to understan
|
|
|
19
19
|
|
|
20
20
|
Identify which module(s) this feature belongs to.
|
|
21
21
|
|
|
22
|
-
1. Read
|
|
22
|
+
1. Read `.ystack/config.json` if it exists. Match the feature to a module by checking `scope` globs:
|
|
23
23
|
```bash
|
|
24
|
-
cat ystack
|
|
24
|
+
cat .ystack/config.json
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
2. If no config exists or no match found, scan the docs directory structure:
|
|
@@ -117,7 +117,7 @@ Create the directory and file:
|
|
|
117
117
|
.context/<feature-id>/DECISIONS.md
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
Use a short, descriptive ID for the feature (e.g., `refund-reason`, `oauth-support`, `dashboard-charts`).
|
|
120
|
+
Use a short, descriptive ID for the feature (e.g., `refund-reason`, `oauth-support`, `dashboard-charts`).
|
|
121
121
|
|
|
122
122
|
**DECISIONS.md format:**
|
|
123
123
|
|
package/skills/docs/SKILL.md
CHANGED
|
@@ -23,11 +23,13 @@ You update documentation to reflect completed, verified work. Docs describe what
|
|
|
23
23
|
|
|
24
24
|
3. Read `.context/<feature-id>/PLAN.md` for the success criteria — these tell you what was built.
|
|
25
25
|
|
|
26
|
+
4. **Read the progress file** — `.ystack/progress/<module>.md` for the affected module. Features marked `[x]` are completed and may need doc updates. Features still `[ ]` must NOT be documented. The progress file is the gate between implementation and documentation.
|
|
27
|
+
|
|
26
28
|
## Phase 1: Map Changes to Docs
|
|
27
29
|
|
|
28
30
|
Identify which documentation pages are affected.
|
|
29
31
|
|
|
30
|
-
1. **Read the module registry** (
|
|
32
|
+
1. **Read the module registry** (`.ystack/config.json`) to find the module-to-doc mapping. Match changed file paths against module `scope` globs. If no registry exists, fall back to scanning `docs/src/content/_meta.ts`.
|
|
31
33
|
|
|
32
34
|
2. **Map changed code paths to doc pages:**
|
|
33
35
|
|
|
@@ -97,7 +99,7 @@ export default {
|
|
|
97
99
|
}
|
|
98
100
|
```
|
|
99
101
|
|
|
100
|
-
Check
|
|
102
|
+
Check `.ystack/config.json` `docs.framework` to know which format to use.
|
|
101
103
|
|
|
102
104
|
## Phase 4: Update Structural Files
|
|
103
105
|
|