@metagptx/deepflow-suite-cli 0.2.11-dev.20261020.1 → 0.3.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 +44 -50
- package/bin/deepflow-suite.js +4 -4
- package/bundle/deepflow-release.json +21 -46
- package/bundle/skills/deepflow-suite/SKILL.md +7 -1
- package/bundle/skills/df-agentctrl/SKILL.md +5 -1
- package/bundle/skills/df-agentrt/SKILL.md +15 -0
- package/bundle/skills/df-agentrt-repo/SKILL.md +4 -0
- package/bundle/skills/df-task/SKILL.md +4 -0
- package/bundle/skills/df-taskdriver/SKILL.md +5 -1
- package/bundle/skills/df-workbench/SKILL.md +10 -2
- package/manifest.json +12 -30
- package/package.json +8 -17
- package/runtime/df-agentctrl/commands.mjs +93 -0
- package/runtime/df-agentctrl/df-agentctrl.mjs +119 -0
- package/runtime/df-agentctrl/release.json +1 -0
- package/runtime/df-agentrt/agentrt.mjs +3 -0
- package/runtime/df-agentrt/df-agentrt.mjs +3 -0
- package/runtime/df-agentrt/release.json +1 -0
- package/runtime/df-agentrt/result.mjs +44 -0
- package/runtime/df-agentrt/runtime.mjs +358 -0
- package/runtime/df-agentrt/usage.mjs +16 -0
- package/runtime/df-task/commands.mjs +188 -0
- package/runtime/df-task/df-task.mjs +167 -0
- package/runtime/df-task/protocol.mjs +3 -0
- package/runtime/df-task/read.mjs +127 -0
- package/runtime/df-task/release.json +1 -0
- package/runtime/df-taskdriver/df-taskdriver.mjs +131 -0
- package/runtime/df-taskdriver/release.json +1 -0
- package/runtime/{df-workbench.mjs → df-workbench/df-workbench.mjs} +11 -2
- package/runtime/df-workbench/release.json +1 -0
- package/scripts/build-package.mjs +29 -25
- package/src/bundle.mjs +4 -1
- package/src/cli-install.mjs +35 -125
- package/src/commands/doctor.mjs +2 -0
- package/src/linker.mjs +2 -2
- package/src/manifest.mjs +7 -13
- package/src/package-cli.mjs +44 -60
- package/bin/df-agentctrl.js +0 -10
- package/bin/df-agentrt.js +0 -10
- package/bin/df-task.js +0 -10
- package/bin/df-taskdriver.js +0 -10
- package/bin/df-workbench.js +0 -10
- package/bundle/skills/deepflow-suite/manifest.yaml +0 -20
- package/bundle/skills/df-task/skill_test.go +0 -201
- package/bundle/skills/df-workbench/manifest.yaml +0 -5
- package/scripts/build-runtime-packages.mjs +0 -38
- package/skills/deepflow-suite/SKILL.md +0 -81
- package/skills/deepflow-suite/manifest.yaml +0 -20
- package/src/cli-launcher.mjs +0 -71
- package/src/runtime-build.mjs +0 -193
- package/src/runtime-package.mjs +0 -31
package/src/cli-install.mjs
CHANGED
|
@@ -1,140 +1,50 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import path from "node:path";
|
|
1
|
+
import { access, lstat, mkdir, mkdtemp, readFile, realpath, rename, rm, symlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
child.once("error", reject);
|
|
10
|
-
child.once("exit", (code, signal) => {
|
|
11
|
-
if (code === 0) resolve();
|
|
12
|
-
else reject(new Error(`${command} ${args.join(" ")} exited with ${code ?? signal}`));
|
|
13
|
-
});
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function replaceLink(source, target) {
|
|
18
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
5
|
+
const destinationFor = (binDir) => binDir ? path.resolve(binDir) : path.join(os.homedir(), '.local', 'bin');
|
|
6
|
+
async function inspect(target) { try { return await lstat(target); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } }
|
|
7
|
+
async function matches(target, source) {
|
|
19
8
|
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
const current = path.resolve(path.dirname(target), await readlink(target));
|
|
25
|
-
if (current === source) return "unchanged";
|
|
26
|
-
await unlink(target);
|
|
27
|
-
} catch (error) {
|
|
28
|
-
if (error.code !== "ENOENT") throw error;
|
|
29
|
-
}
|
|
30
|
-
await symlink(path.relative(path.dirname(target), source), target);
|
|
31
|
-
return "created";
|
|
9
|
+
if (process.platform === 'win32') return (await readFile(target, 'utf8')).includes(source);
|
|
10
|
+
return await realpath(target) === await realpath(source);
|
|
11
|
+
} catch { return false; }
|
|
32
12
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
13
|
+
export async function installWorkspaceClis({ workspaceRoot, manifest, binDir, force = false }) {
|
|
14
|
+
const destination = destinationFor(binDir), unchanged = [], targets = [];
|
|
15
|
+
for (const cli of manifest.clis) {
|
|
16
|
+
const source = path.resolve(workspaceRoot, cli.project, cli.entry);
|
|
36
17
|
await access(source);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
18
|
+
const target = path.join(destination, cli.name + (process.platform === 'win32' ? '.cmd' : ''));
|
|
19
|
+
if (await matches(target, source)) { unchanged.push(cli.name); continue; }
|
|
20
|
+
const stats = await inspect(target);
|
|
21
|
+
if (stats?.isDirectory() || (stats && !force)) throw Error(`refusing to replace existing CLI without --force: ${target}`);
|
|
22
|
+
targets.push({ name: cli.name, source, target });
|
|
40
23
|
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function installWorkspaceClis({ workspaceRoot, manifest, binDir, force = false }) {
|
|
44
|
-
const destination = binDir
|
|
45
|
-
? path.resolve(binDir)
|
|
46
|
-
: path.join(process.env.HOME ?? os.homedir(), ".local", "bin");
|
|
47
|
-
const result = { linked: [], built: [] };
|
|
48
|
-
|
|
49
24
|
await mkdir(destination, { recursive: true });
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
const stats = await lstat(target);
|
|
54
|
-
if (!force) {
|
|
55
|
-
throw new Error(`refusing to replace existing CLI without --force: ${target}`);
|
|
56
|
-
}
|
|
57
|
-
if (stats.isDirectory()) throw new Error(`refusing to replace CLI directory: ${target}`);
|
|
58
|
-
} catch (error) {
|
|
59
|
-
if (error.code !== "ENOENT") throw error;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const backup = await mkdtemp(path.join(destination, ".deepflow-cli-backup-"));
|
|
64
|
-
const backedUp = [];
|
|
65
|
-
const touched = [];
|
|
25
|
+
const backup = await mkdtemp(path.join(destination, '.deepflow-cli-backup-'));
|
|
26
|
+
const backedUp = [], installed = [];
|
|
66
27
|
try {
|
|
67
|
-
for (const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
} catch (error) {
|
|
73
|
-
if (error.code !== "ENOENT") throw error;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
for (const cli of manifest.clis) {
|
|
78
|
-
const projectRoot = path.join(workspaceRoot, cli.project);
|
|
79
|
-
const target = path.join(destination, cli.name);
|
|
80
|
-
|
|
81
|
-
if (cli.name === "df-workbench") {
|
|
82
|
-
const source = path.join(projectRoot, "cli", "df-workbench.mjs");
|
|
83
|
-
if (!(await sourceExists(source))) throw new Error(`missing CLI source: ${source}`);
|
|
84
|
-
await replaceLink(source, target);
|
|
85
|
-
touched.push(cli.name);
|
|
86
|
-
result.linked.push(cli.name);
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (cli.name === "df-agentrt") {
|
|
91
|
-
await run("make", ["build"], { cwd: projectRoot, env: process.env });
|
|
92
|
-
const source = path.join(projectRoot, "deploy", "bin", "df-agentrt");
|
|
93
|
-
if (!(await sourceExists(source))) throw new Error(`missing built CLI: ${source}`);
|
|
94
|
-
await replaceLink(source, target);
|
|
95
|
-
touched.push(cli.name);
|
|
96
|
-
result.built.push(cli.name);
|
|
97
|
-
continue;
|
|
28
|
+
for (const entry of targets) {
|
|
29
|
+
if (await inspect(entry.target)) { await rename(entry.target, path.join(backup, entry.name)); backedUp.push(entry); }
|
|
30
|
+
if (process.platform === 'win32') await writeFile(entry.target, `@ECHO off\r\n"${process.execPath}" "${entry.source}" %*\r\n`);
|
|
31
|
+
else await symlink(path.relative(destination, entry.source), entry.target);
|
|
32
|
+
installed.push(entry);
|
|
98
33
|
}
|
|
99
|
-
|
|
100
|
-
await run("make", ["install-cli", `BIN_DIR=${destination}`], {
|
|
101
|
-
cwd: projectRoot,
|
|
102
|
-
env: process.env,
|
|
103
|
-
});
|
|
104
|
-
touched.push(cli.name);
|
|
105
|
-
result.built.push(cli.name);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
await rm(backup, { recursive: true, force: true });
|
|
109
|
-
return { binDir: destination, ...result };
|
|
34
|
+
return { binDir: destination, linked: installed.map((entry) => entry.name), unchanged };
|
|
110
35
|
} catch (error) {
|
|
111
|
-
for (const
|
|
112
|
-
for (const
|
|
36
|
+
for (const entry of installed) await rm(entry.target, { force: true });
|
|
37
|
+
for (const entry of backedUp) await rename(path.join(backup, entry.name), entry.target);
|
|
113
38
|
throw error;
|
|
114
|
-
} finally {
|
|
115
|
-
await rm(backup, { recursive: true, force: true });
|
|
116
|
-
}
|
|
39
|
+
} finally { await rm(backup, { recursive: true, force: true }); }
|
|
117
40
|
}
|
|
118
|
-
|
|
119
41
|
export async function verifyWorkspaceClis({ workspaceRoot, manifest, binDir }) {
|
|
120
|
-
const
|
|
121
|
-
? path.resolve(binDir)
|
|
122
|
-
: path.join(process.env.HOME ?? os.homedir(), ".local", "bin");
|
|
123
|
-
const missing = [];
|
|
124
|
-
const invalid = [];
|
|
42
|
+
const bin = destinationFor(binDir), missing = [], invalid = [];
|
|
125
43
|
for (const cli of manifest.clis) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (stats.isDirectory()) invalid.push({ name: cli.name, reason: "target is a directory" });
|
|
131
|
-
} catch (error) {
|
|
132
|
-
if (error.code === "ENOENT") {
|
|
133
|
-
missing.push(cli.name);
|
|
134
|
-
} else {
|
|
135
|
-
invalid.push({ name: cli.name, reason: error.message });
|
|
136
|
-
}
|
|
137
|
-
}
|
|
44
|
+
const target = path.join(bin, cli.name + (process.platform === 'win32' ? '.cmd' : ''));
|
|
45
|
+
const source = path.resolve(workspaceRoot, cli.project, cli.entry);
|
|
46
|
+
if (!await inspect(target)) missing.push(cli.name);
|
|
47
|
+
else if (!await matches(target, source)) invalid.push({ name: cli.name, reason: 'CLI points to another installation', expected: source, actual: target });
|
|
138
48
|
}
|
|
139
|
-
return { binDir:
|
|
49
|
+
return { binDir: bin, missing, invalid };
|
|
140
50
|
}
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -25,6 +25,8 @@ export async function doctor(options) {
|
|
|
25
25
|
const clis = await verifyPackageClis({
|
|
26
26
|
packageRoot: path.dirname(options.manifestPath),
|
|
27
27
|
expectedVersion: manifest.version,
|
|
28
|
+
checkPath: true,
|
|
29
|
+
env: options.env ?? process.env,
|
|
28
30
|
});
|
|
29
31
|
return {
|
|
30
32
|
ok: clis.ok,
|
package/src/linker.mjs
CHANGED
|
@@ -24,7 +24,7 @@ async function inspectTarget(targetPath) {
|
|
|
24
24
|
|
|
25
25
|
export async function installWorkspaceSkills({ workspaceRoot, targetDir, force = false, manifestPath }) {
|
|
26
26
|
const manifest = await loadManifest(manifestPath);
|
|
27
|
-
const skills = resolveSkillSources(manifest, workspaceRoot
|
|
27
|
+
const skills = resolveSkillSources(manifest, workspaceRoot);
|
|
28
28
|
const destination = targetDir ?? path.join(workspaceRoot, ".agents", "skills");
|
|
29
29
|
|
|
30
30
|
for (const skill of skills) {
|
|
@@ -68,7 +68,7 @@ export async function installWorkspaceSkills({ workspaceRoot, targetDir, force =
|
|
|
68
68
|
|
|
69
69
|
export async function verifyWorkspaceSkills({ workspaceRoot, targetDir, manifestPath }) {
|
|
70
70
|
const manifest = await loadManifest(manifestPath);
|
|
71
|
-
const skills = resolveSkillSources(manifest, workspaceRoot
|
|
71
|
+
const skills = resolveSkillSources(manifest, workspaceRoot);
|
|
72
72
|
const destination = targetDir ?? path.join(workspaceRoot, ".agents", "skills");
|
|
73
73
|
const missing = [];
|
|
74
74
|
const invalid = [];
|
package/src/manifest.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
-
import { CLI_NAMES, DISTRIBUTION,
|
|
5
|
+
import { CLI_NAMES, DISTRIBUTION, SKILL_NAMES } from "./constants.mjs";
|
|
6
6
|
|
|
7
7
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
8
|
|
|
@@ -10,6 +10,7 @@ export const defaultManifestPath = path.join(packageRoot, "manifest.json");
|
|
|
10
10
|
|
|
11
11
|
export async function loadManifest(manifestPath = defaultManifestPath) {
|
|
12
12
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
13
|
+
if (!manifest.version) manifest.version = JSON.parse(await readFile(path.join(path.dirname(manifestPath), 'package.json'), 'utf8')).version;
|
|
13
14
|
validateManifest(manifest);
|
|
14
15
|
return manifest;
|
|
15
16
|
}
|
|
@@ -50,23 +51,16 @@ export function validateManifest(manifest) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
for (const cli of manifest.clis) {
|
|
53
|
-
if (!cli || typeof cli.name !== "string" || typeof cli.project !== "string" || typeof cli.
|
|
54
|
-
throw new Error("DeepFlow manifest CLIs must define name, project, and
|
|
54
|
+
if (!cli || typeof cli.name !== "string" || typeof cli.project !== "string" || typeof cli.entry !== "string") {
|
|
55
|
+
throw new Error("DeepFlow manifest CLIs must define name, project, and entry");
|
|
55
56
|
}
|
|
57
|
+
if (path.isAbsolute(cli.entry) || cli.entry.split(/[\\/]/).includes('..') || !cli.entry.endsWith('.mjs')) throw new Error(`invalid Node CLI entry: ${cli.name}`);
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
export function resolveSkillSources(manifest, workspaceRoot
|
|
61
|
+
export function resolveSkillSources(manifest, workspaceRoot) {
|
|
60
62
|
return manifest.skills.map((skill) => ({
|
|
61
63
|
...skill,
|
|
62
|
-
sourcePath: path.resolve(workspaceRoot,
|
|
64
|
+
sourcePath: path.resolve(workspaceRoot, skill.source),
|
|
63
65
|
}));
|
|
64
66
|
}
|
|
65
|
-
|
|
66
|
-
export function packageManifest() {
|
|
67
|
-
return {
|
|
68
|
-
distribution: DISTRIBUTION,
|
|
69
|
-
package: PACKAGE_NAME,
|
|
70
|
-
cli: "deepflow-suite",
|
|
71
|
-
};
|
|
72
|
-
}
|
package/src/package-cli.mjs
CHANGED
|
@@ -1,66 +1,50 @@
|
|
|
1
|
-
import { access,
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const PACKAGE_BINARIES = [
|
|
9
|
-
|
|
10
|
-
async function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (!
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import { access, readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { CLI_NAMES } from './constants.mjs';
|
|
7
|
+
|
|
8
|
+
export const PACKAGE_BINARIES = ['deepflow-suite', ...CLI_NAMES];
|
|
9
|
+
const execute = promisify(execFile);
|
|
10
|
+
export async function commandOnPath(name, env = process.env) {
|
|
11
|
+
const extensions = process.platform === 'win32' ? ['', '.cmd', '.exe', '.bat'] : [''];
|
|
12
|
+
for (const dir of (env.PATH || '').split(path.delimiter)) {
|
|
13
|
+
if (!dir) continue;
|
|
14
|
+
for (const extension of extensions) {
|
|
15
|
+
const candidate = path.join(dir, name + extension);
|
|
16
|
+
try { await access(candidate, process.platform === 'win32' ? constants.F_OK : constants.X_OK); return candidate; } catch {}
|
|
17
|
+
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
expectedVersion,
|
|
24
|
-
platform = process.platform,
|
|
25
|
-
arch = process.arch,
|
|
26
|
-
}) {
|
|
27
|
-
const missing = [];
|
|
28
|
-
const invalid = [];
|
|
29
|
-
const binaries = [];
|
|
20
|
+
export async function verifyPackageClis({ packageRoot, expectedVersion, checkPath = false, env = process.env }) {
|
|
21
|
+
const packageJson = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
22
|
+
const missing = [], invalid = [], binaries = [];
|
|
30
23
|
for (const name of PACKAGE_BINARIES) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const packageName = runtimePackageForPlatform(platform, arch);
|
|
37
|
-
const platformInfo = { platform, arch, package: packageName };
|
|
38
|
-
if (!packageName) {
|
|
39
|
-
invalid.push({ name: "platform", reason: `unsupported DeepFlow CLI platform: ${platform}/${arch}` });
|
|
40
|
-
} else {
|
|
24
|
+
const relative = packageJson.bin?.[name];
|
|
25
|
+
const entry = relative && path.resolve(packageRoot, relative);
|
|
26
|
+
if (!entry || !entry.startsWith(path.resolve(packageRoot) + path.sep)) { missing.push(name); continue; }
|
|
41
27
|
try {
|
|
42
|
-
|
|
43
|
-
await
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
28
|
+
await access(entry);
|
|
29
|
+
const { stdout } = await execute(process.execPath, [entry, '--cli-version', '--json'], { env, timeout: 10000 });
|
|
30
|
+
const version = JSON.parse(stdout);
|
|
31
|
+
if (version.name !== name || version.version !== expectedVersion) throw Error(`CLI version ${version.version} does not match ${expectedVersion}`);
|
|
32
|
+
await execute(process.execPath, [entry, '--help'], { env, timeout: 10000 });
|
|
33
|
+
const binary = { name, path: entry, version: version.version };
|
|
34
|
+
if (checkPath) {
|
|
35
|
+
binary.resolvedPath = await commandOnPath(name, env);
|
|
36
|
+
if (!binary.resolvedPath) missing.push(name);
|
|
37
|
+
else {
|
|
38
|
+
const canonical = await realpath(binary.resolvedPath);
|
|
39
|
+
const matches = process.platform === 'win32' && /\.(cmd|bat)$/i.test(canonical)
|
|
40
|
+
? (await readFile(canonical, 'utf8')).replaceAll('\\', '/').includes(relative.replaceAll('\\', '/'))
|
|
41
|
+
&& [path.resolve(path.dirname(canonical), 'node_modules', packageJson.name), path.resolve(path.dirname(canonical), '..', packageJson.name)].includes(await realpath(packageRoot))
|
|
42
|
+
: canonical === await realpath(entry);
|
|
43
|
+
if (!matches) invalid.push({ name, reason: 'another installation shadows this Suite on PATH', expected: entry, actual: binary.resolvedPath });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
binaries.push(binary);
|
|
47
|
+
} catch (error) { invalid.push({ name, reason: error.message }); }
|
|
51
48
|
}
|
|
52
|
-
|
|
53
|
-
const workbenchEntry = path.join(packageRoot, "runtime", "df-workbench.mjs");
|
|
54
|
-
if (!await regularFile(workbenchEntry)) invalid.push({ name: "df-workbench", reason: "packaged Workbench CLI entry is missing" });
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
ok: missing.length === 0 && invalid.length === 0,
|
|
58
|
-
platform: platformInfo,
|
|
59
|
-
package: packageRoot,
|
|
60
|
-
binaries,
|
|
61
|
-
missing,
|
|
62
|
-
invalid,
|
|
63
|
-
};
|
|
49
|
+
return { ok: !missing.length && !invalid.length, package: packageRoot, version: expectedVersion, binaries, missing, invalid };
|
|
64
50
|
}
|
|
65
|
-
|
|
66
|
-
export { PACKAGE_BINARIES };
|
package/bin/df-agentctrl.js
DELETED
package/bin/df-agentrt.js
DELETED
package/bin/df-task.js
DELETED
package/bin/df-taskdriver.js
DELETED
package/bin/df-workbench.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
id: deepflow-suite
|
|
2
|
-
version: 0.2.9
|
|
3
|
-
kind: composite-skill
|
|
4
|
-
requires:
|
|
5
|
-
skills:
|
|
6
|
-
- df-workbench
|
|
7
|
-
- df-task
|
|
8
|
-
- df-taskdriver
|
|
9
|
-
- df-agentctrl
|
|
10
|
-
- df-agentrt
|
|
11
|
-
- df-agentrt-repo
|
|
12
|
-
cli:
|
|
13
|
-
- df-workbench
|
|
14
|
-
- df-task
|
|
15
|
-
- df-taskdriver
|
|
16
|
-
- df-agentctrl
|
|
17
|
-
- df-agentrt
|
|
18
|
-
resolution:
|
|
19
|
-
managed_by: deepflow-suite
|
|
20
|
-
install_missing_dependencies: false
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
package skill
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"encoding/json"
|
|
5
|
-
"os"
|
|
6
|
-
"path/filepath"
|
|
7
|
-
"strings"
|
|
8
|
-
"testing"
|
|
9
|
-
|
|
10
|
-
"github.com/deepflow/deepflow-task/internal/authz"
|
|
11
|
-
"github.com/deepflow/deepflow-task/internal/model"
|
|
12
|
-
"github.com/deepflow/deepflow-task/internal/validate"
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
func readFile(t *testing.T, path string) string {
|
|
16
|
-
t.Helper()
|
|
17
|
-
data, err := os.ReadFile(path)
|
|
18
|
-
if err != nil {
|
|
19
|
-
t.Fatal(err)
|
|
20
|
-
}
|
|
21
|
-
return string(data)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
func TestSkillHasFrontmatter(t *testing.T) {
|
|
25
|
-
content := readFile(t, "SKILL.md")
|
|
26
|
-
if !strings.HasPrefix(content, "---\n") {
|
|
27
|
-
t.Fatal("SKILL.md must start with YAML frontmatter")
|
|
28
|
-
}
|
|
29
|
-
frontmatter, _, found := strings.Cut(strings.TrimPrefix(content, "---\n"), "\n---")
|
|
30
|
-
if !found {
|
|
31
|
-
t.Fatal("frontmatter is not closed")
|
|
32
|
-
}
|
|
33
|
-
for _, key := range []string{"name:", "description:"} {
|
|
34
|
-
if !strings.Contains(frontmatter, key) {
|
|
35
|
-
t.Errorf("frontmatter is missing %s", key)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if !strings.Contains(frontmatter, "name: df-task") {
|
|
39
|
-
t.Errorf("frontmatter name must be df-task, got %q", frontmatter)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
func TestSkillReferenceDocumentsEveryFactType(t *testing.T) {
|
|
44
|
-
// **十六种全要,不是只要智能体写得了的那九种。** 这份 reference 的作用是让智能体
|
|
45
|
-
// 读日志时认得每一条事实 —— 只遍历两个 Run 身份的可写集合,把「## 外部输入」
|
|
46
|
-
// 或「## 推进器写的」整张表删掉照样全绿,而 `external_result` 与 `invocation_ended`
|
|
47
|
-
// 是三条触发事实里的两条,认不出它们,读日志就读漏了最关键的两种。
|
|
48
|
-
reference := readFile(t, filepath.Join("references", "fact-types.md"))
|
|
49
|
-
for _, factType := range model.FactTypes() {
|
|
50
|
-
if !strings.Contains(reference, string(factType)) {
|
|
51
|
-
t.Errorf("references/fact-types.md 没有记 %s", factType)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
// 「一共十六种」这个数字也要钉住。不钉的话,将来加第十六种类型时 Task 10 的
|
|
55
|
-
// `len(payloadSchemas) != len(model.FactTypes())` 会红,而这份文档会悄悄过期。
|
|
56
|
-
// **两个条件要各给自己的报错。** 合成一句 `if A || B` 的话,「有人把文档里那句话
|
|
57
|
-
// 删了而类型数还是 15」这一路会红出「文档写的是十六种,而 FactTypes() 有 15 种」
|
|
58
|
-
// —— 一句不含任何矛盾的话,看的人不知道该改文档还是改代码。而这一路恰好是最
|
|
59
|
-
// 常见的那一路:改文档比加第十六种类型常见得多。
|
|
60
|
-
if len(model.FactTypes()) != 16 {
|
|
61
|
-
t.Errorf("model.FactTypes() 有 %d 种,而 fact-types.md 与本断言都按十六种写的 —— 加了新类型要同时改这两处", len(model.FactTypes()))
|
|
62
|
-
}
|
|
63
|
-
if !strings.Contains(reference, "一共十六种事实") {
|
|
64
|
-
t.Error("fact-types.md 里找不到「一共十六种事实」这句 —— 这个数字是文档过期的唯一机械信号,不要删")
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
func TestSkillCommandTablesMatchTheAuthorizationMatrix(t *testing.T) {
|
|
69
|
-
// 两个方向都要验。只验「不该出现的没出现」,那么往调度一节里插一行
|
|
70
|
-
// `--type run_outcome` 照样全绿,而任务 Run 读到它就会去写,然后拿一个 403;
|
|
71
|
-
// 只验「该出现的出现了」,那么删掉一整行命令也照样全绿,而 Agent 在 Run 里
|
|
72
|
-
// 只有这份文档可读 —— 少一行,那条事实它就永远不会写。
|
|
73
|
-
content := readFile(t, "SKILL.md")
|
|
74
|
-
sections := map[model.ActorKind]string{
|
|
75
|
-
model.ActorTaskRun: "## 任务 Run 可以写什么",
|
|
76
|
-
model.ActorSchedulerRun: "## 调度 Run 可以写什么",
|
|
77
|
-
}
|
|
78
|
-
for kind, heading := range sections {
|
|
79
|
-
// strings.Cut 返回三个值:before、after、found。这里要的是标题**之后**那段,
|
|
80
|
-
// 所以第一个返回值丢掉 —— 接成两个变量编译就过不去。
|
|
81
|
-
_, afterHeading, found := strings.Cut(content, heading)
|
|
82
|
-
if !found {
|
|
83
|
-
t.Fatalf("SKILL.md is missing the %q section", heading)
|
|
84
|
-
}
|
|
85
|
-
// 截到下一个 `## ` 为止。两节各自只谈自己那个身份,所以「职责边界」一节里
|
|
86
|
-
// 那句「写一条 summary 或 help_requested 说明情况」不会被算进调度一节。
|
|
87
|
-
section, _, _ := strings.Cut(afterHeading, "\n## ")
|
|
88
|
-
for _, factType := range model.FactTypes() {
|
|
89
|
-
// 正向只认表格那一列的形状 `| ` + 类型名 + `` ` ``。用裸的 Contains 会被
|
|
90
|
-
// 同一节的正文喂饱 —— 「`run_outcome` 只是你的主张」「读日志时你看到的是
|
|
91
|
-
// `dispatch_intent`」这些句子里都有类型名,于是删掉对应那行命令照样全绿,
|
|
92
|
-
// 而 Agent 在 Run 里只有这份文档可读,少一行那条事实它就永远不会写。
|
|
93
|
-
inTable := strings.Contains(section, "| `"+string(factType)+"`")
|
|
94
|
-
// 反向要宽:正文里出现一个写不了的类型,同样会把 Agent 带去拿 403。
|
|
95
|
-
mentioned := strings.Contains(section, string(factType))
|
|
96
|
-
switch {
|
|
97
|
-
case authz.CanWrite(kind, factType) && !inTable:
|
|
98
|
-
t.Errorf("%s 的命令表里没有 %s,而这个身份写得了它", heading, factType)
|
|
99
|
-
case !authz.CanWrite(kind, factType) && mentioned:
|
|
100
|
-
t.Errorf("%s 一节提了 %s,而这个身份写它会拿 403", heading, factType)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
// 上面那两节里没有推进器的身份,所以唤起生命周期靠这一条兜:整份文档里都不许
|
|
105
|
-
// 出现「让 Agent 用 append 写它」的样子。
|
|
106
|
-
for _, factType := range authz.WritableBy(model.ActorDriver) {
|
|
107
|
-
if strings.Contains(content, string(factType)+" --file") {
|
|
108
|
-
t.Errorf("SKILL.md shows an agent writing %s; only the driver writes the invocation lifecycle", factType)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
func TestSkillStatesTheEnvironmentContract(t *testing.T) {
|
|
114
|
-
content := readFile(t, "SKILL.md")
|
|
115
|
-
// DF_TASK_ID 和 DF_RUN_ID 也在这份清单里:SKILL.md 的示例命令用了它们,
|
|
116
|
-
// 而它们由 runtime 注入、不由本服务定义。示例里出现却没说明来源,
|
|
117
|
-
// Agent 就会以为要自己编一个。
|
|
118
|
-
for _, needle := range []string{"DF_TASK_BASE_URL", "DF_TASK_ACTOR_KIND", "DF_TASK_ACTOR_ID", "DF_TASK_ID", "DF_RUN_ID", "--json"} {
|
|
119
|
-
if !strings.Contains(content, needle) {
|
|
120
|
-
t.Errorf("SKILL.md does not mention %s", needle)
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 推进器拉起的 Run 里没有人可问。环境门禁若在 Run 内也要求询问,调度 Agent
|
|
126
|
-
// 每一轮都会停下不读事实,而推进器照常记下执行结束——多轮执行就此静默断掉。
|
|
127
|
-
func TestSkillResolvesTheRunEnvironmentWithoutAsking(t *testing.T) {
|
|
128
|
-
content := readFile(t, "SKILL.md")
|
|
129
|
-
for _, needle := range []string{"### Run 内的目标环境", "`DF_TASK_BASE_URL` 由部署注入", "不根据同名 Service DNS 猜成 dev", "不询问"} {
|
|
130
|
-
if !strings.Contains(content, needle) {
|
|
131
|
-
t.Errorf("SKILL.md does not state %q", needle)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
func TestSkillStatesFactsAreAppendOnly(t *testing.T) {
|
|
137
|
-
content := readFile(t, "SKILL.md")
|
|
138
|
-
if !strings.Contains(content, "只追加") {
|
|
139
|
-
t.Fatal("SKILL.md must state that facts are append-only")
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
func TestPayloadExamplesPassServerValidation(t *testing.T) {
|
|
144
|
-
entries, err := os.ReadDir(filepath.Join("assets", "payload-examples"))
|
|
145
|
-
if err != nil {
|
|
146
|
-
t.Fatal(err)
|
|
147
|
-
}
|
|
148
|
-
if len(entries) == 0 {
|
|
149
|
-
t.Fatal("there must be at least one payload example")
|
|
150
|
-
}
|
|
151
|
-
checked := 0
|
|
152
|
-
for _, entry := range entries {
|
|
153
|
-
// 只看 .json。这台机器上到处都是 .DS_Store,不过滤的话它会被当成
|
|
154
|
-
// 一个叫 ".DS_Store" 的事实类型,测试在一个与代码无关的原因上变红。
|
|
155
|
-
if !strings.HasSuffix(entry.Name(), ".json") {
|
|
156
|
-
continue
|
|
157
|
-
}
|
|
158
|
-
checked++
|
|
159
|
-
name := strings.TrimSuffix(entry.Name(), ".json")
|
|
160
|
-
factType := model.FactType(name)
|
|
161
|
-
if !model.KnownFactType(factType) {
|
|
162
|
-
t.Errorf("%s is not a fact type", entry.Name())
|
|
163
|
-
continue
|
|
164
|
-
}
|
|
165
|
-
raw := readFile(t, filepath.Join("assets", "payload-examples", entry.Name()))
|
|
166
|
-
if !json.Valid([]byte(raw)) {
|
|
167
|
-
t.Errorf("%s is not valid JSON", entry.Name())
|
|
168
|
-
continue
|
|
169
|
-
}
|
|
170
|
-
if err := validate.Payload(factType, []byte(raw)); err != nil {
|
|
171
|
-
t.Errorf("%s would be rejected by the server: %v", entry.Name(), err)
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
if checked == 0 {
|
|
175
|
-
t.Fatal("the directory has entries but none of them is a .json example")
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
func TestEveryHandWrittenPayloadHasAnExample(t *testing.T) {
|
|
180
|
-
// 凡是 SKILL.md 让智能体自己写 payload 的事实类型,都要有一份能抄的样例。
|
|
181
|
-
// 语法糖那三种(dispatch / wait / handoff)的字段由 CLI 填,不需要样例。
|
|
182
|
-
// **这里两个身份都要走。** 只走任务 Run 的话,调度 Run 那两种手写类型就露在外面:
|
|
183
|
-
// `adjudication` 的 `subject` 是全服务唯一一个数字字段,没样例的调度智能体会写成
|
|
184
|
-
// 字符串然后吃一个 422;而日志只追加,它既改不了那条,也没有别的地方能学对。
|
|
185
|
-
sugar := map[model.FactType]bool{
|
|
186
|
-
model.FactDispatchIntent: true,
|
|
187
|
-
model.FactWaitingReason: true,
|
|
188
|
-
model.FactHandoffToHuman: true,
|
|
189
|
-
}
|
|
190
|
-
for _, kind := range []model.ActorKind{model.ActorTaskRun, model.ActorSchedulerRun} {
|
|
191
|
-
for _, factType := range authz.WritableBy(kind) {
|
|
192
|
-
if sugar[factType] {
|
|
193
|
-
continue
|
|
194
|
-
}
|
|
195
|
-
path := filepath.Join("assets", "payload-examples", string(factType)+".json")
|
|
196
|
-
if _, err := os.Stat(path); err != nil {
|
|
197
|
-
t.Errorf("%s 缺样例,而 %s 要手写这份 payload:%v", factType, kind, err)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|