opencode-overclock 0.4.0 → 0.5.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 +72 -17
- package/package.json +5 -3
- package/skills/codebase-design/DEEPENING.md +35 -0
- package/skills/codebase-design/DESIGN-IT-TWICE.md +34 -0
- package/skills/codebase-design/SKILL.md +93 -0
- package/skills/diagnosing-bugs/SKILL.md +123 -0
- package/skills/domain-modeling/ADR-FORMAT.md +55 -0
- package/skills/domain-modeling/CONTEXT-FORMAT.md +32 -0
- package/skills/domain-modeling/SKILL.md +102 -0
- package/skills/doubt/SKILL.md +80 -0
- package/skills/grilling/SKILL.md +96 -0
- package/skills/source-discipline/SKILL.md +78 -0
- package/skills/tdd/SKILL.md +87 -0
- package/skills/to-spec/SKILL.md +69 -0
- package/skills/to-spec/SPEC-TEMPLATE.md +50 -0
- package/skills/to-tickets/SKILL.md +74 -0
- package/skills/to-tickets/TICKET-TEMPLATE.md +41 -0
- package/src/core/lifecycle.ts +18 -4
- package/src/core/types.ts +29 -0
- package/src/features/guard.ts +258 -12
- package/src/features/index.ts +13 -1
- package/src/features/recovery.ts +13 -3
- package/src/features/safety.ts +147 -0
- package/src/features/sched.ts +46 -10
- package/src/features/tasks.ts +87 -20
- package/src/features/truncator.ts +26 -9
- package/src/features/usage.ts +20 -0
- package/src/features/workflow.ts +270 -0
- package/src/lib/exec.ts +7 -1
- package/src/platform/process/exec.ts +252 -11
- package/src/platform/session/inject.ts +8 -1
- package/src/platform/storage/state.ts +26 -4
- package/src/v2/host.ts +4 -1
- package/src/workflow/agents/codebase-researcher.ts +27 -0
- package/src/workflow/agents/craftsman.ts +26 -0
- package/src/workflow/agents/design-explorer.ts +33 -0
- package/src/workflow/agents/doc-writer.ts +24 -0
- package/src/workflow/agents/doubt-reviewer.ts +26 -0
- package/src/workflow/agents/engineering-coach.ts +23 -0
- package/src/workflow/agents/performance-auditor.ts +29 -0
- package/src/workflow/agents/security-auditor.ts +23 -0
- package/src/workflow/agents/spec-reviewer.ts +15 -0
- package/src/workflow/agents/standards-reviewer.ts +24 -0
- package/src/workflow/agents/test-engineer.ts +28 -0
- package/src/workflow/catalog.ts +210 -0
- package/src/workflow/templates/build.ts +47 -0
- package/src/workflow/templates/define.ts +45 -0
- package/src/workflow/templates/diagnose.ts +58 -0
- package/src/workflow/templates/plan.ts +52 -0
- package/src/workflow/templates/ship.ts +64 -0
package/src/features/tasks.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { FeatureModule } from "../types.ts"
|
|
|
4
4
|
import { ensureStateDir, shellQuote, writeJson } from "../lib/state.ts"
|
|
5
5
|
import { taskStore, type TaskMirrorEntry } from "../lib/mirror.ts"
|
|
6
6
|
import { inject, toast } from "../lib/inject.ts"
|
|
7
|
-
import { NON_INTERACTIVE_ENV } from "../lib/exec.ts"
|
|
7
|
+
import { killProcessTree, NON_INTERACTIVE_ENV, redactSensitiveOutput, sanitizeEnv } from "../lib/exec.ts"
|
|
8
8
|
import { spawnTaskPane, type TmuxPane } from "../lib/tmux.ts"
|
|
9
9
|
|
|
10
10
|
const z = tool.schema
|
|
@@ -127,12 +127,27 @@ function startStallWatchdog(
|
|
|
127
127
|
}, checkIntervalMs)
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
export async function readLogTail(logPath: string, tailLines = 50): Promise<string> {
|
|
131
|
+
const file = Bun.file(logPath)
|
|
132
|
+
if (!(await file.exists())) return "(no output)"
|
|
133
|
+
const s = await stat(logPath).catch(() => null)
|
|
134
|
+
if (!s || s.size === 0) return "(no output)"
|
|
135
|
+
|
|
136
|
+
const effectiveTail = Math.max(1, tailLines)
|
|
137
|
+
// Cap read window to last 512KB to prevent memory exhaustion on giant log files
|
|
138
|
+
const maxBytes = 512 * 1024
|
|
139
|
+
const start = Math.max(0, s.size - maxBytes)
|
|
140
|
+
const text = await (start > 0 ? file.slice(start).text() : file.text())
|
|
141
|
+
const lines = text.trimEnd().split("\n")
|
|
142
|
+
return lines.slice(-effectiveTail).join("\n")
|
|
143
|
+
}
|
|
144
|
+
|
|
130
145
|
/** Exported for tests. onExit fires after status/exitCode settled. */
|
|
131
146
|
export function createTaskManager(opts: {
|
|
132
147
|
logDir: string
|
|
133
148
|
/** mirror JSON path, written on every state change (spawn/exit/kill); omit to disable */
|
|
134
149
|
mirrorPath?: string
|
|
135
|
-
onExit?: (task: TaskRecord) => void
|
|
150
|
+
onExit?: (task: TaskRecord) => void | Promise<void>
|
|
136
151
|
/** enables the stall watchdog; absent -> no polling at all */
|
|
137
152
|
onStall?: (task: TaskRecord, tail: string) => void
|
|
138
153
|
stallCheckIntervalMs?: number
|
|
@@ -140,9 +155,27 @@ export function createTaskManager(opts: {
|
|
|
140
155
|
stallTailBytes?: number
|
|
141
156
|
/** spawn a tmux split pane to tail task logs (only if TMUX is active) */
|
|
142
157
|
tmux?: boolean
|
|
158
|
+
sanitizeEnv?: boolean
|
|
159
|
+
envAllowlist?: string[]
|
|
160
|
+
maxTasks?: number
|
|
143
161
|
}): TaskManager {
|
|
144
162
|
const tasks = new Map<string, TaskEntry>()
|
|
145
163
|
let counter = 0
|
|
164
|
+
const maxRetainedTasks = opts.maxTasks ?? 100
|
|
165
|
+
|
|
166
|
+
function pruneFinishedTasks(): void {
|
|
167
|
+
if (tasks.size <= maxRetainedTasks) return
|
|
168
|
+
const finished: string[] = []
|
|
169
|
+
for (const [id, entry] of tasks) {
|
|
170
|
+
if (entry.status !== "running") {
|
|
171
|
+
finished.push(id)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const toRemove = tasks.size - maxRetainedTasks
|
|
175
|
+
for (let i = 0; i < Math.min(toRemove, finished.length); i++) {
|
|
176
|
+
tasks.delete(finished[i]!)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
146
179
|
|
|
147
180
|
function persistMirror(): void {
|
|
148
181
|
if (!opts.mirrorPath) return
|
|
@@ -165,14 +198,38 @@ export function createTaskManager(opts: {
|
|
|
165
198
|
}): TaskRecord {
|
|
166
199
|
const id = `t${(++counter).toString(36)}-${crypto.randomUUID().slice(0, 6)}`
|
|
167
200
|
const logPath = `${opts.logDir}/${id}.log`
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
201
|
+
|
|
202
|
+
let proc: Bun.Subprocess
|
|
203
|
+
try {
|
|
204
|
+
proc = Bun.spawn(["bash", "-c", `(${input.command}) >> ${shellQuote(logPath)} 2>&1`], {
|
|
205
|
+
cwd: input.cwd,
|
|
206
|
+
env: {
|
|
207
|
+
...(opts.sanitizeEnv === false ? process.env : sanitizeEnv(process.env, opts.envAllowlist)),
|
|
208
|
+
...NON_INTERACTIVE_ENV,
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
} catch (e) {
|
|
212
|
+
console.warn(`[overclock] failed to spawn task ${id}: ${e}`)
|
|
213
|
+
const failedEntry: TaskEntry = {
|
|
214
|
+
id,
|
|
215
|
+
description: input.description,
|
|
216
|
+
command: input.command,
|
|
217
|
+
cwd: input.cwd,
|
|
218
|
+
sessionID: input.sessionID,
|
|
219
|
+
status: "exited",
|
|
220
|
+
exitCode: 1,
|
|
221
|
+
logPath,
|
|
222
|
+
proc: null as any,
|
|
223
|
+
stallNotified: false,
|
|
224
|
+
startedAt: Date.now(),
|
|
225
|
+
}
|
|
226
|
+
tasks.set(id, failedEntry)
|
|
227
|
+
pruneFinishedTasks()
|
|
228
|
+
persistMirror()
|
|
229
|
+
opts.onExit?.(strip(failedEntry))
|
|
230
|
+
return strip(failedEntry)
|
|
231
|
+
}
|
|
232
|
+
|
|
176
233
|
const entry: TaskEntry = {
|
|
177
234
|
id,
|
|
178
235
|
description: input.description,
|
|
@@ -219,7 +276,12 @@ export function createTaskManager(opts: {
|
|
|
219
276
|
if (entry.status === "running") entry.status = "exited"
|
|
220
277
|
entry.exitCode = code
|
|
221
278
|
persistMirror()
|
|
222
|
-
|
|
279
|
+
try {
|
|
280
|
+
await opts.onExit?.(strip(entry))
|
|
281
|
+
} finally {
|
|
282
|
+
pruneFinishedTasks()
|
|
283
|
+
persistMirror()
|
|
284
|
+
}
|
|
223
285
|
})
|
|
224
286
|
return strip(entry)
|
|
225
287
|
}
|
|
@@ -228,13 +290,16 @@ export function createTaskManager(opts: {
|
|
|
228
290
|
const entry = tasks.get(id)
|
|
229
291
|
if (!entry || entry.status !== "running") return false
|
|
230
292
|
entry.status = "killed"
|
|
293
|
+
pruneFinishedTasks()
|
|
231
294
|
persistMirror()
|
|
232
295
|
if (entry.stallTimer) clearInterval(entry.stallTimer)
|
|
233
296
|
entry.stallTimer = undefined
|
|
234
297
|
if (entry.tmuxPane) void entry.tmuxPane.close()
|
|
235
|
-
entry.proc
|
|
236
|
-
|
|
237
|
-
|
|
298
|
+
if (entry.proc) {
|
|
299
|
+
void killProcessTree(entry.proc, "SIGTERM")
|
|
300
|
+
const hard = setTimeout(() => void killProcessTree(entry.proc, "SIGKILL"), 3000)
|
|
301
|
+
entry.proc.exited.then(() => clearTimeout(hard))
|
|
302
|
+
}
|
|
238
303
|
return true
|
|
239
304
|
}
|
|
240
305
|
|
|
@@ -249,10 +314,7 @@ export function createTaskManager(opts: {
|
|
|
249
314
|
output: async (id, tailLines = 50) => {
|
|
250
315
|
const e = tasks.get(id)
|
|
251
316
|
if (!e) return `no task ${id}`
|
|
252
|
-
|
|
253
|
-
if (!(await file.exists())) return "(no output)"
|
|
254
|
-
const lines = (await file.text()).split("\n")
|
|
255
|
-
return lines.slice(-tailLines - 1).join("\n")
|
|
317
|
+
return readLogTail(e.logPath, tailLines)
|
|
256
318
|
},
|
|
257
319
|
killAll: () => {
|
|
258
320
|
for (const id of tasks.keys()) kill(id)
|
|
@@ -285,9 +347,13 @@ export const tasks: FeatureModule = {
|
|
|
285
347
|
logDir,
|
|
286
348
|
mirrorPath: taskStore.path(ctx.directory),
|
|
287
349
|
tmux: options.tmux === true,
|
|
350
|
+
sanitizeEnv: options.sanitizeEnv !== false,
|
|
351
|
+
envAllowlist: Array.isArray(options.envAllowlist) ? (options.envAllowlist as string[]) : undefined,
|
|
352
|
+
maxTasks: typeof options.maxTasks === "number" ? options.maxTasks : 100,
|
|
288
353
|
onExit: async (task) => {
|
|
289
354
|
if (task.status === "killed") return
|
|
290
|
-
const
|
|
355
|
+
const rawTail = await readLogTail(task.logPath, 20)
|
|
356
|
+
const tail = redactSensitiveOutput(rawTail)
|
|
291
357
|
const ok = task.exitCode === 0
|
|
292
358
|
await toast(
|
|
293
359
|
ctx.client,
|
|
@@ -305,12 +371,13 @@ export const tasks: FeatureModule = {
|
|
|
305
371
|
stallThresholdMs,
|
|
306
372
|
stallCheckIntervalMs,
|
|
307
373
|
onStall: async (task: TaskRecord, tail: string) => {
|
|
374
|
+
const safeTail = redactSensitiveOutput(tail.trimEnd())
|
|
308
375
|
await toast(ctx.client, `task ${task.id} looks stalled (waiting for input?)`, "warning")
|
|
309
376
|
await inject(
|
|
310
377
|
ctx.client,
|
|
311
378
|
task.sessionID,
|
|
312
379
|
`[background task ${task.id} "${task.description}" appears to be waiting for interactive input]\n` +
|
|
313
|
-
`last output:\n${
|
|
380
|
+
`last output:\n${safeTail}\n\n` +
|
|
314
381
|
`The command is likely blocked on a prompt. Kill it with ${shared.toolName("task_kill")} and re-run non-interactively ` +
|
|
315
382
|
`(e.g. pipe input like \`echo y | cmd\`, or pass a --yes/--force flag).`,
|
|
316
383
|
)
|
|
@@ -15,6 +15,7 @@ export interface TruncateResult {
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Smartly truncate output preserving top context and bottom tail.
|
|
18
|
+
* Enforces strict character and line bounds so oversized lines never overflow context.
|
|
18
19
|
*/
|
|
19
20
|
export function truncateOutput(
|
|
20
21
|
content: string,
|
|
@@ -22,16 +23,23 @@ export function truncateOutput(
|
|
|
22
23
|
headLinesCount = DEFAULT_HEAD_LINES,
|
|
23
24
|
tailLinesCount = DEFAULT_TAIL_LINES,
|
|
24
25
|
): TruncateResult {
|
|
25
|
-
|
|
26
|
+
const effectiveMax = Math.max(100, maxChars)
|
|
27
|
+
if (content.length <= effectiveMax) {
|
|
26
28
|
return { text: content, truncated: false, omittedLines: 0, omittedChars: 0 }
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
const headCount = Math.max(0, headLinesCount)
|
|
32
|
+
const tailCount = Math.max(0, tailLinesCount)
|
|
29
33
|
const lines = content.split("\n")
|
|
30
|
-
|
|
34
|
+
|
|
35
|
+
const maxHeadChars = Math.floor(effectiveMax * 0.3)
|
|
36
|
+
const maxTailChars = Math.floor(effectiveMax * 0.7)
|
|
37
|
+
|
|
38
|
+
if (lines.length <= headCount + tailCount) {
|
|
31
39
|
// If few lines but very long strings, hard slice
|
|
32
|
-
const head = content.slice(0,
|
|
33
|
-
const tail = content.slice(-
|
|
34
|
-
const omittedChars = content.length - head.length - tail.length
|
|
40
|
+
const head = content.slice(0, maxHeadChars)
|
|
41
|
+
const tail = maxTailChars > 0 ? content.slice(-maxTailChars) : ""
|
|
42
|
+
const omittedChars = Math.max(0, content.length - head.length - tail.length)
|
|
35
43
|
return {
|
|
36
44
|
text: `${head}\n\n[... truncated ${omittedChars} characters to stay within context limits ...]\n\n${tail}`,
|
|
37
45
|
truncated: true,
|
|
@@ -40,10 +48,19 @@ export function truncateOutput(
|
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
let head = headCount > 0 ? lines.slice(0, headCount).join("\n") : ""
|
|
52
|
+
let tail = tailCount > 0 ? lines.slice(-tailCount).join("\n") : ""
|
|
53
|
+
|
|
54
|
+
// Guard against giant single lines in head or tail violating maxChars limit
|
|
55
|
+
if (head.length > maxHeadChars) {
|
|
56
|
+
head = head.slice(0, maxHeadChars) + "\n... [line truncated]"
|
|
57
|
+
}
|
|
58
|
+
if (tail.length > maxTailChars) {
|
|
59
|
+
tail = "[line truncated] ...\n" + (maxTailChars > 0 ? tail.slice(-maxTailChars) : "")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const omittedLines = Math.max(0, lines.length - headCount - tailCount)
|
|
63
|
+
const omittedChars = Math.max(0, content.length - head.length - tail.length)
|
|
47
64
|
|
|
48
65
|
const text = `${head}\n\n[... truncated ${omittedLines} lines (${omittedChars} chars) to stay within context limits ...]\n\n${tail}`
|
|
49
66
|
return {
|
package/src/features/usage.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface SessionUsage {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const RETENTION_DAYS = 60
|
|
15
|
+
const MAX_SEEN_PER_DAY = 1000
|
|
16
|
+
const MAX_TRACKED_SESSIONS = 200
|
|
15
17
|
|
|
16
18
|
function zeroTokens(): UsageTokens {
|
|
17
19
|
return { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 }
|
|
@@ -73,6 +75,7 @@ export function createUsageTracker(opts: UsageTrackerOpts): UsageTracker {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
async function flush(): Promise<void> {
|
|
78
|
+
pruneOldDays(state, Date.now())
|
|
76
79
|
await writeJson(opts.statePath, state)
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -93,6 +96,13 @@ export function createUsageTracker(opts: UsageTrackerOpts): UsageTracker {
|
|
|
93
96
|
|
|
94
97
|
function onEvent(event: { type: string; properties?: unknown }): void {
|
|
95
98
|
try {
|
|
99
|
+
if (event.type === "session.deleted") {
|
|
100
|
+
const props = event.properties as { sessionID?: string; info?: { id?: string } } | undefined
|
|
101
|
+
const id = props?.sessionID ?? props?.info?.id
|
|
102
|
+
if (id) sessions.delete(id)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
96
106
|
if (event.type !== "message.updated") return
|
|
97
107
|
const info = (event.properties as any)?.info
|
|
98
108
|
if (!info || info.role !== "assistant" || !info.time?.completed) return
|
|
@@ -103,6 +113,10 @@ export function createUsageTracker(opts: UsageTrackerOpts): UsageTracker {
|
|
|
103
113
|
const bucket = state.days[day] ?? emptyBucket()
|
|
104
114
|
state.days[day] = bucket
|
|
105
115
|
if (bucket.seen.includes(info.id)) return
|
|
116
|
+
|
|
117
|
+
if (bucket.seen.length >= MAX_SEEN_PER_DAY) {
|
|
118
|
+
bucket.seen = bucket.seen.slice(-MAX_SEEN_PER_DAY + 1)
|
|
119
|
+
}
|
|
106
120
|
bucket.seen.push(info.id)
|
|
107
121
|
|
|
108
122
|
const cost = info.cost ?? 0
|
|
@@ -126,6 +140,12 @@ export function createUsageTracker(opts: UsageTrackerOpts): UsageTracker {
|
|
|
126
140
|
sess.tokens.output += tok.output
|
|
127
141
|
sessions.set(info.sessionID, sess)
|
|
128
142
|
|
|
143
|
+
if (sessions.size > MAX_TRACKED_SESSIONS) {
|
|
144
|
+
// Evict oldest session to bound memory
|
|
145
|
+
const oldest = sessions.keys().next().value
|
|
146
|
+
if (oldest) sessions.delete(oldest)
|
|
147
|
+
}
|
|
148
|
+
|
|
129
149
|
scheduleFlush()
|
|
130
150
|
} catch (e) {
|
|
131
151
|
console.warn(`[overclock] usage: event handling failed: ${e}`)
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { resolve, dirname } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import type { FeatureModule, WorkflowOptions } from "../core/types.ts"
|
|
5
|
+
import { DEFINE_TEMPLATE } from "../workflow/templates/define.ts"
|
|
6
|
+
import { PLAN_TEMPLATE } from "../workflow/templates/plan.ts"
|
|
7
|
+
import { BUILD_TEMPLATE } from "../workflow/templates/build.ts"
|
|
8
|
+
import { DIAGNOSE_TEMPLATE } from "../workflow/templates/diagnose.ts"
|
|
9
|
+
import { SHIP_TEMPLATE } from "../workflow/templates/ship.ts"
|
|
10
|
+
import { STANDARDS_REVIEWER_PROMPT } from "../workflow/agents/standards-reviewer.ts"
|
|
11
|
+
import { SPEC_REVIEWER_PROMPT } from "../workflow/agents/spec-reviewer.ts"
|
|
12
|
+
import { SECURITY_AUDITOR_PROMPT } from "../workflow/agents/security-auditor.ts"
|
|
13
|
+
import { TEST_ENGINEER_PROMPT } from "../workflow/agents/test-engineer.ts"
|
|
14
|
+
import { PERFORMANCE_AUDITOR_PROMPT } from "../workflow/agents/performance-auditor.ts"
|
|
15
|
+
import { DOUBT_REVIEWER_PROMPT } from "../workflow/agents/doubt-reviewer.ts"
|
|
16
|
+
import { CODEBASE_RESEARCHER_PROMPT } from "../workflow/agents/codebase-researcher.ts"
|
|
17
|
+
import { DESIGN_EXPLORER_PROMPT } from "../workflow/agents/design-explorer.ts"
|
|
18
|
+
import { ENGINEERING_COACH_PROMPT } from "../workflow/agents/engineering-coach.ts"
|
|
19
|
+
import { CRAFTSMAN_PROMPT } from "../workflow/agents/craftsman.ts"
|
|
20
|
+
import { DOC_WRITER_PROMPT } from "../workflow/agents/doc-writer.ts"
|
|
21
|
+
|
|
22
|
+
function getBundledSkillsDir(customPath?: string): string {
|
|
23
|
+
if (customPath) return customPath
|
|
24
|
+
const currentDir =
|
|
25
|
+
typeof import.meta.dir === "string" ? import.meta.dir : dirname(fileURLToPath(import.meta.url))
|
|
26
|
+
return resolve(currentDir, "../../skills")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const WORKFLOW_COMMANDS = {
|
|
30
|
+
define: {
|
|
31
|
+
description: "Interrogate requirements and draft SPEC.md with recommended defaults",
|
|
32
|
+
template: DEFINE_TEMPLATE,
|
|
33
|
+
},
|
|
34
|
+
plan: {
|
|
35
|
+
description: "Decompose spec into vertical tracer-bullet tasks in tasks/plan.md",
|
|
36
|
+
template: PLAN_TEMPLATE,
|
|
37
|
+
},
|
|
38
|
+
build: {
|
|
39
|
+
description: "Autonomous TDD implementation with tripwires and atomic commits",
|
|
40
|
+
template: BUILD_TEMPLATE,
|
|
41
|
+
},
|
|
42
|
+
diagnose: {
|
|
43
|
+
description: "Disciplined bug reproduction and isolation loop ([DEBUG-xxxx] tags)",
|
|
44
|
+
template: DIAGNOSE_TEMPLATE,
|
|
45
|
+
},
|
|
46
|
+
ship: {
|
|
47
|
+
description: "3-way parallel review (Standards, Spec, Security) with GO/NO-GO verdict",
|
|
48
|
+
template: SHIP_TEMPLATE,
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const WORKFLOW_AGENTS = {
|
|
53
|
+
"standards-reviewer": {
|
|
54
|
+
mode: "subagent" as const,
|
|
55
|
+
description: "Senior Staff Engineer auditing diffs for repo conventions and code smells",
|
|
56
|
+
prompt: STANDARDS_REVIEWER_PROMPT,
|
|
57
|
+
tools: {
|
|
58
|
+
write: false,
|
|
59
|
+
edit: false,
|
|
60
|
+
},
|
|
61
|
+
permission: {
|
|
62
|
+
edit: "deny" as const,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
"spec-reviewer": {
|
|
66
|
+
mode: "subagent" as const,
|
|
67
|
+
description: "Product Engineer auditing diffs strictly against originating specifications",
|
|
68
|
+
prompt: SPEC_REVIEWER_PROMPT,
|
|
69
|
+
tools: {
|
|
70
|
+
write: false,
|
|
71
|
+
edit: false,
|
|
72
|
+
},
|
|
73
|
+
permission: {
|
|
74
|
+
edit: "deny" as const,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
"security-auditor": {
|
|
78
|
+
mode: "subagent" as const,
|
|
79
|
+
description: "Adversarial Security Engineer auditing diffs for OWASP vulnerabilities and secrets",
|
|
80
|
+
prompt: SECURITY_AUDITOR_PROMPT,
|
|
81
|
+
tools: {
|
|
82
|
+
write: false,
|
|
83
|
+
edit: false,
|
|
84
|
+
},
|
|
85
|
+
permission: {
|
|
86
|
+
edit: "deny" as const,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
"test-engineer": {
|
|
90
|
+
mode: "subagent" as const,
|
|
91
|
+
description: "QA Engineer auditing test strategy, coverage gaps, and Prove-It verification",
|
|
92
|
+
prompt: TEST_ENGINEER_PROMPT,
|
|
93
|
+
tools: {
|
|
94
|
+
write: false,
|
|
95
|
+
edit: false,
|
|
96
|
+
},
|
|
97
|
+
permission: {
|
|
98
|
+
edit: "deny" as const,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
"performance-auditor": {
|
|
102
|
+
mode: "subagent" as const,
|
|
103
|
+
description: "Senior Performance Engineer auditing latency, N+1 queries, and resource leaks",
|
|
104
|
+
prompt: PERFORMANCE_AUDITOR_PROMPT,
|
|
105
|
+
tools: {
|
|
106
|
+
write: false,
|
|
107
|
+
edit: false,
|
|
108
|
+
},
|
|
109
|
+
permission: {
|
|
110
|
+
edit: "deny" as const,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
"doubt-reviewer": {
|
|
114
|
+
mode: "all" as const,
|
|
115
|
+
description: "Adversarial Verification Engineer evaluating artifacts without author bias",
|
|
116
|
+
prompt: DOUBT_REVIEWER_PROMPT,
|
|
117
|
+
tools: {
|
|
118
|
+
write: false,
|
|
119
|
+
edit: false,
|
|
120
|
+
},
|
|
121
|
+
permission: {
|
|
122
|
+
edit: "deny" as const,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
"codebase-researcher": {
|
|
126
|
+
mode: "all" as const,
|
|
127
|
+
description: "Scout Agent tracing seams, dependencies, and call graphs without polluting context",
|
|
128
|
+
prompt: CODEBASE_RESEARCHER_PROMPT,
|
|
129
|
+
tools: {
|
|
130
|
+
write: false,
|
|
131
|
+
edit: false,
|
|
132
|
+
},
|
|
133
|
+
permission: {
|
|
134
|
+
edit: "deny" as const,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
"design-explorer": {
|
|
138
|
+
mode: "all" as const,
|
|
139
|
+
description: "Principal Architect producing contrasting 'Design It Twice' interface proposals",
|
|
140
|
+
prompt: DESIGN_EXPLORER_PROMPT,
|
|
141
|
+
tools: {
|
|
142
|
+
write: false,
|
|
143
|
+
edit: false,
|
|
144
|
+
},
|
|
145
|
+
permission: {
|
|
146
|
+
edit: "deny" as const,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
"engineering-coach": {
|
|
150
|
+
mode: "all" as const,
|
|
151
|
+
description: "Elite Staff Mentor providing Socratic debugging guidance and design critique",
|
|
152
|
+
prompt: ENGINEERING_COACH_PROMPT,
|
|
153
|
+
tools: {
|
|
154
|
+
write: false,
|
|
155
|
+
edit: false,
|
|
156
|
+
},
|
|
157
|
+
permission: {
|
|
158
|
+
edit: "deny" as const,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
craftsman: {
|
|
162
|
+
mode: "all" as const,
|
|
163
|
+
description:
|
|
164
|
+
"Disciplined software craftsman enforcing TDD, minimal vertical slices, and clean architecture",
|
|
165
|
+
prompt: CRAFTSMAN_PROMPT,
|
|
166
|
+
},
|
|
167
|
+
"doc-writer": {
|
|
168
|
+
mode: "all" as const,
|
|
169
|
+
description:
|
|
170
|
+
"Technical writer synthesizing accurate documentation, API references, and architecture records from code",
|
|
171
|
+
prompt: DOC_WRITER_PROMPT,
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const workflow: FeatureModule = {
|
|
176
|
+
name: "workflow",
|
|
177
|
+
defaultEnabled: true,
|
|
178
|
+
tools: [],
|
|
179
|
+
async init(_ctx, options) {
|
|
180
|
+
const opts = (options ?? {}) as WorkflowOptions
|
|
181
|
+
if (opts.enabled === false) {
|
|
182
|
+
return {}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const skillsPath = getBundledSkillsDir(opts.skillsPath)
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
config: async (cfg: any) => {
|
|
189
|
+
if (opts.commands !== false) {
|
|
190
|
+
cfg.command = {
|
|
191
|
+
...WORKFLOW_COMMANDS,
|
|
192
|
+
...(cfg.command ?? {}),
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (opts.subagents !== false) {
|
|
197
|
+
cfg.agent = {
|
|
198
|
+
...WORKFLOW_AGENTS,
|
|
199
|
+
...(cfg.agent ?? {}),
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (existsSync(skillsPath)) {
|
|
204
|
+
cfg.skills = typeof cfg.skills === "object" && cfg.skills !== null ? cfg.skills : {}
|
|
205
|
+
if (!Array.isArray(cfg.skills.paths)) {
|
|
206
|
+
cfg.skills.paths = []
|
|
207
|
+
}
|
|
208
|
+
if (!cfg.skills.paths.includes(skillsPath)) {
|
|
209
|
+
cfg.skills.paths.push(skillsPath)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
setup: async (v2Context, options) => {
|
|
217
|
+
const opts = (options ?? {}) as WorkflowOptions
|
|
218
|
+
if (opts.enabled === false) return
|
|
219
|
+
|
|
220
|
+
if (opts.commands !== false && v2Context.command?.transform) {
|
|
221
|
+
await v2Context.command.transform(async (draft) => {
|
|
222
|
+
for (const [name, cmd] of Object.entries(WORKFLOW_COMMANDS)) {
|
|
223
|
+
draft.update(name, (current) => {
|
|
224
|
+
current.name = current.name ?? name
|
|
225
|
+
current.description = current.description ?? cmd.description
|
|
226
|
+
current.template = current.template ?? cmd.template
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (opts.subagents !== false && v2Context.agent?.transform) {
|
|
233
|
+
await v2Context.agent.transform(async (draft) => {
|
|
234
|
+
for (const [id, ag] of Object.entries(WORKFLOW_AGENTS)) {
|
|
235
|
+
draft.update(id, (current) => {
|
|
236
|
+
current.mode = current.mode ?? ag.mode
|
|
237
|
+
current.description = current.description ?? ag.description
|
|
238
|
+
current.system = current.system ?? ag.prompt
|
|
239
|
+
if ("permission" in ag && ag.permission?.edit === "deny") {
|
|
240
|
+
const perms = (current.permissions as any[]) ?? []
|
|
241
|
+
const hasDenyEdit = perms.some((p: any) => p.action === "edit" && p.effect === "deny")
|
|
242
|
+
if (!hasDenyEdit) {
|
|
243
|
+
perms.push({
|
|
244
|
+
action: "edit",
|
|
245
|
+
resource: "*",
|
|
246
|
+
effect: "deny",
|
|
247
|
+
})
|
|
248
|
+
current.permissions = perms as any
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const skillsPath = getBundledSkillsDir(opts.skillsPath)
|
|
257
|
+
if (existsSync(skillsPath) && v2Context.skill?.transform) {
|
|
258
|
+
await v2Context.skill.transform(async (draft) => {
|
|
259
|
+
const existing = draft.list?.() ?? []
|
|
260
|
+
const alreadyAdded = existing.some((s: any) => s.type === "directory" && s.path === skillsPath)
|
|
261
|
+
if (!alreadyAdded) {
|
|
262
|
+
draft.source({
|
|
263
|
+
type: "directory",
|
|
264
|
+
path: skillsPath,
|
|
265
|
+
} as any)
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
}
|
package/src/lib/exec.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export {
|
|
2
|
-
NON_INTERACTIVE_ENV,
|
|
3
2
|
execBash,
|
|
3
|
+
killProcessTree,
|
|
4
4
|
shellQuote,
|
|
5
|
+
sanitizeEnv,
|
|
6
|
+
redactSensitiveOutput,
|
|
7
|
+
NON_INTERACTIVE_ENV,
|
|
8
|
+
DEFAULT_PRESERVED_ENV,
|
|
9
|
+
SENSITIVE_ENV_PATTERN,
|
|
10
|
+
SENSITIVE_OUTPUT_PATTERNS,
|
|
5
11
|
type ExecBashOptions,
|
|
6
12
|
type ExecBashResult,
|
|
7
13
|
} from "../platform/process/exec.ts"
|