@tea-agent/loop-agent 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +60 -0
- package/README.md +1 -1
- package/dist/executors/dag-pi-executor.js +69 -2
- package/dist/executors/pi-sdk-executor.js +61 -0
- package/dist/executors/shell-executor.js +136 -79
- package/dist/shared/operator/capabilities.js +22 -1
- package/dist/shared/operator/command-lifecycle.js +94 -0
- package/dist/shared/operator/index.js +1 -0
- package/dist/worker/cli.js +10 -24
- package/dist/worker/console/doctor.js +11 -4
- package/dist/worker/console/observe-health-match.js +18 -15
- package/dist/worker/console/operator-actions.js +2 -1
- package/dist/worker/feature/acceptance-policy.js +227 -0
- package/dist/worker/feature/decision-loader.js +1 -1
- package/dist/worker/feature/next-action.js +56 -6
- package/dist/worker/feature/profile-schema.js +3 -0
- package/dist/worker/feature/reducer.js +1 -0
- package/dist/worker/feature/review.js +72 -8
- package/dist/worker/feature/scaffold.js +14 -0
- package/dist/worker/loop-agent/controller-protocol.js +143 -0
- package/dist/worker/materialize/harness-task-lifecycle-probe.js +126 -0
- package/dist/worker/materialize/harness-task-lineage.js +220 -0
- package/dist/worker/materialize/harness-task-materializer.js +350 -80
- package/dist/worker/observability/progress-composite.js +1 -0
- package/dist/worker/observability/read-model.js +66 -19
- package/dist/worker/pool/attempt-identity.js +41 -0
- package/dist/worker/pool/attempt-lease.js +184 -0
- package/dist/worker/pool/attempt-transition.js +210 -0
- package/dist/worker/pool/begin-attempt-with-lease.js +26 -0
- package/dist/worker/pool/begin-attempt.js +35 -0
- package/dist/worker/pool/failure-routing.js +49 -0
- package/dist/worker/pool/recovery-decision.js +163 -0
- package/dist/worker/pool/run-owner-store.js +126 -0
- package/dist/worker/pool/run-store.js +32 -46
- package/dist/worker/pool/runtime-reconcile-inventory.js +127 -0
- package/dist/worker/pool/state-projection.js +57 -0
- package/dist/worker/run-task/run-task.js +31 -4
- package/dist/worker/runner/run-ready.js +64 -14
- package/dist/worker/runner/single-task-attempt.js +42 -13
- package/dist/worker/task-graph/acceptance-schema.js +3 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +162 -7
- package/dist/workflows/dag/backend-test-result-contract.js +105 -67
- package/dist/workflows/dag/backend-test-scenario-param.js +92 -30
- package/dist/workflows/dag/backend-test-writer-completeness.js +55 -0
- package/dist/workflows/dag/init-hybrid.js +44 -47
- package/dist/workflows/dag/rerun-task.js +86 -0
- package/docs/README.md +3 -1
- package/docs/architecture/evolution.md +1 -1
- package/docs/operations/README.md +1 -0
- package/docs/templates/README.md +1 -0
- package/docs/templates/agent-worker-production-readiness-checklist.md +45 -0
- package/docs/templates/backend-test-dag.json +40 -60
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +1 -1
- package/docs/templates/init-managed-agents.md +1 -1
- package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +11 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +11 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +11 -0
- package/package.json +1 -1
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/agent-worker/references/agent-worker-operator.md +1 -1
- package/skills/loop-agent/references/command-reference.md +2 -2
- package/skills/loop-agent/references/harness-policy.md +1 -1
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import YAML from "yaml";
|
|
5
4
|
import { writeTaskConfig, writeTaskArtifactFile, } from "../../infrastructure/harness/task-store.js";
|
|
5
|
+
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
6
6
|
import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
7
7
|
import { adoptTaskContract } from "../../task/contract/adopt.js";
|
|
8
8
|
import { observeTaskContractState } from "../../task/contract/observe.js";
|
|
9
9
|
import { sha256Utf8 } from "../../task/contract/hash.js";
|
|
10
10
|
import { resolveLoopAgentProfile, } from "../profile-mapping.js";
|
|
11
|
-
import { mapRiskLevelToComplexity } from "../task-spec/complexity-mapping.js";
|
|
12
11
|
import { resolveWorkflow } from "../task-spec/workflow-routing.js";
|
|
13
12
|
import { validateTaskSpec } from "../task-spec/validate.js";
|
|
13
|
+
import { mapRiskLevelToComplexity } from "../task-spec/complexity-mapping.js";
|
|
14
|
+
import { detectPausedAssociatedRun, detectRecoveryRequired, } from "./harness-task-lifecycle-probe.js";
|
|
15
|
+
import { buildHarnessTaskId as buildHarnessTaskLocatorId, buildHarnessTaskLineage, computeRevisionDigests, expandAllowedPathsForWorkflow, parseLineageIndex, parseLineageRevisionManifest, resolveRevisionDigestsForTaskSpec, } from "./harness-task-lineage.js";
|
|
16
|
+
const LINEAGE_MANIFEST_NAME = "lineage-revision-manifest.json";
|
|
17
|
+
const LINEAGE_INDEX_DIR = "_lineage-index";
|
|
14
18
|
export async function materializeTaskSpec(options) {
|
|
19
|
+
const result = await materializeTaskSpecV2(options);
|
|
20
|
+
return result.manifest;
|
|
21
|
+
}
|
|
22
|
+
export async function materializeTaskSpecV2(options) {
|
|
15
23
|
const now = options.now ?? new Date();
|
|
16
24
|
const validation = await validateTaskSpec(options.taskSpec, {
|
|
17
25
|
taskSpecPath: options.taskSpecPath,
|
|
@@ -20,43 +28,179 @@ export async function materializeTaskSpec(options) {
|
|
|
20
28
|
const codes = validation.errors.map((error) => error.code).join(", ");
|
|
21
29
|
throw new Error(`TaskSpec validation failed: ${codes}`);
|
|
22
30
|
}
|
|
23
|
-
const
|
|
24
|
-
|
|
31
|
+
const resolved = await resolveRevisionDigestsForTaskSpec({
|
|
32
|
+
taskSpec: options.taskSpec,
|
|
33
|
+
taskSpecPath: options.taskSpecPath,
|
|
34
|
+
});
|
|
35
|
+
const lineage = resolved.lineage;
|
|
36
|
+
const digests = resolved.digests;
|
|
37
|
+
const harnessTaskId = buildHarnessTaskLocatorId(lineage.lineageId, digests.revisionId);
|
|
38
|
+
const lineageIndex = await readLineageIndex(options.repoRoot, lineage.lineageId);
|
|
39
|
+
const existingManifest = await readLineageRevisionManifest(options.repoRoot, harnessTaskId);
|
|
40
|
+
assertManifestDigestConsistency(existingManifest, digests, harnessTaskId);
|
|
41
|
+
const indexEntry = lineageIndex?.revisions.find((entry) => entry.revisionId === digests.revisionId);
|
|
42
|
+
if (indexEntry &&
|
|
43
|
+
indexEntry.harnessTaskId !== harnessTaskId &&
|
|
44
|
+
indexEntry.sourceDigest === digests.sourceDigest &&
|
|
45
|
+
indexEntry.contractDigest === digests.contractDigest) {
|
|
46
|
+
throw new Error(`lineage index collision for ${lineage.lineageId}: revision ${digests.revisionId} maps to ${indexEntry.harnessTaskId}, expected ${harnessTaskId}`);
|
|
47
|
+
}
|
|
25
48
|
const existingConfig = await loadExistingTaskConfig(options.repoRoot, harnessTaskId);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
49
|
+
const pausedRun = existingConfig
|
|
50
|
+
? await detectPausedAssociatedRun(options.repoRoot, harnessTaskId)
|
|
51
|
+
: undefined;
|
|
52
|
+
if (pausedRun) {
|
|
53
|
+
const manifest = await buildManifestFromExisting({
|
|
54
|
+
repoRoot: options.repoRoot,
|
|
55
|
+
harnessTaskId,
|
|
56
|
+
taskSpec: options.taskSpec,
|
|
57
|
+
lineage,
|
|
58
|
+
digests,
|
|
59
|
+
workflow: resolved.workflow,
|
|
60
|
+
loopAgentProfile: resolved.loopAgentProfile,
|
|
61
|
+
materializeOutcome: "resume-existing",
|
|
31
62
|
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
63
|
+
return {
|
|
64
|
+
outcome: {
|
|
65
|
+
type: "resume-existing",
|
|
66
|
+
revisionId: digests.revisionId,
|
|
67
|
+
runId: pausedRun.runId,
|
|
68
|
+
},
|
|
69
|
+
manifest,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (existingConfig && existingManifest) {
|
|
73
|
+
const recovery = await detectRecoveryRequired(options.repoRoot, harnessTaskId, digests.revisionId);
|
|
74
|
+
if (recovery) {
|
|
75
|
+
return {
|
|
76
|
+
outcome: {
|
|
77
|
+
type: "recovery-required",
|
|
78
|
+
revisionId: digests.revisionId,
|
|
79
|
+
recovery,
|
|
80
|
+
},
|
|
81
|
+
manifest: await buildManifestFromExisting({
|
|
82
|
+
repoRoot: options.repoRoot,
|
|
83
|
+
harnessTaskId,
|
|
84
|
+
taskSpec: options.taskSpec,
|
|
85
|
+
lineage,
|
|
86
|
+
digests,
|
|
87
|
+
workflow: resolved.workflow,
|
|
88
|
+
loopAgentProfile: resolved.loopAgentProfile,
|
|
89
|
+
materializeOutcome: "recovery-required",
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
37
92
|
}
|
|
38
93
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
94
|
+
let outcome;
|
|
95
|
+
let previousRevisionId;
|
|
96
|
+
if (existingConfig && existingManifest) {
|
|
97
|
+
outcome = {
|
|
98
|
+
type: "idempotent-reuse",
|
|
99
|
+
lineageId: lineage.lineageId,
|
|
100
|
+
revisionId: digests.revisionId,
|
|
101
|
+
harnessTaskId,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
else if (lineageIndex && lineageIndex.revisions.length > 0) {
|
|
105
|
+
previousRevisionId = lineageIndex.currentRevisionId;
|
|
106
|
+
outcome = {
|
|
107
|
+
type: "new-revision",
|
|
108
|
+
lineageId: lineage.lineageId,
|
|
109
|
+
previousRevisionId,
|
|
110
|
+
revisionId: digests.revisionId,
|
|
111
|
+
harnessTaskId,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
outcome = {
|
|
116
|
+
type: "created",
|
|
117
|
+
lineageId: lineage.lineageId,
|
|
118
|
+
revisionId: digests.revisionId,
|
|
119
|
+
harnessTaskId,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const skipContractRewrite = outcome.type === "idempotent-reuse";
|
|
123
|
+
if (!existingConfig) {
|
|
124
|
+
await ensureHarnessTaskCreated({
|
|
125
|
+
repoRoot: options.repoRoot,
|
|
126
|
+
harnessTaskId,
|
|
127
|
+
title: options.taskSpec.title,
|
|
128
|
+
client: options.client,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (!skipContractRewrite) {
|
|
132
|
+
await writeHarnessTaskMaterials({
|
|
133
|
+
repoRoot: options.repoRoot,
|
|
134
|
+
harnessTaskId,
|
|
135
|
+
taskSpec: options.taskSpec,
|
|
136
|
+
taskSpecPath: options.taskSpecPath,
|
|
137
|
+
workflow: resolved.workflow,
|
|
138
|
+
taskKind: resolved.taskKind,
|
|
139
|
+
allowedPaths: resolved.allowedPaths,
|
|
140
|
+
now,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
await touchIdempotentReuseTask({
|
|
145
|
+
repoRoot: options.repoRoot,
|
|
146
|
+
harnessTaskId,
|
|
147
|
+
title: options.taskSpec.title,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const revision = {
|
|
151
|
+
schemaVersion: 1,
|
|
152
|
+
lineageId: lineage.lineageId,
|
|
153
|
+
revisionId: digests.revisionId,
|
|
154
|
+
sourceDigest: digests.sourceDigest,
|
|
155
|
+
contractDigest: digests.contractDigest,
|
|
156
|
+
createdAt: existingManifest?.revision.createdAt ?? now.toISOString(),
|
|
157
|
+
};
|
|
158
|
+
const lineageManifest = {
|
|
159
|
+
schemaVersion: 1,
|
|
160
|
+
lineage,
|
|
161
|
+
revision,
|
|
162
|
+
harnessTaskId,
|
|
163
|
+
...(previousRevisionId ? { previousRevisionId } : {}),
|
|
164
|
+
};
|
|
165
|
+
await writeLineageRevisionManifest(options.repoRoot, harnessTaskId, lineageManifest);
|
|
166
|
+
await upsertLineageIndex({
|
|
167
|
+
repoRoot: options.repoRoot,
|
|
168
|
+
lineage,
|
|
169
|
+
digests,
|
|
170
|
+
harnessTaskId,
|
|
171
|
+
createdAt: revision.createdAt,
|
|
172
|
+
});
|
|
173
|
+
const manifest = await buildMaterializeManifest({
|
|
174
|
+
repoRoot: options.repoRoot,
|
|
175
|
+
harnessTaskId,
|
|
176
|
+
taskSpec: options.taskSpec,
|
|
177
|
+
lineage,
|
|
178
|
+
digests,
|
|
179
|
+
workflow: resolved.workflow,
|
|
180
|
+
loopAgentProfile: resolved.loopAgentProfile,
|
|
181
|
+
materializeOutcome: outcome.type,
|
|
182
|
+
});
|
|
183
|
+
await writeTaskArtifactFile(options.repoRoot, harnessTaskId, "materialize-manifest.json", `${JSON.stringify(manifest, null, 2)}\n`);
|
|
184
|
+
return { outcome, manifest };
|
|
185
|
+
}
|
|
186
|
+
async function writeHarnessTaskMaterials(input) {
|
|
187
|
+
const paths = getTaskPaths(input.repoRoot, input.harnessTaskId);
|
|
188
|
+
const baseConfig = await loadTaskConfig(input.repoRoot, input.harnessTaskId);
|
|
45
189
|
const taskConfig = {
|
|
46
190
|
...baseConfig,
|
|
47
|
-
taskId: harnessTaskId,
|
|
48
|
-
title:
|
|
49
|
-
taskKind:
|
|
50
|
-
featureId:
|
|
51
|
-
allowedPaths,
|
|
52
|
-
forbiddenPaths:
|
|
53
|
-
hardConstraints:
|
|
54
|
-
complexity: mapRiskLevelToComplexity(
|
|
55
|
-
capabilities:
|
|
56
|
-
verifyMode:
|
|
57
|
-
verifyPreset:
|
|
58
|
-
verifyQuota:
|
|
59
|
-
verifyCommands:
|
|
191
|
+
taskId: input.harnessTaskId,
|
|
192
|
+
title: input.taskSpec.title,
|
|
193
|
+
taskKind: input.taskKind,
|
|
194
|
+
featureId: input.taskSpec.feature_id,
|
|
195
|
+
allowedPaths: input.allowedPaths,
|
|
196
|
+
forbiddenPaths: input.taskSpec.constraints.forbidden_paths,
|
|
197
|
+
hardConstraints: input.taskSpec.constraints.hard_constraints,
|
|
198
|
+
complexity: mapRiskLevelToComplexity(input.taskSpec.risk_level),
|
|
199
|
+
capabilities: input.taskSpec.capabilities ?? [],
|
|
200
|
+
verifyMode: input.taskSpec.verify.mode,
|
|
201
|
+
verifyPreset: input.taskSpec.verify.preset,
|
|
202
|
+
verifyQuota: input.taskSpec.verify.quota,
|
|
203
|
+
verifyCommands: input.taskSpec.verify.commands
|
|
60
204
|
.filter((command) => command.required)
|
|
61
205
|
.map((command) => ({
|
|
62
206
|
label: command.label,
|
|
@@ -65,7 +209,7 @@ export async function materializeTaskSpec(options) {
|
|
|
65
209
|
})),
|
|
66
210
|
autoCommitAfterVerify: false,
|
|
67
211
|
};
|
|
68
|
-
await writeTaskConfig(
|
|
212
|
+
await writeTaskConfig(input.repoRoot, input.harnessTaskId, taskConfig);
|
|
69
213
|
const sourcePaths = {
|
|
70
214
|
requirementPath: path.join(paths.sourceDir, "需求.md"),
|
|
71
215
|
constraintsPath: path.join(paths.sourceDir, "执行约束.md"),
|
|
@@ -73,48 +217,170 @@ export async function materializeTaskSpec(options) {
|
|
|
73
217
|
};
|
|
74
218
|
await mkdir(paths.sourceDir, { recursive: true });
|
|
75
219
|
const referencePaths = await materializeSourceDocs({
|
|
76
|
-
taskSpec:
|
|
77
|
-
taskSpecPath:
|
|
220
|
+
taskSpec: input.taskSpec,
|
|
221
|
+
taskSpecPath: input.taskSpecPath,
|
|
78
222
|
sourceDir: paths.sourceDir,
|
|
79
223
|
});
|
|
80
224
|
const sourceDocTrace = await buildSourceDocTrace({
|
|
81
|
-
taskSpec:
|
|
82
|
-
taskSpecPath:
|
|
225
|
+
taskSpec: input.taskSpec,
|
|
226
|
+
taskSpecPath: input.taskSpecPath,
|
|
83
227
|
referencePaths,
|
|
84
228
|
});
|
|
85
229
|
const acceptanceSummaries = await buildAcceptanceSummaries({
|
|
86
|
-
taskSpec:
|
|
87
|
-
taskSpecPath:
|
|
230
|
+
taskSpec: input.taskSpec,
|
|
231
|
+
taskSpecPath: input.taskSpecPath,
|
|
88
232
|
referencePaths,
|
|
89
233
|
});
|
|
90
|
-
await writeFile(sourcePaths.requirementPath, renderRequirementMarkdown(
|
|
234
|
+
await writeFile(sourcePaths.requirementPath, renderRequirementMarkdown(input.taskSpec, {
|
|
91
235
|
sourceDocTrace,
|
|
92
236
|
acceptanceSummaries,
|
|
93
237
|
}), "utf-8");
|
|
94
|
-
await writeFile(sourcePaths.constraintsPath, renderConstraintsMarkdown(
|
|
95
|
-
await writeFile(sourcePaths.taskYamlPath, YAML.stringify(
|
|
238
|
+
await writeFile(sourcePaths.constraintsPath, renderConstraintsMarkdown(input.taskSpec), "utf-8");
|
|
239
|
+
await writeFile(sourcePaths.taskYamlPath, YAML.stringify(input.taskSpec), "utf-8");
|
|
96
240
|
await ensureMaterializedTaskContractManaged({
|
|
97
|
-
repoRoot:
|
|
98
|
-
taskId: harnessTaskId,
|
|
99
|
-
taskSpecPath:
|
|
241
|
+
repoRoot: input.repoRoot,
|
|
242
|
+
taskId: input.harnessTaskId,
|
|
243
|
+
taskSpecPath: input.taskSpecPath,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
async function touchIdempotentReuseTask(input) {
|
|
247
|
+
const baseConfig = await loadTaskConfig(input.repoRoot, input.harnessTaskId);
|
|
248
|
+
if (baseConfig.title === input.title)
|
|
249
|
+
return;
|
|
250
|
+
await writeTaskConfig(input.repoRoot, input.harnessTaskId, {
|
|
251
|
+
...baseConfig,
|
|
252
|
+
title: input.title,
|
|
100
253
|
});
|
|
101
|
-
|
|
102
|
-
|
|
254
|
+
}
|
|
255
|
+
async function ensureHarnessTaskCreated(input) {
|
|
256
|
+
const newTaskResult = await input.client.run(["task", "advance", input.harnessTaskId, input.title, "--json"], {
|
|
257
|
+
cwd: input.repoRoot,
|
|
258
|
+
artifactName: `task-advance-create-${input.harnessTaskId}`,
|
|
259
|
+
});
|
|
260
|
+
if (!newTaskResult.ok) {
|
|
261
|
+
const racedConfig = await loadExistingTaskConfig(input.repoRoot, input.harnessTaskId);
|
|
262
|
+
if (!racedConfig) {
|
|
263
|
+
throw new Error(`loop-agent task advance (create) failed with exit ${newTaskResult.exitCode}: ${newTaskResult.stderr}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function assertManifestDigestConsistency(existingManifest, digests, harnessTaskId) {
|
|
268
|
+
if (!existingManifest)
|
|
269
|
+
return;
|
|
270
|
+
if (existingManifest.revision.sourceDigest !== digests.sourceDigest ||
|
|
271
|
+
existingManifest.revision.contractDigest !== digests.contractDigest) {
|
|
272
|
+
throw new Error(`lineage manifest digest mismatch for ${harnessTaskId}: stored revision does not match canonical TaskSpec digest (fail-closed)`);
|
|
273
|
+
}
|
|
274
|
+
if (existingManifest.revision.revisionId !== digests.revisionId) {
|
|
275
|
+
throw new Error(`lineage manifest revisionId mismatch for ${harnessTaskId}: stored ${existingManifest.revision.revisionId} vs canonical ${digests.revisionId}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function lineageIndexPath(repoRoot, lineageId) {
|
|
279
|
+
return path.join(repoRoot, ".harness", "tasks", LINEAGE_INDEX_DIR, `${lineageId}.json`);
|
|
280
|
+
}
|
|
281
|
+
function lineageManifestPath(repoRoot, harnessTaskId) {
|
|
282
|
+
return path.join(repoRoot, ".harness", "tasks", harnessTaskId, "artifacts", LINEAGE_MANIFEST_NAME);
|
|
283
|
+
}
|
|
284
|
+
async function readLineageRevisionManifest(repoRoot, harnessTaskId) {
|
|
285
|
+
try {
|
|
286
|
+
const raw = await readFile(lineageManifestPath(repoRoot, harnessTaskId), "utf-8");
|
|
287
|
+
return parseLineageRevisionManifest(JSON.parse(raw));
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
if (isNotFound(error))
|
|
291
|
+
return undefined;
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function writeLineageRevisionManifest(repoRoot, harnessTaskId, manifest) {
|
|
296
|
+
await writeTaskArtifactFile(repoRoot, harnessTaskId, LINEAGE_MANIFEST_NAME, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
297
|
+
}
|
|
298
|
+
async function readLineageIndex(repoRoot, lineageId) {
|
|
299
|
+
try {
|
|
300
|
+
const raw = await readFile(lineageIndexPath(repoRoot, lineageId), "utf-8");
|
|
301
|
+
return parseLineageIndex(JSON.parse(raw));
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (isNotFound(error))
|
|
305
|
+
return undefined;
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function upsertLineageIndex(input) {
|
|
310
|
+
const indexPath = lineageIndexPath(input.repoRoot, input.lineage.lineageId);
|
|
311
|
+
await mkdir(path.dirname(indexPath), { recursive: true });
|
|
312
|
+
const existing = await readLineageIndex(input.repoRoot, input.lineage.lineageId);
|
|
313
|
+
const revisions = existing?.revisions ?? [];
|
|
314
|
+
const withoutCurrent = revisions.filter((entry) => entry.revisionId !== input.digests.revisionId);
|
|
315
|
+
const next = {
|
|
103
316
|
schemaVersion: 1,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
317
|
+
lineageId: input.lineage.lineageId,
|
|
318
|
+
featureId: input.lineage.featureId,
|
|
319
|
+
taskId: input.lineage.taskId,
|
|
320
|
+
currentRevisionId: input.digests.revisionId,
|
|
321
|
+
revisions: [
|
|
322
|
+
...withoutCurrent,
|
|
323
|
+
{
|
|
324
|
+
revisionId: input.digests.revisionId,
|
|
325
|
+
harnessTaskId: input.harnessTaskId,
|
|
326
|
+
sourceDigest: input.digests.sourceDigest,
|
|
327
|
+
contractDigest: input.digests.contractDigest,
|
|
328
|
+
createdAt: input.createdAt,
|
|
329
|
+
},
|
|
330
|
+
].sort((left, right) => left.revisionId.localeCompare(right.revisionId)),
|
|
331
|
+
};
|
|
332
|
+
await writeJsonAtomic(indexPath, next, { repoRoot: input.repoRoot });
|
|
333
|
+
}
|
|
334
|
+
async function buildManifestFromExisting(input) {
|
|
335
|
+
return buildMaterializeManifest(input);
|
|
336
|
+
}
|
|
337
|
+
async function buildMaterializeManifest(input) {
|
|
338
|
+
const paths = getTaskPaths(input.repoRoot, input.harnessTaskId);
|
|
339
|
+
const referencePaths = await readExistingReferencePaths(paths.sourceDir, input.taskSpec);
|
|
340
|
+
return {
|
|
341
|
+
schemaVersion: 1,
|
|
342
|
+
businessId: input.taskSpec.id,
|
|
343
|
+
harnessTaskId: input.harnessTaskId,
|
|
344
|
+
featureId: input.taskSpec.feature_id,
|
|
345
|
+
lineageId: input.lineage.lineageId,
|
|
346
|
+
revisionId: input.digests.revisionId,
|
|
347
|
+
sourceDigest: input.digests.sourceDigest,
|
|
348
|
+
contractDigest: input.digests.contractDigest,
|
|
349
|
+
materializeOutcome: input.materializeOutcome,
|
|
350
|
+
workflow: input.workflow,
|
|
351
|
+
loopAgentProfile: input.loopAgentProfile,
|
|
109
352
|
taskConfigPath: paths.taskConfigPath,
|
|
110
353
|
source: {
|
|
111
|
-
|
|
112
|
-
|
|
354
|
+
requirementPath: path.join(paths.sourceDir, "需求.md"),
|
|
355
|
+
constraintsPath: path.join(paths.sourceDir, "执行约束.md"),
|
|
356
|
+
taskYamlPath: path.join(paths.sourceDir, "task.yaml"),
|
|
113
357
|
referencePaths,
|
|
114
358
|
},
|
|
115
359
|
};
|
|
116
|
-
|
|
117
|
-
|
|
360
|
+
}
|
|
361
|
+
// Re-export lineage helpers used by tests and callers.
|
|
362
|
+
export { expandAllowedPathsForWorkflow } from "./harness-task-lineage.js";
|
|
363
|
+
async function readExistingReferencePaths(sourceDir, taskSpec) {
|
|
364
|
+
const referenceDir = path.join(sourceDir, "references");
|
|
365
|
+
const referencePaths = {};
|
|
366
|
+
for (const key of Object.keys(taskSpec.source_docs)) {
|
|
367
|
+
const candidates = [
|
|
368
|
+
path.join(referenceDir, `${key}.md`),
|
|
369
|
+
path.join(referenceDir, `${key}.yaml`),
|
|
370
|
+
path.join(referenceDir, `${key}.yml`),
|
|
371
|
+
];
|
|
372
|
+
for (const candidate of candidates) {
|
|
373
|
+
try {
|
|
374
|
+
await readFile(candidate);
|
|
375
|
+
referencePaths[key] = candidate;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// continue
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return referencePaths;
|
|
118
384
|
}
|
|
119
385
|
async function materializeSourceDocs(input) {
|
|
120
386
|
const referenceDir = path.join(input.sourceDir, "references");
|
|
@@ -132,20 +398,6 @@ async function materializeSourceDocs(input) {
|
|
|
132
398
|
}
|
|
133
399
|
return referencePaths;
|
|
134
400
|
}
|
|
135
|
-
export function buildHarnessTaskId(taskSpec, now) {
|
|
136
|
-
const date = now.toISOString().slice(0, 10);
|
|
137
|
-
const slug = slugify(`${taskSpec.id}-${taskSpec.title}`);
|
|
138
|
-
return `${date}-${slug}`;
|
|
139
|
-
}
|
|
140
|
-
function slugify(value) {
|
|
141
|
-
return value
|
|
142
|
-
.normalize("NFKD")
|
|
143
|
-
.replace(/[\u0300-\u036f]/g, "")
|
|
144
|
-
.toLowerCase()
|
|
145
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
146
|
-
.replace(/^-+|-+$/g, "")
|
|
147
|
-
.replace(/-{2,}/g, "-");
|
|
148
|
-
}
|
|
149
401
|
/** Exported for unit tests: derived contract must point back to immutable references. */
|
|
150
402
|
async function ensureMaterializedTaskContractManaged(input) {
|
|
151
403
|
const state = await observeTaskContractState(input.repoRoot, input.taskId);
|
|
@@ -217,6 +469,7 @@ export function renderRequirementMarkdown(taskSpec, options = {}) {
|
|
|
217
469
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
218
470
|
}
|
|
219
471
|
async function buildSourceDocTrace(input) {
|
|
472
|
+
const { createHash } = await import("node:crypto");
|
|
220
473
|
const entries = [];
|
|
221
474
|
for (const [key, relativePath] of Object.entries(input.taskSpec.source_docs)) {
|
|
222
475
|
if (!relativePath)
|
|
@@ -351,15 +604,6 @@ function bulletLines(values) {
|
|
|
351
604
|
return ["- None"];
|
|
352
605
|
return values.map((value) => `- ${value}`);
|
|
353
606
|
}
|
|
354
|
-
/** Union TaskSpec allowed_paths with workflow-owned write roots. */
|
|
355
|
-
export function expandAllowedPathsForWorkflow(workflow, allowedPaths) {
|
|
356
|
-
const extras = workflow === "backend-test"
|
|
357
|
-
? ["testcase/**", "docs/test-reports/**"]
|
|
358
|
-
: workflow === "frontend-test"
|
|
359
|
-
? ["testcase/frontend/**", "docs/test-reports/**"]
|
|
360
|
-
: [];
|
|
361
|
-
return Array.from(new Set([...allowedPaths, ...extras]));
|
|
362
|
-
}
|
|
363
607
|
async function loadExistingTaskConfig(repoRoot, taskId) {
|
|
364
608
|
try {
|
|
365
609
|
return await loadTaskConfig(repoRoot, taskId);
|
|
@@ -376,3 +620,29 @@ function isNotFound(error) {
|
|
|
376
620
|
"code" in error &&
|
|
377
621
|
error.code === "ENOENT");
|
|
378
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* Resolve the bounded harness task locator for a TaskSpec.
|
|
625
|
+
* `now` is accepted for legacy callers but does not affect identity.
|
|
626
|
+
*/
|
|
627
|
+
export async function buildHarnessTaskId(taskSpec, _now, taskSpecPath) {
|
|
628
|
+
if (taskSpecPath) {
|
|
629
|
+
const resolved = await resolveRevisionDigestsForTaskSpec({
|
|
630
|
+
taskSpec,
|
|
631
|
+
taskSpecPath,
|
|
632
|
+
});
|
|
633
|
+
return buildHarnessTaskLocatorId(resolved.lineage.lineageId, resolved.digests.revisionId);
|
|
634
|
+
}
|
|
635
|
+
const lineage = buildHarnessTaskLineage(taskSpec);
|
|
636
|
+
const resolvedWorkflow = resolveWorkflow(taskSpec);
|
|
637
|
+
const profileMapping = resolveLoopAgentProfile(taskSpec);
|
|
638
|
+
const allowedPaths = expandAllowedPathsForWorkflow(resolvedWorkflow.workflow, taskSpec.constraints.allowed_paths);
|
|
639
|
+
const { revisionId } = computeRevisionDigests({
|
|
640
|
+
taskSpec,
|
|
641
|
+
workflow: resolvedWorkflow.workflow,
|
|
642
|
+
taskKind: resolvedWorkflow.taskKind,
|
|
643
|
+
loopAgentProfile: profileMapping.loopAgentProfile,
|
|
644
|
+
allowedPaths,
|
|
645
|
+
sourceRefHashes: {},
|
|
646
|
+
});
|
|
647
|
+
return buildHarnessTaskLocatorId(lineage.lineageId, revisionId);
|
|
648
|
+
}
|
|
@@ -21,6 +21,7 @@ export function createCompositeProgressReporter(options) {
|
|
|
21
21
|
batchRunId: input.batchRunId ?? context.batchRunId,
|
|
22
22
|
workerRunId: input.workerRunId ?? context.workerRunId,
|
|
23
23
|
taskId: input.taskId ?? context.taskId,
|
|
24
|
+
featureId: input.featureId ?? context.featureId,
|
|
24
25
|
}, { now });
|
|
25
26
|
await eventStore.append(event);
|
|
26
27
|
}
|