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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/SKILL.md +395 -0
- package/data/registry.json +42 -0
- package/package.json +40 -0
- package/references/capability-registry.md +92 -0
- package/references/delegate-integration.md +115 -0
- package/references/handoff-schema.md +137 -0
- package/references/routing-rules.md +99 -0
- package/scripts/discover.mjs +132 -0
- package/scripts/resume.mjs +70 -0
- package/scripts/route.mjs +244 -0
- package/scripts/run-state.mjs +343 -0
- package/scripts/setup.mjs +123 -0
- package/scripts/smoke-test.mjs +89 -0
|
@@ -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
|
+
}
|