@brainervirus/workit-core 0.4.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/LICENSE +21 -0
- package/README.md +180 -0
- package/commands/wk-changelog.md +2 -0
- package/commands/wk-commit.md +2 -0
- package/commands/wk-docs-refresh.md +2 -0
- package/commands/wk-handoff.md +2 -0
- package/commands/wk-implement.md +2 -0
- package/commands/wk-init.md +2 -0
- package/commands/wk-issue-update.md +2 -0
- package/commands/wk-meetings.md +2 -0
- package/commands/wk-pr.md +2 -0
- package/commands/wk-release-notes.md +2 -0
- package/commands/wk-status.md +2 -0
- package/commands/wk-verify.md +2 -0
- package/package.json +43 -0
- package/scripts/_shared/common.sh +158 -0
- package/scripts/changelog-context.sh +42 -0
- package/scripts/docs-refresh-context.sh +40 -0
- package/scripts/fixtures/sample-plan.md +15 -0
- package/scripts/fixtures/sample-sdd/progress.md +2 -0
- package/scripts/init/apply.sh +5 -0
- package/scripts/init/status.sh +5 -0
- package/scripts/init/toolkit-status.sh +5 -0
- package/scripts/install-cursor-plugin.sh +83 -0
- package/scripts/install-opencode-plugin.sh +76 -0
- package/scripts/pr-create.sh +5 -0
- package/scripts/pr-ready-context.sh +88 -0
- package/scripts/present/ascii-wireframe.sh +5 -0
- package/scripts/present/flow-diagram.sh +5 -0
- package/scripts/release-notes-context.sh +40 -0
- package/scripts/rewrite-workspace-deps.ts +17 -0
- package/scripts/run-cursor-mcp.sh +10 -0
- package/scripts/sync-runtime.sh +96 -0
- package/scripts/update-superpowers.sh +56 -0
- package/scripts/vcs/config.sh +5 -0
- package/scripts/vcs/merged-style.sh +5 -0
- package/scripts/vcs/token-create-urls.sh +5 -0
- package/scripts/vcs/verify-token.sh +5 -0
- package/scripts/verify-project.sh +140 -0
- package/scripts/youtrack/api.sh +5 -0
- package/scripts/youtrack/config.sh +5 -0
- package/scripts/youtrack/greeting.sh +5 -0
- package/scripts/youtrack/parse-duration.sh +5 -0
- package/scripts/youtrack/token-create-url.sh +5 -0
- package/scripts/youtrack/verify-token.sh +5 -0
- package/scripts/youtrack/work-date-ms.sh +5 -0
- package/skills/wk-changelog/SKILL.md +15 -0
- package/skills/wk-commit/SKILL.md +16 -0
- package/skills/wk-docs-refresh/SKILL.md +15 -0
- package/skills/wk-handoff/SKILL.md +17 -0
- package/skills/wk-implement/SKILL.md +41 -0
- package/skills/wk-init/SKILL.md +31 -0
- package/skills/wk-issue-update/SKILL.md +27 -0
- package/skills/wk-issue-update/references/youtrack-update-style.md +81 -0
- package/skills/wk-meetings/SKILL.md +17 -0
- package/skills/wk-pr/SKILL.md +27 -0
- package/skills/wk-release-notes/SKILL.md +15 -0
- package/skills/wk-status/SKILL.md +16 -0
- package/skills/wk-verify/SKILL.md +16 -0
- package/src/core/branch.ts +246 -0
- package/src/core/changelog.ts +312 -0
- package/src/core/config-guard.ts +26 -0
- package/src/core/config.ts +73 -0
- package/src/core/detector.ts +207 -0
- package/src/core/doc-render.ts +14 -0
- package/src/core/docs-repo.ts +196 -0
- package/src/core/docs-validate.ts +255 -0
- package/src/core/flow-state.ts +225 -0
- package/src/core/git.ts +56 -0
- package/src/core/gitignore.ts +43 -0
- package/src/core/handoff-context.ts +115 -0
- package/src/core/hygiene.ts +77 -0
- package/src/core/init.ts +443 -0
- package/src/core/parse-sections.ts +24 -0
- package/src/core/plan-tasks.ts +33 -0
- package/src/core/ports/init-apply.ts +15 -0
- package/src/core/ports/init-status.ts +4 -0
- package/src/core/ports/init-toolkit-status.ts +4 -0
- package/src/core/ports/pr-create.ts +22 -0
- package/src/core/ports/present-ascii.ts +10 -0
- package/src/core/ports/present-flow.ts +10 -0
- package/src/core/ports/vcs-config.ts +14 -0
- package/src/core/ports/vcs-merged-style.ts +5 -0
- package/src/core/ports/vcs-token-create-urls.ts +4 -0
- package/src/core/ports/vcs-verify-token.ts +4 -0
- package/src/core/ports/youtrack-api.ts +17 -0
- package/src/core/ports/youtrack-config.ts +23 -0
- package/src/core/ports/youtrack-greeting.ts +10 -0
- package/src/core/ports/youtrack-parse-duration.ts +14 -0
- package/src/core/ports/youtrack-token-create-url.ts +4 -0
- package/src/core/ports/youtrack-verify-token.ts +12 -0
- package/src/core/ports/youtrack-work-date-ms.ts +10 -0
- package/src/core/pr-create.ts +212 -0
- package/src/core/present.ts +100 -0
- package/src/core/reminder.ts +82 -0
- package/src/core/repo-tool.ts +9 -0
- package/src/core/rules.ts +122 -0
- package/src/core/scripts.ts +42 -0
- package/src/core/sdd.ts +188 -0
- package/src/core/templates.ts +37 -0
- package/src/core/vcs-config.ts +252 -0
- package/src/core/verify-parse.ts +27 -0
- package/src/core/workspaces.ts +81 -0
- package/src/core/youtrack.ts +488 -0
- package/src/core.ts +62 -0
- package/src/state.ts +22 -0
- package/src/tools/docs-repo.ts +42 -0
- package/src/tools/flow.ts +88 -0
- package/src/tools/handoff.ts +160 -0
- package/src/tools/index.ts +22 -0
- package/src/tools/present.ts +45 -0
- package/src/tools/repo.ts +357 -0
- package/src/tools/rules.ts +30 -0
- package/src/tools/sdd.ts +189 -0
- package/src/tools/templates.ts +27 -0
- package/src/tools/youtrack.ts +356 -0
- package/templates/execution-contract.md +56 -0
- package/templates/greeting.md +1 -0
- package/templates/headers.md +3 -0
- package/templates/hygiene/.editorconfig +8 -0
- package/templates/hygiene/.gitattributes +3 -0
- package/templates/hygiene/CHANGELOG.md +14 -0
- package/templates/hygiene/CONTRIBUTING.md +3 -0
- package/templates/hygiene/LICENSE +21 -0
- package/templates/hygiene/README.md +3 -0
- package/templates/issue-update.md +6 -0
- package/templates/plan-template.md +25 -0
- package/templates/spec-template.md +51 -0
- package/templates/superpowers-doc-contract.md +69 -0
- package/vendor/superpowers/skills/brainstorming/SKILL.md +159 -0
- package/vendor/superpowers/skills/brainstorming/scripts/frame-template.html +213 -0
- package/vendor/superpowers/skills/brainstorming/scripts/helper.js +167 -0
- package/vendor/superpowers/skills/brainstorming/scripts/server.cjs +723 -0
- package/vendor/superpowers/skills/brainstorming/scripts/start-server.sh +209 -0
- package/vendor/superpowers/skills/brainstorming/scripts/stop-server.sh +120 -0
- package/vendor/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/vendor/superpowers/skills/brainstorming/visual-companion.md +291 -0
- package/vendor/superpowers/skills/dispatching-parallel-agents/SKILL.md +185 -0
- package/vendor/superpowers/skills/executing-plans/SKILL.md +70 -0
- package/vendor/superpowers/skills/finishing-a-development-branch/SKILL.md +241 -0
- package/vendor/superpowers/skills/receiving-code-review/SKILL.md +213 -0
- package/vendor/superpowers/skills/requesting-code-review/SKILL.md +103 -0
- package/vendor/superpowers/skills/requesting-code-review/code-reviewer.md +172 -0
- package/vendor/superpowers/skills/subagent-driven-development/SKILL.md +418 -0
- package/vendor/superpowers/skills/subagent-driven-development/implementer-prompt.md +139 -0
- package/vendor/superpowers/skills/subagent-driven-development/scripts/review-package +44 -0
- package/vendor/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +22 -0
- package/vendor/superpowers/skills/subagent-driven-development/scripts/task-brief +40 -0
- package/vendor/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +188 -0
- package/vendor/superpowers/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/vendor/superpowers/skills/systematic-debugging/SKILL.md +296 -0
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/vendor/superpowers/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/vendor/superpowers/skills/systematic-debugging/find-polluter.sh +63 -0
- package/vendor/superpowers/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/vendor/superpowers/skills/systematic-debugging/test-academic.md +14 -0
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/vendor/superpowers/skills/test-driven-development/SKILL.md +371 -0
- package/vendor/superpowers/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/vendor/superpowers/skills/using-git-worktrees/SKILL.md +202 -0
- package/vendor/superpowers/skills/using-superpowers/SKILL.md +62 -0
- package/vendor/superpowers/skills/using-superpowers/references/antigravity-tools.md +23 -0
- package/vendor/superpowers/skills/using-superpowers/references/codex-tools.md +39 -0
- package/vendor/superpowers/skills/using-superpowers/references/pi-tools.md +16 -0
- package/vendor/superpowers/skills/verification-before-completion/SKILL.md +139 -0
- package/vendor/superpowers/skills/writing-plans/SKILL.md +174 -0
- package/vendor/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/vendor/superpowers/skills/writing-skills/SKILL.md +689 -0
- package/vendor/superpowers/skills/writing-skills/anthropic-best-practices.md +1150 -0
- package/vendor/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/vendor/superpowers/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/vendor/superpowers/skills/writing-skills/persuasion-principles.md +187 -0
- package/vendor/superpowers/skills/writing-skills/render-graphs.js +168 -0
- package/vendor/superpowers/skills/writing-skills/testing-skills-with-subagents.md +384 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveWorkspaceRoot } from "./scripts";
|
|
4
|
+
|
|
5
|
+
const CATEGORIES = [
|
|
6
|
+
"Added",
|
|
7
|
+
"Changed",
|
|
8
|
+
"Deprecated",
|
|
9
|
+
"Removed",
|
|
10
|
+
"Fixed",
|
|
11
|
+
"Security",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
// Port of scripts/changelog/apply-unreleased.py — merge Keep a Changelog
|
|
15
|
+
// entries into ## [Unreleased] without duplicating ### headings.
|
|
16
|
+
const CAT_RE = /^###\s+(?:Added|Changed|Deprecated|Removed|Fixed|Security)\s*$/i;
|
|
17
|
+
const UNRELEASED_RE = /^##\s+\[Unreleased\]\s*$/i;
|
|
18
|
+
const VERSION_RE = /^##\s+\[/;
|
|
19
|
+
const BULLET_RE = /^([-*]\s+)(.+?)\s*$/;
|
|
20
|
+
const HEADING_RE = /^###\s+/;
|
|
21
|
+
|
|
22
|
+
const SKELETON = `# Changelog
|
|
23
|
+
|
|
24
|
+
All notable changes to this project will be documented in this file.
|
|
25
|
+
|
|
26
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
27
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
28
|
+
|
|
29
|
+
## [Unreleased]
|
|
30
|
+
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
function normalizeBullet(text: string): string {
|
|
34
|
+
text = text.trim();
|
|
35
|
+
const m = BULLET_RE.exec(text);
|
|
36
|
+
if (m) return m[2].trim();
|
|
37
|
+
if (text.startsWith("- ") || text.startsWith("* ")) return text.slice(2).trim();
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatBullet(text: string): string {
|
|
42
|
+
return `- ${normalizeBullet(text)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function splitUnreleased(text: string): [string, string, string] {
|
|
46
|
+
const lines = text.split(/(?<=\n)/);
|
|
47
|
+
let start: number | null = null;
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
if (UNRELEASED_RE.test(lines[i].replace(/\n$/, ""))) {
|
|
50
|
+
start = i;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (start === null) return [text, "", ""];
|
|
55
|
+
|
|
56
|
+
let end = lines.length;
|
|
57
|
+
for (let j = start + 1; j < lines.length; j++) {
|
|
58
|
+
const stripped = lines[j].replace(/\n$/, "");
|
|
59
|
+
// Next version heading ends Unreleased (ignore a stray second Unreleased)
|
|
60
|
+
if (VERSION_RE.test(stripped)) {
|
|
61
|
+
if (UNRELEASED_RE.test(stripped)) continue;
|
|
62
|
+
end = j;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const before = lines.slice(0, start + 1).join("");
|
|
68
|
+
const body = lines.slice(start + 1, end).join("");
|
|
69
|
+
const after = lines.slice(end).join("");
|
|
70
|
+
return [before, body, after];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function canonicalCategory(heading: string): string | null {
|
|
74
|
+
const match = CAT_RE.exec(heading.replace(/\r?\n$/, ""));
|
|
75
|
+
if (!match) return null;
|
|
76
|
+
return CATEGORIES.find((c) => c.toLowerCase() === match[0].replace(/^###\s+/, "").trim().toLowerCase()) ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function splitSections(body: string): [string[], Array<{ heading: string; category: string | null; body: string[] }>] {
|
|
80
|
+
const preamble: string[] = [];
|
|
81
|
+
const sections: Array<{ heading: string; category: string | null; body: string[] }> = [];
|
|
82
|
+
let current: { heading: string; category: string | null; body: string[] } | null = null;
|
|
83
|
+
for (const line of body.split(/(?<=\n)/)) {
|
|
84
|
+
if (HEADING_RE.test(line)) {
|
|
85
|
+
current = { heading: line, category: canonicalCategory(line), body: [] };
|
|
86
|
+
sections.push(current);
|
|
87
|
+
} else if (current === null) {
|
|
88
|
+
preamble.push(line);
|
|
89
|
+
} else {
|
|
90
|
+
current.body.push(line);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return [preamble, sections];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function bulletBlocks(lines: string[]): Array<[string | null, string[]]> {
|
|
97
|
+
const blocks: Array<[string | null, string[]]> = [];
|
|
98
|
+
let current: string[] = [];
|
|
99
|
+
let key: string | null = null;
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
const match = BULLET_RE.exec(line.replace(/\r\n$/, "").replace(/\n$/, ""));
|
|
102
|
+
if (match) {
|
|
103
|
+
if (current.length) blocks.push([key, current]);
|
|
104
|
+
key = normalizeBullet(line).toLowerCase();
|
|
105
|
+
current = [line];
|
|
106
|
+
} else {
|
|
107
|
+
current.push(line);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (current.length) blocks.push([key, current]);
|
|
111
|
+
return blocks;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function mergeSections(
|
|
115
|
+
body: string,
|
|
116
|
+
entries: Record<string, string[]>,
|
|
117
|
+
normalizeOnly: boolean,
|
|
118
|
+
): [string, Record<string, number>, Record<string, number> | null, string | null] {
|
|
119
|
+
const [preamble, sections] = splitSections(body);
|
|
120
|
+
const first: Record<string, { heading: string; body: string[] }> = {};
|
|
121
|
+
const rendered: Array<{ heading: string; body: string[] }> = [];
|
|
122
|
+
for (const section of sections) {
|
|
123
|
+
if (section.category && section.category in first) {
|
|
124
|
+
first[section.category].body.push(...section.body);
|
|
125
|
+
} else {
|
|
126
|
+
const entry = { heading: section.heading, body: section.body };
|
|
127
|
+
rendered.push(entry);
|
|
128
|
+
if (section.category) first[section.category] = entry;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const added: Record<string, number> = {};
|
|
133
|
+
if (!normalizeOnly) {
|
|
134
|
+
for (const [rawCategory, bullets] of Object.entries(entries)) {
|
|
135
|
+
const category = CATEGORIES.find((c) => c.toLowerCase() === rawCategory.toLowerCase());
|
|
136
|
+
if (category === undefined) {
|
|
137
|
+
return ["", {}, {}, `invalid category: ${rawCategory}`];
|
|
138
|
+
}
|
|
139
|
+
let section = first[category];
|
|
140
|
+
if (section === undefined) {
|
|
141
|
+
section = { heading: `### ${category}\n`, body: ["\n"] };
|
|
142
|
+
rendered.push(section);
|
|
143
|
+
first[category] = section;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const seen = new Set<string>();
|
|
147
|
+
const kept: string[] = [];
|
|
148
|
+
for (const [key, block] of bulletBlocks(section.body)) {
|
|
149
|
+
if (key !== null && seen.has(key)) continue;
|
|
150
|
+
if (key !== null) seen.add(key);
|
|
151
|
+
kept.push(...block);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const fresh: string[] = [];
|
|
155
|
+
for (const bullet of bullets ?? []) {
|
|
156
|
+
const key = normalizeBullet(bullet).toLowerCase();
|
|
157
|
+
if (!key || seen.has(key)) continue;
|
|
158
|
+
seen.add(key);
|
|
159
|
+
fresh.push(formatBullet(bullet) + "\n");
|
|
160
|
+
}
|
|
161
|
+
if (fresh.length) {
|
|
162
|
+
let insertAt = 0;
|
|
163
|
+
while (insertAt < kept.length && !kept[insertAt].trim()) insertAt++;
|
|
164
|
+
section.body = [...kept.slice(0, insertAt), ...fresh, ...kept.slice(insertAt)];
|
|
165
|
+
} else {
|
|
166
|
+
section.body = kept;
|
|
167
|
+
}
|
|
168
|
+
added[category] = fresh.length;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let output = preamble.join("");
|
|
173
|
+
for (const section of rendered) {
|
|
174
|
+
output += section.heading + section.body.join("");
|
|
175
|
+
}
|
|
176
|
+
const counts: Record<string, number> = {};
|
|
177
|
+
for (const [category, section] of Object.entries(first)) {
|
|
178
|
+
counts[category] = bulletBlocks(section.body).filter(([key]) => key !== null).length;
|
|
179
|
+
}
|
|
180
|
+
return [output, counts, added, null];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function ensureFile(target: string): string {
|
|
184
|
+
if (fs.existsSync(target)) return fs.readFileSync(target, "utf8");
|
|
185
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
186
|
+
fs.writeFileSync(target, SKELETON, "utf8");
|
|
187
|
+
return SKELETON;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function hasUnreleasedHeading(text: string): boolean {
|
|
191
|
+
return text.split("\n").some((line) => UNRELEASED_RE.test(line.replace(/\n$/, "")));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function applyChangelog(
|
|
195
|
+
target: string,
|
|
196
|
+
entries: Record<string, string[]>,
|
|
197
|
+
normalizeOnly: boolean,
|
|
198
|
+
): Record<string, any> {
|
|
199
|
+
let text = ensureFile(target);
|
|
200
|
+
if (!hasUnreleasedHeading(text)) {
|
|
201
|
+
const lines = text.split(/(?<=\n)/);
|
|
202
|
+
let insertAt = lines.length;
|
|
203
|
+
for (let i = 0; i < lines.length; i++) {
|
|
204
|
+
const stripped = lines[i].replace(/\n$/, "");
|
|
205
|
+
if (VERSION_RE.test(stripped) && !UNRELEASED_RE.test(stripped)) {
|
|
206
|
+
insertAt = i;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
text = lines.slice(0, insertAt).join("") + "## [Unreleased]\n\n" + lines.slice(insertAt).join("");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const [before, body, after] = splitUnreleased(text);
|
|
214
|
+
const [newBody, counts, addedCounts, error] = mergeSections(body, entries, normalizeOnly);
|
|
215
|
+
if (error) return { error };
|
|
216
|
+
// before already includes "## [Unreleased]\n"
|
|
217
|
+
const out = (before.endsWith("\n") ? before : before + "\n") + newBody + after;
|
|
218
|
+
fs.writeFileSync(target, out, "utf8");
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
path: target,
|
|
222
|
+
normalize_only: normalizeOnly,
|
|
223
|
+
added: addedCounts,
|
|
224
|
+
categories: counts,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function normalizeEntries(entries: any): { data: Record<string, string[]> } | { error: string } {
|
|
229
|
+
if (!entries) return { data: {} };
|
|
230
|
+
if (Array.isArray(entries)) {
|
|
231
|
+
const grouped: Record<string, string[]> = {};
|
|
232
|
+
for (const item of entries) {
|
|
233
|
+
const cat = item.category ?? item.type;
|
|
234
|
+
const text = item.text ?? item.entry ?? "";
|
|
235
|
+
if (!cat || !text) {
|
|
236
|
+
return { error: "each entry needs category + text" };
|
|
237
|
+
}
|
|
238
|
+
const canon = CATEGORIES.find((c) => c.toLowerCase() === String(cat).toLowerCase());
|
|
239
|
+
if (!canon) return { error: `invalid category: ${cat}` };
|
|
240
|
+
(grouped[canon] ??= []).push(text);
|
|
241
|
+
}
|
|
242
|
+
return { data: grouped };
|
|
243
|
+
}
|
|
244
|
+
if (typeof entries === "object") {
|
|
245
|
+
const grouped: Record<string, string[]> = {};
|
|
246
|
+
for (const [cat, bullets] of Object.entries(entries)) {
|
|
247
|
+
const canon = CATEGORIES.find((c) => c.toLowerCase() === String(cat).toLowerCase());
|
|
248
|
+
if (!canon) return { error: `invalid category: ${cat}` };
|
|
249
|
+
const list = Array.isArray(bullets) ? bullets : [bullets];
|
|
250
|
+
grouped[canon] = list.filter(Boolean).map(String);
|
|
251
|
+
}
|
|
252
|
+
return { data: grouped };
|
|
253
|
+
}
|
|
254
|
+
return { error: "entries must be object or array" };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function changelogApply({
|
|
258
|
+
entries,
|
|
259
|
+
path: changelogPath,
|
|
260
|
+
normalize_only,
|
|
261
|
+
workspace_root,
|
|
262
|
+
}: {
|
|
263
|
+
entries?: any;
|
|
264
|
+
path?: string;
|
|
265
|
+
normalize_only?: boolean;
|
|
266
|
+
workspace_root: string;
|
|
267
|
+
}): Record<string, any> {
|
|
268
|
+
const cwd = resolveWorkspaceRoot(workspace_root);
|
|
269
|
+
const normalized = normalize_only
|
|
270
|
+
? { data: {} as Record<string, string[]> }
|
|
271
|
+
: normalizeEntries(entries);
|
|
272
|
+
if ("error" in normalized) return { error: normalized.error };
|
|
273
|
+
if (!normalize_only && Object.keys(normalized.data).length === 0) {
|
|
274
|
+
return { error: "entries required unless normalize_only" };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const rel = changelogPath || "CHANGELOG.md";
|
|
278
|
+
const target = path.resolve(cwd, rel);
|
|
279
|
+
const root = path.resolve(cwd);
|
|
280
|
+
if (target !== root && !target.startsWith(root + path.sep)) {
|
|
281
|
+
return { error: "changelog path must be inside workspace_root" };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
return applyChangelog(target, normalized.data, Boolean(normalize_only));
|
|
286
|
+
} catch (err) {
|
|
287
|
+
return { error: err instanceof Error ? err.message : "changelog apply failed" };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function changelogUnreleasedStats(workspace_root: string, changelogPath = "CHANGELOG.md") {
|
|
292
|
+
const cwd = resolveWorkspaceRoot(workspace_root);
|
|
293
|
+
const abs = path.isAbsolute(changelogPath)
|
|
294
|
+
? changelogPath
|
|
295
|
+
: path.join(cwd, changelogPath);
|
|
296
|
+
if (!fs.existsSync(abs)) return { exists: false };
|
|
297
|
+
const text = fs.readFileSync(abs, "utf8");
|
|
298
|
+
const m = text.match(/##\s+\[Unreleased\]([\s\S]*?)(?=\n##\s+\[|$)/i);
|
|
299
|
+
if (!m) return { exists: true, has_unreleased: false };
|
|
300
|
+
const body = m[1];
|
|
301
|
+
const headings = [...body.matchAll(/^###\s+(\w+)\s*$/gim)].map((x) => x[1]);
|
|
302
|
+
const dupes = headings.filter((h, i) =>
|
|
303
|
+
headings.slice(0, i).some((p) => p.toLowerCase() === h.toLowerCase()),
|
|
304
|
+
);
|
|
305
|
+
return {
|
|
306
|
+
exists: true,
|
|
307
|
+
has_unreleased: true,
|
|
308
|
+
category_headings: headings,
|
|
309
|
+
duplicate_category_headings: dupes,
|
|
310
|
+
needs_normalize: dupes.length > 0,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { initStatus } from "./init";
|
|
2
|
+
|
|
3
|
+
export const ALL_ITEM_IDS = ["youtrack_json", "youtrack_token", "vcs_json", "gitlab_token", "github_token"];
|
|
4
|
+
export const CONFIG_GAP_MARKER = "workflow config missing";
|
|
5
|
+
|
|
6
|
+
export function describeConfigGaps(scope?: string[]): { missing: string[]; ok: boolean } {
|
|
7
|
+
const all = scope ?? ALL_ITEM_IDS;
|
|
8
|
+
try {
|
|
9
|
+
const status: unknown = initStatus();
|
|
10
|
+
if (!status || typeof status !== "object" || (status as Record<string, unknown>).error) return { missing: all, ok: false };
|
|
11
|
+
const items = (status as Record<string, unknown>).items;
|
|
12
|
+
if (!Array.isArray(items) || items.length === 0) return { missing: all, ok: false };
|
|
13
|
+
const known = all;
|
|
14
|
+
const missing = items
|
|
15
|
+
.filter((item) => item && (item as Record<string, unknown>).ok === false)
|
|
16
|
+
.map((item) => String((item as Record<string, unknown>).id))
|
|
17
|
+
.filter((id) => known.includes(id));
|
|
18
|
+
return { missing, ok: missing.length === 0 };
|
|
19
|
+
} catch {
|
|
20
|
+
return { missing: all, ok: false };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function configGuardError(missing: string[]): string {
|
|
25
|
+
return `${CONFIG_GAP_MARKER}: ${missing.join(", ")}. Run \`npx workit init\` or \`/wk-init\` to configure.`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export type BranchPreset = "gitflow" | "github-flow" | "trunk-based" | "custom";
|
|
6
|
+
|
|
7
|
+
export type ToolkitConfig = {
|
|
8
|
+
locale: string;
|
|
9
|
+
localeOptions: string[];
|
|
10
|
+
timezone: string;
|
|
11
|
+
branchPolicy: { preset: BranchPreset; allowed: string[]; protected: string[] };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const PRESETS: Record<BranchPreset, { allowed: string[]; protected: string[] }> = {
|
|
15
|
+
gitflow: { allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"], protected: ["main", "develop", "master", "prod", "production"] },
|
|
16
|
+
"github-flow": { allowed: ["*"], protected: ["main"] },
|
|
17
|
+
"trunk-based": { allowed: ["*"], protected: ["main"] },
|
|
18
|
+
custom: { allowed: [], protected: [] },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const configDir = (): string =>
|
|
22
|
+
process.env.WORKFLOW_TOOLKIT_CONFIG
|
|
23
|
+
?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR
|
|
24
|
+
?? path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "workflow-toolkit");
|
|
25
|
+
|
|
26
|
+
export const LOCALE_RE = /^[a-z]{2,3}(-[A-Z]{2})?$/;
|
|
27
|
+
|
|
28
|
+
const DEFAULTS: ToolkitConfig = {
|
|
29
|
+
locale: "en",
|
|
30
|
+
localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
|
|
31
|
+
timezone: "America/Santiago",
|
|
32
|
+
branchPolicy: { preset: "gitflow", allowed: [...PRESETS.gitflow.allowed], protected: [...PRESETS.gitflow.protected] },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const readSafe = (p: string): string | null => {
|
|
36
|
+
try { return readFileSync(p, "utf8"); } catch { return null; }
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const readConfig = (): ToolkitConfig => {
|
|
40
|
+
const raw = readSafe(path.join(configDir(), "config.json"));
|
|
41
|
+
if (!raw) return DEFAULTS;
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(raw) as Partial<ToolkitConfig>;
|
|
44
|
+
const locale = LOCALE_RE.test(String(parsed.locale ?? "")) ? parsed.locale as string : DEFAULTS.locale;
|
|
45
|
+
const preset = (parsed.branchPolicy?.preset ?? "gitflow") as BranchPreset;
|
|
46
|
+
const presetOk = Object.hasOwn(PRESETS, preset) ? preset : "gitflow";
|
|
47
|
+
const presetDefs = PRESETS[presetOk];
|
|
48
|
+
return {
|
|
49
|
+
locale,
|
|
50
|
+
localeOptions: Array.isArray(parsed.localeOptions) ? parsed.localeOptions : DEFAULTS.localeOptions,
|
|
51
|
+
timezone: parsed.timezone ?? DEFAULTS.timezone,
|
|
52
|
+
branchPolicy: {
|
|
53
|
+
preset: presetOk,
|
|
54
|
+
allowed: Array.isArray(parsed.branchPolicy?.allowed) ? parsed.branchPolicy.allowed : presetDefs.allowed,
|
|
55
|
+
protected: Array.isArray(parsed.branchPolicy?.protected) ? parsed.branchPolicy.protected : presetDefs.protected,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
} catch {
|
|
59
|
+
return DEFAULTS;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const writeConfig = (config: ToolkitConfig): void => {
|
|
64
|
+
const dir = configDir();
|
|
65
|
+
mkdirSync(dir, { recursive: true });
|
|
66
|
+
writeFileSync(path.join(dir, "config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const resolveBranchPolicy = (config: ToolkitConfig): { allowed: RegExp[]; protected: Set<string> } => {
|
|
70
|
+
const allowed = config.branchPolicy.allowed.map((p) =>
|
|
71
|
+
new RegExp(`^${p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"));
|
|
72
|
+
return { allowed, protected: new Set(config.branchPolicy.protected.map((p) => p.toLowerCase())) };
|
|
73
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CONFIG_GAP_MARKER } from "./config-guard";
|
|
4
|
+
import { parseTasksFromPlan } from "./docs-validate";
|
|
5
|
+
|
|
6
|
+
export type Detection = { choices: string[]; pattern: "alpha" | "numeric" } | null;
|
|
7
|
+
|
|
8
|
+
export const detectConfigGapError = (text: string): boolean =>
|
|
9
|
+
text.includes(CONFIG_GAP_MARKER);
|
|
10
|
+
|
|
11
|
+
// Enforcement-rail detectors: case-insensitive word-boundary heuristics.
|
|
12
|
+
// Conservative bias (D-03): require 1+ signal word AND 0 evidence words;
|
|
13
|
+
// when any evidence wording appears, do NOT fire.
|
|
14
|
+
|
|
15
|
+
const COMPLETION_CLAIMS = /\b(?:done|fixed|passing|green|complete|all set)\b/i;
|
|
16
|
+
const VERIFICATION_EVIDENCE =
|
|
17
|
+
/\bbun run check\b|\bworkflow_verify\b|\bbun test\b|\bchecks?\s+pass(?:es|ing)?\b|\btests?\s+pass(?:es|ing)?\b/i;
|
|
18
|
+
|
|
19
|
+
// Claims completion without verification-command evidence in the same text.
|
|
20
|
+
export const detectVerificationClaim = (text: string): boolean =>
|
|
21
|
+
COMPLETION_CLAIMS.test(text) && !VERIFICATION_EVIDENCE.test(text);
|
|
22
|
+
|
|
23
|
+
const IMPLEMENTATION_SIGNALS =
|
|
24
|
+
/\b(?:changed|implemented|implementing|added|refactored|commit(?:ted|s)?|edited)\b/i;
|
|
25
|
+
const FAILING_TEST_WORDS =
|
|
26
|
+
/\b(?:failing test|test failed|watch it fail|red-green|tdd|test first)\b/i;
|
|
27
|
+
|
|
28
|
+
// Implementation signal without a preceding failing-test mention.
|
|
29
|
+
export const detectUntestedImplementation = (text: string): boolean =>
|
|
30
|
+
IMPLEMENTATION_SIGNALS.test(text) && !FAILING_TEST_WORDS.test(text);
|
|
31
|
+
|
|
32
|
+
const IMPLEMENTATION_ACTION =
|
|
33
|
+
/\b(?:implement|add the feature|write the code|create the component|build (?:the )?(?:feature|component|module|command|screen|service))\b/i;
|
|
34
|
+
const DESIGN_WORDS =
|
|
35
|
+
/\b(?:design|spec|brainstorm|approved|plan|requested|instruction|as you asked|as you said)\b/i;
|
|
36
|
+
|
|
37
|
+
// Implementation action without a presented/approved design.
|
|
38
|
+
export const detectImplementationWithoutDesign = (text: string): boolean =>
|
|
39
|
+
IMPLEMENTATION_ACTION.test(text) && !DESIGN_WORDS.test(text);
|
|
40
|
+
|
|
41
|
+
const FIX_SIGNALS = /\b(?:fixed|fix|patch|solved)\b/i;
|
|
42
|
+
const ROOT_CAUSE_WORDS =
|
|
43
|
+
/\b(?:root cause|caused by|reproduced|stack trace|investigation|because)\b/i;
|
|
44
|
+
|
|
45
|
+
// Fix proposal without root-cause evidence.
|
|
46
|
+
export const detectFixWithoutRootCause = (text: string): boolean =>
|
|
47
|
+
FIX_SIGNALS.test(text) && !ROOT_CAUSE_WORDS.test(text);
|
|
48
|
+
|
|
49
|
+
const ACCEPTANCE_SIGNALS = /\b(?:agreed|makes sense|good point|will implement)\b/i;
|
|
50
|
+
const REVIEW_VERIFICATION_WORDS =
|
|
51
|
+
/\b(?:verif(?:y|ied)|check(?:ed|ing)?|reproduced|tested|confirmed)\b/i;
|
|
52
|
+
|
|
53
|
+
// Review acceptance without verification wording.
|
|
54
|
+
export const detectBlindReviewAcceptance = (text: string): boolean =>
|
|
55
|
+
ACCEPTANCE_SIGNALS.test(text) && !REVIEW_VERIFICATION_WORDS.test(text);
|
|
56
|
+
|
|
57
|
+
// Instruction-option detector: a clickable `question` option whose label is an
|
|
58
|
+
// instruction to type free text. Clicking such an option returns the label
|
|
59
|
+
// literal, not the typed value. Conservative: instruction verb + free-text noun.
|
|
60
|
+
export const INSTRUCTION_OPTION_RE =
|
|
61
|
+
/^(type|provide|paste|enter|write|give me)\b.*\b(url|id|issue|text|notes|number)\b/i;
|
|
62
|
+
|
|
63
|
+
// Accepts the question tool-call input: an array of questions OR { questions: [...] }.
|
|
64
|
+
export const detectInstructionOption = (questions: unknown): boolean => {
|
|
65
|
+
const list = Array.isArray(questions)
|
|
66
|
+
? questions
|
|
67
|
+
: (questions as { questions?: unknown } | null)?.questions;
|
|
68
|
+
if (!Array.isArray(list)) return false;
|
|
69
|
+
for (const q of list) {
|
|
70
|
+
if (!q || typeof q !== "object") continue;
|
|
71
|
+
const options = (q as { options?: unknown }).options;
|
|
72
|
+
if (!Array.isArray(options)) continue;
|
|
73
|
+
for (const opt of options) {
|
|
74
|
+
if (!opt || typeof opt !== "object") continue;
|
|
75
|
+
const label = (opt as { label?: unknown }).label;
|
|
76
|
+
if (typeof label !== "string" || !label) continue;
|
|
77
|
+
if (INSTRUCTION_OPTION_RE.test(label)) return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Raw delivery: an UNLABELED fenced block carrying doc markers — the agent pasted
|
|
84
|
+
// the doc instead of rendering it. Rendered docs keep labeled fences (```mermaid)
|
|
85
|
+
// and never match; reminders use angle-bracket blocks, so no false positives.
|
|
86
|
+
// Labeled blocks are stripped first so their plain closing fence (```) can't match.
|
|
87
|
+
export const detectRawDocDelivery = (text: string): boolean =>
|
|
88
|
+
/^```\s*$/m.test(text.replace(/```\S[^\n]*\r?\n[\s\S]*?```/g, "")) &&
|
|
89
|
+
(text.includes("# Spec") || text.includes("# Plan") ||
|
|
90
|
+
text.includes("**Spec:**") || text.includes("**Branch:**"));
|
|
91
|
+
|
|
92
|
+
// Interrogative gate: a literal question mark OR explicit interrogative phrases.
|
|
93
|
+
// Plain "I want to confirm..." or "the script which runs" must NOT match.
|
|
94
|
+
const INTERROGATIVE = /[?¿]|which\s+one|choose\s+(?:one|between|among)|do\s+you\s+(?:want|prefer)|want\s+me\s+to/i;
|
|
95
|
+
|
|
96
|
+
export const detectProseChoices = (text: string): Detection => {
|
|
97
|
+
if (!INTERROGATIVE.test(text)) return null;
|
|
98
|
+
|
|
99
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
100
|
+
|
|
101
|
+
const alphaAll = [...text.matchAll(/([a-dA-D])[.)]\s+([^\n]*?)(?=\s+[a-dA-D][.)]\s|$)/g)]
|
|
102
|
+
.map((m) => ({ letter: m[1].toLowerCase(), choice: m[2].trim() }));
|
|
103
|
+
const alphaLines = lines
|
|
104
|
+
.map((l) => /^([a-dA-D])[.)]\s+(.+)$/.exec(l))
|
|
105
|
+
.filter((m): m is RegExpExecArray => Boolean(m))
|
|
106
|
+
.map((m) => ({ letter: m[1].toLowerCase(), choice: m[2] }));
|
|
107
|
+
const alpha = alphaAll.length >= alphaLines.length ? alphaAll : alphaLines;
|
|
108
|
+
|
|
109
|
+
if (alpha.length >= 2) {
|
|
110
|
+
const letters = alpha.map((a) => a.letter);
|
|
111
|
+
const expected = ["a", "b", "c", "d"].slice(0, alpha.length);
|
|
112
|
+
if (letters.every((l, i) => l === expected[i])) {
|
|
113
|
+
return { choices: alpha.map((a) => a.choice), pattern: "alpha" };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const numericAll = [...text.matchAll(/(\d+)[.)]\s+([^\n]*?)(?=\s+\d+[.)]\s|$)/g)]
|
|
118
|
+
.map((m) => ({ num: Number(m[1]), choice: m[2].trim() }));
|
|
119
|
+
const numericLines = lines
|
|
120
|
+
.map((l) => /^(\d+)[.)]\s+(.+)$/.exec(l))
|
|
121
|
+
.filter((m): m is RegExpExecArray => Boolean(m))
|
|
122
|
+
.map((m) => ({ num: Number(m[1]), choice: m[2] }));
|
|
123
|
+
const numeric = numericAll.length >= numericLines.length ? numericAll : numericLines;
|
|
124
|
+
|
|
125
|
+
if (numeric.length >= 2) {
|
|
126
|
+
const nums = numeric.map((n) => n.num);
|
|
127
|
+
if (nums.every((n, i) => n === i + 1)) {
|
|
128
|
+
return { choices: numeric.map((n) => n.choice), pattern: "numeric" };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const stripFences = (text: string): string => {
|
|
136
|
+
const lines = text.split("\n");
|
|
137
|
+
const out: string[] = [];
|
|
138
|
+
let inFence = false;
|
|
139
|
+
for (const line of lines) {
|
|
140
|
+
if (line.startsWith("```")) { inFence = !inFence; continue; }
|
|
141
|
+
if (!inFence) out.push(line);
|
|
142
|
+
}
|
|
143
|
+
return out.join("\n");
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const detectBacktickDocRefs = (text: string): string[] | null => {
|
|
147
|
+
const body = stripFences(text);
|
|
148
|
+
const refs = [...body.matchAll(/`docs\/[^`\s]+\.md`/g)].map((m) => m[0]);
|
|
149
|
+
if (!refs.length) return null;
|
|
150
|
+
if (/\[[^\]]+\]\(docs\//.test(body)) return null;
|
|
151
|
+
return refs;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// The rail has no terminal FlowStatus — a fully completed SDD ledger is done,
|
|
155
|
+
// but only if its complete set covers every task id in the plan (the last
|
|
156
|
+
// task's append may have been skipped, leaving an all-complete partial ledger).
|
|
157
|
+
const isPlanComplete = (slugDir: string): boolean => {
|
|
158
|
+
const ledger = path.join(slugDir, "sdd", "progress.md");
|
|
159
|
+
if (!existsSync(ledger)) return false; // no ledger yet — not provably complete
|
|
160
|
+
let taskLines: string[];
|
|
161
|
+
try {
|
|
162
|
+
taskLines = readFileSync(ledger, "utf8")
|
|
163
|
+
.split("\n")
|
|
164
|
+
.map((l) => l.trim())
|
|
165
|
+
.filter((l) => /^Task \s*\d+:/i.test(l));
|
|
166
|
+
} catch {
|
|
167
|
+
return true; // unreadable ledger → exclude the slug
|
|
168
|
+
}
|
|
169
|
+
if (taskLines.length === 0 || !taskLines.every((l) => /^Task \s*\d+:\s*complete\b/i.test(l))) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const planTasks = parseTasksFromPlan(readFileSync(path.join(slugDir, "plan.md"), "utf8"));
|
|
174
|
+
if (planTasks.length === 0) return true;
|
|
175
|
+
const completeIds = new Set(
|
|
176
|
+
taskLines.map((l) => Number(/^Task\s*(\d+):/i.exec(l)?.[1])).filter(Number.isFinite),
|
|
177
|
+
);
|
|
178
|
+
return planTasks.every((t) => completeIds.has(t.id));
|
|
179
|
+
} catch {
|
|
180
|
+
return false; // unreadable/missing plan.md — not provably complete, rail stays on
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const findActiveSubagentDrivenPlans = (root: string): string[] => {
|
|
185
|
+
const docsDir = path.join(root, "docs");
|
|
186
|
+
if (!existsSync(docsDir)) return [];
|
|
187
|
+
const slugs: string[] = [];
|
|
188
|
+
for (const slug of readdirSync(docsDir)) {
|
|
189
|
+
const file = path.join(docsDir, slug, "sdd", "flow.json");
|
|
190
|
+
try {
|
|
191
|
+
const flow = JSON.parse(readFileSync(file, "utf8")) as {
|
|
192
|
+
menu?: { chosen?: string };
|
|
193
|
+
plan?: { status?: string };
|
|
194
|
+
};
|
|
195
|
+
if (
|
|
196
|
+
flow.menu?.chosen === "subagent-driven" &&
|
|
197
|
+
flow.plan?.status === "approved" &&
|
|
198
|
+
!isPlanComplete(path.join(docsDir, slug))
|
|
199
|
+
) {
|
|
200
|
+
slugs.push(slug);
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// skip unreadable or malformed flow.json
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return slugs;
|
|
207
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const MAX_LINES = 150;
|
|
2
|
+
export const MAX_BYTES = 8192;
|
|
3
|
+
export const MAX_MERMAID = 3;
|
|
4
|
+
|
|
5
|
+
const MERMAID_FENCE = /^```mermaid\s*$/gm;
|
|
6
|
+
|
|
7
|
+
export const shouldRenderDoc = (text: string): boolean => {
|
|
8
|
+
const lines = text.split(/\r?\n/);
|
|
9
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
10
|
+
if (lines.length > MAX_LINES) return false;
|
|
11
|
+
if (Buffer.byteLength(text, "utf8") > MAX_BYTES) return false;
|
|
12
|
+
const mermaidCount = text.match(MERMAID_FENCE)?.length ?? 0;
|
|
13
|
+
return mermaidCount <= MAX_MERMAID;
|
|
14
|
+
};
|