@mrclrchtr/supi-flow 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -0
- package/package.json +32 -0
- package/prompts/supi-coding-retro.md +18 -0
- package/skills/supi-flow-apply/SKILL.md +119 -0
- package/skills/supi-flow-archive/SKILL.md +101 -0
- package/skills/supi-flow-brainstorm/SKILL.md +117 -0
- package/skills/supi-flow-debug/SKILL.md +151 -0
- package/skills/supi-flow-plan/SKILL.md +117 -0
- package/skills/supi-flow-slop-detect/SKILL.md +393 -0
- package/skills/supi-flow-slop-detect/references/vocabulary.json +161 -0
- package/skills/supi-flow-slop-detect/scripts/slop-helpers.ts +301 -0
- package/skills/supi-flow-slop-detect/scripts/slop-scan-structural.ts +269 -0
- package/skills/supi-flow-slop-detect/scripts/slop-scan-vocab.ts +161 -0
- package/skills/supi-flow-slop-detect/scripts/slop-scan.ts +209 -0
- package/src/cli.ts +80 -0
- package/src/index.ts +167 -0
- package/src/tools/flow-tools.ts +337 -0
- package/src/tools/tndm-cli.ts +246 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
type ExecResult = { stdout: string; stderr: string };
|
|
4
|
+
|
|
5
|
+
function toString(data: string | Buffer): string {
|
|
6
|
+
return typeof data === "string" ? data : data.toString("utf-8");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function run(
|
|
10
|
+
file: string,
|
|
11
|
+
args: string[],
|
|
12
|
+
options?: { maxBuffer?: number; timeout?: number },
|
|
13
|
+
): Promise<ExecResult> {
|
|
14
|
+
return new Promise<ExecResult>((resolve, reject) => {
|
|
15
|
+
const child = execFile(file, args, options, (error, stdout, stderr) => {
|
|
16
|
+
if (error) {
|
|
17
|
+
const msg = toString(stderr).trim() || error.message;
|
|
18
|
+
reject(new Error(`"${file} ${args.join(" ")}" failed: ${msg}`));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
resolve({
|
|
22
|
+
stdout: toString(stdout).trim(),
|
|
23
|
+
stderr: toString(stderr).trim(),
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Run a tndm subcommand and return stdout/stderr.
|
|
31
|
+
* Throws on non-zero exit, timeout, or other exec error.
|
|
32
|
+
*/
|
|
33
|
+
export async function tndm(args: string[]): Promise<ExecResult> {
|
|
34
|
+
return run("tndm", args, { timeout: 30_000 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run a tndm subcommand with `--json` and parse the structured output.
|
|
39
|
+
* Throws if exit is non-zero or JSON is invalid.
|
|
40
|
+
*/
|
|
41
|
+
export async function tndmJson<T = Record<string, unknown>>(
|
|
42
|
+
args: string[],
|
|
43
|
+
): Promise<T> {
|
|
44
|
+
const { stdout } = await tndm([...args, "--json"]);
|
|
45
|
+
if (!stdout) {
|
|
46
|
+
throw new Error(`tndm returned empty output for: ${args.join(" ")}`);
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(stdout) as T;
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`tndm returned invalid JSON for: ${args.join(" ")}\nOutput: ${stdout}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run `git add .tndm/` and `git commit -m <message>`.
|
|
59
|
+
* Uses `git diff --cached --quiet` to check for staged changes via exit code,
|
|
60
|
+
* avoiding locale-dependent string parsing.
|
|
61
|
+
* Throws on non-zero exit from `git commit`.
|
|
62
|
+
*/
|
|
63
|
+
export async function gitAddCommit(message: string): Promise<{ commitHash: string }> {
|
|
64
|
+
await run("git", ["add", ".tndm/"]);
|
|
65
|
+
|
|
66
|
+
// Check exit code instead of parsing locale-dependent output strings.
|
|
67
|
+
// git diff --cached --quiet exits 0 (no staged changes), non-zero (changes exist or error).
|
|
68
|
+
try {
|
|
69
|
+
await run("git", ["diff", "--cached", "--quiet"]);
|
|
70
|
+
// Exit 0: no changes staged — nothing to commit
|
|
71
|
+
return { commitHash: "" };
|
|
72
|
+
} catch {
|
|
73
|
+
// Exit non-zero: changes exist, or a real git error.
|
|
74
|
+
// Proceed to commit; real errors will surface there.
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { stdout } = await run("git", ["commit", "-m", message]);
|
|
78
|
+
const match = stdout.match(/\[[^\]]+ ([a-f0-9]+)\]/);
|
|
79
|
+
return { commitHash: match ? match[1] : "" };
|
|
80
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
import { tndmJson } from "./cli.js";
|
|
6
|
+
import { supi_tndm_cli_params, executeTndmCli } from "./tools/tndm-cli.js";
|
|
7
|
+
import {
|
|
8
|
+
supiFlowStartParams,
|
|
9
|
+
supiFlowPlanParams,
|
|
10
|
+
supiFlowCompleteTaskParams,
|
|
11
|
+
supiFlowCloseParams,
|
|
12
|
+
executeFlowStart,
|
|
13
|
+
executeFlowPlan,
|
|
14
|
+
executeFlowCompleteTask,
|
|
15
|
+
executeFlowClose,
|
|
16
|
+
} from "./tools/flow-tools.js";
|
|
17
|
+
|
|
18
|
+
const baseDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
19
|
+
|
|
20
|
+
export default function (pi: ExtensionAPI) {
|
|
21
|
+
// ── Resource discovery ──────────────────────────────────────
|
|
22
|
+
pi.on("resources_discover", () => ({
|
|
23
|
+
skillPaths: [join(baseDir, "skills")],
|
|
24
|
+
promptPaths: [join(baseDir, "prompts")],
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// ── Tool: supi_tndm_cli ─────────────────────────────────────
|
|
28
|
+
pi.registerTool({
|
|
29
|
+
name: "supi_tndm_cli",
|
|
30
|
+
label: "TNDM CLI",
|
|
31
|
+
description:
|
|
32
|
+
"Execute tndm ticket operations. Action determines which params apply:\n" +
|
|
33
|
+
"- create: title (required), status, priority, type, tags, depends_on, effort, content\n" +
|
|
34
|
+
"- update: id (required), title, status, priority, type, tags, add_tags, remove_tags, depends_on, effort, content\n" +
|
|
35
|
+
"- show: id (required)\n" +
|
|
36
|
+
"- list: all (boolean), definition (ready|questions|unknown)\n" +
|
|
37
|
+
"- awareness: against (git ref, required)",
|
|
38
|
+
promptSnippet: "Execute tndm ticket operations via supi_tndm_cli",
|
|
39
|
+
promptGuidelines: [
|
|
40
|
+
"Use supi_tndm_cli for direct tndm operations instead of running tndm via bash",
|
|
41
|
+
],
|
|
42
|
+
parameters: supi_tndm_cli_params,
|
|
43
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
44
|
+
return executeTndmCli(params);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ── Tool: supi_flow_start ───────────────────────────────────
|
|
49
|
+
pi.registerTool({
|
|
50
|
+
name: "supi_flow_start",
|
|
51
|
+
label: "Flow Start",
|
|
52
|
+
description:
|
|
53
|
+
"Start a new flow: creates a TNDM ticket with status=todo and tag=flow:brainstorm. " +
|
|
54
|
+
"Stores known design context in content.md and returns the ticket ID.",
|
|
55
|
+
promptSnippet: "Begin a new flow by creating a TNDM ticket",
|
|
56
|
+
promptGuidelines: [
|
|
57
|
+
"Use supi_flow_start at the beginning of every brainstorm to create the required ticket",
|
|
58
|
+
"Always include context (design intent/summary) when known",
|
|
59
|
+
],
|
|
60
|
+
parameters: supiFlowStartParams,
|
|
61
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
62
|
+
return executeFlowStart(params);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ── Tool: supi_flow_plan ────────────────────────────────────
|
|
67
|
+
pi.registerTool({
|
|
68
|
+
name: "supi_flow_plan",
|
|
69
|
+
label: "Flow Plan",
|
|
70
|
+
description:
|
|
71
|
+
"Store an implementation plan in a ticket's plan.md while keeping content.md as the canonical design summary. " +
|
|
72
|
+
"Updates tags from flow:brainstorm to flow:planned. Tasks must be numbered as '**Task {N}**' in the plan.",
|
|
73
|
+
promptSnippet: "Store a plan in a TNDM ticket",
|
|
74
|
+
promptGuidelines: [
|
|
75
|
+
"Use supi_flow_plan after creating a plan to persist it in the ticket",
|
|
76
|
+
"Number tasks sequentially as **Task 1**, **Task 2**, etc.",
|
|
77
|
+
],
|
|
78
|
+
parameters: supiFlowPlanParams,
|
|
79
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
80
|
+
return executeFlowPlan(params);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ── Tool: supi_flow_complete_task ───────────────────────────
|
|
85
|
+
pi.registerTool({
|
|
86
|
+
name: "supi_flow_complete_task",
|
|
87
|
+
label: "Flow Complete Task",
|
|
88
|
+
description:
|
|
89
|
+
"Mark a task as done in a ticket's plan.md by task number (1-based). " +
|
|
90
|
+
"Finds '- [ ] **Task N:**' and changes to '- [x] **Task N:**'.",
|
|
91
|
+
promptSnippet: "Check off a completed plan task in a TNDM ticket",
|
|
92
|
+
promptGuidelines: [
|
|
93
|
+
"Use supi_flow_complete_task after each task's verification passes during apply",
|
|
94
|
+
"Call this with the task number, not the description text",
|
|
95
|
+
],
|
|
96
|
+
parameters: supiFlowCompleteTaskParams,
|
|
97
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
98
|
+
return executeFlowCompleteTask(params);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── Tool: supi_flow_close ───────────────────────────────────
|
|
103
|
+
pi.registerTool({
|
|
104
|
+
name: "supi_flow_close",
|
|
105
|
+
label: "Flow Close",
|
|
106
|
+
description:
|
|
107
|
+
"Close a ticket and finalize the flow. " +
|
|
108
|
+
"Writes verification results to archive.md, sets status=done, tags=flow:done, " +
|
|
109
|
+
"and auto-commits .tndm/ changes.",
|
|
110
|
+
promptSnippet: "Close a TNDM ticket after implementation and verification",
|
|
111
|
+
promptGuidelines: [
|
|
112
|
+
"Use supi_flow_close at the end of the archive phase after all verification is complete",
|
|
113
|
+
"Pass the full verification evidence as verification_results",
|
|
114
|
+
],
|
|
115
|
+
parameters: supiFlowCloseParams,
|
|
116
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
117
|
+
return executeFlowClose(params);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── Command: /supi-flow-status ──────────────────────────────
|
|
122
|
+
pi.registerCommand("supi-flow-status", {
|
|
123
|
+
description: "Show current flow workflow state",
|
|
124
|
+
handler: async (_args, ctx) => {
|
|
125
|
+
const tickets = await tndmJson<Array<{ id: string; status: string; tags?: string[] }>>([
|
|
126
|
+
"ticket",
|
|
127
|
+
"list",
|
|
128
|
+
]);
|
|
129
|
+
const activeTickets = tickets.filter((ticket) => {
|
|
130
|
+
if (ticket.status === "done") return false;
|
|
131
|
+
const tags = ticket.tags ?? [];
|
|
132
|
+
return tags.includes("flow:brainstorm") || tags.includes("flow:planned") || tags.includes("flow:applying");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (activeTickets.length === 0) {
|
|
136
|
+
ctx.ui.notify("No active flow tickets. Start with /skill:supi-flow-brainstorm.", "info");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const lines = activeTickets.map((ticket) => {
|
|
141
|
+
const tags = ticket.tags ?? [];
|
|
142
|
+
const nextStep = tags.includes("flow:applying")
|
|
143
|
+
? `/skill:supi-flow-archive ${ticket.id}`
|
|
144
|
+
: tags.includes("flow:planned")
|
|
145
|
+
? `/skill:supi-flow-apply ${ticket.id}`
|
|
146
|
+
: `/skill:supi-flow-plan ${ticket.id}`;
|
|
147
|
+
return `${ticket.id} (${ticket.status}) -> ${nextStep}`;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
ctx.ui.notify(`Active flow tickets:\n${lines.join("\n")}`, "info");
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── Command: /supi-flow ─────────────────────────────────────
|
|
155
|
+
pi.registerCommand("supi-flow", {
|
|
156
|
+
description: "List available flow workflow commands",
|
|
157
|
+
handler: async (_args, ctx) => {
|
|
158
|
+
ctx.ui.notify(
|
|
159
|
+
"Flow: /skill:supi-flow-brainstorm -> /skill:supi-flow-plan -> /skill:supi-flow-apply -> /skill:supi-flow-archive\n" +
|
|
160
|
+
" /supi-flow-status -- show current state\n" +
|
|
161
|
+
" /supi-flow -- this help\n" +
|
|
162
|
+
"Available tools: supi_tndm_cli, supi_flow_start, supi_flow_plan, supi_flow_complete_task, supi_flow_close",
|
|
163
|
+
"info",
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { type Static, Type } from "typebox";
|
|
4
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
5
|
+
import { gitAddCommit, tndm, tndmJson } from "../cli.js";
|
|
6
|
+
|
|
7
|
+
// ─── supi_flow_start ───────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export const supiFlowStartParams = Type.Object({
|
|
10
|
+
title: Type.String({ description: "Ticket title describing the change" }),
|
|
11
|
+
priority: Type.Optional(
|
|
12
|
+
StringEnum(["p0", "p1", "p2", "p3", "p4"] as const, {
|
|
13
|
+
description: "Priority (default: p2)",
|
|
14
|
+
}),
|
|
15
|
+
),
|
|
16
|
+
type: Type.Optional(
|
|
17
|
+
StringEnum(["task", "bug", "feature", "chore", "epic"] as const, {
|
|
18
|
+
description: "Ticket type (default: task)",
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
context: Type.Optional(
|
|
22
|
+
Type.String({
|
|
23
|
+
description: "Brief context to store in ticket content (brainstorm intent / design summary)",
|
|
24
|
+
}),
|
|
25
|
+
),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type FlowStartParams = Static<typeof supiFlowStartParams>;
|
|
29
|
+
|
|
30
|
+
export async function executeFlowStart(params: FlowStartParams) {
|
|
31
|
+
const args: string[] = [
|
|
32
|
+
"ticket",
|
|
33
|
+
"create",
|
|
34
|
+
params.title,
|
|
35
|
+
"--status",
|
|
36
|
+
"todo",
|
|
37
|
+
"--tags",
|
|
38
|
+
"flow:brainstorm",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
if (params.priority) args.push("--priority", params.priority);
|
|
42
|
+
if (params.type) args.push("--type", params.type);
|
|
43
|
+
|
|
44
|
+
const result = await tndm(args);
|
|
45
|
+
const ticketId = result.stdout.trim();
|
|
46
|
+
|
|
47
|
+
if (params.context) {
|
|
48
|
+
await tndm(["ticket", "update", ticketId, "--content", params.context]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text" as const,
|
|
55
|
+
text: `Created ticket ${ticketId} with status=todo and flow:brainstorm tag.`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
details: { action: "flow_start", ticketId, status: "todo", tags: "flow:brainstorm" },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── supi_flow_plan ────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export const supiFlowPlanParams = Type.Object({
|
|
65
|
+
ticket_id: Type.String({ description: "Ticket ID (e.g. TNDM-A1B2C3)" }),
|
|
66
|
+
plan_content: Type.String({
|
|
67
|
+
description:
|
|
68
|
+
"Markdown plan content with tasks numbered as '**Task {N}**'.\n\n- [ ] **Task 1**: Description\n - File: path/to/file\n - Verification: command",
|
|
69
|
+
}),
|
|
70
|
+
append: Type.Optional(
|
|
71
|
+
Type.Boolean({
|
|
72
|
+
description:
|
|
73
|
+
"If true, append to existing content. If false (default), replace content entirely.",
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export type FlowPlanParams = Static<typeof supiFlowPlanParams>;
|
|
79
|
+
|
|
80
|
+
export async function executeFlowPlan(params: FlowPlanParams) {
|
|
81
|
+
// Create a "plan" document and get its path
|
|
82
|
+
const docResult = await tndm(["ticket", "doc", "create", params.ticket_id, "plan"]);
|
|
83
|
+
const docPath = docResult.stdout.trim();
|
|
84
|
+
|
|
85
|
+
let content = params.plan_content;
|
|
86
|
+
|
|
87
|
+
if (params.append) {
|
|
88
|
+
try {
|
|
89
|
+
const existingContent = readFileSync(docPath, "utf-8");
|
|
90
|
+
if (existingContent) {
|
|
91
|
+
content = existingContent + "\n\n" + content;
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// If reading fails, just use the new content
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Write the plan content to the document file
|
|
99
|
+
writeFileSync(docPath, content, "utf-8");
|
|
100
|
+
|
|
101
|
+
// Sync fingerprints and update tags
|
|
102
|
+
await tndm(["ticket", "sync", params.ticket_id]);
|
|
103
|
+
await tndm([
|
|
104
|
+
"ticket",
|
|
105
|
+
"update",
|
|
106
|
+
params.ticket_id,
|
|
107
|
+
"--add-tags",
|
|
108
|
+
"flow:planned",
|
|
109
|
+
"--remove-tags",
|
|
110
|
+
"flow:brainstorm",
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
type: "text" as const,
|
|
117
|
+
text: `Plan stored in ${params.ticket_id} (${docPath}). Tags updated to flow:planned.`,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
details: { action: "flow_plan", ticketId: params.ticket_id, tags: "flow:planned", path: docPath },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── supi_flow_complete_task ───────────────────────────────────
|
|
125
|
+
|
|
126
|
+
export const supiFlowCompleteTaskParams = Type.Object({
|
|
127
|
+
ticket_id: Type.String({ description: "Ticket ID (e.g. TNDM-A1B2C3)" }),
|
|
128
|
+
task_number: Type.Number({
|
|
129
|
+
description: "1-based task number to mark as complete (e.g. 1, 2, 3)",
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export type FlowCompleteTaskParams = Static<typeof supiFlowCompleteTaskParams>;
|
|
134
|
+
|
|
135
|
+
type CheckTaskResult =
|
|
136
|
+
| { kind: "unchecked"; updatedContent: string }
|
|
137
|
+
| { kind: "already_checked" }
|
|
138
|
+
| { kind: "not_found" };
|
|
139
|
+
|
|
140
|
+
function checkTask(content: string, taskNumber: number): CheckTaskResult {
|
|
141
|
+
// Match a task line like "- [ ] **Task N:**" or " - [ ] **Task N:**"
|
|
142
|
+
const lines = content.split("\n");
|
|
143
|
+
for (let i = 0; i < lines.length; i++) {
|
|
144
|
+
const line = lines[i];
|
|
145
|
+
const trimmed = line.trimStart();
|
|
146
|
+
|
|
147
|
+
const uncheckedMatch = trimmed.match(
|
|
148
|
+
new RegExp(`^- \\[ \\] \\*\\*Task ${taskNumber}\\*\\*:`),
|
|
149
|
+
);
|
|
150
|
+
if (uncheckedMatch) {
|
|
151
|
+
// Replace the [ ] with [x] in the trimmed version
|
|
152
|
+
const indent = line.slice(0, line.length - trimmed.length);
|
|
153
|
+
lines[i] = indent + trimmed.replace("- [ ]", "- [x]");
|
|
154
|
+
return { kind: "unchecked", updatedContent: lines.join("\n") };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const checkedMatch = trimmed.match(
|
|
158
|
+
new RegExp(`^- \\[x\\] \\*\\*Task ${taskNumber}\\*\\*:`),
|
|
159
|
+
);
|
|
160
|
+
if (checkedMatch) {
|
|
161
|
+
return { kind: "already_checked" };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { kind: "not_found" };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function executeFlowCompleteTask(params: FlowCompleteTaskParams) {
|
|
168
|
+
const showResult = await tndmJson<{
|
|
169
|
+
id: string;
|
|
170
|
+
content_path?: string;
|
|
171
|
+
documents?: Array<{ name: string; path: string }>;
|
|
172
|
+
}>([
|
|
173
|
+
"ticket",
|
|
174
|
+
"show",
|
|
175
|
+
params.ticket_id,
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
const contentPath = showResult.content_path;
|
|
179
|
+
if (!contentPath) {
|
|
180
|
+
return {
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: "text" as const,
|
|
184
|
+
text: `No content path found in ticket ${params.ticket_id}.`,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
details: { action: "flow_complete_task", ticketId: params.ticket_id, error: "No content path" },
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const planDocument = showResult.documents?.find((document) => document.name === "plan");
|
|
192
|
+
if (!planDocument) {
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: "text" as const,
|
|
197
|
+
text: `No plan file is registered in ticket ${params.ticket_id}.`,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
details: {
|
|
201
|
+
action: "flow_complete_task",
|
|
202
|
+
ticketId: params.ticket_id,
|
|
203
|
+
error: "No plan file",
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const planPath = join(dirname(contentPath), planDocument.path);
|
|
209
|
+
|
|
210
|
+
let content: string;
|
|
211
|
+
try {
|
|
212
|
+
content = readFileSync(planPath, "utf-8");
|
|
213
|
+
} catch {
|
|
214
|
+
return {
|
|
215
|
+
content: [
|
|
216
|
+
{
|
|
217
|
+
type: "text" as const,
|
|
218
|
+
text: `No plan file found at ${planPath}. No tasks to complete.`,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
details: { action: "flow_complete_task", ticketId: params.ticket_id, error: "No plan file" },
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const result = checkTask(content, params.task_number);
|
|
226
|
+
|
|
227
|
+
switch (result.kind) {
|
|
228
|
+
case "unchecked":
|
|
229
|
+
writeFileSync(planPath, result.updatedContent, "utf-8");
|
|
230
|
+
await tndm(["ticket", "sync", params.ticket_id]);
|
|
231
|
+
return {
|
|
232
|
+
content: [
|
|
233
|
+
{
|
|
234
|
+
type: "text" as const,
|
|
235
|
+
text: `Task ${params.task_number} checked off in ${params.ticket_id}.`,
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
details: {
|
|
239
|
+
action: "flow_complete_task",
|
|
240
|
+
ticketId: params.ticket_id,
|
|
241
|
+
taskNumber: params.task_number,
|
|
242
|
+
completed: true,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
case "already_checked":
|
|
247
|
+
return {
|
|
248
|
+
content: [
|
|
249
|
+
{
|
|
250
|
+
type: "text" as const,
|
|
251
|
+
text: `Task ${params.task_number} is already checked off in ${params.ticket_id}.`,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
details: {
|
|
255
|
+
action: "flow_complete_task",
|
|
256
|
+
ticketId: params.ticket_id,
|
|
257
|
+
taskNumber: params.task_number,
|
|
258
|
+
completed: true,
|
|
259
|
+
skipped: true,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
case "not_found":
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Task ${params.task_number} not found in ticket ${params.ticket_id}.` +
|
|
266
|
+
` Task must exist as '- [ ] **Task N:**' or '- [x] **Task N:**'.`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─── supi_flow_close ───────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
export const supiFlowCloseParams = Type.Object({
|
|
274
|
+
ticket_id: Type.String({ description: "Ticket ID (e.g. TNDM-A1B2C3)" }),
|
|
275
|
+
verification_results: Type.Optional(
|
|
276
|
+
Type.String({
|
|
277
|
+
description:
|
|
278
|
+
"Verification results / evidence from the agent. Appended to the ticket content under ## Verification Results.",
|
|
279
|
+
}),
|
|
280
|
+
),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
export type FlowCloseParams = Static<typeof supiFlowCloseParams>;
|
|
284
|
+
|
|
285
|
+
export async function executeFlowClose(params: FlowCloseParams) {
|
|
286
|
+
let archivePath = "";
|
|
287
|
+
|
|
288
|
+
if (params.verification_results) {
|
|
289
|
+
// Create/register archive.md via document registry, then write results
|
|
290
|
+
try {
|
|
291
|
+
const docResult = await tndm(["ticket", "doc", "create", params.ticket_id, "archive"]);
|
|
292
|
+
archivePath = docResult.stdout.trim();
|
|
293
|
+
writeFileSync(archivePath, `# Archive\n\n${params.verification_results}\n`, "utf-8");
|
|
294
|
+
await tndm(["ticket", "sync", params.ticket_id]);
|
|
295
|
+
} catch {
|
|
296
|
+
// Non-fatal if doc create fails
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await tndm([
|
|
301
|
+
"ticket",
|
|
302
|
+
"update",
|
|
303
|
+
params.ticket_id,
|
|
304
|
+
"--status",
|
|
305
|
+
"done",
|
|
306
|
+
"--add-tags",
|
|
307
|
+
"flow:done",
|
|
308
|
+
"--remove-tags",
|
|
309
|
+
"flow:applying",
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
let commitHash = "";
|
|
313
|
+
try {
|
|
314
|
+
const commitResult = await gitAddCommit(`chore(tndm): close ${params.ticket_id}`);
|
|
315
|
+
commitHash = commitResult.commitHash;
|
|
316
|
+
} catch {
|
|
317
|
+
// Non-fatal if commit fails
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
content: [
|
|
322
|
+
{
|
|
323
|
+
type: "text" as const,
|
|
324
|
+
text: `Ticket ${params.ticket_id} closed (status=done, flow:done).${
|
|
325
|
+
commitHash ? ` Committed as ${commitHash}.` : ""
|
|
326
|
+
}`,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
details: {
|
|
330
|
+
action: "flow_close",
|
|
331
|
+
ticketId: params.ticket_id,
|
|
332
|
+
status: "done",
|
|
333
|
+
tags: "flow:done",
|
|
334
|
+
commitHash,
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|