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
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// Generic preset entrypoint runner. Domain logic belongs in preset packages.
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { normalizeTarget, readFileSafe, readJsonSafe, sanitizeDeep, toPosix, walkFiles, } from "./core-shared.mjs";
|
|
9
|
+
import { beginGovernanceSync, commitGovernanceSync, releaseGovernanceSync } from "./governance-sync.mjs";
|
|
10
|
+
import { parseTaskMetadata } from "./task-metadata.mjs";
|
|
11
|
+
import { taskIdForDirectory } from "./task-scanner.mjs";
|
|
12
|
+
import { resolveTaskDirectory } from "./task-lifecycle.mjs";
|
|
13
|
+
import { evaluateTemplateValues, assertPresetWriteScope } from "./preset-engine.mjs";
|
|
14
|
+
import { buildPresetAudit, readPresetPackage } from "./preset-registry.mjs";
|
|
15
|
+
const materializationSchemaVersion = "preset-materialization/v1";
|
|
16
|
+
const maxMaterializedFileBytes = 10 * 1024 * 1024;
|
|
17
|
+
const maxMaterializedWrites = 500;
|
|
18
|
+
export function runPresetEntrypoint(presetId, entrypointName, { taskRef = "", targetInput = ".", json = false } = {}) {
|
|
19
|
+
const target = normalizeTarget(targetInput);
|
|
20
|
+
const preset = readPresetPackage(presetId, { targetInput });
|
|
21
|
+
const entrypoint = preset.entrypoints?.[entrypointName];
|
|
22
|
+
if (!entrypoint)
|
|
23
|
+
throw new Error(`Preset ${preset.id} does not declare entrypoint: ${entrypointName}`);
|
|
24
|
+
if (!["script", "check"].includes(entrypoint.type))
|
|
25
|
+
throw new Error(`Preset entrypoint ${entrypointName} is not runnable by preset run`);
|
|
26
|
+
if (!taskRef)
|
|
27
|
+
throw new Error("preset run requires --task <task-id>");
|
|
28
|
+
const taskDir = resolveTaskDirectory(target, taskRef);
|
|
29
|
+
const taskPlan = readFileSafe(path.join(taskDir, "task_plan.md"));
|
|
30
|
+
const metadata = parseTaskMetadata(taskPlan);
|
|
31
|
+
if (metadata.preset !== preset.id)
|
|
32
|
+
throw new Error(`Task ${taskRef} was created by preset ${metadata.preset || "none"}, not ${preset.id}`);
|
|
33
|
+
const taskId = taskIdForDirectory(target, taskDir);
|
|
34
|
+
const resolvedInputs = readResolvedInputs(target, metadata);
|
|
35
|
+
const values = evaluateTemplateValues(preset, resolvedInputs, { taskId, taskTitle: taskId, moduleKey: "" });
|
|
36
|
+
const outputRoot = fs.mkdtempSync(path.join(os.tmpdir(), `harness-preset-${preset.id}-${entrypointName}-`));
|
|
37
|
+
const manifestPath = path.join(outputRoot, "materialization-manifest.json");
|
|
38
|
+
const contextPath = path.join(outputRoot, "preset-context.json");
|
|
39
|
+
const beforeSnapshot = targetSnapshot(target.projectRoot);
|
|
40
|
+
try {
|
|
41
|
+
const context = {
|
|
42
|
+
schemaVersion: "preset-run-context/v1",
|
|
43
|
+
preset: { id: preset.id, version: preset.version, source: preset.source },
|
|
44
|
+
entrypoint: entrypointName,
|
|
45
|
+
task: {
|
|
46
|
+
id: taskId,
|
|
47
|
+
ref: taskRef,
|
|
48
|
+
dir: toPosix(path.relative(target.projectRoot, taskDir)),
|
|
49
|
+
taskPlanPath: toPosix(path.relative(target.projectRoot, path.join(taskDir, "task_plan.md"))),
|
|
50
|
+
},
|
|
51
|
+
targetRoot: target.projectRoot,
|
|
52
|
+
targetRootPolicy: "read-only; direct target mutation before manifest materialization is a hard failure",
|
|
53
|
+
outputRoot,
|
|
54
|
+
materializationManifestPath: manifestPath,
|
|
55
|
+
inputs: sanitizeDeep(resolvedInputs),
|
|
56
|
+
values: sanitizeDeep(values),
|
|
57
|
+
audit: buildPresetAudit(preset, {
|
|
58
|
+
taskId,
|
|
59
|
+
targetRoot: target.projectRoot,
|
|
60
|
+
entrypoint: entrypointName,
|
|
61
|
+
writeScopes: entrypoint.writes,
|
|
62
|
+
resolvedInputs,
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`);
|
|
66
|
+
const commandPath = path.join(preset.directory, entrypoint.command || "");
|
|
67
|
+
const script = spawnSync(process.execPath, [commandPath], {
|
|
68
|
+
cwd: outputRoot,
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
env: {
|
|
71
|
+
...process.env,
|
|
72
|
+
HARNESS_PRESET_CONTEXT: contextPath,
|
|
73
|
+
},
|
|
74
|
+
timeout: 120000,
|
|
75
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
76
|
+
});
|
|
77
|
+
if (script.error)
|
|
78
|
+
throw script.error;
|
|
79
|
+
if (script.status !== 0) {
|
|
80
|
+
throw new Error(`Preset entrypoint ${preset.id}.${entrypointName} failed with ${script.status}\n${script.stderr || script.stdout || ""}`.trim());
|
|
81
|
+
}
|
|
82
|
+
const afterScriptSnapshot = targetSnapshot(target.projectRoot);
|
|
83
|
+
assertSnapshotsEqual(beforeSnapshot, afterScriptSnapshot, "Preset script mutated target before materialization");
|
|
84
|
+
const manifest = readMaterializationManifest(manifestPath);
|
|
85
|
+
const materialization = validateMaterializationManifest(preset, entrypoint, manifest, { outputRoot, targetRoot: target.projectRoot });
|
|
86
|
+
const governanceContext = beginGovernanceSync(target, {
|
|
87
|
+
operation: `preset-run ${preset.id}.${entrypointName}`,
|
|
88
|
+
allowDirtyWorktree: true,
|
|
89
|
+
allowedRelativePaths: materialization.map((item) => item.destination),
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
materializeWrites(target.projectRoot, materialization);
|
|
93
|
+
const commit = commitGovernanceSync(governanceContext, materialization.map((item) => item.destination), {
|
|
94
|
+
message: `chore(harness): run preset ${preset.id} ${entrypointName}`,
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
preset: preset.id,
|
|
98
|
+
entrypoint: entrypointName,
|
|
99
|
+
taskId,
|
|
100
|
+
status: manifest.status || (entrypoint.type === "check" ? "pass" : "ok"),
|
|
101
|
+
materialized: materialization.map((item) => ({
|
|
102
|
+
source: item.source,
|
|
103
|
+
destination: item.destination,
|
|
104
|
+
type: item.type,
|
|
105
|
+
sha256: item.sha256,
|
|
106
|
+
})),
|
|
107
|
+
governance: { commit },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
releaseGovernanceSync(governanceContext);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
fs.rmSync(outputRoot, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function readResolvedInputs(target, metadata) {
|
|
119
|
+
const evidenceBundle = String(metadata.evidenceBundle || "").replace(/^TARGET:/, "").replace(/^\/+/, "");
|
|
120
|
+
if (!evidenceBundle)
|
|
121
|
+
return {};
|
|
122
|
+
const auditPath = path.join(target.projectRoot, evidenceBundle, "preset-audit.json");
|
|
123
|
+
const audit = readJsonSafe(auditPath, {});
|
|
124
|
+
return audit.resolvedInputs || {};
|
|
125
|
+
}
|
|
126
|
+
function readMaterializationManifest(manifestPath) {
|
|
127
|
+
if (!fs.existsSync(manifestPath))
|
|
128
|
+
throw new Error("Preset entrypoint did not emit materialization manifest");
|
|
129
|
+
const manifest = readJsonSafe(manifestPath, null);
|
|
130
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
|
|
131
|
+
throw new Error("Invalid preset materialization manifest");
|
|
132
|
+
if (manifest.schemaVersion !== materializationSchemaVersion)
|
|
133
|
+
throw new Error(`Invalid preset materialization schema: ${manifest.schemaVersion || "(missing)"}`);
|
|
134
|
+
if (!Array.isArray(manifest.writes))
|
|
135
|
+
throw new Error("Preset materialization manifest writes must be an array");
|
|
136
|
+
if (manifest.writes.length > maxMaterializedWrites)
|
|
137
|
+
throw new Error(`Preset materialization manifest has too many writes: ${manifest.writes.length}`);
|
|
138
|
+
return manifest;
|
|
139
|
+
}
|
|
140
|
+
function validateMaterializationManifest(preset, entrypoint, manifest, { outputRoot, targetRoot }) {
|
|
141
|
+
const seenDestinations = new Set();
|
|
142
|
+
const writes = manifest.writes.map((write, index) => {
|
|
143
|
+
const source = normalizeManifestRelativePath(write.source, "Manifest source");
|
|
144
|
+
const destination = normalizeManifestRelativePath(write.destination, "Manifest destination");
|
|
145
|
+
if (seenDestinations.has(destination))
|
|
146
|
+
throw new Error(`Duplicate materialization destination: ${destination}`);
|
|
147
|
+
seenDestinations.add(destination);
|
|
148
|
+
assertEntrypointWriteScope(preset, entrypoint, destination);
|
|
149
|
+
const sourcePath = path.join(outputRoot, source);
|
|
150
|
+
assertOutputSource(outputRoot, sourcePath, source);
|
|
151
|
+
const stat = fs.lstatSync(sourcePath);
|
|
152
|
+
if (stat.size > maxMaterializedFileBytes)
|
|
153
|
+
throw new Error(`Manifest source exceeds size limit: ${source}`);
|
|
154
|
+
assertDestinationParent(targetRoot, destination);
|
|
155
|
+
return {
|
|
156
|
+
source,
|
|
157
|
+
sourcePath,
|
|
158
|
+
destination,
|
|
159
|
+
destinationPath: path.join(targetRoot, destination),
|
|
160
|
+
type: String(write.type || "text"),
|
|
161
|
+
visibility: String(write.visibility || ""),
|
|
162
|
+
sha256: sha256File(sourcePath),
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
enforcePublicRedaction(manifest, writes, { outputRoot });
|
|
166
|
+
return writes;
|
|
167
|
+
}
|
|
168
|
+
function normalizeManifestRelativePath(value, label) {
|
|
169
|
+
const raw = String(value || "").trim();
|
|
170
|
+
const normalized = toPosix(path.normalize(raw));
|
|
171
|
+
if (!raw || path.isAbsolute(raw) || normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
172
|
+
throw new Error(`${label} escapes preset output root: ${raw || "(missing)"}`);
|
|
173
|
+
}
|
|
174
|
+
return normalized;
|
|
175
|
+
}
|
|
176
|
+
function assertOutputSource(outputRoot, sourcePath, source) {
|
|
177
|
+
if (!fs.existsSync(sourcePath))
|
|
178
|
+
throw new Error(`Manifest source missing: ${source}`);
|
|
179
|
+
const stat = fs.lstatSync(sourcePath);
|
|
180
|
+
if (stat.isSymbolicLink())
|
|
181
|
+
throw new Error(`Manifest source must not be a symlink: ${source}`);
|
|
182
|
+
if (!stat.isFile())
|
|
183
|
+
throw new Error(`Manifest source must be a file: ${source}`);
|
|
184
|
+
const realRoot = fs.realpathSync(outputRoot);
|
|
185
|
+
const realSource = fs.realpathSync(sourcePath);
|
|
186
|
+
if (!isInside(realRoot, realSource))
|
|
187
|
+
throw new Error(`Manifest source escapes preset output root: ${source}`);
|
|
188
|
+
}
|
|
189
|
+
function assertDestinationParent(targetRoot, destination) {
|
|
190
|
+
let parent = path.dirname(path.join(targetRoot, destination));
|
|
191
|
+
const realTarget = fs.realpathSync(targetRoot);
|
|
192
|
+
while (!fs.existsSync(parent) && parent !== targetRoot && parent !== path.dirname(parent))
|
|
193
|
+
parent = path.dirname(parent);
|
|
194
|
+
if (fs.existsSync(parent)) {
|
|
195
|
+
const stat = fs.lstatSync(parent);
|
|
196
|
+
if (stat.isSymbolicLink())
|
|
197
|
+
throw new Error(`Manifest destination parent must not be a symlink: ${destination}`);
|
|
198
|
+
const realParent = fs.realpathSync(parent);
|
|
199
|
+
if (!isInside(realTarget, realParent))
|
|
200
|
+
throw new Error(`Manifest destination parent escapes target root: ${destination}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function assertEntrypointWriteScope(preset, entrypoint, destination) {
|
|
204
|
+
assertPresetWriteScope(preset, destination);
|
|
205
|
+
if (!entrypoint.writes.some((scope) => matchesScope(scope, destination))) {
|
|
206
|
+
throw new Error(`Preset write scope violation for ${destination}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function matchesScope(scope, relativePath) {
|
|
210
|
+
const normalizedScope = toPosix(path.normalize(String(scope || "")));
|
|
211
|
+
if (normalizedScope.endsWith("/**")) {
|
|
212
|
+
const prefix = normalizedScope.slice(0, -3);
|
|
213
|
+
return relativePath === prefix || relativePath.startsWith(`${prefix}/`);
|
|
214
|
+
}
|
|
215
|
+
return relativePath === normalizedScope;
|
|
216
|
+
}
|
|
217
|
+
function enforcePublicRedaction(manifest, writes, { outputRoot }) {
|
|
218
|
+
const publicWrites = writes.filter((write) => write.visibility === "public" || write.destination.startsWith("docs-release/"));
|
|
219
|
+
if (publicWrites.length === 0)
|
|
220
|
+
return;
|
|
221
|
+
const reportSource = normalizeManifestRelativePath(manifest.publicRedactionReport?.source || "", "Public redaction report source");
|
|
222
|
+
const reportPath = path.join(outputRoot, reportSource);
|
|
223
|
+
assertOutputSource(outputRoot, reportPath, reportSource);
|
|
224
|
+
const report = readJsonSafe(reportPath, null);
|
|
225
|
+
if (!report || report.status !== "pass")
|
|
226
|
+
throw new Error("Public materialization requires a passing public redaction report");
|
|
227
|
+
}
|
|
228
|
+
function materializeWrites(targetRoot, writes) {
|
|
229
|
+
const backups = [];
|
|
230
|
+
try {
|
|
231
|
+
for (const write of writes) {
|
|
232
|
+
const destinationPath = write.destinationPath;
|
|
233
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
234
|
+
const existed = fs.existsSync(destinationPath);
|
|
235
|
+
const backupPath = existed ? `${destinationPath}.backup-${process.pid}-${crypto.randomBytes(4).toString("hex")}` : "";
|
|
236
|
+
if (existed) {
|
|
237
|
+
fs.copyFileSync(destinationPath, backupPath);
|
|
238
|
+
backups.push({ destinationPath, backupPath, existed });
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
backups.push({ destinationPath, backupPath: "", existed });
|
|
242
|
+
}
|
|
243
|
+
const tempPath = `${destinationPath}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
|
|
244
|
+
fs.copyFileSync(write.sourcePath, tempPath);
|
|
245
|
+
fs.renameSync(tempPath, destinationPath);
|
|
246
|
+
}
|
|
247
|
+
for (const backup of backups) {
|
|
248
|
+
if (backup.backupPath)
|
|
249
|
+
fs.rmSync(backup.backupPath, { force: true });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
for (const backup of backups.reverse()) {
|
|
254
|
+
try {
|
|
255
|
+
if (backup.existed && backup.backupPath && fs.existsSync(backup.backupPath))
|
|
256
|
+
fs.renameSync(backup.backupPath, backup.destinationPath);
|
|
257
|
+
else if (!backup.existed)
|
|
258
|
+
fs.rmSync(backup.destinationPath, { force: true });
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// Preserve the original materialization failure.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function targetSnapshot(root) {
|
|
268
|
+
const entries = new Map();
|
|
269
|
+
for (const filePath of walkFiles(root)) {
|
|
270
|
+
const relative = toPosix(path.relative(root, filePath));
|
|
271
|
+
if (relative.startsWith(".harness/locks/"))
|
|
272
|
+
continue;
|
|
273
|
+
const stat = fs.lstatSync(filePath);
|
|
274
|
+
entries.set(relative, `${stat.size}:${sha256File(filePath)}`);
|
|
275
|
+
}
|
|
276
|
+
return entries;
|
|
277
|
+
}
|
|
278
|
+
function assertSnapshotsEqual(before, after, message) {
|
|
279
|
+
const changed = [];
|
|
280
|
+
const paths = new Set([...before.keys(), ...after.keys()]);
|
|
281
|
+
for (const item of paths) {
|
|
282
|
+
if (before.get(item) !== after.get(item))
|
|
283
|
+
changed.push(item);
|
|
284
|
+
}
|
|
285
|
+
if (changed.length)
|
|
286
|
+
throw new Error(`${message}: ${changed.slice(0, 12).join(", ")}`);
|
|
287
|
+
}
|
|
288
|
+
function sha256File(filePath) {
|
|
289
|
+
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
290
|
+
}
|
|
291
|
+
function isInside(root, candidate) {
|
|
292
|
+
const relative = path.relative(root, candidate);
|
|
293
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
294
|
+
}
|
package/dist/lib/task-index.mjs
CHANGED
|
@@ -56,6 +56,7 @@ export function buildTaskIndex(targetInput) {
|
|
|
56
56
|
supersedes: task.supersedes || [],
|
|
57
57
|
supersededBy: task.supersededBy || "",
|
|
58
58
|
deletionState: task.deletionState || "active",
|
|
59
|
+
archiveMetadata: task.archiveMetadata || {},
|
|
59
60
|
hiddenByDefault: task.hiddenByDefault === true,
|
|
60
61
|
repairPrompt: task.repairPrompt || "",
|
|
61
62
|
repairActions: repairActions(task),
|
|
@@ -570,9 +570,11 @@ export function updateModuleStep(targetInput, moduleKey, stepId, { state = "" }
|
|
|
570
570
|
releaseGovernanceSync(governanceContext);
|
|
571
571
|
}
|
|
572
572
|
}
|
|
573
|
-
export function listLifecycleTasks(targetInput, { state = "", moduleKey = "", queue = "", preset = "", review = "", lesson = "", search = "", missingMaterials = false } = {}) {
|
|
573
|
+
export function listLifecycleTasks(targetInput, { state = "", moduleKey = "", queue = "", preset = "", review = "", lesson = "", search = "", missingMaterials = false, includeArchived = false } = {}) {
|
|
574
574
|
const target = normalizeTarget(targetInput);
|
|
575
575
|
let tasks = collectTasks(target);
|
|
576
|
+
if (!includeArchived)
|
|
577
|
+
tasks = tasks.filter((task) => task.deletionState === "active" && task.hiddenByDefault !== true);
|
|
576
578
|
if (state)
|
|
577
579
|
tasks = tasks.filter((task) => task.state === String(state).toLowerCase().replaceAll("-", "_"));
|
|
578
580
|
if (moduleKey)
|
|
@@ -58,6 +58,7 @@ export function parseTaskTombstone(taskPlanContent) {
|
|
|
58
58
|
supersededBy: "",
|
|
59
59
|
supersedes: topLevelSupersedes,
|
|
60
60
|
deleteReason: "",
|
|
61
|
+
archiveMetadata: {},
|
|
61
62
|
hiddenByDefault: false,
|
|
62
63
|
reopenEligible: false,
|
|
63
64
|
archiveEligible: false,
|
|
@@ -71,6 +72,7 @@ export function parseTaskTombstone(taskPlanContent) {
|
|
|
71
72
|
supersededBy: fields.get("superseded by") || fields.get("替代任务") || "",
|
|
72
73
|
supersedes: [...new Set([...topLevelSupersedes, ...splitList(fields.get("supersedes") || fields.get("合并自") || "")])],
|
|
73
74
|
deleteReason: fields.get("reason") || fields.get("原因") || "",
|
|
75
|
+
archiveMetadata: Object.fromEntries([...fields.entries()].filter(([key]) => !["state", "状态", "superseded by", "替代任务", "supersedes", "合并自", "reason", "原因", "reopen eligible", "可重新打开", "archive eligible", "可归档"].includes(key))),
|
|
74
76
|
hiddenByDefault: true,
|
|
75
77
|
reopenEligible: parseTombstoneBooleanLike(fields.get("reopen eligible") || fields.get("可重新打开")),
|
|
76
78
|
archiveEligible: parseTombstoneBooleanLike(fields.get("archive eligible") || fields.get("可归档")),
|
|
@@ -344,6 +344,7 @@ export function collectTasks(target, { requireGeneratedScaffoldProvenance = fals
|
|
|
344
344
|
supersededBy: tombstone.supersededBy,
|
|
345
345
|
supersedes: tombstone.supersedes,
|
|
346
346
|
deleteReason: tombstone.deleteReason,
|
|
347
|
+
archiveMetadata: tombstone.archiveMetadata || {},
|
|
347
348
|
hiddenByDefault: tombstone.hiddenByDefault,
|
|
348
349
|
reopenEligible: tombstone.reopenEligible,
|
|
349
350
|
archiveEligible: tombstone.archiveEligible,
|
|
@@ -40,6 +40,7 @@ export function softDeleteTask(targetInput, taskRef, { reason = "" } = {}) {
|
|
|
40
40
|
export function archiveTask(targetInput, taskRef, { reason = "" } = {}) {
|
|
41
41
|
const target = normalizeTarget(targetInput);
|
|
42
42
|
const task = resolveTask(target, taskRef);
|
|
43
|
+
assertArchiveEligible(task);
|
|
43
44
|
return writeDeletionState(target, task, "archived", reason || "archive", "task-archive");
|
|
44
45
|
}
|
|
45
46
|
export function reopenTask(targetInput, taskRef, { reason = "" } = {}) {
|
|
@@ -100,6 +101,17 @@ function resolveTask(target, ref) {
|
|
|
100
101
|
throw new Error(`Ambiguous task reference: ${ref}`);
|
|
101
102
|
throw new Error(`Task not found: ${ref}`);
|
|
102
103
|
}
|
|
104
|
+
function assertArchiveEligible(task) {
|
|
105
|
+
if (task.state === "blocked" || (task.taskQueues || []).includes("blocked")) {
|
|
106
|
+
throw new Error("blocked tasks cannot be archived without an explicit human waiver");
|
|
107
|
+
}
|
|
108
|
+
const blockingRisks = (task.risks || []).filter((risk) => risk.open !== "no" && (risk.blocksRelease === "yes" || ["P0", "P1", "P2"].includes(risk.severity)));
|
|
109
|
+
if (blockingRisks.length)
|
|
110
|
+
throw new Error("tasks with open blocking review findings cannot be archived without an explicit human waiver");
|
|
111
|
+
if (task.materialsReady === false && task.reviewStatus !== "confirmed") {
|
|
112
|
+
throw new Error("tasks with incomplete closeout materials cannot be archived without an explicit human waiver");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
103
115
|
function writeTombstone(target, task, fields) {
|
|
104
116
|
const taskPlanPath = path.join(target.projectRoot, task.taskPlanPath.replace(/^TARGET:/, ""));
|
|
105
117
|
const content = readFileSafe(taskPlanPath).replace(/\n##\s*(?:Task Tombstone|任务墓碑)\s*$[\s\S]*?(?=^##\s+|(?![\s\S]))/im, "");
|
package/docs-release/README.md
CHANGED
|
@@ -71,7 +71,7 @@ Not every document is written for the same reader.
|
|
|
71
71
|
- `guides/migration-playbook.md` / `guides/migration-playbook.en-US.md` — smooth migration guide for existing legacy harness projects. 旧 Harness 项目的平滑迁移指南。
|
|
72
72
|
- `guides/legacy-migration-agent-prompt.md` / `guides/legacy-migration-agent-prompt.zh-CN.md` — prompt contract for agents running baseline or full legacy migration. 给迁移 Agent 使用的执行合同。
|
|
73
73
|
- `guides/full-legacy-migration-subagent-strategy.md` / `guides/full-legacy-migration-subagent-strategy.zh-CN.md` — full readable cutover strategy with subagent roles, adversarial review, and dashboard/CLI proof gates. 完整可读迁移的 subagent 分工、对抗审查和 Dashboard/CLI 证据门禁。
|
|
74
|
-
- `guides/typescript-runtime-migration-closeout.md` — public closeout for the TypeScript runtime source-twin migration and the
|
|
74
|
+
- `guides/typescript-runtime-migration-closeout.md` — public closeout for the TypeScript runtime source-twin migration and the documented preset/dashboard JavaScript exceptions. TypeScript runtime source twin 迁移收口和已记录的 preset/dashboard JavaScript 例外。
|
|
75
75
|
|
|
76
76
|
## Repository Operating Models / 仓库运行模式
|
|
77
77
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coding-agent-harness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Document governance kernel for long-running coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"build:runtime": "node scripts/build-dist.mts",
|
|
32
32
|
"prepack": "node scripts/build-dist.mts --quiet",
|
|
33
33
|
"typecheck": "npm exec --yes --package typescript@5.9.3 -- tsc -p tsconfig.json",
|
|
34
|
-
"typecheck:guards": "node dist/check-type-boundaries.mjs",
|
|
34
|
+
"typecheck:guards": "node dist/check-type-boundaries.mjs && node dist/check-no-ts-nocheck.mjs",
|
|
35
|
+
"check:nocheck": "node dist/check-no-ts-nocheck.mjs",
|
|
35
36
|
"status": "node dist/harness.mjs status --json .",
|
|
36
37
|
"dashboard": "node dist/harness.mjs dashboard --out tmp/harness-dashboard.html examples/minimal-project",
|
|
37
38
|
"dashboard:folder": "node dist/harness.mjs dashboard --out-dir tmp/harness-dashboard examples/minimal-project",
|
|
@@ -62,6 +63,9 @@
|
|
|
62
63
|
"docs-release/",
|
|
63
64
|
"examples/"
|
|
64
65
|
],
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@types/node": "24"
|
|
68
|
+
},
|
|
65
69
|
"engines": {
|
|
66
70
|
"node": ">=24"
|
|
67
71
|
},
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const context = JSON.parse(fs.readFileSync(process.env.HARNESS_PRESET_CONTEXT, "utf8"));
|
|
7
|
+
const release = String(context.inputs.release || "").trim();
|
|
8
|
+
const releaseRoot = path.join(context.targetRoot, "coding-agent-harness/governance/releases", release);
|
|
9
|
+
const required = ["INDEX.md", "task-aggregate.json", "task-archive-plan.md", "public-summary.md", "public-redaction-report.json"];
|
|
10
|
+
const missing = required.filter((file) => !fs.existsSync(path.join(releaseRoot, file)));
|
|
11
|
+
if (missing.length) {
|
|
12
|
+
console.error(`release package missing files: ${missing.join(", ")}`);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
const redaction = JSON.parse(fs.readFileSync(path.join(releaseRoot, "public-redaction-report.json"), "utf8"));
|
|
16
|
+
if (redaction.status !== "pass") {
|
|
17
|
+
console.error("release public redaction report is not passing");
|
|
18
|
+
process.exit(3);
|
|
19
|
+
}
|
|
20
|
+
fs.writeFileSync(context.materializationManifestPath, `${JSON.stringify({
|
|
21
|
+
schemaVersion: "preset-materialization/v1",
|
|
22
|
+
status: "pass",
|
|
23
|
+
writes: [],
|
|
24
|
+
}, null, 2)}\n`);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
id: release-closeout
|
|
2
|
+
version: 1
|
|
3
|
+
purpose: Create and run a release closeout task that aggregates version tasks into an auditable release package
|
|
4
|
+
compatibleBudgets: [complex]
|
|
5
|
+
localeSupport: [en-US, zh-CN]
|
|
6
|
+
task:
|
|
7
|
+
kind: release-closeout
|
|
8
|
+
defaultTaskId: release-closeout
|
|
9
|
+
defaultOutcome: Produce a release closeout task whose runner entrypoints generate the version package, archive plan, and public summary.
|
|
10
|
+
projectLevelOnly: true
|
|
11
|
+
inputs:
|
|
12
|
+
release:
|
|
13
|
+
type: text
|
|
14
|
+
flag: --release
|
|
15
|
+
required: true
|
|
16
|
+
previousRelease:
|
|
17
|
+
type: text
|
|
18
|
+
flag: --previous-release
|
|
19
|
+
required: false
|
|
20
|
+
nextRelease:
|
|
21
|
+
type: text
|
|
22
|
+
flag: --next-release
|
|
23
|
+
required: false
|
|
24
|
+
taskQuery:
|
|
25
|
+
type: text
|
|
26
|
+
flag: --task-query
|
|
27
|
+
required: false
|
|
28
|
+
taskList:
|
|
29
|
+
type: json-file
|
|
30
|
+
flag: --task-list
|
|
31
|
+
required: false
|
|
32
|
+
publicSummary:
|
|
33
|
+
type: flag
|
|
34
|
+
flag: --public-summary
|
|
35
|
+
required: false
|
|
36
|
+
templateValues:
|
|
37
|
+
release:
|
|
38
|
+
from: inputs.release
|
|
39
|
+
previousRelease:
|
|
40
|
+
from: inputs.previousRelease
|
|
41
|
+
nextRelease:
|
|
42
|
+
from: inputs.nextRelease
|
|
43
|
+
taskQuery:
|
|
44
|
+
from: inputs.taskQuery
|
|
45
|
+
taskId:
|
|
46
|
+
from: task.id
|
|
47
|
+
entrypoints:
|
|
48
|
+
newTask:
|
|
49
|
+
type: template
|
|
50
|
+
writes: [coding-agent-harness/planning/tasks/**]
|
|
51
|
+
audit: true
|
|
52
|
+
templates:
|
|
53
|
+
taskPlanAppend: templates/task_plan.append.md
|
|
54
|
+
executionStrategyAppend: templates/execution_strategy.append.md
|
|
55
|
+
findingsSeed: templates/findings.seed.md
|
|
56
|
+
reviewSeed: templates/review.seed.md
|
|
57
|
+
plan:
|
|
58
|
+
type: script
|
|
59
|
+
command: scripts/generate-release-package.mjs
|
|
60
|
+
writes: [coding-agent-harness/governance/releases/**]
|
|
61
|
+
reads: [coding-agent-harness/planning/tasks/**]
|
|
62
|
+
audit: true
|
|
63
|
+
scaffold:
|
|
64
|
+
type: script
|
|
65
|
+
command: scripts/generate-release-package.mjs
|
|
66
|
+
writes: [coding-agent-harness/governance/releases/**]
|
|
67
|
+
reads: [coding-agent-harness/planning/tasks/**]
|
|
68
|
+
audit: true
|
|
69
|
+
check:
|
|
70
|
+
type: check
|
|
71
|
+
command: checks/check-release-package.mjs
|
|
72
|
+
writes: [coding-agent-harness/governance/releases/**]
|
|
73
|
+
reads: [coding-agent-harness/governance/releases/**, coding-agent-harness/planning/tasks/**]
|
|
74
|
+
audit: true
|
|
75
|
+
evidence:
|
|
76
|
+
bundleDir: artifacts/preset
|
|
77
|
+
files:
|
|
78
|
+
release:
|
|
79
|
+
path: release.txt
|
|
80
|
+
type: text
|
|
81
|
+
value: inputs.release
|
|
82
|
+
presetAudit:
|
|
83
|
+
path: preset-audit.json
|
|
84
|
+
type: preset-audit
|
|
85
|
+
presetManifest:
|
|
86
|
+
path: preset-manifest.json
|
|
87
|
+
type: preset-manifest
|
|
88
|
+
writeScope:
|
|
89
|
+
path: write-scope.json
|
|
90
|
+
type: write-scope
|
|
91
|
+
audit:
|
|
92
|
+
manifestRequired: true
|
|
93
|
+
evidenceFiles: [preset-audit.json, preset-manifest.json, write-scope.json]
|
|
94
|
+
writeScopes:
|
|
95
|
+
taskDocs:
|
|
96
|
+
path: coding-agent-harness/planning/tasks/**
|
|
97
|
+
access: write
|
|
98
|
+
releasePackage:
|
|
99
|
+
path: coding-agent-harness/governance/releases/**
|
|
100
|
+
access: write
|