@leing2021/super-pi 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +372 -0
- package/extensions/.gitkeep +0 -0
- package/extensions/ce-core/index.ts +528 -0
- package/extensions/ce-core/tools/artifact-helper.ts +73 -0
- package/extensions/ce-core/tools/ask-user-question.ts +53 -0
- package/extensions/ce-core/tools/brainstorm-dialog.ts +167 -0
- package/extensions/ce-core/tools/parallel-subagent.ts +54 -0
- package/extensions/ce-core/tools/pattern-extractor.ts +139 -0
- package/extensions/ce-core/tools/plan-diff.ts +136 -0
- package/extensions/ce-core/tools/review-router.ts +95 -0
- package/extensions/ce-core/tools/session-checkpoint.ts +212 -0
- package/extensions/ce-core/tools/session-history.ts +117 -0
- package/extensions/ce-core/tools/subagent.ts +59 -0
- package/extensions/ce-core/tools/task-splitter.ts +122 -0
- package/extensions/ce-core/tools/workflow-state.ts +80 -0
- package/extensions/ce-core/tools/worktree-manager.ts +131 -0
- package/extensions/ce-core/utils/artifact-paths.ts +23 -0
- package/extensions/ce-core/utils/name-utils.ts +8 -0
- package/package.json +53 -0
- package/skills/.gitkeep +0 -0
- package/skills/01-brainstorm/SKILL.md +120 -0
- package/skills/01-brainstorm/references/builder-mode.md +39 -0
- package/skills/01-brainstorm/references/handoff.md +8 -0
- package/skills/01-brainstorm/references/premise-challenge.md +23 -0
- package/skills/01-brainstorm/references/requirements-template.md +25 -0
- package/skills/01-brainstorm/references/startup-diagnostic.md +108 -0
- package/skills/02-plan/SKILL.md +77 -0
- package/skills/02-plan/references/ceo-review-mode.md +130 -0
- package/skills/02-plan/references/handoff.md +8 -0
- package/skills/02-plan/references/implementation-unit-template.md +25 -0
- package/skills/02-plan/references/plan-template.md +21 -0
- package/skills/03-work/SKILL.md +65 -0
- package/skills/03-work/references/handoff.md +8 -0
- package/skills/03-work/references/progress-update-format.md +17 -0
- package/skills/04-review/SKILL.md +72 -0
- package/skills/04-review/references/findings-schema.md +17 -0
- package/skills/04-review/references/handoff.md +20 -0
- package/skills/04-review/references/qa-test-mode.md +134 -0
- package/skills/04-review/references/reviewer-selection.md +24 -0
- package/skills/05-compound/SKILL.md +32 -0
- package/skills/05-compound/assets/solution-template.md +27 -0
- package/skills/05-compound/references/category-map.md +9 -0
- package/skills/05-compound/references/overlap-rules.md +7 -0
- package/skills/05-compound/references/solution-schema.yaml +27 -0
- package/skills/05-compound/references/solution-search-strategy.md +60 -0
- package/skills/06-next/SKILL.md +37 -0
- package/skills/06-next/references/recommendation-logic.md +46 -0
- package/skills/07-worktree/SKILL.md +35 -0
- package/skills/07-worktree/references/worktree-lifecycle.md +22 -0
- package/skills/08-status/SKILL.md +41 -0
- package/skills/08-status/references/artifact-locations.md +10 -0
- package/skills/09-help/SKILL.md +37 -0
- package/skills/09-help/references/workflow-sequence.md +9 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { normalizeSlug } from "../utils/name-utils"
|
|
4
|
+
|
|
5
|
+
export interface SessionCheckpointInput {
|
|
6
|
+
operation: "save" | "load" | "list" | "fail" | "retry"
|
|
7
|
+
repoRoot: string
|
|
8
|
+
planPath?: string
|
|
9
|
+
completedUnits?: string[]
|
|
10
|
+
failedUnit?: string
|
|
11
|
+
error?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CheckpointEntry {
|
|
15
|
+
planPath: string
|
|
16
|
+
completedUnits: string[]
|
|
17
|
+
updatedAt: string
|
|
18
|
+
status?: "active" | "failed"
|
|
19
|
+
failedUnit?: string
|
|
20
|
+
error?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SessionCheckpointResult {
|
|
24
|
+
operation: string
|
|
25
|
+
planPath?: string
|
|
26
|
+
completedUnits?: string[]
|
|
27
|
+
updatedAt?: string
|
|
28
|
+
status?: string
|
|
29
|
+
failedUnit?: string
|
|
30
|
+
error?: string
|
|
31
|
+
strategy?: string
|
|
32
|
+
retryFrom?: string
|
|
33
|
+
checkpoints?: CheckpointEntry[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function checkpointDir(repoRoot: string): string {
|
|
37
|
+
return path.join(repoRoot, ".context", "compound-engineering", "checkpoints")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function checkpointPath(repoRoot: string, planPath: string): string {
|
|
41
|
+
const slug = normalizeSlug(planPath.replace(/\//g, "-"))
|
|
42
|
+
return path.join(checkpointDir(repoRoot), `${slug}.json`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createSessionCheckpointTool() {
|
|
46
|
+
return {
|
|
47
|
+
name: "session_checkpoint",
|
|
48
|
+
async execute(input: SessionCheckpointInput): Promise<SessionCheckpointResult> {
|
|
49
|
+
switch (input.operation) {
|
|
50
|
+
case "save":
|
|
51
|
+
return saveCheckpoint(input)
|
|
52
|
+
case "load":
|
|
53
|
+
return loadCheckpoint(input)
|
|
54
|
+
case "list":
|
|
55
|
+
return listCheckpoints(input)
|
|
56
|
+
case "fail":
|
|
57
|
+
return failCheckpoint(input)
|
|
58
|
+
case "retry":
|
|
59
|
+
return retryCheckpoint(input)
|
|
60
|
+
default:
|
|
61
|
+
throw new Error(`Unknown operation: ${input.operation}`)
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function readEntry(repoRoot: string, planPath: string): Promise<CheckpointEntry | null> {
|
|
68
|
+
const filePath = checkpointPath(repoRoot, planPath)
|
|
69
|
+
try {
|
|
70
|
+
const content = await readFile(filePath, "utf8")
|
|
71
|
+
return JSON.parse(content)
|
|
72
|
+
} catch {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function writeEntry(repoRoot: string, entry: CheckpointEntry): Promise<void> {
|
|
78
|
+
const filePath = checkpointPath(repoRoot, entry.planPath)
|
|
79
|
+
await mkdir(path.dirname(filePath), { recursive: true })
|
|
80
|
+
await writeFile(filePath, JSON.stringify(entry, null, 2), "utf8")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function saveCheckpoint(input: SessionCheckpointInput): Promise<SessionCheckpointResult> {
|
|
84
|
+
const planPath = required(input.planPath, "planPath")
|
|
85
|
+
const completedUnits = input.completedUnits ?? []
|
|
86
|
+
|
|
87
|
+
const entry: CheckpointEntry = {
|
|
88
|
+
planPath,
|
|
89
|
+
completedUnits,
|
|
90
|
+
updatedAt: new Date().toISOString(),
|
|
91
|
+
status: "active",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await writeEntry(input.repoRoot, entry)
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
operation: "save",
|
|
98
|
+
planPath,
|
|
99
|
+
completedUnits,
|
|
100
|
+
updatedAt: entry.updatedAt,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function loadCheckpoint(input: SessionCheckpointInput): Promise<SessionCheckpointResult> {
|
|
105
|
+
const planPath = required(input.planPath, "planPath")
|
|
106
|
+
const entry = await readEntry(input.repoRoot, planPath)
|
|
107
|
+
|
|
108
|
+
if (!entry) {
|
|
109
|
+
return { operation: "load", planPath, completedUnits: [] }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
operation: "load",
|
|
114
|
+
planPath: entry.planPath,
|
|
115
|
+
completedUnits: entry.completedUnits,
|
|
116
|
+
updatedAt: entry.updatedAt,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function listCheckpoints(input: SessionCheckpointInput): Promise<SessionCheckpointResult> {
|
|
121
|
+
const dir = checkpointDir(input.repoRoot)
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const files = await readdir(dir)
|
|
125
|
+
const checkpoints: CheckpointEntry[] = []
|
|
126
|
+
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
if (file.endsWith(".json")) {
|
|
129
|
+
try {
|
|
130
|
+
const content = await readFile(path.join(dir, file), "utf8")
|
|
131
|
+
checkpoints.push(JSON.parse(content))
|
|
132
|
+
} catch {
|
|
133
|
+
// Skip malformed files
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { operation: "list", checkpoints }
|
|
139
|
+
} catch {
|
|
140
|
+
return { operation: "list", checkpoints: [] }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function failCheckpoint(input: SessionCheckpointInput): Promise<SessionCheckpointResult> {
|
|
145
|
+
const planPath = required(input.planPath, "planPath")
|
|
146
|
+
const entry = await readEntry(input.repoRoot, planPath)
|
|
147
|
+
|
|
148
|
+
if (!entry) {
|
|
149
|
+
throw new Error("No checkpoint found. Use 'save' first.")
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
entry.status = "failed"
|
|
153
|
+
entry.failedUnit = input.failedUnit ?? ""
|
|
154
|
+
entry.error = input.error ?? ""
|
|
155
|
+
entry.updatedAt = new Date().toISOString()
|
|
156
|
+
|
|
157
|
+
await writeEntry(input.repoRoot, entry)
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
operation: "fail",
|
|
161
|
+
planPath: entry.planPath,
|
|
162
|
+
status: "failed",
|
|
163
|
+
failedUnit: entry.failedUnit,
|
|
164
|
+
error: entry.error,
|
|
165
|
+
completedUnits: entry.completedUnits,
|
|
166
|
+
updatedAt: entry.updatedAt,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function retryCheckpoint(input: SessionCheckpointInput): Promise<SessionCheckpointResult> {
|
|
171
|
+
const planPath = required(input.planPath, "planPath")
|
|
172
|
+
const entry = await readEntry(input.repoRoot, planPath)
|
|
173
|
+
|
|
174
|
+
if (!entry) {
|
|
175
|
+
throw new Error("No checkpoint found.")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (entry.status !== "failed") {
|
|
179
|
+
throw new Error("Checkpoint is not in a failed state. Nothing to retry.")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Determine retry strategy based on error type
|
|
183
|
+
const errorLower = (entry.error ?? "").toLowerCase()
|
|
184
|
+
let strategy = "retry-unit"
|
|
185
|
+
|
|
186
|
+
if (errorLower.includes("timeout")) {
|
|
187
|
+
strategy = "retry-with-longer-timeout"
|
|
188
|
+
} else if (errorLower.includes("permission") || errorLower.includes("access")) {
|
|
189
|
+
strategy = "check-permissions-then-retry"
|
|
190
|
+
} else if (errorLower.includes("typeerror") || errorLower.includes("syntax")) {
|
|
191
|
+
strategy = "fix-code-then-retry"
|
|
192
|
+
} else if (errorLower.includes("not found") || errorLower.includes("enoent")) {
|
|
193
|
+
strategy = "verify-files-then-retry"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
operation: "retry",
|
|
198
|
+
planPath: entry.planPath,
|
|
199
|
+
status: "retry",
|
|
200
|
+
retryFrom: entry.failedUnit,
|
|
201
|
+
completedUnits: entry.completedUnits,
|
|
202
|
+
strategy,
|
|
203
|
+
updatedAt: entry.updatedAt,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function required(value: string | undefined, field: string): string {
|
|
208
|
+
if (!value) {
|
|
209
|
+
throw new Error(`session_checkpoint requires ${field}`)
|
|
210
|
+
}
|
|
211
|
+
return value
|
|
212
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { normalizeSlug } from "../utils/name-utils"
|
|
4
|
+
|
|
5
|
+
export interface HistoryEntry {
|
|
6
|
+
id: string
|
|
7
|
+
skill: string
|
|
8
|
+
artifactPath: string
|
|
9
|
+
summary: string
|
|
10
|
+
timestamp: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SessionHistoryInput {
|
|
14
|
+
operation: "record" | "query" | "latest"
|
|
15
|
+
repoRoot: string
|
|
16
|
+
skill?: string
|
|
17
|
+
artifactPath?: string
|
|
18
|
+
summary?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SessionHistoryResult {
|
|
22
|
+
operation: string
|
|
23
|
+
entries: HistoryEntry[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function historyDir(repoRoot: string): string {
|
|
27
|
+
return path.join(repoRoot, ".context", "compound-engineering", "history")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let _counter = 0
|
|
31
|
+
|
|
32
|
+
export function _resetCounter() { _counter = 0 }
|
|
33
|
+
|
|
34
|
+
export function createSessionHistoryTool() {
|
|
35
|
+
return {
|
|
36
|
+
name: "session_history",
|
|
37
|
+
async execute(input: SessionHistoryInput): Promise<SessionHistoryResult> {
|
|
38
|
+
switch (input.operation) {
|
|
39
|
+
case "record":
|
|
40
|
+
return recordExecution(input)
|
|
41
|
+
case "query":
|
|
42
|
+
return queryHistory(input)
|
|
43
|
+
case "latest":
|
|
44
|
+
return latestPerSkill(input)
|
|
45
|
+
default:
|
|
46
|
+
throw new Error(`Unknown operation: ${input.operation}`)
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function ensureDir(dir: string): Promise<void> {
|
|
53
|
+
await mkdir(dir, { recursive: true })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readAllEntries(repoRoot: string): Promise<HistoryEntry[]> {
|
|
57
|
+
const dir = historyDir(repoRoot)
|
|
58
|
+
try {
|
|
59
|
+
const files = await readdir(dir)
|
|
60
|
+
const entries: HistoryEntry[] = []
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
if (file.endsWith(".json")) {
|
|
63
|
+
try {
|
|
64
|
+
const content = await readFile(path.join(dir, file), "utf8")
|
|
65
|
+
entries.push(JSON.parse(content))
|
|
66
|
+
} catch {
|
|
67
|
+
// skip malformed
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return entries.sort((a, b) => a.id.localeCompare(b.id))
|
|
72
|
+
} catch {
|
|
73
|
+
return []
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function recordExecution(input: SessionHistoryInput): Promise<SessionHistoryResult> {
|
|
78
|
+
const dir = historyDir(input.repoRoot)
|
|
79
|
+
await ensureDir(dir)
|
|
80
|
+
|
|
81
|
+
const id = `${Date.now()}-${String(++_counter).padStart(6, "0")}-${normalizeSlug(input.skill ?? "unknown")}`
|
|
82
|
+
const entry: HistoryEntry = {
|
|
83
|
+
id,
|
|
84
|
+
skill: input.skill ?? "",
|
|
85
|
+
artifactPath: input.artifactPath ?? "",
|
|
86
|
+
summary: input.summary ?? "",
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const filePath = path.join(dir, `${id}.json`)
|
|
91
|
+
await writeFile(filePath, JSON.stringify(entry, null, 2), "utf8")
|
|
92
|
+
|
|
93
|
+
return { operation: "record", entries: [entry] }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function queryHistory(input: SessionHistoryInput): Promise<SessionHistoryResult> {
|
|
97
|
+
const all = await readAllEntries(input.repoRoot)
|
|
98
|
+
const filtered = input.skill
|
|
99
|
+
? all.filter((e) => e.skill === input.skill)
|
|
100
|
+
: all
|
|
101
|
+
|
|
102
|
+
return { operation: "query", entries: filtered }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function latestPerSkill(input: SessionHistoryInput): Promise<SessionHistoryResult> {
|
|
106
|
+
const all = await readAllEntries(input.repoRoot)
|
|
107
|
+
const latestMap = new Map<string, HistoryEntry>()
|
|
108
|
+
|
|
109
|
+
for (const entry of all) {
|
|
110
|
+
const existing = latestMap.get(entry.skill)
|
|
111
|
+
if (!existing || entry.id > existing.id) {
|
|
112
|
+
latestMap.set(entry.skill, entry)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { operation: "latest", entries: Array.from(latestMap.values()) }
|
|
117
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface SubagentTask {
|
|
2
|
+
agent: string
|
|
3
|
+
task: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface SubagentInput {
|
|
7
|
+
agent?: string
|
|
8
|
+
task?: string
|
|
9
|
+
chain?: SubagentTask[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SubagentResult {
|
|
13
|
+
mode: "single" | "chain"
|
|
14
|
+
outputs: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type SubagentRunner = (prompt: string) => Promise<string>
|
|
18
|
+
|
|
19
|
+
export function createSubagentTool() {
|
|
20
|
+
return {
|
|
21
|
+
name: "subagent",
|
|
22
|
+
async execute(input: SubagentInput, runner: SubagentRunner): Promise<SubagentResult> {
|
|
23
|
+
const hasSingle = Boolean(input.agent && input.task)
|
|
24
|
+
const hasChain = Boolean(input.chain && input.chain.length > 0)
|
|
25
|
+
|
|
26
|
+
if (Number(hasSingle) + Number(hasChain) !== 1) {
|
|
27
|
+
throw new Error("Provide exactly one mode: single or chain")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (hasSingle) {
|
|
31
|
+
const prompt = buildPrompt(input.agent!, input.task!)
|
|
32
|
+
const output = await runner(prompt)
|
|
33
|
+
return {
|
|
34
|
+
mode: "single",
|
|
35
|
+
outputs: [output],
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const outputs: string[] = []
|
|
40
|
+
let previous = ""
|
|
41
|
+
|
|
42
|
+
for (const task of input.chain ?? []) {
|
|
43
|
+
const prompt = buildPrompt(task.agent, task.task.replace(/\{previous\}/g, previous))
|
|
44
|
+
const output = await runner(prompt)
|
|
45
|
+
outputs.push(output)
|
|
46
|
+
previous = output
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
mode: "chain",
|
|
51
|
+
outputs,
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildPrompt(agent: string, task: string): string {
|
|
58
|
+
return `/skill:${agent} ${task}`
|
|
59
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export interface SplitterUnit {
|
|
2
|
+
name: string
|
|
3
|
+
files: string[]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface SplitterGroup {
|
|
7
|
+
units: string[]
|
|
8
|
+
parallelSafe: boolean
|
|
9
|
+
sharedFiles?: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TaskSplitterInput {
|
|
13
|
+
units: SplitterUnit[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TaskSplitterResult {
|
|
17
|
+
groups: SplitterGroup[]
|
|
18
|
+
independentUnits: string[]
|
|
19
|
+
dependentUnits: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createTaskSplitterTool() {
|
|
23
|
+
return {
|
|
24
|
+
name: "task_splitter",
|
|
25
|
+
execute(input: TaskSplitterInput): TaskSplitterResult {
|
|
26
|
+
if (input.units.length === 0) {
|
|
27
|
+
return { groups: [], independentUnits: [], dependentUnits: [] }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Build file → unit names map
|
|
31
|
+
const fileToUnits = new Map<string, string[]>()
|
|
32
|
+
for (const unit of input.units) {
|
|
33
|
+
for (const file of unit.files) {
|
|
34
|
+
const existing = fileToUnits.get(file) ?? []
|
|
35
|
+
existing.push(unit.name)
|
|
36
|
+
fileToUnits.set(file, existing)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Union-Find: merge units that share files
|
|
41
|
+
const parent = new Map<string, string>()
|
|
42
|
+
const sharedFilesMap = new Map<string, Set<string>>()
|
|
43
|
+
|
|
44
|
+
function find(name: string): string {
|
|
45
|
+
if (!parent.has(name)) parent.set(name, name)
|
|
46
|
+
let root = name
|
|
47
|
+
while (parent.get(root) !== root) {
|
|
48
|
+
root = parent.get(root)!
|
|
49
|
+
}
|
|
50
|
+
// Path compression
|
|
51
|
+
let current = name
|
|
52
|
+
while (current !== root) {
|
|
53
|
+
const next = parent.get(current)!
|
|
54
|
+
parent.set(current, root)
|
|
55
|
+
current = next
|
|
56
|
+
}
|
|
57
|
+
return root
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function union(a: string, b: string) {
|
|
61
|
+
const rootA = find(a)
|
|
62
|
+
const rootB = find(b)
|
|
63
|
+
if (rootA !== rootB) {
|
|
64
|
+
parent.set(rootA, rootB)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Initialize all units
|
|
69
|
+
for (const unit of input.units) {
|
|
70
|
+
find(unit.name)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Merge units sharing files
|
|
74
|
+
for (const [file, unitNames] of fileToUnits) {
|
|
75
|
+
if (unitNames.length > 1) {
|
|
76
|
+
for (let i = 1; i < unitNames.length; i++) {
|
|
77
|
+
union(unitNames[0], unitNames[i])
|
|
78
|
+
}
|
|
79
|
+
// Track shared files per root
|
|
80
|
+
const root = find(unitNames[0])
|
|
81
|
+
if (!sharedFilesMap.has(root)) sharedFilesMap.set(root, new Set())
|
|
82
|
+
sharedFilesMap.get(root)!.add(file)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Build groups by root
|
|
87
|
+
const rootToUnits = new Map<string, string[]>()
|
|
88
|
+
for (const unit of input.units) {
|
|
89
|
+
const root = find(unit.name)
|
|
90
|
+
const group = rootToUnits.get(root) ?? []
|
|
91
|
+
group.push(unit.name)
|
|
92
|
+
rootToUnits.set(root, group)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build output
|
|
96
|
+
const groups: SplitterGroup[] = []
|
|
97
|
+
const independentUnits: string[] = []
|
|
98
|
+
const dependentUnits: string[] = []
|
|
99
|
+
|
|
100
|
+
for (const [root, unitNames] of rootToUnits) {
|
|
101
|
+
const hasShared = sharedFilesMap.has(root) && sharedFilesMap.get(root)!.size > 0
|
|
102
|
+
const parallelSafe = !hasShared
|
|
103
|
+
|
|
104
|
+
const group: SplitterGroup = {
|
|
105
|
+
units: unitNames,
|
|
106
|
+
parallelSafe,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!parallelSafe) {
|
|
110
|
+
group.sharedFiles = Array.from(sharedFilesMap.get(root)!)
|
|
111
|
+
dependentUnits.push(...unitNames)
|
|
112
|
+
} else {
|
|
113
|
+
independentUnits.push(...unitNames)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
groups.push(group)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { groups, independentUnits, dependentUnits }
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readdirSync, statSync, existsSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
export interface WorkflowCategoryState {
|
|
5
|
+
count: number
|
|
6
|
+
latest: string | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WorkflowStateInput {
|
|
10
|
+
repoRoot: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface WorkflowStateResult {
|
|
14
|
+
brainstorms: WorkflowCategoryState
|
|
15
|
+
plans: WorkflowCategoryState
|
|
16
|
+
solutions: WorkflowCategoryState
|
|
17
|
+
runs: WorkflowCategoryState
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function emptyCategory(): WorkflowCategoryState {
|
|
21
|
+
return { count: 0, latest: null }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function scanDir(dirPath: string): WorkflowCategoryState {
|
|
25
|
+
if (!existsSync(dirPath)) {
|
|
26
|
+
return emptyCategory()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const files = collectFiles(dirPath)
|
|
30
|
+
|
|
31
|
+
if (files.length === 0) {
|
|
32
|
+
return emptyCategory()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sorted = files.sort()
|
|
36
|
+
const latest = sorted[sorted.length - 1]
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
count: files.length,
|
|
40
|
+
latest: path.basename(latest),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function collectFiles(dirPath: string): string[] {
|
|
45
|
+
const results: string[] = []
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const entries = readdirSync(dirPath, { withFileTypes: true })
|
|
49
|
+
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
const fullPath = path.join(dirPath, entry.name)
|
|
52
|
+
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
results.push(...collectFiles(fullPath))
|
|
55
|
+
} else if (entry.isFile()) {
|
|
56
|
+
results.push(fullPath)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Directory not readable, treat as empty
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return results
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createWorkflowStateTool() {
|
|
67
|
+
return {
|
|
68
|
+
name: "workflow_state",
|
|
69
|
+
async execute(input: WorkflowStateInput): Promise<WorkflowStateResult> {
|
|
70
|
+
const repoRoot = input.repoRoot
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
brainstorms: scanDir(path.join(repoRoot, "docs", "brainstorms")),
|
|
74
|
+
plans: scanDir(path.join(repoRoot, "docs", "plans")),
|
|
75
|
+
solutions: scanDir(path.join(repoRoot, "docs", "solutions")),
|
|
76
|
+
runs: scanDir(path.join(repoRoot, ".context", "compound-engineering")),
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|