agentplane 0.1.8 → 0.1.9
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/assets/AGENTS.md +13 -2
- package/dist/backends/task-backend.d.ts +12 -0
- package/dist/backends/task-backend.d.ts.map +1 -1
- package/dist/backends/task-backend.js +41 -4
- package/dist/cli/command-guide.d.ts.map +1 -1
- package/dist/cli/command-guide.js +6 -7
- package/dist/cli/run-cli.d.ts.map +1 -1
- package/dist/cli/run-cli.js +57 -2
- package/dist/commands/guard/index.d.ts +24 -3
- package/dist/commands/guard/index.d.ts.map +1 -1
- package/dist/commands/guard/index.js +175 -61
- package/dist/commands/hooks/index.d.ts.map +1 -1
- package/dist/commands/hooks/index.js +39 -29
- package/dist/commands/recipes.d.ts +75 -6
- package/dist/commands/recipes.d.ts.map +1 -1
- package/dist/commands/recipes.js +15 -521
- package/dist/commands/scenario.d.ts +7 -0
- package/dist/commands/scenario.d.ts.map +1 -0
- package/dist/commands/scenario.js +501 -0
- package/dist/commands/shared/task-backend.d.ts +19 -3
- package/dist/commands/shared/task-backend.d.ts.map +1 -1
- package/dist/commands/shared/task-backend.js +13 -5
- package/dist/commands/task/block.d.ts.map +1 -1
- package/dist/commands/task/block.js +22 -16
- package/dist/commands/task/comment.d.ts.map +1 -1
- package/dist/commands/task/comment.js +9 -2
- package/dist/commands/task/finish.d.ts.map +1 -1
- package/dist/commands/task/finish.js +36 -26
- package/dist/commands/task/set-status.d.ts.map +1 -1
- package/dist/commands/task/set-status.js +18 -4
- package/dist/commands/task/shared.d.ts +3 -2
- package/dist/commands/task/shared.d.ts.map +1 -1
- package/dist/commands/task/shared.js +18 -28
- package/dist/commands/task/start.d.ts.map +1 -1
- package/dist/commands/task/start.js +24 -18
- package/dist/commands/task/verify-record.d.ts.map +1 -1
- package/dist/commands/task/verify-record.js +8 -1
- package/dist/shared/git-log.d.ts +5 -0
- package/dist/shared/git-log.d.ts.map +1 -0
- package/dist/shared/git-log.js +14 -0
- package/dist/shared/git-path.d.ts +3 -0
- package/dist/shared/git-path.d.ts.map +1 -0
- package/dist/shared/git-path.js +30 -0
- package/dist/shared/guards.d.ts +2 -0
- package/dist/shared/guards.d.ts.map +1 -0
- package/dist/shared/guards.js +3 -0
- package/dist/shared/protected-paths.d.ts +12 -0
- package/dist/shared/protected-paths.d.ts.map +1 -0
- package/dist/shared/protected-paths.js +51 -0
- package/dist/shared/strings.d.ts +2 -0
- package/dist/shared/strings.d.ts.map +1 -0
- package/dist/shared/strings.js +14 -0
- package/package.json +2 -2
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { atomicWriteFile, resolveProject } from "@agentplaneorg/core";
|
|
6
|
+
import { mapCoreError } from "../cli/error-map.js";
|
|
7
|
+
import { fileExists, getPathKind } from "../cli/fs-utils.js";
|
|
8
|
+
import { emptyStateMessage, usageMessage } from "../cli/output.js";
|
|
9
|
+
import { CliError } from "../shared/errors.js";
|
|
10
|
+
import { dedupeStrings } from "../shared/strings.js";
|
|
11
|
+
import { RECIPES_DIR_NAME, RECIPES_SCENARIOS_DIR_NAME, RECIPES_SCENARIOS_INDEX_NAME, normalizeScenarioToolStep, readInstalledRecipesFile, readRecipeManifest, readScenarioDefinition, readScenarioIndex, resolveInstalledRecipeDir, resolveInstalledRecipesPath, resolveProjectRecipesCacheDir, } from "./recipes.js";
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
const SCENARIO_USAGE = "Usage: agentplane scenario <list|info|run> [args]";
|
|
14
|
+
const SCENARIO_USAGE_EXAMPLE = "agentplane scenario list";
|
|
15
|
+
const SCENARIO_INFO_USAGE = "Usage: agentplane scenario info <recipe:scenario>";
|
|
16
|
+
const SCENARIO_INFO_USAGE_EXAMPLE = "agentplane scenario info viewer:demo";
|
|
17
|
+
const SCENARIO_RUN_USAGE = "Usage: agentplane scenario run <recipe:scenario>";
|
|
18
|
+
const SCENARIO_RUN_USAGE_EXAMPLE = "agentplane scenario run viewer:demo";
|
|
19
|
+
const SCENARIO_REPORT_NAME = "report.json";
|
|
20
|
+
const SENSITIVE_ARG_FLAGS = new Set([
|
|
21
|
+
"--token",
|
|
22
|
+
"--secret",
|
|
23
|
+
"--password",
|
|
24
|
+
"--api-key",
|
|
25
|
+
"--apikey",
|
|
26
|
+
"--access-key",
|
|
27
|
+
"--client-secret",
|
|
28
|
+
"--auth",
|
|
29
|
+
"--authorization",
|
|
30
|
+
"--bearer",
|
|
31
|
+
]);
|
|
32
|
+
function redactArgs(args) {
|
|
33
|
+
const out = [...args];
|
|
34
|
+
for (let i = 0; i < out.length; i++) {
|
|
35
|
+
const arg = out[i];
|
|
36
|
+
if (!arg)
|
|
37
|
+
continue;
|
|
38
|
+
const eqIndex = arg.indexOf("=");
|
|
39
|
+
const flag = eqIndex === -1 ? arg : arg.slice(0, eqIndex);
|
|
40
|
+
if (!SENSITIVE_ARG_FLAGS.has(flag))
|
|
41
|
+
continue;
|
|
42
|
+
if (eqIndex !== -1) {
|
|
43
|
+
out[i] = `${flag}=<redacted>`;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
out[i] = flag;
|
|
47
|
+
if (i + 1 < out.length && !out[i + 1]?.startsWith("-")) {
|
|
48
|
+
out[i + 1] = "<redacted>";
|
|
49
|
+
i += 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
function isNotGitRepoError(err) {
|
|
55
|
+
if (err instanceof Error) {
|
|
56
|
+
return err.message.startsWith("Not a git repository");
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
async function getGitDiffSummary(cwd) {
|
|
61
|
+
try {
|
|
62
|
+
const [diff, staged, status] = await Promise.all([
|
|
63
|
+
execFileAsync("git", ["diff", "--stat"], { cwd }),
|
|
64
|
+
execFileAsync("git", ["diff", "--stat", "--staged"], { cwd }),
|
|
65
|
+
execFileAsync("git", ["status", "--porcelain"], { cwd }),
|
|
66
|
+
]);
|
|
67
|
+
const diffStat = String(diff.stdout).trim();
|
|
68
|
+
const stagedStat = String(staged.stdout).trim();
|
|
69
|
+
const statusLines = String(status.stdout).trim();
|
|
70
|
+
return {
|
|
71
|
+
diff_stat: diffStat || undefined,
|
|
72
|
+
staged_stat: stagedStat || undefined,
|
|
73
|
+
status: statusLines
|
|
74
|
+
? statusLines
|
|
75
|
+
.split("\n")
|
|
76
|
+
.map((line) => line.trim())
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
: [],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (isNotGitRepoError(err))
|
|
83
|
+
return undefined;
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function collectScenarioEnvKeys(stepEnv) {
|
|
88
|
+
return dedupeStrings([
|
|
89
|
+
...Object.keys(stepEnv ?? {}),
|
|
90
|
+
"AGENTPLANE_RUN_DIR",
|
|
91
|
+
"AGENTPLANE_STEP_DIR",
|
|
92
|
+
"AGENTPLANE_RECIPES_CACHE_DIR",
|
|
93
|
+
"AGENTPLANE_RECIPE_ID",
|
|
94
|
+
"AGENTPLANE_SCENARIO_ID",
|
|
95
|
+
"AGENTPLANE_TOOL_ID",
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
async function writeScenarioReport(opts) {
|
|
99
|
+
const report = {
|
|
100
|
+
schema_version: 1,
|
|
101
|
+
recipe: opts.recipeId,
|
|
102
|
+
scenario: opts.scenarioId,
|
|
103
|
+
run_id: opts.runId,
|
|
104
|
+
started_at: opts.startedAt,
|
|
105
|
+
ended_at: new Date().toISOString(),
|
|
106
|
+
status: opts.status,
|
|
107
|
+
steps: opts.steps,
|
|
108
|
+
git: opts.gitSummary,
|
|
109
|
+
};
|
|
110
|
+
await atomicWriteFile(path.join(opts.runDir, SCENARIO_REPORT_NAME), `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
111
|
+
}
|
|
112
|
+
async function cmdScenarioList(opts) {
|
|
113
|
+
try {
|
|
114
|
+
const installed = await readInstalledRecipesFile(resolveInstalledRecipesPath());
|
|
115
|
+
const entries = [];
|
|
116
|
+
for (const recipe of installed.recipes) {
|
|
117
|
+
const recipeDir = resolveInstalledRecipeDir(recipe);
|
|
118
|
+
const scenariosDir = path.join(recipeDir, RECIPES_SCENARIOS_DIR_NAME);
|
|
119
|
+
if ((await getPathKind(scenariosDir)) === "dir") {
|
|
120
|
+
const files = await readdir(scenariosDir);
|
|
121
|
+
const jsonFiles = files.filter((entry) => entry.toLowerCase().endsWith(".json")).toSorted();
|
|
122
|
+
for (const file of jsonFiles) {
|
|
123
|
+
const scenario = await readScenarioDefinition(path.join(scenariosDir, file));
|
|
124
|
+
entries.push({ recipeId: recipe.id, scenarioId: scenario.id, summary: scenario.summary });
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const scenariosIndexPath = path.join(recipeDir, RECIPES_SCENARIOS_INDEX_NAME);
|
|
129
|
+
if (await fileExists(scenariosIndexPath)) {
|
|
130
|
+
const index = await readScenarioIndex(scenariosIndexPath);
|
|
131
|
+
for (const scenario of index.scenarios) {
|
|
132
|
+
entries.push({
|
|
133
|
+
recipeId: recipe.id,
|
|
134
|
+
scenarioId: scenario.id,
|
|
135
|
+
summary: scenario.summary,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (entries.length === 0) {
|
|
141
|
+
process.stdout.write(`${emptyStateMessage("scenarios", "Install a recipe to add scenarios.")}\n`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
const sorted = entries.toSorted((a, b) => {
|
|
145
|
+
const byRecipe = a.recipeId.localeCompare(b.recipeId);
|
|
146
|
+
if (byRecipe !== 0)
|
|
147
|
+
return byRecipe;
|
|
148
|
+
return a.scenarioId.localeCompare(b.scenarioId);
|
|
149
|
+
});
|
|
150
|
+
for (const entry of sorted) {
|
|
151
|
+
process.stdout.write(`${entry.recipeId}:${entry.scenarioId} - ${entry.summary ?? "No summary"}\n`);
|
|
152
|
+
}
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
if (err instanceof CliError)
|
|
157
|
+
throw err;
|
|
158
|
+
throw mapCoreError(err, { command: "scenario list", root: opts.rootOverride ?? null });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function cmdScenarioInfo(opts) {
|
|
162
|
+
try {
|
|
163
|
+
const [recipeId, scenarioId] = opts.id.split(":");
|
|
164
|
+
if (!recipeId || !scenarioId) {
|
|
165
|
+
throw new CliError({
|
|
166
|
+
exitCode: 2,
|
|
167
|
+
code: "E_USAGE",
|
|
168
|
+
message: usageMessage(SCENARIO_INFO_USAGE, SCENARIO_INFO_USAGE_EXAMPLE),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const installed = await readInstalledRecipesFile(resolveInstalledRecipesPath());
|
|
172
|
+
const entry = installed.recipes.find((recipe) => recipe.id === recipeId);
|
|
173
|
+
if (!entry) {
|
|
174
|
+
throw new CliError({
|
|
175
|
+
exitCode: 5,
|
|
176
|
+
code: "E_IO",
|
|
177
|
+
message: `Recipe not installed: ${recipeId}`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const recipeDir = resolveInstalledRecipeDir(entry);
|
|
181
|
+
const scenariosDir = path.join(recipeDir, RECIPES_SCENARIOS_DIR_NAME);
|
|
182
|
+
let scenario = null;
|
|
183
|
+
if ((await getPathKind(scenariosDir)) === "dir") {
|
|
184
|
+
const files = await readdir(scenariosDir);
|
|
185
|
+
const jsonFiles = files.filter((file) => file.toLowerCase().endsWith(".json")).toSorted();
|
|
186
|
+
for (const file of jsonFiles) {
|
|
187
|
+
const candidate = await readScenarioDefinition(path.join(scenariosDir, file));
|
|
188
|
+
if (candidate.id === scenarioId) {
|
|
189
|
+
scenario = candidate;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
let summary;
|
|
195
|
+
if (!scenario) {
|
|
196
|
+
const scenariosIndexPath = path.join(recipeDir, RECIPES_SCENARIOS_INDEX_NAME);
|
|
197
|
+
if (await fileExists(scenariosIndexPath)) {
|
|
198
|
+
const index = await readScenarioIndex(scenariosIndexPath);
|
|
199
|
+
const entrySummary = index.scenarios.find((item) => item.id === scenarioId);
|
|
200
|
+
summary = entrySummary?.summary;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!scenario && !summary) {
|
|
204
|
+
throw new CliError({
|
|
205
|
+
exitCode: 5,
|
|
206
|
+
code: "E_IO",
|
|
207
|
+
message: `Scenario not found: ${recipeId}:${scenarioId}`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
process.stdout.write(`Scenario: ${recipeId}:${scenarioId}\n`);
|
|
211
|
+
if (summary)
|
|
212
|
+
process.stdout.write(`Summary: ${summary}\n`);
|
|
213
|
+
if (!scenario) {
|
|
214
|
+
process.stdout.write("Details: Scenario definition not found in recipe.\n");
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
if (scenario.summary)
|
|
218
|
+
process.stdout.write(`Summary: ${scenario.summary}\n`);
|
|
219
|
+
if (scenario.description)
|
|
220
|
+
process.stdout.write(`Description: ${scenario.description}\n`);
|
|
221
|
+
process.stdout.write(`Goal: ${scenario.goal}\n`);
|
|
222
|
+
process.stdout.write(`Inputs: ${JSON.stringify(scenario.inputs, null, 2)}\n`);
|
|
223
|
+
process.stdout.write(`Outputs: ${JSON.stringify(scenario.outputs, null, 2)}\n`);
|
|
224
|
+
process.stdout.write("Steps:\n");
|
|
225
|
+
let stepIndex = 1;
|
|
226
|
+
for (const step of scenario.steps) {
|
|
227
|
+
process.stdout.write(` ${stepIndex}. ${JSON.stringify(step)}\n`);
|
|
228
|
+
stepIndex += 1;
|
|
229
|
+
}
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
if (err instanceof CliError)
|
|
234
|
+
throw err;
|
|
235
|
+
throw mapCoreError(err, { command: "scenario info", root: opts.rootOverride ?? null });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function executeRecipeTool(opts) {
|
|
239
|
+
try {
|
|
240
|
+
const command = opts.runtime === "node" ? "node" : "bash";
|
|
241
|
+
const { stdout, stderr } = await execFileAsync(command, [opts.entrypoint, ...opts.args], {
|
|
242
|
+
cwd: opts.cwd,
|
|
243
|
+
env: opts.env,
|
|
244
|
+
});
|
|
245
|
+
return { exitCode: 0, stdout: String(stdout), stderr: String(stderr) };
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
let execErr = null;
|
|
249
|
+
if (err && typeof err === "object") {
|
|
250
|
+
execErr = err;
|
|
251
|
+
}
|
|
252
|
+
const exitCode = typeof execErr?.code === "number" ? execErr.code : 1;
|
|
253
|
+
return {
|
|
254
|
+
exitCode,
|
|
255
|
+
stdout: String(execErr?.stdout ?? ""),
|
|
256
|
+
stderr: String(execErr?.stderr ?? ""),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function sanitizeRunId(value) {
|
|
261
|
+
return value.replaceAll(/[^a-zA-Z0-9._-]/g, "_");
|
|
262
|
+
}
|
|
263
|
+
async function cmdScenarioRun(opts) {
|
|
264
|
+
try {
|
|
265
|
+
const resolved = await resolveProject({
|
|
266
|
+
cwd: opts.cwd,
|
|
267
|
+
rootOverride: opts.rootOverride ?? null,
|
|
268
|
+
});
|
|
269
|
+
const [recipeId, scenarioId] = opts.id.split(":");
|
|
270
|
+
if (!recipeId || !scenarioId) {
|
|
271
|
+
throw new CliError({
|
|
272
|
+
exitCode: 2,
|
|
273
|
+
code: "E_USAGE",
|
|
274
|
+
message: usageMessage(SCENARIO_RUN_USAGE, SCENARIO_RUN_USAGE_EXAMPLE),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
const installed = await readInstalledRecipesFile(resolveInstalledRecipesPath());
|
|
278
|
+
const entry = installed.recipes.find((recipe) => recipe.id === recipeId);
|
|
279
|
+
if (!entry) {
|
|
280
|
+
throw new CliError({
|
|
281
|
+
exitCode: 5,
|
|
282
|
+
code: "E_IO",
|
|
283
|
+
message: `Recipe not installed: ${recipeId}`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const recipeDir = resolveInstalledRecipeDir(entry);
|
|
287
|
+
const manifestPath = path.join(recipeDir, "manifest.json");
|
|
288
|
+
const manifest = await readRecipeManifest(manifestPath);
|
|
289
|
+
const scenariosDir = path.join(recipeDir, RECIPES_SCENARIOS_DIR_NAME);
|
|
290
|
+
if ((await getPathKind(scenariosDir)) !== "dir") {
|
|
291
|
+
throw new CliError({
|
|
292
|
+
exitCode: 5,
|
|
293
|
+
code: "E_IO",
|
|
294
|
+
message: `Scenario definitions not found for recipe: ${recipeId}`,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
let scenario = null;
|
|
298
|
+
const files = await readdir(scenariosDir);
|
|
299
|
+
const jsonFiles = files.filter((file) => file.toLowerCase().endsWith(".json")).toSorted();
|
|
300
|
+
for (const file of jsonFiles) {
|
|
301
|
+
const candidate = await readScenarioDefinition(path.join(scenariosDir, file));
|
|
302
|
+
if (candidate.id === scenarioId) {
|
|
303
|
+
scenario = candidate;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (!scenario) {
|
|
308
|
+
throw new CliError({
|
|
309
|
+
exitCode: 5,
|
|
310
|
+
code: "E_IO",
|
|
311
|
+
message: `Scenario not found: ${recipeId}:${scenarioId}`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const runsRoot = path.join(resolved.agentplaneDir, RECIPES_DIR_NAME, recipeId, "runs");
|
|
315
|
+
await mkdir(runsRoot, { recursive: true });
|
|
316
|
+
const recipesCacheDir = resolveProjectRecipesCacheDir(resolved);
|
|
317
|
+
await mkdir(recipesCacheDir, { recursive: true });
|
|
318
|
+
const runStartedAt = new Date().toISOString();
|
|
319
|
+
const runId = `${new Date()
|
|
320
|
+
.toISOString()
|
|
321
|
+
.replaceAll(":", "-")
|
|
322
|
+
.replaceAll(".", "-")}-${sanitizeRunId(scenarioId)}`;
|
|
323
|
+
const runDir = path.join(runsRoot, runId);
|
|
324
|
+
await mkdir(runDir, { recursive: true });
|
|
325
|
+
const stepsMeta = [];
|
|
326
|
+
const stepsReport = [];
|
|
327
|
+
for (let index = 0; index < scenario.steps.length; index++) {
|
|
328
|
+
const step = normalizeScenarioToolStep(scenario.steps[index], `${recipeId}:${scenarioId}`);
|
|
329
|
+
const toolEntry = manifest.tools?.find((tool) => tool?.id === step.tool);
|
|
330
|
+
if (!toolEntry) {
|
|
331
|
+
throw new CliError({
|
|
332
|
+
exitCode: 5,
|
|
333
|
+
code: "E_IO",
|
|
334
|
+
message: `Tool not found in recipe manifest: ${step.tool}`,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const runtime = toolEntry.runtime === "node" || toolEntry.runtime === "bash" ? toolEntry.runtime : "";
|
|
338
|
+
const entrypoint = typeof toolEntry.entrypoint === "string" ? toolEntry.entrypoint : "";
|
|
339
|
+
if (!runtime || !entrypoint) {
|
|
340
|
+
throw new CliError({
|
|
341
|
+
exitCode: 3,
|
|
342
|
+
code: "E_VALIDATION",
|
|
343
|
+
message: `Tool entry is missing runtime/entrypoint: ${step.tool}`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (Array.isArray(toolEntry.permissions) && toolEntry.permissions.length > 0) {
|
|
347
|
+
process.stdout.write(`Warning: tool ${toolEntry.id} declares permissions: ${toolEntry.permissions.join(", ")}\n`);
|
|
348
|
+
}
|
|
349
|
+
const entrypointPath = path.join(recipeDir, entrypoint);
|
|
350
|
+
if (!(await fileExists(entrypointPath))) {
|
|
351
|
+
throw new CliError({
|
|
352
|
+
exitCode: 5,
|
|
353
|
+
code: "E_IO",
|
|
354
|
+
message: `Tool entrypoint not found: ${entrypoint}`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const stepDir = path.join(runDir, `step-${index + 1}-${sanitizeRunId(step.tool)}`);
|
|
358
|
+
await mkdir(stepDir, { recursive: true });
|
|
359
|
+
const stepEnvKeys = collectScenarioEnvKeys(step.env);
|
|
360
|
+
const env = {
|
|
361
|
+
...process.env,
|
|
362
|
+
...step.env,
|
|
363
|
+
AGENTPLANE_RUN_DIR: runDir,
|
|
364
|
+
AGENTPLANE_STEP_DIR: stepDir,
|
|
365
|
+
AGENTPLANE_RECIPES_CACHE_DIR: recipesCacheDir,
|
|
366
|
+
AGENTPLANE_RECIPE_ID: recipeId,
|
|
367
|
+
AGENTPLANE_SCENARIO_ID: scenarioId,
|
|
368
|
+
AGENTPLANE_TOOL_ID: step.tool,
|
|
369
|
+
};
|
|
370
|
+
const startedAt = Date.now();
|
|
371
|
+
const result = await executeRecipeTool({
|
|
372
|
+
runtime,
|
|
373
|
+
entrypoint: entrypointPath,
|
|
374
|
+
args: step.args,
|
|
375
|
+
cwd: recipeDir,
|
|
376
|
+
env,
|
|
377
|
+
});
|
|
378
|
+
const durationMs = Date.now() - startedAt;
|
|
379
|
+
await atomicWriteFile(path.join(stepDir, "stdout.log"), result.stdout, "utf8");
|
|
380
|
+
await atomicWriteFile(path.join(stepDir, "stderr.log"), result.stderr, "utf8");
|
|
381
|
+
stepsMeta.push({
|
|
382
|
+
tool: step.tool,
|
|
383
|
+
runtime,
|
|
384
|
+
entrypoint,
|
|
385
|
+
exitCode: result.exitCode,
|
|
386
|
+
duration_ms: durationMs,
|
|
387
|
+
});
|
|
388
|
+
stepsReport.push({
|
|
389
|
+
step: index + 1,
|
|
390
|
+
tool: step.tool,
|
|
391
|
+
runtime,
|
|
392
|
+
entrypoint,
|
|
393
|
+
args: redactArgs(step.args),
|
|
394
|
+
env_keys: stepEnvKeys,
|
|
395
|
+
exit_code: result.exitCode,
|
|
396
|
+
duration_ms: durationMs,
|
|
397
|
+
});
|
|
398
|
+
if (result.exitCode !== 0) {
|
|
399
|
+
const gitSummary = await getGitDiffSummary(resolved.gitRoot);
|
|
400
|
+
await writeScenarioReport({
|
|
401
|
+
runDir,
|
|
402
|
+
recipeId,
|
|
403
|
+
scenarioId,
|
|
404
|
+
runId,
|
|
405
|
+
startedAt: runStartedAt,
|
|
406
|
+
status: "failed",
|
|
407
|
+
steps: stepsReport,
|
|
408
|
+
gitSummary,
|
|
409
|
+
});
|
|
410
|
+
await atomicWriteFile(path.join(runDir, "meta.json"), `${JSON.stringify({
|
|
411
|
+
recipe: recipeId,
|
|
412
|
+
scenario: scenarioId,
|
|
413
|
+
run_id: runId,
|
|
414
|
+
steps: stepsMeta,
|
|
415
|
+
}, null, 2)}\n`, "utf8");
|
|
416
|
+
throw new CliError({
|
|
417
|
+
exitCode: result.exitCode,
|
|
418
|
+
code: "E_INTERNAL",
|
|
419
|
+
message: `Scenario step failed: ${step.tool}`,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const gitSummary = await getGitDiffSummary(resolved.gitRoot);
|
|
424
|
+
await writeScenarioReport({
|
|
425
|
+
runDir,
|
|
426
|
+
recipeId,
|
|
427
|
+
scenarioId,
|
|
428
|
+
runId,
|
|
429
|
+
startedAt: runStartedAt,
|
|
430
|
+
status: "success",
|
|
431
|
+
steps: stepsReport,
|
|
432
|
+
gitSummary,
|
|
433
|
+
});
|
|
434
|
+
await atomicWriteFile(path.join(runDir, "meta.json"), `${JSON.stringify({
|
|
435
|
+
recipe: recipeId,
|
|
436
|
+
scenario: scenarioId,
|
|
437
|
+
run_id: runId,
|
|
438
|
+
steps: stepsMeta,
|
|
439
|
+
}, null, 2)}\n`, "utf8");
|
|
440
|
+
process.stdout.write(`Run artifacts: ${path.relative(resolved.gitRoot, runDir)}\n`);
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
if (err instanceof CliError)
|
|
445
|
+
throw err;
|
|
446
|
+
throw mapCoreError(err, { command: "scenario run", root: opts.rootOverride ?? null });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
export async function cmdScenario(opts) {
|
|
450
|
+
const sub = opts.command;
|
|
451
|
+
if (!sub) {
|
|
452
|
+
throw new CliError({
|
|
453
|
+
exitCode: 2,
|
|
454
|
+
code: "E_USAGE",
|
|
455
|
+
message: usageMessage(SCENARIO_USAGE, SCENARIO_USAGE_EXAMPLE),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (sub === "list") {
|
|
459
|
+
if (opts.args.length > 0) {
|
|
460
|
+
throw new CliError({
|
|
461
|
+
exitCode: 2,
|
|
462
|
+
code: "E_USAGE",
|
|
463
|
+
message: usageMessage(SCENARIO_USAGE, SCENARIO_USAGE_EXAMPLE),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return await cmdScenarioList({ cwd: opts.cwd, rootOverride: opts.rootOverride });
|
|
467
|
+
}
|
|
468
|
+
if (sub === "info") {
|
|
469
|
+
if (opts.args.length !== 1) {
|
|
470
|
+
throw new CliError({
|
|
471
|
+
exitCode: 2,
|
|
472
|
+
code: "E_USAGE",
|
|
473
|
+
message: usageMessage(SCENARIO_INFO_USAGE, SCENARIO_INFO_USAGE_EXAMPLE),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return await cmdScenarioInfo({
|
|
477
|
+
cwd: opts.cwd,
|
|
478
|
+
rootOverride: opts.rootOverride,
|
|
479
|
+
id: opts.args[0],
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (sub === "run") {
|
|
483
|
+
if (opts.args.length !== 1) {
|
|
484
|
+
throw new CliError({
|
|
485
|
+
exitCode: 2,
|
|
486
|
+
code: "E_USAGE",
|
|
487
|
+
message: usageMessage(SCENARIO_RUN_USAGE, SCENARIO_RUN_USAGE_EXAMPLE),
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return await cmdScenarioRun({
|
|
491
|
+
cwd: opts.cwd,
|
|
492
|
+
rootOverride: opts.rootOverride,
|
|
493
|
+
id: opts.args[0],
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
throw new CliError({
|
|
497
|
+
exitCode: 2,
|
|
498
|
+
code: "E_USAGE",
|
|
499
|
+
message: usageMessage(SCENARIO_USAGE, SCENARIO_USAGE_EXAMPLE),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
import { loadTaskBackend, type TaskData } from "../../backends/task-backend.js";
|
|
2
|
+
export type CommandContext = {
|
|
3
|
+
backend: Awaited<ReturnType<typeof loadTaskBackend>>["backend"];
|
|
4
|
+
backendId: string;
|
|
5
|
+
backendConfigPath: string;
|
|
6
|
+
resolved: Awaited<ReturnType<typeof loadTaskBackend>>["resolved"];
|
|
7
|
+
config: Awaited<ReturnType<typeof loadTaskBackend>>["config"];
|
|
8
|
+
};
|
|
2
9
|
export declare function resolveDocUpdatedBy(task: TaskData, author?: string): string;
|
|
3
10
|
export declare function taskDataToFrontmatter(task: TaskData): Record<string, unknown>;
|
|
11
|
+
export declare function loadCommandContext(opts: {
|
|
12
|
+
cwd: string;
|
|
13
|
+
rootOverride?: string | null;
|
|
14
|
+
}): Promise<CommandContext>;
|
|
15
|
+
export declare function loadTaskFromContext(opts: {
|
|
16
|
+
ctx: CommandContext;
|
|
17
|
+
taskId: string;
|
|
18
|
+
}): Promise<TaskData>;
|
|
4
19
|
export declare function loadBackendTask(opts: {
|
|
5
20
|
cwd: string;
|
|
6
21
|
rootOverride?: string | null;
|
|
7
22
|
taskId: string;
|
|
8
23
|
}): Promise<{
|
|
9
|
-
backend:
|
|
24
|
+
backend: CommandContext["backend"];
|
|
10
25
|
backendId: string;
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
backendConfigPath: string;
|
|
27
|
+
resolved: CommandContext["resolved"];
|
|
28
|
+
config: CommandContext["config"];
|
|
13
29
|
task: TaskData;
|
|
14
30
|
}>;
|
|
15
31
|
//# sourceMappingURL=task-backend.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-backend.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/task-backend.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,KAAK,QAAQ,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"task-backend.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/task-backend.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,KAAK,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAEhF,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IAClE,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;CAC/D,CAAC;AASF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ3E;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAyB7E;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,GAAG,OAAO,CAAC,cAAc,CAAC,CAM1B;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,GAAG,EAAE,cAAc,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAYpB;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC;IACV,OAAO,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACrC,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;IACjC,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC,CAID"}
|
|
@@ -42,14 +42,17 @@ export function taskDataToFrontmatter(task) {
|
|
|
42
42
|
description: task.description ?? "",
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
export async function
|
|
46
|
-
const { backend, backendId, resolved, config } = await loadTaskBackend({
|
|
45
|
+
export async function loadCommandContext(opts) {
|
|
46
|
+
const { backend, backendId, backendConfigPath, resolved, config } = await loadTaskBackend({
|
|
47
47
|
cwd: opts.cwd,
|
|
48
48
|
rootOverride: opts.rootOverride ?? null,
|
|
49
49
|
});
|
|
50
|
-
|
|
50
|
+
return { backend, backendId, backendConfigPath, resolved, config };
|
|
51
|
+
}
|
|
52
|
+
export async function loadTaskFromContext(opts) {
|
|
53
|
+
const task = await opts.ctx.backend.getTask(opts.taskId);
|
|
51
54
|
if (!task) {
|
|
52
|
-
const tasksDir = path.join(resolved.gitRoot, config.paths.workflow_dir);
|
|
55
|
+
const tasksDir = path.join(opts.ctx.resolved.gitRoot, opts.ctx.config.paths.workflow_dir);
|
|
53
56
|
const readmePath = path.join(tasksDir, opts.taskId, "README.md");
|
|
54
57
|
throw new CliError({
|
|
55
58
|
exitCode: 4,
|
|
@@ -57,5 +60,10 @@ export async function loadBackendTask(opts) {
|
|
|
57
60
|
message: `ENOENT: no such file or directory, open '${readmePath}'`,
|
|
58
61
|
});
|
|
59
62
|
}
|
|
60
|
-
return
|
|
63
|
+
return task;
|
|
64
|
+
}
|
|
65
|
+
export async function loadBackendTask(opts) {
|
|
66
|
+
const ctx = await loadCommandContext({ cwd: opts.cwd, rootOverride: opts.rootOverride ?? null });
|
|
67
|
+
const task = await loadTaskFromContext({ ctx, taskId: opts.taskId });
|
|
68
|
+
return { ...ctx, task };
|
|
61
69
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block.d.ts","sourceRoot":"","sources":["../../../src/commands/task/block.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"block.d.ts","sourceRoot":"","sources":["../../../src/commands/task/block.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,WAAW,0EAA0E,CAAC;AACnG,eAAO,MAAM,mBAAmB,gFAC6C,CAAC;AAE9E,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CA4FlB"}
|
|
@@ -1,35 +1,29 @@
|
|
|
1
|
-
import { loadConfig, resolveProject } from "@agentplaneorg/core";
|
|
2
1
|
import { mapBackendError } from "../../cli/error-map.js";
|
|
3
2
|
import { successMessage } from "../../cli/output.js";
|
|
4
3
|
import { formatCommentBodyForCommit } from "../../shared/comment-format.js";
|
|
5
4
|
import { CliError } from "../../shared/errors.js";
|
|
6
5
|
import { commitFromComment } from "../guard/index.js";
|
|
7
|
-
import {
|
|
8
|
-
import { defaultCommitEmojiForStatus, enforceStatusCommitPolicy, isTransitionAllowed, nowIso, requireStructuredComment, } from "./shared.js";
|
|
6
|
+
import { loadCommandContext, loadTaskFromContext } from "../shared/task-backend.js";
|
|
7
|
+
import { appendTaskEvent, defaultCommitEmojiForStatus, enforceStatusCommitPolicy, isTransitionAllowed, nowIso, requireStructuredComment, } from "./shared.js";
|
|
9
8
|
export const BLOCK_USAGE = "Usage: agentplane block <task-id> --author <id> --body <text> [flags]";
|
|
10
9
|
export const BLOCK_USAGE_EXAMPLE = 'agentplane block 202602030608-F1Q8AB --author CODER --body "Blocked: ..."';
|
|
11
10
|
export async function cmdBlock(opts) {
|
|
12
11
|
try {
|
|
13
|
-
const
|
|
12
|
+
const ctx = await loadCommandContext({
|
|
14
13
|
cwd: opts.cwd,
|
|
15
14
|
rootOverride: opts.rootOverride ?? null,
|
|
16
15
|
});
|
|
17
|
-
const loaded = await loadConfig(resolved.agentplaneDir);
|
|
18
16
|
if (opts.commitFromComment) {
|
|
19
17
|
enforceStatusCommitPolicy({
|
|
20
|
-
policy:
|
|
18
|
+
policy: ctx.config.status_commit_policy,
|
|
21
19
|
action: "block",
|
|
22
20
|
confirmed: opts.confirmStatusCommit,
|
|
23
21
|
quiet: opts.quiet,
|
|
24
22
|
});
|
|
25
23
|
}
|
|
26
|
-
const { prefix, min_chars: minChars } =
|
|
24
|
+
const { prefix, min_chars: minChars } = ctx.config.tasks.comments.blocked;
|
|
27
25
|
requireStructuredComment(opts.body, prefix, minChars);
|
|
28
|
-
const
|
|
29
|
-
cwd: opts.cwd,
|
|
30
|
-
rootOverride: opts.rootOverride,
|
|
31
|
-
taskId: opts.taskId,
|
|
32
|
-
});
|
|
26
|
+
const task = await loadTaskFromContext({ ctx, taskId: opts.taskId });
|
|
33
27
|
const currentStatus = String(task.status || "TODO").toUpperCase();
|
|
34
28
|
if (!opts.force && !isTransitionAllowed(currentStatus, "BLOCKED")) {
|
|
35
29
|
throw new CliError({
|
|
@@ -39,28 +33,40 @@ export async function cmdBlock(opts) {
|
|
|
39
33
|
});
|
|
40
34
|
}
|
|
41
35
|
const formattedComment = opts.commitFromComment
|
|
42
|
-
? formatCommentBodyForCommit(opts.body,
|
|
36
|
+
? formatCommentBodyForCommit(opts.body, ctx.config)
|
|
43
37
|
: null;
|
|
44
38
|
const commentBody = formattedComment ?? opts.body;
|
|
45
39
|
const existingComments = Array.isArray(task.comments)
|
|
46
40
|
? task.comments.filter((item) => !!item && typeof item.author === "string" && typeof item.body === "string")
|
|
47
41
|
: [];
|
|
48
42
|
const commentsValue = [...existingComments, { author: opts.author, body: commentBody }];
|
|
43
|
+
const at = nowIso();
|
|
49
44
|
const nextTask = {
|
|
50
45
|
...task,
|
|
51
46
|
status: "BLOCKED",
|
|
52
47
|
comments: commentsValue,
|
|
48
|
+
events: appendTaskEvent(task, {
|
|
49
|
+
type: "status",
|
|
50
|
+
at,
|
|
51
|
+
author: opts.author,
|
|
52
|
+
from: currentStatus,
|
|
53
|
+
to: "BLOCKED",
|
|
54
|
+
note: commentBody,
|
|
55
|
+
}),
|
|
53
56
|
doc_version: 2,
|
|
54
|
-
doc_updated_at:
|
|
57
|
+
doc_updated_at: at,
|
|
55
58
|
doc_updated_by: opts.author,
|
|
56
59
|
};
|
|
57
|
-
await backend.writeTask(nextTask);
|
|
60
|
+
await ctx.backend.writeTask(nextTask);
|
|
58
61
|
let commitInfo = null;
|
|
59
62
|
if (opts.commitFromComment) {
|
|
60
63
|
commitInfo = await commitFromComment({
|
|
61
64
|
cwd: opts.cwd,
|
|
62
65
|
rootOverride: opts.rootOverride,
|
|
63
66
|
taskId: opts.taskId,
|
|
67
|
+
author: opts.author,
|
|
68
|
+
statusFrom: currentStatus,
|
|
69
|
+
statusTo: "BLOCKED",
|
|
64
70
|
commentBody: opts.body,
|
|
65
71
|
formattedComment,
|
|
66
72
|
emoji: opts.commitEmoji ?? defaultCommitEmojiForStatus("BLOCKED"),
|
|
@@ -69,7 +75,7 @@ export async function cmdBlock(opts) {
|
|
|
69
75
|
allowTasks: opts.commitAllowTasks,
|
|
70
76
|
requireClean: opts.commitRequireClean,
|
|
71
77
|
quiet: opts.quiet,
|
|
72
|
-
config:
|
|
78
|
+
config: ctx.config,
|
|
73
79
|
});
|
|
74
80
|
}
|
|
75
81
|
if (!opts.quiet) {
|