adaptive-director-skill 0.1.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.
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * route.mjs
4
+ * ──────────
5
+ * Deterministic routing script.
6
+ * NO reasoning. Applies fixed rules only.
7
+ *
8
+ * Input (stdin JSON or --input flag):
9
+ * {
10
+ * "taskSize": "small" | "medium" | "large",
11
+ * "phase": "plan" | "implement" | "review" | "fix" | "verify",
12
+ * "budget": "conservative" | "balanced" | "quality",
13
+ * "allowMax": boolean,
14
+ * "delegateEnabled": boolean,
15
+ * "currentModel": string (optional, for role check)
16
+ * }
17
+ *
18
+ * Output (stdout JSON):
19
+ * {
20
+ * "agent": string,
21
+ * "model": string | null,
22
+ * "effort": "low" | "medium" | "high" | "max",
23
+ * "execution": "native" | "delegate"
24
+ * }
25
+ *
26
+ * Usage:
27
+ * echo '{"taskSize":"medium","phase":"review","budget":"balanced","allowMax":false,"delegateEnabled":false}' | node scripts/route.mjs
28
+ * node scripts/route.mjs --input '{"taskSize":"medium",...}'
29
+ */
30
+
31
+ import { existsSync, readFileSync } from 'node:fs'
32
+ import { join } from 'node:path'
33
+ import { homedir } from 'node:os'
34
+
35
+ // ─── Registry ─────────────────────────────────────────────────────────────────
36
+
37
+ const REGISTRY_PATH = join(import.meta.dirname ?? '.', '../data/registry.json')
38
+
39
+ function loadRegistry() {
40
+ if (!existsSync(REGISTRY_PATH)) {
41
+ return { models: {}, effort_table: {}, phase_requirements: {} }
42
+ }
43
+ return JSON.parse(readFileSync(REGISTRY_PATH, 'utf8'))
44
+ }
45
+
46
+ // ─── User config ──────────────────────────────────────────────────────────────
47
+
48
+ const USER_CONFIG_PATH = existsSync(join(homedir(), '.adaptive-director', 'config.yaml'))
49
+ ? join(homedir(), '.adaptive-director', 'config.yaml')
50
+ : join(homedir(), '.adaptive-orchestrator', 'config.yaml')
51
+
52
+ function loadUserConfig() {
53
+ if (!existsSync(USER_CONFIG_PATH)) return {}
54
+ // Simple YAML parser (key: value only, no nesting needed here)
55
+ const raw = readFileSync(USER_CONFIG_PATH, 'utf8')
56
+ const config = {}
57
+ for (const line of raw.split('\n')) {
58
+ const m = line.match(/^(\w[\w.]*?):\s*(.+)$/)
59
+ if (m) config[m[1].trim()] = m[2].trim()
60
+ }
61
+ return config
62
+ }
63
+
64
+ // ─── Delegate fleet ───────────────────────────────────────────────────────────
65
+
66
+ function loadDelegateLanes() {
67
+ const paths = [
68
+ join(process.cwd(), '.delegate', 'fleet.yaml'),
69
+ join(homedir(), '.delegate', 'fleet.yaml'),
70
+ ]
71
+
72
+ for (const p of paths) {
73
+ if (!existsSync(p)) continue
74
+ try {
75
+ const raw = readFileSync(p, 'utf8')
76
+ const lanes = {}
77
+ let current = null
78
+ for (const line of raw.split('\n')) {
79
+ const laneMatch = line.match(/^ (\w[\w-]*):\s*$/)
80
+ const implMatch = line.match(/^\s+implementer:\s*(.+)$/)
81
+ const modelMatch = line.match(/^\s+model:\s*(.+)$/)
82
+ const effortMatch = line.match(/^\s+effort:\s*(.+)$/)
83
+ if (laneMatch) { current = laneMatch[1]; lanes[current] = {} }
84
+ if (current && implMatch) lanes[current].agent = implMatch[1].trim()
85
+ if (current && modelMatch) lanes[current].model = modelMatch[1].trim()
86
+ if (current && effortMatch) lanes[current].effort = effortMatch[1].trim()
87
+ }
88
+ return lanes
89
+ } catch { continue }
90
+ }
91
+ return {}
92
+ }
93
+
94
+ // ─── Scoring ──────────────────────────────────────────────────────────────────
95
+
96
+ function modelScore(registry, modelId) {
97
+ return (
98
+ registry.models[modelId] ??
99
+ registry.models[modelId?.toLowerCase()] ??
100
+ registry.models['unknown'] ??
101
+ { planning: 1, coding: 1, review: 1 }
102
+ )
103
+ }
104
+
105
+ function meetsRequirement(registry, modelId, phase) {
106
+ const score = modelScore(registry, modelId)
107
+ const req = registry.phase_requirements?.[phase] ?? {}
108
+ if (req.min_planning && score.planning < req.min_planning) return false
109
+ if (req.min_coding && score.coding < req.min_coding) return false
110
+ if (req.min_review && score.review < req.min_review) return false
111
+ return true
112
+ }
113
+
114
+ // Phase-relevant score for ranking
115
+ function phaseScore(registry, modelId, phase) {
116
+ const s = modelScore(registry, modelId)
117
+ if (phase === 'plan' || phase === 'review' || phase === 'verify') return s.planning + s.review
118
+ return s.coding
119
+ }
120
+
121
+ // ─── Agent → representative model mapping ─────────────────────────────────────
122
+
123
+ const AGENT_MODEL_MAP = {
124
+ claude: 'claude-sonnet-4-5',
125
+ agy: 'claude-sonnet-4-5',
126
+ codex: 'codex-default',
127
+ gemini: 'gemini-2-5-pro',
128
+ opencode: 'gpt-4o',
129
+ aider: 'gpt-4o',
130
+ cursor: 'claude-sonnet-4-5',
131
+ cline: 'claude-sonnet-4-5',
132
+ copilot: 'gpt-4o',
133
+ }
134
+
135
+ // Fallback priority order (best-to-acceptable)
136
+ const AGENT_PRIORITY_FOR_PLANNING = ['claude', 'agy', 'gemini', 'opencode', 'cursor', 'cline', 'copilot', 'aider', 'codex']
137
+ const AGENT_PRIORITY_FOR_CODING = ['codex', 'aider', 'opencode', 'cursor', 'cline', 'claude', 'agy', 'copilot', 'gemini']
138
+
139
+ // ─── Main routing logic ───────────────────────────────────────────────────────
140
+
141
+ function route(input) {
142
+ const {
143
+ taskSize = 'medium',
144
+ phase = 'implement',
145
+ budget = 'balanced',
146
+ allowMax = false,
147
+ delegateEnabled = false,
148
+ currentModel = 'unknown',
149
+ } = input
150
+
151
+ const registry = loadRegistry()
152
+ const userConfig = loadUserConfig()
153
+ const lanes = loadDelegateLanes()
154
+
155
+ // ── 1. User explicit override ────────────────────────────────────────────
156
+ const overrideKey = `agentOverrides.${phase}`
157
+ const explicitAgent = userConfig[overrideKey] ?? userConfig[`override_${phase}`]
158
+ if (explicitAgent) {
159
+ const effort = computeEffort(registry, budget, phase, allowMax)
160
+ return { agent: explicitAgent, model: null, effort, execution: 'native' }
161
+ }
162
+
163
+ // ── 2. Delegate lane (only when delegate is enabled) ────────────────────
164
+ if (delegateEnabled && Object.keys(lanes).length > 0) {
165
+ const laneForPhase = findLaneForPhase(lanes, phase)
166
+ if (laneForPhase) {
167
+ const effort = laneForPhase.effort ?? computeEffort(registry, budget, phase, allowMax)
168
+ return {
169
+ agent: laneForPhase.agent,
170
+ model: laneForPhase.model ?? null,
171
+ effort,
172
+ execution: 'delegate',
173
+ }
174
+ }
175
+ }
176
+
177
+ // ── 3. Built-in registry: best available agent for phase ─────────────────
178
+ const priorityList = (phase === 'plan' || phase === 'review' || phase === 'verify')
179
+ ? AGENT_PRIORITY_FOR_PLANNING
180
+ : AGENT_PRIORITY_FOR_CODING
181
+
182
+ for (const agentId of priorityList) {
183
+ const modelId = AGENT_MODEL_MAP[agentId] ?? 'unknown'
184
+ if (meetsRequirement(registry, modelId, phase)) {
185
+ const effort = computeEffort(registry, budget, phase, allowMax)
186
+ return { agent: agentId, model: modelId, effort, execution: 'native' }
187
+ }
188
+ }
189
+
190
+ // ── 4. Fallback ──────────────────────────────────────────────────────────
191
+ const effort = computeEffort(registry, budget, phase, allowMax)
192
+ return { agent: 'claude', model: null, effort, execution: 'native' }
193
+ }
194
+
195
+ function findLaneForPhase(lanes, phase) {
196
+ // Map phase to lane name heuristic
197
+ const keywords = {
198
+ plan: ['plan', 'planning'],
199
+ implement: ['feature', 'impl', 'code', 'build'],
200
+ review: ['review', 'check'],
201
+ fix: ['fix', 'repair', 'feature'],
202
+ verify: ['test', 'verify', 'qa'],
203
+ }
204
+ const keys = keywords[phase] ?? [phase]
205
+ for (const key of keys) {
206
+ for (const [name, lane] of Object.entries(lanes)) {
207
+ if (name.toLowerCase().includes(key) && lane.agent) return lane
208
+ }
209
+ }
210
+ // Return first lane as generic fallback if delegate is enabled
211
+ const first = Object.values(lanes)[0]
212
+ return first?.agent ? first : null
213
+ }
214
+
215
+ function computeEffort(registry, budget, phase, allowMax) {
216
+ const table = registry.effort_table?.[budget] ?? {}
217
+ let effort = table[phase] ?? 'medium'
218
+ if (effort === 'max' && !allowMax) effort = 'high'
219
+ return effort
220
+ }
221
+
222
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
223
+
224
+ async function main() {
225
+ let input = {}
226
+
227
+ const inputFlag = process.argv.indexOf('--input')
228
+ if (inputFlag !== -1 && process.argv[inputFlag + 1]) {
229
+ input = JSON.parse(process.argv[inputFlag + 1])
230
+ } else if (!process.stdin.isTTY) {
231
+ const chunks = []
232
+ for await (const chunk of process.stdin) chunks.push(chunk)
233
+ const raw = Buffer.concat(chunks).toString('utf8').trim()
234
+ if (raw) input = JSON.parse(raw)
235
+ }
236
+
237
+ const result = route(input)
238
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n')
239
+ }
240
+
241
+ main().catch((err) => {
242
+ process.stderr.write('route.mjs error: ' + err.message + '\n')
243
+ process.exit(1)
244
+ })
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * run-state.mjs
4
+ * ──────────────
5
+ * Manages run workspace files.
6
+ * All operations are deterministic — no reasoning.
7
+ *
8
+ * Commands:
9
+ * node scripts/run-state.mjs init --run-id <id> --task <str> --size <s> --budget <b>
10
+ * node scripts/run-state.mjs update --run-id <id> --status <s> [--phase <p>]
11
+ * node scripts/run-state.mjs read --run-id <id>
12
+ * node scripts/run-state.mjs write-phase --run-id <id> --phase <p> --status <s> --summary <str> [--findings-json <json>]
13
+ * node scripts/run-state.mjs read-phase --run-id <id> --phase <p>
14
+ * node scripts/run-state.mjs build-brief --run-id <id> --phase <p>
15
+ * node scripts/run-state.mjs list
16
+ */
17
+
18
+ import {
19
+ mkdirSync, writeFileSync, readFileSync,
20
+ existsSync, readdirSync
21
+ } from 'node:fs'
22
+ import { join } from 'node:path'
23
+ import { randomBytes } from 'node:crypto'
24
+
25
+ // ─── Workspace root ───────────────────────────────────────────────────────────
26
+
27
+ const RUNS_ROOT = existsSync(join(process.cwd(), '.adaptive-orchestrator', 'runs'))
28
+ ? join(process.cwd(), '.adaptive-orchestrator', 'runs')
29
+ : join(process.cwd(), '.adaptive-director', 'runs')
30
+
31
+ function runDir(runId) { return join(RUNS_ROOT, runId) }
32
+ function metaPath(runId) { return join(runDir(runId), 'metadata.json') }
33
+ function phaseMdPath(runId, phase) { return join(runDir(runId), `${phase}.md`) }
34
+ function phaseJsonPath(runId, phase) { return join(runDir(runId), `${phase}.json`) }
35
+
36
+ // ─── Commands ─────────────────────────────────────────────────────────────────
37
+
38
+ function cmdInit(args) {
39
+ const runId = args['--run-id'] ?? ('run-' + randomBytes(4).toString('hex'))
40
+ const task = args['--task'] ?? ''
41
+ const size = args['--size'] ?? 'medium'
42
+ const budget = args['--budget'] ?? 'balanced'
43
+
44
+ mkdirSync(runDir(runId), { recursive: true })
45
+
46
+ // task.md
47
+ writeFileSync(phaseMdPath(runId, 'task'),
48
+ `# Task\n\n${task}\n\n## Constraints\n\n- Stay in scope\n- Document all decisions\n`, 'utf8')
49
+
50
+ // metadata.json
51
+ const meta = {
52
+ runId,
53
+ task,
54
+ status: 'pending',
55
+ currentPhase: null,
56
+ size,
57
+ budget,
58
+ allowMax: false,
59
+ useDelegate: false,
60
+ routing: [],
61
+ startedAt: new Date().toISOString(),
62
+ updatedAt: new Date().toISOString(),
63
+ }
64
+ writeFileSync(metaPath(runId), JSON.stringify(meta, null, 2), 'utf8')
65
+
66
+ out({ runId, workspacePath: runDir(runId) })
67
+ }
68
+
69
+ function cmdUpdate(args) {
70
+ const runId = requireArg(args, '--run-id')
71
+ const status = args['--status']
72
+ const phase = args['--phase'] ?? null
73
+
74
+ const meta = readMeta(runId)
75
+ if (status) meta.status = status
76
+ if (phase !== null) meta.currentPhase = phase
77
+ meta.updatedAt = new Date().toISOString()
78
+ writeFileSync(metaPath(runId), JSON.stringify(meta, null, 2), 'utf8')
79
+ out({ ok: true, runId, status: meta.status, currentPhase: meta.currentPhase })
80
+ }
81
+
82
+ function cmdRead(args) {
83
+ const runId = requireArg(args, '--run-id')
84
+ out(readMeta(runId))
85
+ }
86
+
87
+ function cmdWritePhase(args) {
88
+ const runId = requireArg(args, '--run-id')
89
+ const phase = requireArg(args, '--phase')
90
+ const status = args['--status'] ?? 'completed'
91
+ const summary = args['--summary'] ?? ''
92
+ const findingsRaw = args['--findings-json']
93
+
94
+ let findings = []
95
+ if (findingsRaw) {
96
+ try { findings = JSON.parse(findingsRaw) } catch { findings = [] }
97
+ }
98
+
99
+ const result = { phase, status, summary, findings }
100
+
101
+ // .json (machine contract)
102
+ writeFileSync(phaseJsonPath(runId, phase), JSON.stringify(result, null, 2), 'utf8')
103
+
104
+ // .md (human readable)
105
+ const lines = [
106
+ `# ${capitalize(phase)} Result`,
107
+ '',
108
+ `**Status:** ${status}`,
109
+ '',
110
+ '## Summary',
111
+ '',
112
+ summary,
113
+ ]
114
+ if (findings.length > 0) {
115
+ lines.push('', '## Findings', '')
116
+ for (const f of findings) {
117
+ lines.push(`### [${f.severity?.toUpperCase() ?? 'NOTE'}] ${f.title ?? ''}`)
118
+ if (f.file) lines.push(`**File:** \`${f.file}\``)
119
+ lines.push('', f.description ?? '', '')
120
+ }
121
+ }
122
+ writeFileSync(phaseMdPath(runId, phase), lines.join('\n'), 'utf8')
123
+
124
+ out({ ok: true, phase, status })
125
+ }
126
+
127
+ function cmdReadPhase(args) {
128
+ const runId = requireArg(args, '--run-id')
129
+ const phase = requireArg(args, '--phase')
130
+ const path = phaseJsonPath(runId, phase)
131
+ if (!existsSync(path)) { out(null); return }
132
+ out(JSON.parse(readFileSync(path, 'utf8')))
133
+ }
134
+
135
+ function cmdBuildBrief(args) {
136
+ const runId = requireArg(args, '--run-id')
137
+ const phase = requireArg(args, '--phase')
138
+
139
+ const dir = runDir(runId)
140
+ const task = safeRead(join(dir, 'task.md'))
141
+ const plan = safeRead(join(dir, 'plan.md'))
142
+ const impl = safeRead(join(dir, 'implementation.md'))
143
+ const rev = safeRead(join(dir, 'review.md'))
144
+
145
+ let brief = ''
146
+
147
+ switch (phase) {
148
+ case 'plan':
149
+ brief = [
150
+ task ?? '',
151
+ '---',
152
+ '# Your Role: Planner',
153
+ '',
154
+ 'Inspect the repository and produce a detailed step-by-step plan.',
155
+ 'After inspecting, if the initial task size estimate seems wrong, say so.',
156
+ '',
157
+ '## Required Output',
158
+ '1. Numbered implementation steps',
159
+ '2. Files/modules involved',
160
+ '3. Acceptance criteria',
161
+ '4. Constraints',
162
+ '5. End with a JSON block:',
163
+ '```json',
164
+ '{"recommended_size": "medium", "reason": "..."}',
165
+ '```',
166
+ ].join('\n')
167
+ break
168
+
169
+ case 'implement':
170
+ brief = [
171
+ task ?? '',
172
+ '',
173
+ '---',
174
+ '',
175
+ '# Approved Plan',
176
+ '',
177
+ plan ?? '(no plan — use best judgment)',
178
+ '',
179
+ '---',
180
+ '',
181
+ '# Your Role: Implementer',
182
+ '',
183
+ '- Implement the approved plan exactly.',
184
+ '- Stay in scope. Do NOT commit.',
185
+ '- Report: changed files, completed work, test results, known issues.',
186
+ ].join('\n')
187
+ break
188
+
189
+ case 'review':
190
+ brief = [
191
+ task ?? '',
192
+ '',
193
+ '---',
194
+ '',
195
+ '# Plan',
196
+ plan ?? '(no plan)',
197
+ '',
198
+ '---',
199
+ '',
200
+ '# Implementation Report',
201
+ impl ?? '(no implementation report)',
202
+ '',
203
+ '---',
204
+ '',
205
+ '# Your Role: Independent Reviewer',
206
+ '',
207
+ 'Review the implementation against the plan.',
208
+ 'Classify EVERY finding as exactly one of:',
209
+ ' CRITICAL — must fix before completion',
210
+ ' WARNING — report, do not block',
211
+ ' SUGGESTION — informational only',
212
+ '',
213
+ 'Format each finding as:',
214
+ ' [CRITICAL] <title>',
215
+ ' File: <filename>',
216
+ ' <description>',
217
+ ].join('\n')
218
+ break
219
+
220
+ case 'fix':
221
+ brief = [
222
+ task ?? '',
223
+ '',
224
+ '---',
225
+ '',
226
+ '# Previous Review (with CRITICAL findings)',
227
+ rev ?? '(no review)',
228
+ '',
229
+ '---',
230
+ '',
231
+ '# Your Role: Fixer',
232
+ '',
233
+ 'Fix ONLY the CRITICAL findings listed above.',
234
+ 'Do not redesign or change scope.',
235
+ 'Do NOT commit.',
236
+ 'Report what you changed.',
237
+ ].join('\n')
238
+ break
239
+
240
+ case 'verify':
241
+ brief = [
242
+ task ?? '',
243
+ '',
244
+ '---',
245
+ '',
246
+ '# Plan',
247
+ plan ?? '(no plan)',
248
+ '',
249
+ '---',
250
+ '',
251
+ '# Your Role: Verifier',
252
+ '',
253
+ 'Verify the implementation:',
254
+ '- Requirements satisfied?',
255
+ '- Tests passing?',
256
+ '- Build successful?',
257
+ '- All CRITICAL findings resolved?',
258
+ '- Implementation matches plan?',
259
+ '',
260
+ 'End with either:',
261
+ ' VERIFIED — all checks passed',
262
+ ' BLOCKED — <reason>',
263
+ ].join('\n')
264
+ break
265
+
266
+ default:
267
+ brief = task ?? ''
268
+ }
269
+
270
+ process.stdout.write(brief + '\n')
271
+ }
272
+
273
+ function cmdList() {
274
+ if (!existsSync(RUNS_ROOT)) { out([]); return }
275
+ const dirs = readdirSync(RUNS_ROOT, { withFileTypes: true })
276
+ .filter(d => d.isDirectory()).map(d => d.name)
277
+ const runs = dirs.map(id => {
278
+ try { return JSON.parse(readFileSync(metaPath(id), 'utf8')) } catch { return null }
279
+ }).filter(Boolean)
280
+ runs.sort((a, b) => b.startedAt.localeCompare(a.startedAt))
281
+ out(runs)
282
+ }
283
+
284
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
285
+
286
+ function readMeta(runId) {
287
+ const path = metaPath(runId)
288
+ if (!existsSync(path)) throw new Error(`Run not found: ${runId}`)
289
+ return JSON.parse(readFileSync(path, 'utf8'))
290
+ }
291
+
292
+ function safeRead(path) {
293
+ return existsSync(path) ? readFileSync(path, 'utf8') : null
294
+ }
295
+
296
+ function requireArg(args, key) {
297
+ if (!args[key]) { process.stderr.write(`Missing required argument: ${key}\n`); process.exit(2) }
298
+ return args[key]
299
+ }
300
+
301
+ function out(data) {
302
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n')
303
+ }
304
+
305
+ function capitalize(s) {
306
+ return s.charAt(0).toUpperCase() + s.slice(1)
307
+ }
308
+
309
+ // ─── Arg parser ───────────────────────────────────────────────────────────────
310
+
311
+ function parseArgs(argv) {
312
+ const args = {}
313
+ for (let i = 0; i < argv.length; i++) {
314
+ if (argv[i].startsWith('--')) {
315
+ args[argv[i]] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true
316
+ }
317
+ }
318
+ return args
319
+ }
320
+
321
+ // ─── Entry ────────────────────────────────────────────────────────────────────
322
+
323
+ const [,, command, ...rest] = process.argv
324
+ const args = parseArgs(rest)
325
+
326
+ try {
327
+ switch (command) {
328
+ case 'init': cmdInit(args); break
329
+ case 'update': cmdUpdate(args); break
330
+ case 'read': cmdRead(args); break
331
+ case 'write-phase': cmdWritePhase(args); break
332
+ case 'read-phase': cmdReadPhase(args); break
333
+ case 'build-brief': cmdBuildBrief(args); break
334
+ case 'list': cmdList(); break
335
+ default:
336
+ process.stderr.write(`Unknown command: ${command}\n`)
337
+ process.stderr.write('Commands: init, update, read, write-phase, read-phase, build-brief, list\n')
338
+ process.exit(2)
339
+ }
340
+ } catch (err) {
341
+ process.stderr.write('run-state.mjs error: ' + err.message + '\n')
342
+ process.exit(1)
343
+ }