@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,30 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { fail, ok } from "../core";
|
|
3
|
+
import { listRules, writeRule, type CanonicalRule } from "../core/rules";
|
|
4
|
+
|
|
5
|
+
const output = (value: unknown) => JSON.stringify(value, null, 2);
|
|
6
|
+
|
|
7
|
+
export function createRuleTools() {
|
|
8
|
+
return {
|
|
9
|
+
workflow_rule_list: tool({
|
|
10
|
+
description: "List canonical rules (config) with platforms and source",
|
|
11
|
+
args: {},
|
|
12
|
+
execute: async () => output(ok({ rules: listRules() })),
|
|
13
|
+
}),
|
|
14
|
+
workflow_rule_edit: tool({
|
|
15
|
+
description: "Write a canonical rule to the toolkit config dir (agent-assisted)",
|
|
16
|
+
args: {
|
|
17
|
+
name: tool.schema.string(),
|
|
18
|
+
description: tool.schema.string(),
|
|
19
|
+
platforms: tool.schema.array(tool.schema.enum(["cursor", "opencode"])),
|
|
20
|
+
body: tool.schema.string(),
|
|
21
|
+
confirmed: tool.schema.boolean(),
|
|
22
|
+
},
|
|
23
|
+
execute: async ({ name, description, platforms, body, confirmed }) => {
|
|
24
|
+
const rule: CanonicalRule = { name, description, platforms, body };
|
|
25
|
+
const result = writeRule(rule, confirmed);
|
|
26
|
+
return output(result.ok ? ok(result) : fail(result.error));
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
}
|
package/src/tools/sdd.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { tool, type ToolContext } from "@opencode-ai/plugin";
|
|
4
|
+
import { fail, ok, resolveGitRevision, resolveInside } from "../core";
|
|
5
|
+
import { resolveBranch, docsBranch } from "../core/branch";
|
|
6
|
+
import { docsValidate } from "../core/docs-validate";
|
|
7
|
+
import { parsePlanTasks, resolveHandoffBranch } from "../core/plan-tasks";
|
|
8
|
+
import {
|
|
9
|
+
sddAppendProgress, sddContext, sddReviewPackage, sddTaskBrief,
|
|
10
|
+
} from "../core/sdd";
|
|
11
|
+
import { WorkflowStateStore } from "../state";
|
|
12
|
+
|
|
13
|
+
const output = (value: unknown) => JSON.stringify(value, null, 2);
|
|
14
|
+
const requireConfirmed = (confirmed: boolean) => confirmed === true
|
|
15
|
+
? null
|
|
16
|
+
: output(fail("confirmed: true required"));
|
|
17
|
+
|
|
18
|
+
const relativePath = (root: string, candidate: string) => {
|
|
19
|
+
if (path.isAbsolute(candidate)) throw new Error("path must be repository-relative");
|
|
20
|
+
resolveInside(root, candidate);
|
|
21
|
+
return candidate;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const normalize = (value: Record<string, unknown>) => {
|
|
25
|
+
if (value.error) return fail(String(value.error));
|
|
26
|
+
if (value.ok === false) return fail("legacy operation reported failure");
|
|
27
|
+
const { ok: _legacyOk, ...data } = value;
|
|
28
|
+
return ok(data);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const invoke = (operation: () => Record<string, unknown>) => {
|
|
32
|
+
try {
|
|
33
|
+
return output(normalize(operation()));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return output(fail(error instanceof Error ? error.message : "workflow operation failed"));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const planPaths = (root: string, planPath: string, suppliedSpecPath?: string) => {
|
|
40
|
+
const resolved = resolveInside(root, planPath);
|
|
41
|
+
const match = readFileSync(resolved, "utf8").match(/^\*\*Spec:\*\*\s*(?:`([^`]+)`|(\S+))/m);
|
|
42
|
+
const spec_path = suppliedSpecPath ?? match?.[1] ?? match?.[2] ?? "";
|
|
43
|
+
if (spec_path) relativePath(root, spec_path);
|
|
44
|
+
return {
|
|
45
|
+
spec_path,
|
|
46
|
+
plan_path: planPath,
|
|
47
|
+
sdd_dir: path.posix.join("docs", path.basename(path.dirname(planPath)), "sdd"),
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function createSddTools(state: WorkflowStateStore) {
|
|
52
|
+
const record = (context: ToolContext, data: Record<string, unknown>) => state.set(context.sessionID, {
|
|
53
|
+
spec: String(data.spec_path ?? ""),
|
|
54
|
+
plan: String(data.plan_path ?? ""),
|
|
55
|
+
sdd: String(data.sdd_dir ?? ""),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
workflow_docs_branch: tool({
|
|
60
|
+
description: "Resolve branch for spec/plan authors: keep current feature|bugfix or create from develop",
|
|
61
|
+
args: {
|
|
62
|
+
plan_path: tool.schema.string().optional(),
|
|
63
|
+
kind: tool.schema.enum(["feature", "bugfix"]).optional(),
|
|
64
|
+
},
|
|
65
|
+
execute: async ({ plan_path, kind }, context) => invoke(() => {
|
|
66
|
+
if (plan_path) relativePath(context.directory, plan_path);
|
|
67
|
+
return docsBranch({
|
|
68
|
+
plan_path,
|
|
69
|
+
kind,
|
|
70
|
+
workspace_root: context.directory,
|
|
71
|
+
}) as Record<string, unknown>;
|
|
72
|
+
}),
|
|
73
|
+
}),
|
|
74
|
+
workflow_docs_validate: tool({
|
|
75
|
+
description: "Hard-fail validate spec/plan headers, link, branch, task order; returns quality findings (hard/warning)",
|
|
76
|
+
args: {
|
|
77
|
+
spec_path: tool.schema.string(),
|
|
78
|
+
plan_path: tool.schema.string(),
|
|
79
|
+
},
|
|
80
|
+
execute: async ({ spec_path, plan_path }, context) => invoke(() => {
|
|
81
|
+
relativePath(context.directory, spec_path);
|
|
82
|
+
relativePath(context.directory, plan_path);
|
|
83
|
+
const result = docsValidate({
|
|
84
|
+
spec_path,
|
|
85
|
+
plan_path,
|
|
86
|
+
workspace_root: context.directory,
|
|
87
|
+
}) as Record<string, unknown>;
|
|
88
|
+
if (result.error) return result;
|
|
89
|
+
if (result.ok === false) return result;
|
|
90
|
+
return result;
|
|
91
|
+
}),
|
|
92
|
+
}),
|
|
93
|
+
workflow_plan_tasks: tool({
|
|
94
|
+
description: "Parse top-level tasks from a workflow plan",
|
|
95
|
+
args: { plan_path: tool.schema.string(), spec_path: tool.schema.string().optional() },
|
|
96
|
+
execute: async ({ plan_path, spec_path }, context) => invoke(() => {
|
|
97
|
+
relativePath(context.directory, plan_path);
|
|
98
|
+
const paths = planPaths(context.directory, plan_path, spec_path);
|
|
99
|
+
const parsed = parsePlanTasks(plan_path, context.directory) as Record<string, unknown>;
|
|
100
|
+
if (parsed.error) return parsed;
|
|
101
|
+
const branch = paths.spec_path
|
|
102
|
+
? resolveHandoffBranch(paths.spec_path, plan_path, context.directory) as Record<string, unknown>
|
|
103
|
+
: {};
|
|
104
|
+
if (branch.error) return branch;
|
|
105
|
+
const data = { ...parsed, ...paths, ...branch };
|
|
106
|
+
record(context, data);
|
|
107
|
+
return data;
|
|
108
|
+
}),
|
|
109
|
+
}),
|
|
110
|
+
workflow_resolve_branch: tool({
|
|
111
|
+
description: "Resolve a branch from repository spec and plan metadata",
|
|
112
|
+
args: { spec_path: tool.schema.string(), plan_path: tool.schema.string() },
|
|
113
|
+
execute: async ({ spec_path, plan_path }, context) => invoke(() => {
|
|
114
|
+
relativePath(context.directory, spec_path);
|
|
115
|
+
relativePath(context.directory, plan_path);
|
|
116
|
+
return resolveBranch({ spec_path, plan_path, workspace_root: context.directory });
|
|
117
|
+
}),
|
|
118
|
+
}),
|
|
119
|
+
workflow_sdd_context: tool({
|
|
120
|
+
description: "Resolve the SDD workspace and progress ledger",
|
|
121
|
+
args: { plan_path: tool.schema.string() },
|
|
122
|
+
execute: async ({ plan_path }, context) => invoke(() => {
|
|
123
|
+
relativePath(context.directory, plan_path);
|
|
124
|
+
const parsed = sddContext({ slug: undefined, plan_path, workspace_root: context.directory }) as Record<string, unknown>;
|
|
125
|
+
if (parsed.error) return parsed;
|
|
126
|
+
const todos = Array.isArray(parsed.todos)
|
|
127
|
+
? parsed.todos.map((todo: Record<string, unknown>) => todo.status === "in_progress"
|
|
128
|
+
? { ...todo, status: "pending" }
|
|
129
|
+
: todo)
|
|
130
|
+
: [];
|
|
131
|
+
const data = { ...parsed, todos, ...planPaths(context.directory, plan_path), sdd_dir: parsed.sdd_dir };
|
|
132
|
+
record(context, data);
|
|
133
|
+
return data;
|
|
134
|
+
}),
|
|
135
|
+
}),
|
|
136
|
+
workflow_sdd_task_brief: tool({
|
|
137
|
+
description: "Write a confirmed task brief",
|
|
138
|
+
args: {
|
|
139
|
+
confirmed: tool.schema.boolean(),
|
|
140
|
+
sdd_dir: tool.schema.string(),
|
|
141
|
+
task_id: tool.schema.number(),
|
|
142
|
+
section_text: tool.schema.string(),
|
|
143
|
+
},
|
|
144
|
+
execute: async ({ confirmed, sdd_dir, task_id, section_text }, context) => {
|
|
145
|
+
const rejected = requireConfirmed(confirmed);
|
|
146
|
+
if (rejected) return rejected;
|
|
147
|
+
return invoke(() => {
|
|
148
|
+
relativePath(context.directory, sdd_dir);
|
|
149
|
+
return sddTaskBrief({ sdd_dir, task_id, section_text, workspace_root: context.directory });
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
workflow_sdd_review_package: tool({
|
|
154
|
+
description: "Write a confirmed task review diff",
|
|
155
|
+
args: {
|
|
156
|
+
confirmed: tool.schema.boolean(),
|
|
157
|
+
sdd_dir: tool.schema.string(),
|
|
158
|
+
base_sha: tool.schema.string(),
|
|
159
|
+
head_sha: tool.schema.string(),
|
|
160
|
+
},
|
|
161
|
+
execute: async ({ confirmed, sdd_dir, base_sha, head_sha }, context) => {
|
|
162
|
+
const rejected = requireConfirmed(confirmed);
|
|
163
|
+
if (rejected) return rejected;
|
|
164
|
+
return invoke(() => {
|
|
165
|
+
relativePath(context.directory, sdd_dir);
|
|
166
|
+
resolveGitRevision(context.directory, base_sha);
|
|
167
|
+
resolveGitRevision(context.directory, head_sha);
|
|
168
|
+
return sddReviewPackage({ sdd_dir, base_sha, head_sha, workspace_root: context.directory });
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
workflow_sdd_append_progress: tool({
|
|
173
|
+
description: "Append one confirmed validated SDD progress line",
|
|
174
|
+
args: {
|
|
175
|
+
confirmed: tool.schema.boolean(),
|
|
176
|
+
progress_path: tool.schema.string(),
|
|
177
|
+
line: tool.schema.string(),
|
|
178
|
+
},
|
|
179
|
+
execute: async ({ confirmed, progress_path, line }, context) => {
|
|
180
|
+
const rejected = requireConfirmed(confirmed);
|
|
181
|
+
if (rejected) return rejected;
|
|
182
|
+
return invoke(() => {
|
|
183
|
+
relativePath(context.directory, progress_path);
|
|
184
|
+
return sddAppendProgress({ progress_path, line, workspace_root: context.directory });
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { fail, ok } from "../core";
|
|
3
|
+
import { listTemplates, writeTemplate, type TemplateName } from "../core/templates";
|
|
4
|
+
|
|
5
|
+
const output = (value: unknown) => JSON.stringify(value, null, 2);
|
|
6
|
+
|
|
7
|
+
export function createTemplateTools() {
|
|
8
|
+
return {
|
|
9
|
+
workflow_template_list: tool({
|
|
10
|
+
description: "List editable templates (issue-update, greeting, headers) with their source",
|
|
11
|
+
args: {},
|
|
12
|
+
execute: async () => output(ok({ templates: listTemplates() })),
|
|
13
|
+
}),
|
|
14
|
+
workflow_template_edit: tool({
|
|
15
|
+
description: "Write an edited template to the toolkit config dir (agent-assisted)",
|
|
16
|
+
args: {
|
|
17
|
+
name: tool.schema.enum(["issue-update", "greeting", "headers"]),
|
|
18
|
+
content: tool.schema.string(),
|
|
19
|
+
confirmed: tool.schema.boolean(),
|
|
20
|
+
},
|
|
21
|
+
execute: async ({ name, content, confirmed }) => {
|
|
22
|
+
const result = writeTemplate(name as TemplateName, content, confirmed);
|
|
23
|
+
return output(result.ok ? ok(result) : fail(result.error));
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { tool, type ToolContext } from "@opencode-ai/plugin";
|
|
5
|
+
import { fail, ok, resolveInside, type Result } from "../core";
|
|
6
|
+
import { configGuardError, describeConfigGaps } from "../core/config-guard";
|
|
7
|
+
import {
|
|
8
|
+
buildDraft as legacyBuildDraft,
|
|
9
|
+
context as legacyContext,
|
|
10
|
+
logTime as legacyLogTime,
|
|
11
|
+
parseDuration as legacyParseDuration,
|
|
12
|
+
parseIssueRef,
|
|
13
|
+
postUpdate as legacyPostUpdate,
|
|
14
|
+
verifyYouTrackToken,
|
|
15
|
+
} from "../core/youtrack";
|
|
16
|
+
|
|
17
|
+
const ISSUE_RE = /^[A-Z]+-\d+$/;
|
|
18
|
+
const output = (value: unknown) => JSON.stringify(value, null, 2);
|
|
19
|
+
const message = (error: unknown) => error instanceof Error ? error.message : String(error);
|
|
20
|
+
|
|
21
|
+
// Both override names point at the config dir itself, same precedence as
|
|
22
|
+
// src/core/config.ts and scripts/init/status.sh: WORKFLOW_TOOLKIT_CONFIG → WORKFLOW_TOOLKIT_CONFIG_DIR → XDG.
|
|
23
|
+
export const configPath = (env: NodeJS.ProcessEnv = process.env, home = os.homedir()) =>
|
|
24
|
+
path.join(
|
|
25
|
+
env.WORKFLOW_TOOLKIT_CONFIG
|
|
26
|
+
?? env.WORKFLOW_TOOLKIT_CONFIG_DIR
|
|
27
|
+
?? path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "workflow-toolkit"),
|
|
28
|
+
"youtrack.json",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export function readCredentials(env: NodeJS.ProcessEnv = process.env, home = os.homedir()) {
|
|
32
|
+
const resolvedConfig = configPath(env, home);
|
|
33
|
+
const config = JSON.parse(readFileSync(resolvedConfig, "utf8")) as { tokenFile?: string };
|
|
34
|
+
const tokenFile = config.tokenFile ?? "youtrack.token";
|
|
35
|
+
const tokenPath = path.resolve(path.dirname(resolvedConfig), tokenFile.replace(/^~(?=\/)/, home));
|
|
36
|
+
if (process.platform !== "win32" && (statSync(tokenPath).mode & 0o777) !== 0o600) throw new Error("youtrack.token mode must be 0600");
|
|
37
|
+
const token = readFileSync(tokenPath, "utf8").trim();
|
|
38
|
+
if (!token) throw new Error("youtrack.token is empty");
|
|
39
|
+
return { configPath: resolvedConfig, token };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const redact = (text: string, token: string) =>
|
|
43
|
+
token ? text.split(token).join("[REDACTED]") : text;
|
|
44
|
+
|
|
45
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
46
|
+
type LegacyValue = Record<string, unknown> | void;
|
|
47
|
+
export type NotApplied = { ok: false; error: string; outcome: "not_applied" };
|
|
48
|
+
|
|
49
|
+
export type YouTrackOperations = {
|
|
50
|
+
verifyToken(): MaybePromise<LegacyValue>;
|
|
51
|
+
context(input: Record<string, unknown>): MaybePromise<LegacyValue>;
|
|
52
|
+
parseDuration(text: string, workspaceRoot: string): MaybePromise<LegacyValue>;
|
|
53
|
+
postComment(issueId: string, markdown: string, workspaceRoot?: string): MaybePromise<LegacyValue>;
|
|
54
|
+
logTime(input: Record<string, unknown>): MaybePromise<LegacyValue>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const unwrap = (value: LegacyValue) => {
|
|
58
|
+
if (!value) return {};
|
|
59
|
+
if (value.error) throw new Error(String(value.error));
|
|
60
|
+
if (value.ok === false) throw new Error(String(value.error ?? "YouTrack operation failed"));
|
|
61
|
+
return (value.data as Record<string, unknown> | undefined) ?? value;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const defaultOperations: YouTrackOperations = {
|
|
65
|
+
verifyToken: () => unwrap(verifyYouTrackToken()),
|
|
66
|
+
context: (input) => legacyContext(input as never),
|
|
67
|
+
parseDuration: (text, workspaceRoot) => legacyParseDuration(text, workspaceRoot),
|
|
68
|
+
postComment: (issueId, markdown, workspaceRoot) => legacyPostUpdate({
|
|
69
|
+
confirmed: true, issueId, markdown, workspace_root: workspaceRoot,
|
|
70
|
+
} as never),
|
|
71
|
+
logTime: (input) => legacyLogTime(input as never),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type PostInput = {
|
|
75
|
+
confirmed: boolean;
|
|
76
|
+
issueId: string;
|
|
77
|
+
markdown: string;
|
|
78
|
+
minutes?: number;
|
|
79
|
+
workspace_root?: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type PostData = {
|
|
83
|
+
issueId: string;
|
|
84
|
+
postedComment: boolean;
|
|
85
|
+
loggedMinutes: number;
|
|
86
|
+
outcome?: "unknown" | "not_applied";
|
|
87
|
+
instructions?: string;
|
|
88
|
+
retry?: "workflow_youtrack_post" | "workflow_youtrack_log_time";
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const notApplied = (value: LegacyValue): value is NotApplied =>
|
|
92
|
+
value?.ok === false && value.outcome === "not_applied";
|
|
93
|
+
|
|
94
|
+
const withWriteFlag = async <T>(fn: () => Promise<T>): Promise<T> => {
|
|
95
|
+
const previous = process.env.WORKFLOW_YT_WRITE;
|
|
96
|
+
process.env.WORKFLOW_YT_WRITE = "1";
|
|
97
|
+
try {
|
|
98
|
+
return await fn();
|
|
99
|
+
} finally {
|
|
100
|
+
if (previous === undefined) delete process.env.WORKFLOW_YT_WRITE;
|
|
101
|
+
else process.env.WORKFLOW_YT_WRITE = previous;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export async function postUpdate(
|
|
106
|
+
input: PostInput,
|
|
107
|
+
operations: Pick<YouTrackOperations, "postComment" | "logTime"> = defaultOperations,
|
|
108
|
+
): Promise<Result<PostData>> {
|
|
109
|
+
if (input.confirmed !== true) return fail("confirmed: true required");
|
|
110
|
+
if (!ISSUE_RE.test(input.issueId)) return fail("invalid issueId");
|
|
111
|
+
if (!input.markdown?.trim()) return fail("markdown required");
|
|
112
|
+
if (input.minutes != null && input.minutes <= 0) return fail("minutes must be positive");
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const comment = await operations.postComment(input.issueId, input.markdown, input.workspace_root);
|
|
116
|
+
if (notApplied(comment)) return fail(comment.error, {
|
|
117
|
+
issueId: input.issueId, postedComment: false, loggedMinutes: 0,
|
|
118
|
+
outcome: "not_applied", retry: "workflow_youtrack_post",
|
|
119
|
+
});
|
|
120
|
+
unwrap(comment);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return fail(message(error), {
|
|
123
|
+
issueId: input.issueId, postedComment: false, loggedMinutes: 0, outcome: "unknown",
|
|
124
|
+
instructions: "Check YouTrack comments manually; do not retry while the outcome is unknown.",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (input.minutes != null) {
|
|
129
|
+
try {
|
|
130
|
+
const time = await operations.logTime({
|
|
131
|
+
issueId: input.issueId,
|
|
132
|
+
minutes: input.minutes,
|
|
133
|
+
text: "workit update",
|
|
134
|
+
workspace_root: input.workspace_root,
|
|
135
|
+
});
|
|
136
|
+
if (notApplied(time)) return fail(time.error, {
|
|
137
|
+
issueId: input.issueId, postedComment: true, loggedMinutes: 0,
|
|
138
|
+
outcome: "not_applied", retry: "workflow_youtrack_log_time",
|
|
139
|
+
});
|
|
140
|
+
unwrap(time);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return fail(message(error), {
|
|
143
|
+
issueId: input.issueId, postedComment: true, loggedMinutes: 0, outcome: "unknown",
|
|
144
|
+
instructions: "Check YouTrack time entries manually; do not retry while the outcome is unknown.",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return ok({
|
|
150
|
+
issueId: input.issueId,
|
|
151
|
+
postedComment: true,
|
|
152
|
+
loggedMinutes: input.minutes ?? 0,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function logTimeUpdate(
|
|
157
|
+
input: Record<string, unknown>,
|
|
158
|
+
operation: Pick<YouTrackOperations, "logTime"> = defaultOperations,
|
|
159
|
+
): Promise<Result<Record<string, unknown>>> {
|
|
160
|
+
try {
|
|
161
|
+
const value = await operation.logTime(input);
|
|
162
|
+
if (notApplied(value)) return fail(value.error, {
|
|
163
|
+
issueId: input.issueId, loggedMinutes: 0, outcome: "not_applied",
|
|
164
|
+
retry: "workflow_youtrack_log_time",
|
|
165
|
+
});
|
|
166
|
+
return ok(unwrap(value));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return fail(message(error), {
|
|
169
|
+
issueId: input.issueId, loggedMinutes: 0, outcome: "unknown",
|
|
170
|
+
instructions: "Check YouTrack time entries manually; do not retry while the outcome is unknown.",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function normalizeContext(value: LegacyValue, mode?: string): LegacyValue {
|
|
176
|
+
if (!value || mode !== "meetings") return value;
|
|
177
|
+
const config = value.config as Record<string, unknown> | undefined;
|
|
178
|
+
const issue = String(config?.meetingIssue || "IRPT-12");
|
|
179
|
+
const { meetingIssues: _meetingIssues, ...singleMeetingConfig } = config ?? {};
|
|
180
|
+
const options = Array.isArray(value.meetingOptions)
|
|
181
|
+
? value.meetingOptions as Array<Record<string, unknown>>
|
|
182
|
+
: [];
|
|
183
|
+
const selected = options.find((option) => option.issue === issue) ?? {
|
|
184
|
+
key: "general", issue, label: issue, workItemText: "Reuniones",
|
|
185
|
+
};
|
|
186
|
+
return {
|
|
187
|
+
...value,
|
|
188
|
+
config: singleMeetingConfig,
|
|
189
|
+
meetingOptions: [selected],
|
|
190
|
+
requiresMeetingChoice: false,
|
|
191
|
+
issueId: issue,
|
|
192
|
+
workItemText: selected.workItemText ?? "Reuniones",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const standardResult = (value: LegacyValue, token = "") => {
|
|
197
|
+
try {
|
|
198
|
+
const data = unwrap(value);
|
|
199
|
+
const { ok: _legacyOk, ...normalized } = data;
|
|
200
|
+
return ok(normalized);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return fail(redact(message(error), token));
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const invoke = async (operation: () => MaybePromise<LegacyValue>, token = "") => {
|
|
207
|
+
try {
|
|
208
|
+
return output(standardResult(await operation(), token));
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return output(fail(redact(message(error), token)));
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const credentials = () => readCredentials();
|
|
215
|
+
const configGap = () => {
|
|
216
|
+
const { missing } = describeConfigGaps(["youtrack_json", "youtrack_token"]);
|
|
217
|
+
return missing.length > 0 ? output(fail(configGuardError(missing))) : null;
|
|
218
|
+
};
|
|
219
|
+
const requireConfirmed = (confirmed: boolean) => confirmed === true
|
|
220
|
+
? null
|
|
221
|
+
: output(fail("confirmed: true required"));
|
|
222
|
+
|
|
223
|
+
const rejectedTimeInput = (issueId: string, minutes: number) => {
|
|
224
|
+
const error = !ISSUE_RE.test(issueId)
|
|
225
|
+
? "invalid issueId"
|
|
226
|
+
: !Number.isFinite(minutes) || minutes <= 0
|
|
227
|
+
? "minutes must be positive"
|
|
228
|
+
: null;
|
|
229
|
+
return error ? output(fail(error, {
|
|
230
|
+
issueId, loggedMinutes: 0, outcome: "not_applied",
|
|
231
|
+
retry: "workflow_youtrack_log_time",
|
|
232
|
+
instructions: "Correct the invalid input, then retry workflow_youtrack_log_time once.",
|
|
233
|
+
})) : null;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export function createYouTrackTools(operations: YouTrackOperations = defaultOperations) {
|
|
237
|
+
return {
|
|
238
|
+
workflow_youtrack_verify_token: tool({
|
|
239
|
+
description: "Verify the configured YouTrack token with a read-only request",
|
|
240
|
+
args: {},
|
|
241
|
+
execute: async () => {
|
|
242
|
+
let token = "";
|
|
243
|
+
try { token = credentials().token; } catch (error) {
|
|
244
|
+
const gap = configGap();
|
|
245
|
+
if (gap) return gap;
|
|
246
|
+
return output(fail(message(error)));
|
|
247
|
+
}
|
|
248
|
+
return invoke(() => operations.verifyToken(), token);
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
workflow_youtrack_parse_issue: tool({
|
|
252
|
+
description: "Parse an existing YouTrack issue URL or id",
|
|
253
|
+
args: { issue_ref: tool.schema.string() },
|
|
254
|
+
execute: async ({ issue_ref }) => invoke(() => parseIssueRef(issue_ref)),
|
|
255
|
+
}),
|
|
256
|
+
workflow_youtrack_context: tool({
|
|
257
|
+
description: "Load YouTrack context for the configured meeting issue or an existing task issue",
|
|
258
|
+
args: {
|
|
259
|
+
mode: tool.schema.enum(["meetings", "task"]).optional(),
|
|
260
|
+
issue_id: tool.schema.string().optional(),
|
|
261
|
+
issue_url: tool.schema.string().optional(),
|
|
262
|
+
issue_ref: tool.schema.string().optional(),
|
|
263
|
+
spec_path: tool.schema.string().optional(),
|
|
264
|
+
plan_path: tool.schema.string().optional(),
|
|
265
|
+
},
|
|
266
|
+
execute: async (input, context) => {
|
|
267
|
+
try {
|
|
268
|
+
for (const candidate of [input.spec_path, input.plan_path].filter(Boolean) as string[]) {
|
|
269
|
+
if (path.isAbsolute(candidate)) throw new Error("path must be repository-relative");
|
|
270
|
+
resolveInside(context.directory, candidate);
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
const detail = message(error);
|
|
274
|
+
return output(fail(detail.includes("repository-relative") ? detail : `path must be repository-relative: ${detail}`));
|
|
275
|
+
}
|
|
276
|
+
let token = "";
|
|
277
|
+
try { token = credentials().token; } catch (error) {
|
|
278
|
+
const gap = configGap();
|
|
279
|
+
if (gap) return gap;
|
|
280
|
+
return output(fail(message(error)));
|
|
281
|
+
}
|
|
282
|
+
return invoke(async () => normalizeContext(
|
|
283
|
+
await operations.context({ ...input, workspace_root: context.directory }), input.mode,
|
|
284
|
+
), token);
|
|
285
|
+
},
|
|
286
|
+
}),
|
|
287
|
+
workflow_youtrack_parse_duration: tool({
|
|
288
|
+
description: "Parse duration text into integer minutes",
|
|
289
|
+
args: { text: tool.schema.string() },
|
|
290
|
+
execute: async ({ text }, context) => invoke(() => operations.parseDuration(text, context.directory)),
|
|
291
|
+
}),
|
|
292
|
+
workflow_youtrack_draft: tool({
|
|
293
|
+
description: "Build an es-CL update comment without posting it",
|
|
294
|
+
args: {
|
|
295
|
+
issueId: tool.schema.string(),
|
|
296
|
+
userNotes: tool.schema.string(),
|
|
297
|
+
greeting: tool.schema.string().optional(),
|
|
298
|
+
projectName: tool.schema.string().optional(),
|
|
299
|
+
includeProjectOpener: tool.schema.boolean().optional(),
|
|
300
|
+
includeFacts: tool.schema.boolean().optional(),
|
|
301
|
+
facts: tool.schema.object({
|
|
302
|
+
progress_excerpt: tool.schema.array(tool.schema.string()).optional(),
|
|
303
|
+
git_commits: tool.schema.array(tool.schema.string()).optional(),
|
|
304
|
+
}).optional(),
|
|
305
|
+
},
|
|
306
|
+
execute: async (input) => invoke(() => legacyBuildDraft(input as never)),
|
|
307
|
+
}),
|
|
308
|
+
workflow_youtrack_log_time: tool({
|
|
309
|
+
description: "Log confirmed time on an existing YouTrack issue without posting a comment",
|
|
310
|
+
args: {
|
|
311
|
+
confirmed: tool.schema.boolean(),
|
|
312
|
+
issueId: tool.schema.string(),
|
|
313
|
+
minutes: tool.schema.number(),
|
|
314
|
+
text: tool.schema.string().optional(),
|
|
315
|
+
dateMs: tool.schema.number().optional(),
|
|
316
|
+
},
|
|
317
|
+
execute: async ({ confirmed, ...input }, context: ToolContext) => {
|
|
318
|
+
const rejected = requireConfirmed(confirmed);
|
|
319
|
+
if (rejected) return rejected;
|
|
320
|
+
const invalid = rejectedTimeInput(input.issueId, input.minutes);
|
|
321
|
+
if (invalid) return invalid;
|
|
322
|
+
let token = "";
|
|
323
|
+
try { token = credentials().token; } catch (error) {
|
|
324
|
+
const gap = configGap();
|
|
325
|
+
if (gap) return gap;
|
|
326
|
+
return output(fail(message(error)));
|
|
327
|
+
}
|
|
328
|
+
const result = await withWriteFlag(() =>
|
|
329
|
+
logTimeUpdate({ ...input, workspace_root: context.directory }, operations));
|
|
330
|
+
return output(result.ok ? result : { ...result, error: redact(result.error, token) });
|
|
331
|
+
},
|
|
332
|
+
}),
|
|
333
|
+
workflow_youtrack_post: tool({
|
|
334
|
+
description: "Post a confirmed es-CL comment, then optionally log time",
|
|
335
|
+
args: {
|
|
336
|
+
confirmed: tool.schema.boolean(),
|
|
337
|
+
issueId: tool.schema.string(),
|
|
338
|
+
markdown: tool.schema.string(),
|
|
339
|
+
minutes: tool.schema.number().optional(),
|
|
340
|
+
},
|
|
341
|
+
execute: async (input, context: ToolContext) => {
|
|
342
|
+
const rejected = requireConfirmed(input.confirmed);
|
|
343
|
+
if (rejected) return rejected;
|
|
344
|
+
let token = "";
|
|
345
|
+
try { token = credentials().token; } catch (error) {
|
|
346
|
+
const gap = configGap();
|
|
347
|
+
if (gap) return gap;
|
|
348
|
+
return output(fail(message(error)));
|
|
349
|
+
}
|
|
350
|
+
const result = await withWriteFlag(() =>
|
|
351
|
+
postUpdate({ ...input, workspace_root: context.directory }, operations));
|
|
352
|
+
return output(result.ok ? result : { ...result, error: redact(result.error, token) });
|
|
353
|
+
},
|
|
354
|
+
}),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Load `using-superpowers`, `subagent-driven-development`, `test-driven-development`, and `verification-before-completion` through OpenCode's `skill` tool. Implement the existing plan; do not re-plan.
|
|
2
|
+
|
|
3
|
+
**Spec:** <SPEC_PATH>
|
|
4
|
+
**Plan:** <PLAN_PATH>
|
|
5
|
+
**Branch:** <BRANCH>
|
|
6
|
+
**SDD:** `<SDD_DIR>`
|
|
7
|
+
|
|
8
|
+
## Hard gates
|
|
9
|
+
|
|
10
|
+
- The parent is coordinator-only: it does not edit product code or perform delegated exploration.
|
|
11
|
+
- Never use a worktree. Branch changes are in-place through `workflow_branch_setup` on `feature/*` or `bugfix/*`; never commit on protected branches.
|
|
12
|
+
- Tracked state, briefs, ledgers, and review diffs live only under `<SDD_DIR>` in `docs/<slug>/sdd/` and use `workflow_sdd_*` tools.
|
|
13
|
+
- Use native `todowrite` for visible task state as well as the tracked ledger.
|
|
14
|
+
- Use native `question` for branch/stash choices and guarded external mutations; call mutation tools only after approval with `confirmed: true`.
|
|
15
|
+
- Use native `task` with only the built-in `explore` and `general` agents.
|
|
16
|
+
|
|
17
|
+
## Flow gates (HARD)
|
|
18
|
+
|
|
19
|
+
- `wk-implement` refuses to run unless the plan is `approved` (flow.json) and the post-plan menu was presented.
|
|
20
|
+
- `wk-handoff` refuses to run unless both spec and plan are `approved`.
|
|
21
|
+
- Sequence is enforced by tools: `workflow_spec_approve` (×2), `workflow_plan_approve` (×2), `workflow_plan_menu` — never skip a step.
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
0. Call `workflow_docs_validate` with the linked spec/plan paths. Hard-fail on any error before todos or branch setup.
|
|
26
|
+
1. Call `workflow_sdd_context` with `<PLAN_PATH>` and initialize `todowrite` from returned tasks.
|
|
27
|
+
2. Call `workflow_plan_tasks`; cache each top-level task's `section_text`.
|
|
28
|
+
3. Mark IDs in `completed_task_ids` completed and never redispatch them.
|
|
29
|
+
4. Call `workflow_resolve_branch`, then show the current branch, target branch, and stash behavior before any in-place checkout/setup mutation.
|
|
30
|
+
5. Always use `question`: for a clean tree ask whether to proceed or cancel; for a dirty tree add the stash choice and describe what will be stashed.
|
|
31
|
+
6. Call `workflow_branch_setup` with `confirmed: true` only after approval.
|
|
32
|
+
|
|
33
|
+
## Remaining-task loop
|
|
34
|
+
|
|
35
|
+
For each top-level task absent from `completed_task_ids`:
|
|
36
|
+
|
|
37
|
+
1. Mark it `in_progress` with `todowrite`.
|
|
38
|
+
2. Create a tracked brief with `workflow_sdd_task_brief` and `confirmed: true`.
|
|
39
|
+
3. Delegate read-only discovery, when needed, to an `explore` agent. Delegate implementation to a fresh `general` agent. Product changes follow TDD.
|
|
40
|
+
4. Create a tracked diff with `workflow_sdd_review_package` and `confirmed: true`.
|
|
41
|
+
5. Delegate spec-compliance review and code-quality review to separate `general` agents.
|
|
42
|
+
6. **Blocking** findings (Critical, Important, or spec-compliance) may trigger at most **two** fix+re-review rounds per task. **Advisory** findings (Minor, style, YAGNI, taste) never pause the loop — append them to `<SDD_DIR>/advisories.md`.
|
|
43
|
+
7. Append the validated ledger entry with `workflow_sdd_append_progress` and `confirmed: true`; mark the todo completed.
|
|
44
|
+
|
|
45
|
+
## Final gate
|
|
46
|
+
|
|
47
|
+
Run a separate full-branch code review, then `workflow_verify`. Present the full `<SDD_DIR>/advisories.md` roll-up once, then use native `question` so the user can choose which advisory items to fix, discuss, or discard. Report exact check results and never infer success. Use `workflow_git_context` for a commit preview and load `wk-commit` through `skill` for an approved commit. If tracked state contains a stash reference, preview reapplication through `question`, then call `workflow_branch_setup` with `confirmed: true` after approval.
|
|
48
|
+
|
|
49
|
+
## Task order
|
|
50
|
+
|
|
51
|
+
<TASK_LIST>
|
|
52
|
+
|
|
53
|
+
## Quality gate (HARD)
|
|
54
|
+
|
|
55
|
+
- Specs/plans are written from `templates/spec-template.md` / `templates/plan-template.md`.
|
|
56
|
+
- After `workflow_docs_validate`, surface `quality` findings (spec scan). Hard findings (missing required section, missing CA-XX) block task start unless the user explicitly waives them. Warnings are advisory.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{greetingText}}
|