coding-agent-harness 1.0.6 → 1.0.8
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/CHANGELOG.md +23 -0
- package/CONTRIBUTING.md +1 -1
- package/dist/build-dist.mjs +13 -0
- package/dist/check-dist-observation.mjs +24 -7
- package/dist/check-no-ts-nocheck.mjs +88 -0
- package/dist/check-type-boundaries.mjs +23 -6
- package/dist/commands/preset-command.mjs +23 -1
- package/dist/commands/task-command.mjs +2 -1
- package/dist/harness.mjs +2 -1
- package/dist/lib/capability-registry.mjs +2 -2
- package/dist/lib/harness-core.mjs +1 -0
- package/dist/lib/preset-engine.mjs +1 -0
- package/dist/lib/preset-registry.mjs +13 -1
- package/dist/lib/preset-runner.mjs +294 -0
- package/dist/lib/task-index.mjs +1 -0
- package/dist/lib/task-lifecycle.mjs +3 -1
- package/dist/lib/task-review-model.mjs +2 -0
- package/dist/lib/task-scanner.mjs +1 -0
- package/dist/lib/task-tombstone-commands.mjs +12 -0
- package/docs-release/README.md +1 -1
- package/package.json +6 -2
- package/presets/release-closeout/checks/check-release-package.mjs +24 -0
- package/presets/release-closeout/preset.yaml +100 -0
- package/presets/release-closeout/scripts/generate-release-package.mjs +210 -0
- package/presets/release-closeout/templates/execution_strategy.append.md +7 -0
- package/presets/release-closeout/templates/findings.seed.md +5 -0
- package/presets/release-closeout/templates/review.seed.md +3 -0
- package/presets/release-closeout/templates/task_plan.append.md +24 -0
- package/references/pull-request-standard.md +2 -2
- package/templates/reference/pull-request-standard.md +2 -2
- package/templates-zh-CN/reference/pull-request-standard.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.8
|
|
4
|
+
|
|
5
|
+
- Preserve the executable bit for the packaged `dist/harness.mjs` npm bin
|
|
6
|
+
entry during dist builds and prepack.
|
|
7
|
+
- Extend release observation checks to fail if the packed or installed
|
|
8
|
+
`harness` bin is not executable.
|
|
9
|
+
- Add `migrate-structure --plan` to the dist and installed-package command
|
|
10
|
+
smoke matrix.
|
|
11
|
+
|
|
12
|
+
## 1.0.7
|
|
13
|
+
|
|
14
|
+
- Generate `harness init --add-npm-scripts` commands with
|
|
15
|
+
`npx --yes coding-agent-harness ...` so target projects can run Harness
|
|
16
|
+
scripts without adding a project dependency or relying on a global install.
|
|
17
|
+
- Isolate the dist observation command matrix from local ignored docs by using
|
|
18
|
+
the minimal target-project fixture for target-facing commands.
|
|
19
|
+
- Preserve the executable bit on the committed `dist/harness.mjs` CLI entry.
|
|
20
|
+
|
|
21
|
+
## 1.0.6
|
|
22
|
+
|
|
23
|
+
- Bump the npm package version after the 1.0.5 publication so the TypeScript
|
|
24
|
+
runtime-source migration can be published again.
|
|
25
|
+
|
|
3
26
|
## 1.0.5
|
|
4
27
|
|
|
5
28
|
- Relicense the public package from MIT to AGPL-3.0-or-later.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -4,7 +4,7 @@ Thanks for helping improve Coding Agent Harness. This repository contains the pu
|
|
|
4
4
|
|
|
5
5
|
## Before You Start
|
|
6
6
|
|
|
7
|
-
- Use Node.js
|
|
7
|
+
- Use Node.js 24 or newer. CI should run on the minimum supported line.
|
|
8
8
|
- Install root dependencies with `npm install` from the repository root.
|
|
9
9
|
- If you change `harness-gui`, also run `npm ci` inside `harness-gui/`.
|
|
10
10
|
- Keep pull requests focused. Separate documentation, CLI/runtime, template, preset, and GUI work when the changes are independent.
|
package/dist/build-dist.mjs
CHANGED
|
@@ -43,6 +43,10 @@ export function buildRuntimeDist({ projectRoot = repoRoot, configPath = path.joi
|
|
|
43
43
|
syncDirectory(buildOutDir, absoluteOutDir);
|
|
44
44
|
fs.rmSync(buildOutDir, { recursive: true, force: true });
|
|
45
45
|
}
|
|
46
|
+
restoreExecutableEntrypoints({
|
|
47
|
+
outDir: absoluteOutDir,
|
|
48
|
+
binRelativePaths: ["harness.mjs"],
|
|
49
|
+
});
|
|
46
50
|
const files = collectFiles(absoluteOutDir).filter((file) => file.endsWith(".mjs")).sort();
|
|
47
51
|
const relativeFiles = files.map((file) => toPosix(path.relative(absoluteOutDir, file)));
|
|
48
52
|
const requiredFiles = [
|
|
@@ -126,6 +130,15 @@ function syncDirectory(sourceDir, targetDir) {
|
|
|
126
130
|
fs.rmSync(path.join(targetDir, entry), { recursive: true, force: true });
|
|
127
131
|
}
|
|
128
132
|
}
|
|
133
|
+
function restoreExecutableEntrypoints({ outDir, binRelativePaths }) {
|
|
134
|
+
for (const relativePath of binRelativePaths) {
|
|
135
|
+
const file = path.join(outDir, relativePath);
|
|
136
|
+
if (!fs.existsSync(file))
|
|
137
|
+
continue;
|
|
138
|
+
const stat = fs.statSync(file);
|
|
139
|
+
fs.chmodSync(file, stat.mode | 0o755);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
129
142
|
function toPosix(value) {
|
|
130
143
|
return value.split(path.sep).join("/");
|
|
131
144
|
}
|
|
@@ -48,7 +48,10 @@ export function checkDistObservation({ projectRoot = defaultProjectRoot, runPack
|
|
|
48
48
|
failures.push({ code: "pack-dry-run-failed", message: `npm pack dry-run failed\nSTDOUT:\n${pack.stdout}\nSTDERR:\n${pack.stderr}` });
|
|
49
49
|
}
|
|
50
50
|
else {
|
|
51
|
-
const
|
|
51
|
+
const packedEntries = JSON.parse(pack.stdout)[0].files;
|
|
52
|
+
const packed = packedEntries.map((file) => file.path).sort();
|
|
53
|
+
const packedModeByPath = new Map(packedEntries.map((file) => [file.path, file.mode]));
|
|
54
|
+
const distHarnessMode = packedModeByPath.get("dist/harness.mjs");
|
|
52
55
|
observations.package = {
|
|
53
56
|
entryCount: packed.length,
|
|
54
57
|
hasDistHarness: packed.includes("dist/harness.mjs"),
|
|
@@ -57,11 +60,16 @@ export function checkDistObservation({ projectRoot = defaultProjectRoot, runPack
|
|
|
57
60
|
hasScriptsHarness: packed.includes("scripts/harness.mjs"),
|
|
58
61
|
hasScripts: packed.some((file) => file.startsWith("scripts/")),
|
|
59
62
|
hasTests: packed.some((file) => file.startsWith("tests/")),
|
|
63
|
+
distHarnessMode,
|
|
64
|
+
distHarnessExecutable: typeof distHarnessMode === "number" && Boolean(distHarnessMode & 0o111),
|
|
60
65
|
};
|
|
61
66
|
for (const required of ["dist/harness.mjs", "dist/postinstall.mjs", "dist/check-dist-observation.mjs"]) {
|
|
62
67
|
if (!packed.includes(required))
|
|
63
68
|
failures.push({ code: "packed-file-missing", file: required, message: `package missing ${required}` });
|
|
64
69
|
}
|
|
70
|
+
if (!observations.package.distHarnessExecutable) {
|
|
71
|
+
failures.push({ code: "packed-bin-not-executable", file: "dist/harness.mjs", mode: distHarnessMode, message: "package bin dist/harness.mjs must be executable" });
|
|
72
|
+
}
|
|
65
73
|
if (observations.package.hasScripts)
|
|
66
74
|
failures.push({ code: "package-includes-scripts", message: "package must not include scripts/** after historical shim deletion" });
|
|
67
75
|
if (observations.package.hasTests)
|
|
@@ -127,12 +135,13 @@ function runMatrix(root, failures, commandMatrix) {
|
|
|
127
135
|
const distHarness = path.join(root, "dist/harness.mjs");
|
|
128
136
|
const matrix = [
|
|
129
137
|
{ id: "help", args: ["--help"] },
|
|
130
|
-
{ id: "status", args: ["status", "--json", "
|
|
131
|
-
{ id: "task-list", args: ["task-list", "--json", "
|
|
132
|
-
{ id: "preset-list", args: ["preset", "list", "--json", "
|
|
138
|
+
{ id: "status", args: ["status", "--json", "examples/minimal-project"] },
|
|
139
|
+
{ id: "task-list", args: ["task-list", "--json", "examples/minimal-project"] },
|
|
140
|
+
{ id: "preset-list", args: ["preset", "list", "--json", "examples/minimal-project"] },
|
|
133
141
|
{ id: "source-check", args: ["check", "--profile", "source-package", "."] },
|
|
134
142
|
{ id: "target-check", args: ["check", "--profile", "target-project", "examples/minimal-project"] },
|
|
135
143
|
{ id: "migrate-plan", args: ["migrate-plan", "--json", "--limit", "20", "examples/minimal-project"] },
|
|
144
|
+
{ id: "migrate-structure-plan", args: ["migrate-structure", "--plan", "--json", "examples/minimal-project"] },
|
|
136
145
|
{ id: "dashboard", args: ["dashboard", "--out-dir", path.join("tmp", `pr-27-observation-dashboard-${process.pid}`), "examples/minimal-project"] },
|
|
137
146
|
];
|
|
138
147
|
for (const entry of matrix) {
|
|
@@ -211,11 +220,15 @@ function runInstalledPackageSmoke(root, failures, observations) {
|
|
|
211
220
|
if (!pkg)
|
|
212
221
|
return;
|
|
213
222
|
const binTarget = fs.existsSync(bin) ? fs.readlinkSync(bin) : "";
|
|
223
|
+
const installedBinFile = path.join(packageRoot, "dist/harness.mjs");
|
|
224
|
+
const installedBinMode = fs.existsSync(installedBinFile) ? fs.statSync(installedBinFile).mode : undefined;
|
|
214
225
|
observations.installSmoke = {
|
|
215
226
|
nodeVersion,
|
|
216
227
|
tempRoot,
|
|
217
228
|
binTarget,
|
|
218
229
|
bin: pkg.bin?.harness,
|
|
230
|
+
binMode: installedBinMode,
|
|
231
|
+
binExecutable: typeof installedBinMode === "number" && Boolean(installedBinMode & 0o111),
|
|
219
232
|
postinstall: pkg.scripts?.postinstall,
|
|
220
233
|
observeDist: pkg.scripts?.["observe:dist"],
|
|
221
234
|
hasTests: fs.existsSync(path.join(packageRoot, "tests")),
|
|
@@ -229,6 +242,9 @@ function runInstalledPackageSmoke(root, failures, observations) {
|
|
|
229
242
|
if (!binTarget.includes("dist/harness.mjs")) {
|
|
230
243
|
failures.push({ code: "installed-bin-link-not-dist", message: `installed bin link does not target dist/harness.mjs: ${binTarget}` });
|
|
231
244
|
}
|
|
245
|
+
if (!observations.installSmoke.binExecutable) {
|
|
246
|
+
failures.push({ code: "installed-bin-not-executable", file: "dist/harness.mjs", mode: installedBinMode, message: "installed package bin dist/harness.mjs must be executable" });
|
|
247
|
+
}
|
|
232
248
|
for (const relative of ["dist/harness.mjs", "dist/postinstall.mjs", "dist/check-dist-observation.mjs"]) {
|
|
233
249
|
if (!fs.existsSync(path.join(packageRoot, relative)))
|
|
234
250
|
failures.push({ code: "installed-file-missing", file: relative, message: `installed package missing ${relative}` });
|
|
@@ -272,12 +288,13 @@ function runInstalledPackageSmoke(root, failures, observations) {
|
|
|
272
288
|
function runInstalledMatrix(root, runtimeEnv, failures, steps) {
|
|
273
289
|
const matrix = [
|
|
274
290
|
{ id: "installed-help", cwd: root, args: ["--help"] },
|
|
275
|
-
{ id: "installed-status", cwd: root, args: ["status", "--json", "
|
|
276
|
-
{ id: "installed-task-list", cwd: root, args: ["task-list", "--json", "
|
|
277
|
-
{ id: "installed-preset-list", cwd: root, args: ["preset", "list", "--json", "
|
|
291
|
+
{ id: "installed-status", cwd: root, args: ["status", "--json", "examples/minimal-project"] },
|
|
292
|
+
{ id: "installed-task-list", cwd: root, args: ["task-list", "--json", "examples/minimal-project"] },
|
|
293
|
+
{ id: "installed-preset-list", cwd: root, args: ["preset", "list", "--json", "examples/minimal-project"] },
|
|
278
294
|
{ id: "installed-source-check", cwd: root, args: ["check", "--profile", "source-package", "."] },
|
|
279
295
|
{ id: "installed-target-check", cwd: root, args: ["check", "--profile", "target-project", "examples/minimal-project"] },
|
|
280
296
|
{ id: "installed-migrate-plan", cwd: root, args: ["migrate-plan", "--json", "--limit", "20", "examples/minimal-project"] },
|
|
297
|
+
{ id: "installed-migrate-structure-plan", cwd: root, args: ["migrate-structure", "--plan", "--json", "examples/minimal-project"] },
|
|
281
298
|
{ id: "installed-dashboard", cwd: root, args: ["dashboard", "--out-dir", path.join("tmp", `pr-27-installed-observation-dashboard-${process.pid}`), "examples/minimal-project"] },
|
|
282
299
|
];
|
|
283
300
|
for (const entry of matrix) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
+
const sourceRoots = ["scripts", "tests"];
|
|
7
|
+
const tsNocheckPattern = /^\s*\/\/\s*@ts-nocheck\b/;
|
|
8
|
+
export function checkNoTsNocheck({ repoRoot = defaultRepoRoot, allowlistPath = path.join(repoRoot, "scripts/ts-nocheck-allowlist.json"), } = {}) {
|
|
9
|
+
const files = collectMtsFiles(repoRoot);
|
|
10
|
+
const allowlist = readAllowlist(allowlistPath);
|
|
11
|
+
const violations = [];
|
|
12
|
+
const observed = new Set();
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
const absolutePath = path.join(repoRoot, file);
|
|
15
|
+
const lines = fs.readFileSync(absolutePath, "utf8").split(/\r?\n/);
|
|
16
|
+
const lineIndex = lines.findIndex((line) => tsNocheckPattern.test(line));
|
|
17
|
+
if (lineIndex === -1)
|
|
18
|
+
continue;
|
|
19
|
+
observed.add(file);
|
|
20
|
+
if (!allowlist.has(file)) {
|
|
21
|
+
violations.push({
|
|
22
|
+
code: "unlisted-ts-nocheck",
|
|
23
|
+
file,
|
|
24
|
+
line: lineIndex + 1,
|
|
25
|
+
message: `${file}:${lineIndex + 1} has @ts-nocheck but is not in scripts/ts-nocheck-allowlist.json`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const file of allowlist) {
|
|
30
|
+
if (!observed.has(file)) {
|
|
31
|
+
violations.push({
|
|
32
|
+
code: "stale-ts-nocheck-allowlist",
|
|
33
|
+
file,
|
|
34
|
+
message: `${file} is listed in scripts/ts-nocheck-allowlist.json but no longer has @ts-nocheck`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { ok: violations.length === 0, violations };
|
|
39
|
+
}
|
|
40
|
+
function collectMtsFiles(repoRoot) {
|
|
41
|
+
const files = [];
|
|
42
|
+
for (const root of sourceRoots) {
|
|
43
|
+
const absoluteRoot = path.join(repoRoot, root);
|
|
44
|
+
if (!fs.existsSync(absoluteRoot))
|
|
45
|
+
continue;
|
|
46
|
+
walk(absoluteRoot, files, repoRoot);
|
|
47
|
+
}
|
|
48
|
+
return files.sort();
|
|
49
|
+
}
|
|
50
|
+
function walk(current, files, repoRoot) {
|
|
51
|
+
const stat = fs.lstatSync(current);
|
|
52
|
+
if (stat.isSymbolicLink())
|
|
53
|
+
return;
|
|
54
|
+
if (stat.isDirectory()) {
|
|
55
|
+
const name = path.basename(current);
|
|
56
|
+
if (name === "node_modules" || name === ".worktrees" || name === "tmp")
|
|
57
|
+
return;
|
|
58
|
+
for (const entry of fs.readdirSync(current))
|
|
59
|
+
walk(path.join(current, entry), files, repoRoot);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (stat.isFile() && current.endsWith(".mts")) {
|
|
63
|
+
files.push(path.relative(repoRoot, current).split(path.sep).join("/"));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function readAllowlist(allowlistPath) {
|
|
67
|
+
if (!allowlistPath || !fs.existsSync(allowlistPath))
|
|
68
|
+
return new Set();
|
|
69
|
+
const parsed = JSON.parse(fs.readFileSync(allowlistPath, "utf8"));
|
|
70
|
+
const files = normalizeAllowlistFiles(parsed);
|
|
71
|
+
return new Set(files);
|
|
72
|
+
}
|
|
73
|
+
function normalizeAllowlistFiles(parsed) {
|
|
74
|
+
if (Array.isArray(parsed))
|
|
75
|
+
return parsed.filter((value) => typeof value === "string");
|
|
76
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
77
|
+
return [];
|
|
78
|
+
const files = parsed.files;
|
|
79
|
+
return Array.isArray(files) ? files.filter((value) => typeof value === "string") : [];
|
|
80
|
+
}
|
|
81
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
82
|
+
const result = checkNoTsNocheck();
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
console.error(result.violations.map((violation) => violation.message).join("\n"));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
console.log("No @ts-nocheck gate passed");
|
|
88
|
+
}
|
|
@@ -6,24 +6,27 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
7
|
const sourceRoots = ["scripts", "tests"];
|
|
8
8
|
const importPattern = /\b(import|export)\s+(type\s+)?(?:[^'"]*?\s+from\s+)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
9
|
-
const tsEscapePattern = /@(ts-ignore|ts-expect-error)\b
|
|
10
|
-
export function checkTypeBoundaries({ repoRoot = defaultRepoRoot } = {}) {
|
|
9
|
+
const tsEscapePattern = /@(ts-ignore|ts-expect-error)\b|\bas\s+unknown\s+as\b|\bRecord\s*<\s*string\s*,\s*any\s*>|(?:^|[^A-Za-z0-9_$])(?:as\s+any|:\s*any\b)/;
|
|
10
|
+
export function checkTypeBoundaries({ repoRoot = defaultRepoRoot, escapeAllowlistPath = path.join(repoRoot, "scripts/type-escape-allowlist.json"), } = {}) {
|
|
11
11
|
const files = collectSourceFiles(repoRoot);
|
|
12
12
|
const violations = [];
|
|
13
|
+
const escapeAllowlist = readEscapeAllowlist(escapeAllowlistPath);
|
|
13
14
|
for (const file of files) {
|
|
14
15
|
const absolutePath = path.join(repoRoot, file);
|
|
15
16
|
const content = fs.readFileSync(absolutePath, "utf8");
|
|
16
17
|
const imports = parseImports(content);
|
|
17
|
-
if (file.endsWith(".ts")) {
|
|
18
|
+
if (file.endsWith(".ts") || file.endsWith(".mts")) {
|
|
18
19
|
const lines = content.split(/\r?\n/);
|
|
19
20
|
for (const [index, line] of lines.entries()) {
|
|
20
21
|
if (tsEscapePattern.test(line)) {
|
|
21
|
-
|
|
22
|
+
const violation = {
|
|
22
23
|
code: "ts-escape-hatch",
|
|
23
24
|
file,
|
|
24
25
|
line: index + 1,
|
|
25
26
|
message: `${file}:${index + 1} uses a TypeScript escape hatch that requires review`,
|
|
26
|
-
}
|
|
27
|
+
};
|
|
28
|
+
if (!isEscapeAllowed(escapeAllowlist, violation))
|
|
29
|
+
violations.push(violation);
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
}
|
|
@@ -127,7 +130,21 @@ function hasTypeScriptSourceExtension(filePath) {
|
|
|
127
130
|
return typeof filePath === "string" && /\.(mts|ts)$/.test(filePath);
|
|
128
131
|
}
|
|
129
132
|
function isTypeOnlyTypeScriptImport(file, imported) {
|
|
130
|
-
return file.endsWith(".ts") && imported.kind === "import" && imported.typeOnly;
|
|
133
|
+
return (file.endsWith(".ts") || file.endsWith(".mts")) && imported.kind === "import" && imported.typeOnly;
|
|
134
|
+
}
|
|
135
|
+
function readEscapeAllowlist(allowlistPath) {
|
|
136
|
+
if (!allowlistPath || !fs.existsSync(allowlistPath))
|
|
137
|
+
return new Set();
|
|
138
|
+
const parsed = JSON.parse(fs.readFileSync(allowlistPath, "utf8"));
|
|
139
|
+
const entries = Array.isArray(parsed) ? parsed : parsed.escapes || [];
|
|
140
|
+
return new Set(entries.map((entry) => {
|
|
141
|
+
if (typeof entry === "string")
|
|
142
|
+
return entry;
|
|
143
|
+
return `${entry.file}:${entry.line}:${entry.code || "ts-escape-hatch"}`;
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
function isEscapeAllowed(allowlist, violation) {
|
|
147
|
+
return allowlist.has(`${violation.file}:${violation.line}:${violation.code}`) || allowlist.has(`${violation.file}:${violation.line}`);
|
|
131
148
|
}
|
|
132
149
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
133
150
|
const result = checkTypeBoundaries();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
-
import { checkPresetPackage, inspectPresetPackage, installPresetPackage, listPresetPackages, seedBundledPresets, uninstallPresetPackage, } from "../lib/harness-core.mjs";
|
|
2
|
+
import { checkPresetPackage, inspectPresetPackage, installPresetPackage, listPresetPackages, seedBundledPresets, runPresetEntrypoint, uninstallPresetPackage, } from "../lib/harness-core.mjs";
|
|
3
3
|
export function runPresetCommand({ args, takeFlag, targetArg }) {
|
|
4
4
|
const subcommand = args.shift() || "list";
|
|
5
5
|
const json = takeFlag("--json");
|
|
@@ -80,6 +80,20 @@ export function runPresetCommand({ args, takeFlag, targetArg }) {
|
|
|
80
80
|
else
|
|
81
81
|
console.log(`${result.removed ? "Removed" : "Preset not installed"}: ${result.id}`);
|
|
82
82
|
}
|
|
83
|
+
else if (subcommand === "run") {
|
|
84
|
+
const taskRef = takeOptionFromArgs(args, "--task", "");
|
|
85
|
+
const id = args.shift();
|
|
86
|
+
const entrypoint = args.shift();
|
|
87
|
+
if (!id)
|
|
88
|
+
throw new Error("Missing preset id");
|
|
89
|
+
if (!entrypoint)
|
|
90
|
+
throw new Error("Missing preset entrypoint");
|
|
91
|
+
const result = runPresetEntrypoint(id, entrypoint, { taskRef, targetInput: targetArg(), json });
|
|
92
|
+
if (json)
|
|
93
|
+
console.log(JSON.stringify(result, null, 2));
|
|
94
|
+
else
|
|
95
|
+
console.log(`Preset run ${result.status}: ${result.preset}.${result.entrypoint} (${result.materialized.length} writes)`);
|
|
96
|
+
}
|
|
83
97
|
else {
|
|
84
98
|
throw new Error(`Unknown preset subcommand: ${subcommand}`);
|
|
85
99
|
}
|
|
@@ -89,3 +103,11 @@ export function runPresetCommand({ args, takeFlag, targetArg }) {
|
|
|
89
103
|
process.exit(1);
|
|
90
104
|
}
|
|
91
105
|
}
|
|
106
|
+
function takeOptionFromArgs(args, name, fallback = "") {
|
|
107
|
+
const index = args.indexOf(name);
|
|
108
|
+
if (index < 0)
|
|
109
|
+
return fallback;
|
|
110
|
+
const value = args[index + 1] || fallback;
|
|
111
|
+
args.splice(index, 2);
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
@@ -128,7 +128,8 @@ export function runTaskCommand(command, { args, takeFlag, takeOption, targetArg
|
|
|
128
128
|
const lesson = takeOption("--lesson", "");
|
|
129
129
|
const search = takeOption("--search", "");
|
|
130
130
|
const missingMaterials = takeFlag("--missing-materials");
|
|
131
|
-
const
|
|
131
|
+
const includeArchived = takeFlag("--include-archived");
|
|
132
|
+
const result = listLifecycleTasks(targetArg(), { state, moduleKey, queue, preset, review, lesson, search, missingMaterials, includeArchived });
|
|
132
133
|
if (json) {
|
|
133
134
|
console.log(JSON.stringify(result, null, 2));
|
|
134
135
|
}
|
package/dist/harness.mjs
CHANGED
|
@@ -90,6 +90,7 @@ Usage:
|
|
|
90
90
|
harness preset install <folder|zip|builtin-id> [--project] [--force] [--json] [target]
|
|
91
91
|
harness preset seed [--project] [--force] [--dry-run] [--json] [target]
|
|
92
92
|
harness preset uninstall <id> [--project] [--json] [target]
|
|
93
|
+
harness preset run <id> <plan|scaffold|check> --task <task-id> [--json] [target]
|
|
93
94
|
harness new-task [task-id] [--module key] [--budget simple|standard|complex] [--preset id] [--from-session session.json] [--long-running] [--title title] [--locale zh-CN|en-US] [--dry-run] [target]
|
|
94
95
|
harness task-start <task-id> [--message text] [target]
|
|
95
96
|
harness task-phase <task-id> <phase-id> [--state done] [--completion 100] [--evidence present] [target]
|
|
@@ -100,7 +101,7 @@ Usage:
|
|
|
100
101
|
harness lesson-promote <task-id> <candidate-id> [--dry-run|--apply] [target]
|
|
101
102
|
harness lesson-sediment <task-id> <candidate-id> [--dry-run] [--title title] [target]
|
|
102
103
|
harness task-complete <task-id> [--message text] [target]
|
|
103
|
-
harness task-list [--json] [--state state] [--module key] [--queue queue] [--preset id] [--review status] [--lesson status] [--missing-materials] [--search text] [target]
|
|
104
|
+
harness task-list [--json] [--state state] [--module key] [--queue queue] [--preset id] [--review status] [--lesson status] [--missing-materials] [--include-archived] [--search text] [target]
|
|
104
105
|
harness task-index [--json] [target]
|
|
105
106
|
harness task-supersede <old-task-id> --by <new-task-id> [--reason text] [target]
|
|
106
107
|
harness task-delete <task-id> --soft [--reason text] [target]
|
|
@@ -571,8 +571,8 @@ function writeNpmScripts(target, { dryRun = true } = {}) {
|
|
|
571
571
|
const pkg = readJsonSafe(packagePath, {});
|
|
572
572
|
const scripts = { ...(pkg.scripts || {}) };
|
|
573
573
|
const additions = {
|
|
574
|
-
"harness:dev": "coding-agent-harness dev .",
|
|
575
|
-
"harness:dashboard": "coding-agent-harness dashboard --out-dir tmp/harness-dashboard .",
|
|
574
|
+
"harness:dev": "npx --yes coding-agent-harness dev .",
|
|
575
|
+
"harness:dashboard": "npx --yes coding-agent-harness dashboard --out-dir tmp/harness-dashboard .",
|
|
576
576
|
};
|
|
577
577
|
let changed = false;
|
|
578
578
|
const scriptChanges = [];
|
|
@@ -10,6 +10,7 @@ export * from "./dashboard-workbench.mjs";
|
|
|
10
10
|
export * from "./migration-planner.mjs";
|
|
11
11
|
export * from "./structure-migration.mjs";
|
|
12
12
|
export * from "./preset-registry.mjs";
|
|
13
|
+
export * from "./preset-runner.mjs";
|
|
13
14
|
export * from "./governance-index-generator.mjs";
|
|
14
15
|
export * from "./task-lifecycle.mjs";
|
|
15
16
|
export * from "./task-lesson-sedimentation.mjs";
|
|
@@ -94,6 +94,7 @@ export function buildPresetContext(preset, { target, taskDir, taskId, taskTitle,
|
|
|
94
94
|
taskId,
|
|
95
95
|
targetRoot: target.projectRoot,
|
|
96
96
|
entrypoint: "newTask",
|
|
97
|
+
resolvedInputs,
|
|
97
98
|
});
|
|
98
99
|
const context = {
|
|
99
100
|
kind: evaluatedValues.kind || preset.task?.kind || "general",
|
|
@@ -258,6 +258,9 @@ export function validatePresetPackage(preset) {
|
|
|
258
258
|
if (!preset.writeScopes.some((scope) => scope.path === writeScope)) {
|
|
259
259
|
failures.push(`${name} writes undeclared scope: ${writeScope}`);
|
|
260
260
|
}
|
|
261
|
+
if (name === "newTask" && !newTaskWriteScopeAllowed(writeScope)) {
|
|
262
|
+
failures.push("newTask entrypoint writes must stay under coding-agent-harness/planning/**");
|
|
263
|
+
}
|
|
261
264
|
}
|
|
262
265
|
if (["script", "check"].includes(entrypoint.type)) {
|
|
263
266
|
const entryPath = path.join(preset.directory, entrypoint.command || "");
|
|
@@ -282,7 +285,7 @@ export function validatePresetPackage(preset) {
|
|
|
282
285
|
}
|
|
283
286
|
return { failures, warnings };
|
|
284
287
|
}
|
|
285
|
-
export function buildPresetAudit(preset, { taskId = "", targetRoot = "", entrypoint = "newTask", writeScopes = [] } = {}) {
|
|
288
|
+
export function buildPresetAudit(preset, { taskId = "", targetRoot = "", entrypoint = "newTask", writeScopes = [], resolvedInputs = {} } = {}) {
|
|
286
289
|
const entrypoints = {
|
|
287
290
|
[entrypoint]: preset.entrypoints[entrypoint],
|
|
288
291
|
};
|
|
@@ -294,6 +297,7 @@ export function buildPresetAudit(preset, { taskId = "", targetRoot = "", entrypo
|
|
|
294
297
|
manifestSha256: preset.manifestSha256,
|
|
295
298
|
entrypoints,
|
|
296
299
|
writeScopes: scopes,
|
|
300
|
+
resolvedInputs,
|
|
297
301
|
taskId,
|
|
298
302
|
targetRoot,
|
|
299
303
|
generatedAt: new Date().toISOString(),
|
|
@@ -526,6 +530,14 @@ function validateAuditEvidenceFiles(preset, failures) {
|
|
|
526
530
|
}
|
|
527
531
|
}
|
|
528
532
|
}
|
|
533
|
+
function newTaskWriteScopeAllowed(writeScope) {
|
|
534
|
+
const normalized = toPosix(path.normalize(String(writeScope || "")));
|
|
535
|
+
const legacyPlanningScope = ["docs", "09-PLANNING"].join("/");
|
|
536
|
+
return (normalized === "coding-agent-harness/planning/**" ||
|
|
537
|
+
normalized.startsWith("coding-agent-harness/planning/") ||
|
|
538
|
+
normalized === `${legacyPlanningScope}/**` ||
|
|
539
|
+
normalized.startsWith(`${legacyPlanningScope}/`));
|
|
540
|
+
}
|
|
529
541
|
function validatePresetPackageFile(preset, relativePath, label, failures) {
|
|
530
542
|
const filePath = path.join(preset.directory, relativePath || "");
|
|
531
543
|
if (!isInside(preset.directory, filePath)) {
|