@tea-agent/loop-agent 0.26.5-beta.2 → 0.27.1-beta.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 +2 -2
- package/CHANGELOG.md +33 -0
- package/README.md +13 -23
- package/dist/application/dag/args.js +7 -7
- package/dist/application/dag/generate-task-dag.js +2 -2
- package/dist/application/dag/run-dag.js +3 -4
- package/dist/application/dag/validate-dag.js +1 -1
- package/dist/application/task-lifecycle/advance.js +845 -0
- package/dist/application/task-lifecycle/gates.js +80 -0
- package/dist/application/task-lifecycle/index.js +7 -0
- package/dist/application/task-lifecycle/observe.js +395 -0
- package/dist/application/task-lifecycle/plan-transitions.js +224 -0
- package/dist/application/task-lifecycle/recommendations.js +180 -0
- package/dist/application/task-lifecycle/record.js +73 -0
- package/dist/application/task-lifecycle/types.js +1 -0
- package/dist/cli/command-definitions.js +14 -111
- package/dist/cli/help.js +6 -7
- package/dist/cli/program.js +26 -90
- package/dist/cli/update/policy.js +0 -1
- package/dist/commands/dag-final-verification.js +1 -1
- package/dist/commands/dag-init-hybrid.js +1 -1
- package/dist/commands/delegate.js +2 -2
- package/dist/commands/init.js +21 -19
- package/dist/commands/run-dag-progress.js +1 -1
- package/dist/commands/status.js +18 -17
- package/dist/commands/study-init.js +2 -2
- package/dist/commands/task-advance.js +335 -0
- package/dist/commands/task-contract.js +3 -5
- package/dist/commands/task-source-prepare.js +9 -4
- package/dist/commands/task-status.js +133 -0
- package/dist/executors/dag-pi-executor.js +18 -22
- package/dist/executors/shell-executor.js +41 -25
- package/dist/governance/manifest-types.js +2 -2
- package/dist/shared/operator/capabilities.js +1358 -243
- package/dist/task/contract/adopt.js +1 -1
- package/dist/task/contract/apply.js +1 -1
- package/dist/task/contract/import-revision.js +1 -1
- package/dist/task/contract/recover.js +2 -2
- package/dist/task/read-model.js +18 -23
- package/dist/task/runtime.js +1 -1
- package/dist/task/source-prepare/completeness.js +1 -1
- package/dist/task/source-prepare/parse-intent.js +6 -1
- package/dist/task/source-prepare/prepare.js +23 -20
- package/dist/worker/console/operator-actions.js +288 -95
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/static/assets/{index-CSRIhuzh.js → index-CNO7n6qB.js} +1 -1
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/materialize/harness-task-materializer.js +6 -5
- package/dist/worker/run-task/run-task.js +204 -112
- package/dist/worker/runner/run-ready.js +1 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +49 -23
- package/dist/workflows/dag/backend-test-pytest-collection.js +57 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +3 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +18 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +46 -7
- package/dist/workflows/dag/init-hybrid.js +6 -35
- package/dist/workflows/dag/output-protocol.js +23 -0
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +3 -3
- package/docs/templates/frontend-implementation-contract.schema.json +2 -2
- package/docs/templates/harness.schema.json +2 -2
- package/docs/templates/init-managed-agents.md +13 -16
- package/docs/templates/production-readiness-checklist.md +3 -3
- package/harness.json +1 -1
- package/package.json +1 -1
- package/scripts/kb-bootstrap-init-skeleton.sh +2 -1
- package/scripts/kb-graph-incremental-prepare.mjs +2 -2
- package/skills/loop-agent/SKILL.md +18 -13
- package/skills/loop-agent/references/README.md +1 -1
- package/skills/loop-agent/references/command-reference.md +55 -84
- package/skills/loop-agent/references/harness-policy.md +19 -23
- package/skills/loop-agent/references/hybrid-dag.md +32 -34
- package/skills/loop-agent/references/long-running-loop.md +2 -2
- package/skills/loop-agent/references/one-shot-runs.md +4 -5
- package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
- package/skills/loop-agent/references/post-implementation-and-patterns.md +4 -4
- package/skills/loop-agent/references/source-and-plan-practice.md +45 -51
- package/skills/loop-agent/references/task-workflow.md +14 -15
|
@@ -11,22 +11,34 @@ export const backendPytestCollectionFindingSchema = z.object({
|
|
|
11
11
|
detail: z.string().min(1),
|
|
12
12
|
}).strict();
|
|
13
13
|
export const backendPytestCollectionFactsSchema = z.object({
|
|
14
|
-
schemaId: z.literal("backend-test-pytest-collection-
|
|
14
|
+
schemaId: z.literal("backend-test-pytest-collection-v2"),
|
|
15
15
|
phase: z.enum(["initial", "final", "effective"]),
|
|
16
16
|
status: z.enum(["PASS", "REPAIRABLE", "BLOCKED"]),
|
|
17
17
|
repairEligible: z.boolean(),
|
|
18
18
|
repairAttempt: z.number().int().min(0).max(1),
|
|
19
|
+
collectionAttempted: z.boolean(),
|
|
19
20
|
mappedScripts: z.array(z.string()).min(1),
|
|
20
|
-
|
|
21
|
+
existingMappedScripts: z.array(z.string()),
|
|
22
|
+
missingMappedScripts: z.array(z.string()),
|
|
23
|
+
assetFiles: z.array(z.string()),
|
|
21
24
|
inputHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
22
|
-
pytestExitCode: z.number().int(),
|
|
25
|
+
pytestExitCode: z.number().int().nullable(),
|
|
23
26
|
collectedItemCount: z.number().int().min(0),
|
|
24
27
|
collectedItemIds: z.array(z.string()),
|
|
25
28
|
findings: z.array(backendPytestCollectionFindingSchema),
|
|
26
29
|
stdoutExcerpt: z.string(),
|
|
27
30
|
stderrExcerpt: z.string(),
|
|
28
31
|
collectionSource: z.enum(["initial", "final"]).optional(),
|
|
29
|
-
}).strict()
|
|
32
|
+
}).strict().superRefine((facts, context) => {
|
|
33
|
+
if (facts.status === "PASS") {
|
|
34
|
+
if (!facts.collectionAttempted || facts.pytestExitCode !== 0 || facts.missingMappedScripts.length > 0 || facts.assetFiles.length === 0) {
|
|
35
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "backend pytest collection PASS requires attempted collection, exit 0, complete mapped scripts and bound assets" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!facts.collectionAttempted && facts.pytestExitCode !== null) {
|
|
39
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "backend pytest collection without an attempt cannot have an exit code" });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
30
42
|
function repoRef(workspaceRoot, absolutePath) {
|
|
31
43
|
return path.relative(workspaceRoot, absolutePath).replaceAll(path.sep, "/");
|
|
32
44
|
}
|
|
@@ -56,10 +68,11 @@ async function walkPythonFiles(root) {
|
|
|
56
68
|
await visit(root);
|
|
57
69
|
return results;
|
|
58
70
|
}
|
|
59
|
-
export async function buildBackendPytestAssetInventory(workspaceRoot, mappedScripts) {
|
|
71
|
+
export async function buildBackendPytestAssetInventory(workspaceRoot, mappedScripts, options = {}) {
|
|
60
72
|
const normalizedScripts = [...new Set(mappedScripts.map((value) => value.replaceAll("\\", "/")))].sort();
|
|
61
|
-
if (normalizedScripts.length === 0)
|
|
73
|
+
if ((options.requireMappedScripts ?? true) && normalizedScripts.length === 0) {
|
|
62
74
|
throw new Error("backend pytest collection requires mapped scripts");
|
|
75
|
+
}
|
|
63
76
|
for (const script of normalizedScripts) {
|
|
64
77
|
if (!/^testcase(?:\/[A-Za-z0-9_.-]+)*\/test_[A-Za-z0-9_.-]+\.py$/.test(script) && !/^testcase\/test_[A-Za-z0-9_.-]+\.py$/.test(script)) {
|
|
65
78
|
throw new Error(`unsafe backend pytest mapped script: ${script}`);
|
|
@@ -123,12 +136,15 @@ export function assessBackendPytestCollection(input) {
|
|
|
123
136
|
const items = collectionItems(input.stdout);
|
|
124
137
|
if (input.exitCode === 0) {
|
|
125
138
|
return backendPytestCollectionFactsSchema.parse({
|
|
126
|
-
schemaId: "backend-test-pytest-collection-
|
|
139
|
+
schemaId: "backend-test-pytest-collection-v2",
|
|
127
140
|
phase: input.phase,
|
|
128
141
|
status: "PASS",
|
|
129
142
|
repairEligible: false,
|
|
130
143
|
repairAttempt: input.phase === "final" ? 1 : 0,
|
|
144
|
+
collectionAttempted: true,
|
|
131
145
|
mappedScripts: input.inventory.mappedScripts,
|
|
146
|
+
existingMappedScripts: input.inventory.mappedScripts,
|
|
147
|
+
missingMappedScripts: [],
|
|
132
148
|
assetFiles: input.inventory.assetFiles,
|
|
133
149
|
inputHashes: input.inventory.inputHashes,
|
|
134
150
|
pytestExitCode: input.exitCode,
|
|
@@ -141,12 +157,15 @@ export function assessBackendPytestCollection(input) {
|
|
|
141
157
|
}
|
|
142
158
|
const classification = classifyCollectionFailure(`${input.stdout}\n${input.stderr}`);
|
|
143
159
|
return backendPytestCollectionFactsSchema.parse({
|
|
144
|
-
schemaId: "backend-test-pytest-collection-
|
|
160
|
+
schemaId: "backend-test-pytest-collection-v2",
|
|
145
161
|
phase: input.phase,
|
|
146
162
|
status: classification.status,
|
|
147
163
|
repairEligible: input.phase === "initial" && classification.status === "REPAIRABLE",
|
|
148
164
|
repairAttempt: input.phase === "final" ? 1 : 0,
|
|
165
|
+
collectionAttempted: true,
|
|
149
166
|
mappedScripts: input.inventory.mappedScripts,
|
|
167
|
+
existingMappedScripts: input.inventory.mappedScripts,
|
|
168
|
+
missingMappedScripts: [],
|
|
150
169
|
assetFiles: input.inventory.assetFiles,
|
|
151
170
|
inputHashes: input.inventory.inputHashes,
|
|
152
171
|
pytestExitCode: input.exitCode,
|
|
@@ -162,6 +181,32 @@ export function assessBackendPytestCollection(input) {
|
|
|
162
181
|
stderrExcerpt: bounded(input.stderr),
|
|
163
182
|
});
|
|
164
183
|
}
|
|
184
|
+
export function assessMissingBackendPytestScripts(input) {
|
|
185
|
+
return backendPytestCollectionFactsSchema.parse({
|
|
186
|
+
schemaId: "backend-test-pytest-collection-v2",
|
|
187
|
+
phase: "initial",
|
|
188
|
+
status: "REPAIRABLE",
|
|
189
|
+
repairEligible: true,
|
|
190
|
+
repairAttempt: 0,
|
|
191
|
+
collectionAttempted: false,
|
|
192
|
+
mappedScripts: [...input.mappedScripts],
|
|
193
|
+
existingMappedScripts: [...input.existingMappedScripts],
|
|
194
|
+
missingMappedScripts: [...input.missingMappedScripts],
|
|
195
|
+
assetFiles: [...input.assetFiles],
|
|
196
|
+
inputHashes: input.inputHashes,
|
|
197
|
+
pytestExitCode: null,
|
|
198
|
+
collectedItemCount: 0,
|
|
199
|
+
collectedItemIds: [],
|
|
200
|
+
findings: input.missingMappedScripts.map((script) => ({
|
|
201
|
+
kind: "missing-mapped-pytest-script",
|
|
202
|
+
classification: "test-asset-defect",
|
|
203
|
+
repairability: "repairable",
|
|
204
|
+
detail: `Markdown-mapped generated pytest script is missing: ${script}`,
|
|
205
|
+
})),
|
|
206
|
+
stdoutExcerpt: "",
|
|
207
|
+
stderrExcerpt: "",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
165
210
|
export function renderBackendPytestCollectionReport(facts) {
|
|
166
211
|
return [
|
|
167
212
|
`# Backend pytest Collection ${facts.phase}`,
|
|
@@ -172,8 +217,11 @@ export function renderBackendPytestCollectionReport(facts) {
|
|
|
172
217
|
"",
|
|
173
218
|
`- Repair eligible: ${facts.repairEligible}`,
|
|
174
219
|
`- Repair attempt: ${facts.repairAttempt}`,
|
|
175
|
-
`-
|
|
220
|
+
`- Collection attempted: ${facts.collectionAttempted}`,
|
|
221
|
+
`- Pytest exit code: ${facts.pytestExitCode ?? "not-run"}`,
|
|
176
222
|
`- Mapped scripts: ${facts.mappedScripts.length}`,
|
|
223
|
+
`- Existing mapped scripts: ${facts.existingMappedScripts.length}`,
|
|
224
|
+
`- Missing mapped scripts: ${facts.missingMappedScripts.length}`,
|
|
177
225
|
`- Bound Python assets: ${facts.assetFiles.length}`,
|
|
178
226
|
`- Collected items: ${facts.collectedItemCount}`,
|
|
179
227
|
"",
|
|
@@ -155,7 +155,7 @@ export const frontendImplementationContractSchema = z
|
|
|
155
155
|
"not-needed",
|
|
156
156
|
]),
|
|
157
157
|
productionDefaultOff: z.literal(true),
|
|
158
|
-
activation: z.preprocess((value) => (value === "" ? "explicit activation boundary" : value), z.string().min(1)),
|
|
158
|
+
activation: z.preprocess((value) => (value === "" || value === null ? "explicit activation boundary" : value), z.string().min(1)),
|
|
159
159
|
endpoints: z.array(z
|
|
160
160
|
.object({
|
|
161
161
|
method: z.enum([
|
|
@@ -171,7 +171,7 @@ export const frontendImplementationContractSchema = z
|
|
|
171
171
|
// Models occasionally emit an empty fixture when Mock is
|
|
172
172
|
// intentionally not needed. Treat it like an omitted optional
|
|
173
173
|
// field; active Mock strategies still fail the refinement below.
|
|
174
|
-
fixture: z.preprocess((value) => (value === "" ? undefined : value), safePath.optional()),
|
|
174
|
+
fixture: z.preprocess((value) => (value === "" || value === null ? undefined : value), safePath.optional()),
|
|
175
175
|
consumer: safePath.optional(),
|
|
176
176
|
})
|
|
177
177
|
.strict()),
|
|
@@ -190,7 +190,7 @@ export const frontendImplementationContractSchema = z
|
|
|
190
190
|
type: z.enum(["static", "unit", "component", "integration", "mock"]),
|
|
191
191
|
commandLabel: z.string().min(1),
|
|
192
192
|
file: safePath,
|
|
193
|
-
symbol: z.preprocess((value) => (value === "" ? undefined : value), z.string().min(1).optional()),
|
|
193
|
+
symbol: z.preprocess((value) => (value === "" || value === null ? undefined : value), z.string().min(1).optional()),
|
|
194
194
|
requirementIds: z.array(id),
|
|
195
195
|
uiStates: z.array(z.string().min(1)),
|
|
196
196
|
})
|
|
@@ -128,6 +128,23 @@ async function checkOpenspecReadEvidence(input) {
|
|
|
128
128
|
}
|
|
129
129
|
return [...matched];
|
|
130
130
|
}
|
|
131
|
+
async function existingOpenspecCandidates(candidates, repoRoot) {
|
|
132
|
+
const existing = [];
|
|
133
|
+
for (const candidate of candidates) {
|
|
134
|
+
if (!isOpenspecSpecFilePath(candidate))
|
|
135
|
+
continue;
|
|
136
|
+
try {
|
|
137
|
+
const info = await stat(path.resolve(repoRoot, candidate));
|
|
138
|
+
if (info.isFile())
|
|
139
|
+
existing.push(candidate);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Model-generated typos and stale capability paths are not readable
|
|
143
|
+
// evidence candidates and must not create a false prewrite block.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return existing;
|
|
147
|
+
}
|
|
131
148
|
export async function runFrontendPrewriteGate(input) {
|
|
132
149
|
const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
|
|
133
150
|
if (workspaceRoot) {
|
|
@@ -188,7 +205,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
188
205
|
throw new Error(`frontend prewrite gate contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`);
|
|
189
206
|
}
|
|
190
207
|
}
|
|
191
|
-
const candidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
208
|
+
const candidatePaths = await existingOpenspecCandidates(input.config.openspecCandidatePaths ?? [], workspaceRoot ?? process.cwd());
|
|
192
209
|
const openspecReadPaths = await checkOpenspecReadEvidence({
|
|
193
210
|
runDir: input.runDir,
|
|
194
211
|
candidatePaths,
|
|
@@ -102,13 +102,52 @@ function renderMarkdown(result, metrics) {
|
|
|
102
102
|
].join("\n");
|
|
103
103
|
}
|
|
104
104
|
function renderHtml(result, metrics) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
const m = metrics.metrics;
|
|
106
|
+
const decision = metrics.status === "ready"
|
|
107
|
+
? { text: "READY", color: "#15815c", bg: "rgba(21,129,92,.12)", border: "rgba(120,200,170,.4)", small: "所有 L-5 门禁达标" }
|
|
108
|
+
: { text: "NOT READY", color: "#d34661", bg: "rgba(211,70,97,.12)", border: "rgba(255,177,192,.4)", small: `${metrics.blockingItems.length} 个阻断项需要处理` };
|
|
109
|
+
const executed = result.totals.passed + result.totals.failed;
|
|
110
|
+
const passRate = m.passRate.ratio === null ? "—" : `${(m.passRate.ratio * 100).toFixed(2)}%`;
|
|
111
|
+
const acRate = m.acCoverage.ratio === null ? "—" : `${(m.acCoverage.ratio * 100).toFixed(2)}%`;
|
|
112
|
+
const automationRate = m.automationCoverage.ratio === null ? "—" : `${(m.automationCoverage.ratio * 100).toFixed(2)}%`;
|
|
113
|
+
const circ = 2 * Math.PI * 46;
|
|
114
|
+
const passLen = executed > 0 ? (result.totals.passed / executed) * circ : 0;
|
|
115
|
+
const failLen = executed > 0 ? (result.totals.failed / executed) * circ : 0;
|
|
116
|
+
const statusColor = (status) => status === "pass" ? "#15815c" : status === "fail" ? "#d34661" : "#c87918";
|
|
117
|
+
const statusBg = (status) => status === "pass" ? "#e7f7f0" : status === "fail" ? "#fff0f3" : "#fff7e8";
|
|
118
|
+
const gate = (label, metric, value, target, detail, evidence = "") => {
|
|
119
|
+
const color = statusColor(metric.status);
|
|
120
|
+
const width = metric.ratio === null ? 0 : Math.min(100, metric.ratio * 100);
|
|
121
|
+
const statusLabel = metric.status === "unavailable" ? "不可用" : metric.status === "pass" ? "达标" : "未达标";
|
|
122
|
+
const evidenceBlock = evidence
|
|
123
|
+
? `<details style="margin-top:10px;padding-top:9px;border-top:1px solid #e5ebf3"><summary style="color:#4775ef;font-size:12px;font-weight:750;cursor:pointer;list-style:none">查看依据</summary><div style="margin-top:7px;color:#718097;font-size:12px">${evidence}</div></details>`
|
|
124
|
+
: "";
|
|
125
|
+
return `<div style="padding:14px 15px;border:1px solid #e5ebf3;border-radius:11px;background:#fcfdff;margin-top:8px"><div style="display:flex;align-items:center;gap:10px"><span style="flex:1;color:#263c5e;font-size:13px;font-weight:750">${escapeHtml(label)}</span><strong style="color:${color};font-size:15px">${escapeHtml(value)}</strong><span style="padding:3px 9px;border-radius:999px;color:${color};background:${statusBg(metric.status)};font-size:11px;font-weight:800">${statusLabel}</span></div><div style="height:6px;margin:10px 0 7px;background:#e7edf5;border-radius:99px;overflow:hidden"><i style="display:block;height:100%;width:${width}%;border-radius:inherit;background:${color}"></i></div><div style="display:flex;justify-content:space-between;color:#718097;font-size:12px"><span>${escapeHtml(detail)}</span><span>${escapeHtml(target)}</span></div>${evidenceBlock}</div>`;
|
|
126
|
+
};
|
|
127
|
+
const cases = result.cases.length > 0
|
|
128
|
+
? result.cases.map((item) => {
|
|
129
|
+
const passed = item.status === "passed";
|
|
130
|
+
const color = passed ? "#15815c" : item.status === "blocked" ? "#c87918" : "#d34661";
|
|
131
|
+
const bg = passed ? "#e7f7f0" : item.status === "blocked" ? "#fff7e8" : "#fff0f3";
|
|
132
|
+
return `<details style="border:1px solid ${passed ? "#d7ece2" : "#ecd4d9"};border-radius:13px;background:${passed ? "#fbfffd" : "#fffdfd"};overflow:hidden;margin-top:10px"${passed ? "" : " open"}><summary style="display:flex;align-items:center;gap:10px;padding:15px;cursor:pointer;list-style:none"><span style="color:#17365d;font:700 12px ui-monospace,SFMono-Regular,Menlo,monospace">${escapeHtml(item.caseId)}</span><span style="flex:1;color:#17365d;font-size:14px;font-weight:750">${escapeHtml(item.caseContent.purpose)}</span><span style="padding:3px 9px;border-radius:999px;color:${color};background:${bg};font-size:11px;font-weight:800">${escapeHtml(item.status.toUpperCase())}</span></summary><div style="padding:0 15px 15px;border-top:1px solid #edf1f6"><div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;margin-top:14px"><div style="padding:9px 10px;border-radius:8px;background:#f6f8fa"><label style="display:block;color:#718097;font-size:11px">AC 映射</label><span style="display:block;margin-top:2px;color:#172033;font-size:12px;font-weight:650">${escapeHtml(item.acIds.join(", ") || "未关联")}</span></div><div style="padding:9px 10px;border-radius:8px;background:#f6f8fa"><label style="display:block;color:#718097;font-size:11px">浏览器证据</label><span style="display:block;margin-top:2px;color:#172033;font-size:12px;font-weight:650">${item.evidence.length} 条</span></div></div></div></details>`;
|
|
133
|
+
}).join("")
|
|
134
|
+
: `<div style="color:#718097;font-size:13px">无用例执行数据</div>`;
|
|
135
|
+
const blocking = metrics.blockingItems.length > 0
|
|
136
|
+
? metrics.blockingItems.map((item) => `<li style="margin:6px 0">${escapeHtml(item)}</li>`).join("")
|
|
137
|
+
: "<li>无阻断项</li>";
|
|
138
|
+
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none';style-src 'unsafe-inline'"><title>前端测试 · L-5</title><style>summary::-webkit-details-marker{display:none}@media(max-width:760px){.hero,.split,.gates{grid-template-columns:1fr!important}.kpis{grid-template-columns:repeat(2,1fr)!important}}@media(max-width:480px){.page{padding:24px 14px!important}.kpis{grid-template-columns:1fr!important}.hero-header{flex-direction:column!important}.case-meta{grid-template-columns:1fr!important}}</style></head><body style="margin:0;background:radial-gradient(circle at 8% 0,#edf4ff 0,transparent 36rem),#f5f7fb;color:#17243b;font:15px/1.6 Inter,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif"><div class="page" style="max-width:1180px;margin:0 auto;padding:42px 28px 72px">
|
|
139
|
+
<header class="hero-header" style="position:relative;overflow:hidden;padding:34px 38px;border:1px solid #294b78;border-radius:26px;background:linear-gradient(125deg,#102849 0%,#173d6d 57%,#245b91 100%);box-shadow:0 18px 40px rgba(16,40,73,.18);display:flex;align-items:flex-start;justify-content:space-between;gap:28px"><div><div style="color:#9fc6ee;font-size:11px;font-weight:800;letter-spacing:.2em">FRONTEND TEST · L-5 QUALITY VIEW</div><h1 style="margin:12px 0 10px;color:#f7fbff;font-size:clamp(30px,3.8vw,46px);font-weight:800;letter-spacing:-.055em;line-height:1.1">前端测试 L-5 报告</h1><p style="color:#bdd2e9;font-size:13px;margin:0">${escapeHtml(result.sourceBinding.taskId)} · ${result.totals.cases} 个用例</p></div><div style="min-width:220px;padding:16px 18px;border:1px solid ${decision.border};border-radius:18px;background:${decision.bg};box-shadow:0 8px 24px rgba(0,0,0,.08)"><div style="display:flex;align-items:center;gap:9px;color:${decision.color};font-size:17px;font-weight:800"><i style="width:11px;height:11px;border-radius:50%;background:${decision.color};box-shadow:0 0 0 5px ${decision.color}1a;display:inline-block"></i>L-5 ${decision.text}</div><small style="display:block;margin:7px 0 0 20px;color:${decision.color};opacity:.85;font-size:12px">${decision.small}</small></div></header>
|
|
140
|
+
<section class="kpis" style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:18px">
|
|
141
|
+
${[["测试通过率", passRate, `${result.totals.passed} / ${executed} · 目标 100%`, "#4775ef"], ["AC 验收覆盖", acRate, `${m.acCoverage.numerator ?? "—"} / ${m.acCoverage.denominator ?? "—"} · 目标 100%`, "#c87918"], ["自动化覆盖率", automationRate, `${m.automationCoverage.numerator ?? "—"} / ${m.automationCoverage.denominator ?? "—"} · 目标 ≥90%`, "#7589d9"], ["Critical 风险", String(m.criticalRisks.numerator ?? 0), m.criticalRisks.status === "pass" ? "无阻断项" : "阻断级风险", m.criticalRisks.status === "pass" ? "#15815c" : "#d34661"]].map(([label, value, foot, color]) => `<div style="position:relative;min-height:132px;padding:21px 22px;overflow:hidden;background:#fff;border:1px solid #e1e8f2;border-radius:15px;box-shadow:0 6px 18px rgba(25,53,92,.045)"><i style="position:absolute;top:0;left:0;right:0;height:3px;background:${color};display:block"></i><div style="color:#748198;font-size:13px;font-weight:650">${label}</div><div style="margin-top:19px;color:#13213a;font-size:35px;font-weight:800;letter-spacing:-.06em;line-height:1">${value}</div><div style="margin-top:9px;color:${color};font-size:12px">${foot}</div></div>`).join("")}
|
|
142
|
+
</section>
|
|
143
|
+
<section class="split" style="display:grid;grid-template-columns:1fr 1.35fr;gap:12px;margin-top:18px">
|
|
144
|
+
<div style="padding:20px 22px;border:1px solid #dfe7f1;border-radius:15px;background:rgba(255,255,255,.82);box-shadow:0 7px 20px rgba(25,53,92,.04)"><div><span style="color:#8796aa;font-size:10px;font-weight:800;letter-spacing:.16em">RUN OUTCOME</span><h2 style="margin:3px 0 0;color:#17365d;font-size:17px">本轮执行结果</h2></div><div style="position:relative;display:flex;align-items:center;gap:20px;margin-top:15px"><svg viewBox="0 0 120 120" style="width:116px;height:116px;flex:0 0 116px;transform:rotate(-90deg)" aria-label="${result.totals.passed} 通过,${result.totals.failed} 失败"><circle cx="60" cy="60" r="46" fill="none" stroke="#edf1f6" stroke-width="12"></circle><circle cx="60" cy="60" r="46" fill="none" stroke="#15815c" stroke-width="12" stroke-dasharray="${passLen.toFixed(2)} ${circ.toFixed(2)}" stroke-linecap="round"></circle><circle cx="60" cy="60" r="46" fill="none" stroke="#d34661" stroke-width="12" stroke-dasharray="${failLen.toFixed(2)} ${circ.toFixed(2)}" stroke-dashoffset="${(-passLen).toFixed(2)}"></circle></svg><div style="position:absolute;display:flex;width:116px;height:116px;flex-direction:column;align-items:center;justify-content:center;pointer-events:none"><strong style="color:#17365d;font-size:20px">${passRate}</strong><span style="color:#718097;font-size:10px">通过率</span></div><div style="display:grid;gap:6px;flex:1"><div style="color:#718097;font-size:12px">通过 <strong style="float:right;color:#17243b">${result.totals.passed}</strong></div><div style="color:#718097;font-size:12px">失败 <strong style="float:right;color:#17243b">${result.totals.failed}</strong></div><div style="color:#718097;font-size:12px">阻塞 <strong style="float:right;color:#17243b">${result.totals.blocked}</strong></div></div></div></div>
|
|
145
|
+
<div style="padding:20px 22px;border:1px solid #dfe7f1;border-radius:15px;background:rgba(255,255,255,.82);box-shadow:0 7px 20px rgba(25,53,92,.04)"><div style="display:flex;align-items:flex-start;justify-content:space-between;gap:16px"><div><span style="color:#8796aa;font-size:10px;font-weight:800;letter-spacing:.16em">QUALITY SIGNALS</span><h2 style="margin:3px 0 0;color:#17365d;font-size:17px">质量信号</h2></div><span style="color:#718097;font-size:11px">按 L-5 门槛判定</span></div>${gate("测试通过率", m.passRate, passRate, "目标 100%", `${result.totals.passed} / ${executed} 个已执行用例`, `失败 ${result.totals.failed},阻塞 ${result.totals.blocked}。<code style="color:#17365d;font:11px ui-monospace,SFMono-Regular,Menlo,monospace">frontend-test-result-v1</code>`)}${gate("AC 验收覆盖", m.acCoverage, acRate, "目标 100%", `${m.acCoverage.numerator ?? "—"} / ${m.acCoverage.denominator ?? "—"}`, "来自 frontend-test-result-v1 acceptanceCoverage")}${gate("自动化覆盖率", m.automationCoverage, automationRate, "目标 ≥90%", `${m.automationCoverage.numerator ?? "—"} / ${m.automationCoverage.denominator ?? "—"}`, "由已执行 Case 与总 Case 确定性计算")}${gate("用例跳过数", m.skipped, String(result.totals.blocked), "目标 0", `${result.totals.blocked} / ${result.totals.cases}`, m.skipped.reason ?? "无阻塞用例")}${gate("Line 代码覆盖", m.lineCoverage, m.lineCoverage.ratio === null ? "—" : metricValue(m.lineCoverage), "目标 ≥80%", m.lineCoverage.reason ?? "validated coverage artifact", "前端测试当前未提供可信代码覆盖产物")}${gate("Branch 代码覆盖", m.branchCoverage, m.branchCoverage.ratio === null ? "—" : metricValue(m.branchCoverage), "目标 ≥70%", m.branchCoverage.reason ?? "validated coverage artifact", "前端测试当前未提供可信代码覆盖产物")}</div>
|
|
146
|
+
</section>
|
|
147
|
+
<section class="gates" style="display:grid;grid-template-columns:1.28fr .86fr;gap:20px;margin-top:20px"><div style="background:#fff;border:1px solid #e0e7f0;border-radius:15px;box-shadow:0 7px 20px rgba(25,53,92,.04);padding:22px"><div style="display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:18px"><h2 style="margin:0;color:#17365d;font-size:19px">L-5 门禁摘要</h2><span style="color:#718097;font-size:13px">阻断项与执行门槛</span></div>${gate("测试通过率", m.passRate, passRate, "目标 100%", `${result.totals.passed} / ${executed}`, `失败 ${result.totals.failed},阻塞 ${result.totals.blocked}。<code style="color:#17365d;font:11px ui-monospace,SFMono-Regular,Menlo,monospace">frontend-test-result-v1</code>`)}${gate("AC 验收覆盖", m.acCoverage, acRate, "目标 100%", `${m.acCoverage.numerator ?? "—"} / ${m.acCoverage.denominator ?? "—"}`, "来自 frontend-test-result-v1 acceptanceCoverage")}${gate("自动化覆盖率", m.automationCoverage, automationRate, "目标 ≥90%", `${m.automationCoverage.numerator ?? "—"} / ${m.automationCoverage.denominator ?? "—"}`, "由已执行 Case 与总 Case 确定性计算")}${gate("阻塞/跳过用例", m.skipped, String(result.totals.blocked), "目标 0", `${result.totals.blocked} / ${result.totals.cases}`, m.skipped.reason ?? "无阻塞用例")}${gate("Line 代码覆盖", m.lineCoverage, m.lineCoverage.ratio === null ? "—" : metricValue(m.lineCoverage), "目标 ≥80%", m.lineCoverage.reason ?? "validated coverage artifact", "前端测试当前未提供可信代码覆盖产物")}${gate("Branch 代码覆盖", m.branchCoverage, m.branchCoverage.ratio === null ? "—" : metricValue(m.branchCoverage), "目标 ≥70%", m.branchCoverage.reason ?? "validated coverage artifact", "前端测试当前未提供可信代码覆盖产物")}</div><div style="background:#fff;border:1px solid #e0e7f0;border-radius:15px;box-shadow:0 7px 20px rgba(25,53,92,.04);padding:22px"><div style="display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:18px"><h2 style="margin:0;color:#17365d;font-size:19px">阻断项</h2><span style="color:#718097;font-size:13px">${metrics.blockingItems.length} 条</span></div><ul style="margin:0;padding-left:20px;color:#718097;font-size:12px">${blocking}</ul><div style="margin-top:16px;padding:12px;border-radius:10px;background:#fff7e8;color:#8b641f;font-size:12px">前端 DAG 当前没有可信的 line/branch coverage artifact,必须保持 unavailable,不得推断。</div></div></section>
|
|
148
|
+
<section style="background:#fff;border:1px solid #e0e7f0;border-radius:15px;box-shadow:0 7px 20px rgba(25,53,92,.04);padding:22px;margin-top:20px"><div style="display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:12px"><h2 style="margin:0;color:#17365d;font-size:19px">用例执行明细</h2><span style="color:#718097;font-size:13px">${result.cases.length} 条 · Result v1</span></div>${cases}</section>
|
|
149
|
+
<section style="background:#fff;border:1px solid #e0e7f0;border-radius:15px;box-shadow:0 7px 20px rgba(25,53,92,.04);padding:22px;margin-top:20px"><p style="margin:0;color:#718097;font-size:12px">报告由 frontend-test L-5 shell 节点基于 <code style="color:#17365d;font:11px ui-monospace,SFMono-Regular,Menlo,monospace">frontend-test-result-v1</code> 确定性渲染。L-5 判定依据:pass=100%、AC=100%、automation≥90%、line≥80%、branch≥70%、blocked=0 且无阻断级 Critical 风险。</p></section>
|
|
150
|
+
</div></body></html>`;
|
|
112
151
|
}
|
|
113
152
|
async function writePairAtomic(markdownPath, markdown, htmlPath, html) {
|
|
114
153
|
await mkdir(path.dirname(markdownPath), { recursive: true });
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { access, readdir, readFile, realpath } from "node:fs/promises";
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
6
5
|
import { assertValidDagSpec } from "./validate.js";
|
|
@@ -1079,37 +1078,10 @@ function isFrontendLintVerifyCommand(command) {
|
|
|
1079
1078
|
const text = `${command.label}\n${command.args.join(" ")}`;
|
|
1080
1079
|
return /\b(?:lint|eslint)\b/i.test(text);
|
|
1081
1080
|
}
|
|
1082
|
-
function isManagedCiWrapperVerifyCommand(command) {
|
|
1083
|
-
const text = `${command.label}\n${command.args.join(" ")}`.replaceAll("\\", "/");
|
|
1084
|
-
return /\bscripts\/ci(?:-tests)?\.sh\b/.test(text);
|
|
1085
|
-
}
|
|
1086
|
-
function repoHasNpmScript(repoRoot, scriptName) {
|
|
1087
|
-
if (!repoRoot)
|
|
1088
|
-
return false;
|
|
1089
|
-
const packagePath = path.join(repoRoot, "package.json");
|
|
1090
|
-
if (!existsSync(packagePath))
|
|
1091
|
-
return false;
|
|
1092
|
-
try {
|
|
1093
|
-
const decoded = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
1094
|
-
return typeof decoded.scripts?.[scriptName] === "string";
|
|
1095
|
-
}
|
|
1096
|
-
catch {
|
|
1097
|
-
return false;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
1081
|
function partitionFrontendStaticVerifyCommands(input) {
|
|
1101
1082
|
const commands = input.commands ?? [];
|
|
1102
1083
|
const lintCommands = commands.filter(isFrontendLintVerifyCommand);
|
|
1103
1084
|
const staticCommands = commands.filter((command) => !isFrontendLintVerifyCommand(command));
|
|
1104
|
-
if (lintCommands.length === 0 &&
|
|
1105
|
-
commands.some(isManagedCiWrapperVerifyCommand) &&
|
|
1106
|
-
repoHasNpmScript(input.repoRoot, "lint")) {
|
|
1107
|
-
lintCommands.push({
|
|
1108
|
-
args: ["npm", "run", "lint"],
|
|
1109
|
-
cwd: input.repoRoot,
|
|
1110
|
-
label: "npm run lint",
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1113
1085
|
return {
|
|
1114
1086
|
lint: {
|
|
1115
1087
|
...(lintCommands.length > 0 ? { commands: lintCommands } : {}),
|
|
@@ -1211,7 +1183,6 @@ async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
|
1211
1183
|
const staticCommands = firstExisting([
|
|
1212
1184
|
"typecheck",
|
|
1213
1185
|
"check-types",
|
|
1214
|
-
"lint",
|
|
1215
1186
|
"check",
|
|
1216
1187
|
"build",
|
|
1217
1188
|
]);
|
|
@@ -3688,7 +3659,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3688
3659
|
"Do not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON. For best-effort cleanup, catch only the narrow transport exception actually raised by the selected HTTP client (for example `requests.RequestException` or `urllib.error.URLError`); never use bare `except`, `Exception`, or `BaseException` with `pass`.",
|
|
3689
3660
|
].join("\n\n"),
|
|
3690
3661
|
};
|
|
3691
|
-
const collectionAssess = shellNode("assess-backend-pytest-collection-shell", [generatePytest.id], "markdown-collection-assess", "
|
|
3662
|
+
const collectionAssess = shellNode("assess-backend-pytest-collection-shell", [generatePytest.id], "markdown-collection-assess", "Resolve final Markdown-mapped scripts before any business test body execution. A safe deterministic mapped script that the pytest writer omitted is REPAIRABLE without starting pytest; otherwise run pytest collection only over existing mapped scripts. Materialize hash-bound PASS/REPAIRABLE/BLOCKED facts. Only missing mapped generated scripts and generated testcase-local syntax/import inconsistencies are repairable; dependency, plugin, production-module, environment, safety and unknown failures remain blocked.", "Run-owned reports/backend-test-pytest-collection-initial.md and contracts/backend-test-pytest-collection-initial.json with bounded diagnostics, asset hashes, collected item IDs and deterministic repair eligibility.", [], 120000);
|
|
3692
3663
|
const repairPytest = {
|
|
3693
3664
|
id: "repair-backend-pytest-collection-pi",
|
|
3694
3665
|
depends_on: [collectionAssess.id],
|
|
@@ -3716,7 +3687,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3716
3687
|
outputContract: "First non-empty line is IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked, followed by a concise repair summary. Modify only generated pytest scripts/helpers/factories and preserve every Markdown Case, Test Point, primary symbol and assertion meaning.",
|
|
3717
3688
|
subtask_prompt: [
|
|
3718
3689
|
"Repair the generated backend pytest asset as one bounded program using the direct upstream collection assessment. This is the only repair attempt and happens before any business test body execution.",
|
|
3719
|
-
"Fix only collection-proven generated testcase-local syntax, module path, missing symbol, circular import, fixture-name, decorator or parameterization inconsistencies. Inspect all affected importers and providers so the repair is cross-file consistent.",
|
|
3690
|
+
"Fix only collection-proven generated testcase-local defects: create the exact safe missing mapped test_*.py paths listed by the initial facts, or repair syntax, module path, missing symbol, circular import, fixture-name, decorator or parameterization inconsistencies. Inspect all affected importers and providers so the repair is cross-file consistent; do not create unrelated pytest scripts.",
|
|
3720
3691
|
"Preserve final testcase/md/** semantics, every Case ID, Rule/Test Point binding, primary symbol, parameter ID, expected status/body/schema assertion, HTTP logging, redaction and truncation behavior.",
|
|
3721
3692
|
"Do not read task source/** or reinterpret requirements. Do not modify Markdown, conftest, pytest config, production code or dependencies.",
|
|
3722
3693
|
"Do not add skip/skipif/xfail, remove tests, reduce collected items, loosen assertions, swallow exceptions, use try/except ImportError fallback, mutate sys.path/PYTHONPATH, or replace the real API with mocks.",
|
|
@@ -5239,16 +5210,16 @@ export async function requireManagedTaskContractBinding(input) {
|
|
|
5239
5210
|
const expectedRevision = state.ref?.revision ?? 0;
|
|
5240
5211
|
const observedCanonicalHash = state.observedCanonicalHash ?? "<observedCanonicalHash-from-show>";
|
|
5241
5212
|
const recovery = state.effectiveStatus === "transaction-incomplete"
|
|
5242
|
-
? `Recover the unfinished transaction first: loop-agent task
|
|
5213
|
+
? `Recover the unfinished transaction first: loop-agent task advance ${input.taskId} --json`
|
|
5243
5214
|
: [
|
|
5244
5215
|
"After reviewing the show JSON, either adopt the current on-disk facts:",
|
|
5245
|
-
`loop-agent task
|
|
5216
|
+
`loop-agent task advance ${input.taskId} --from-draft --expected-revision ${expectedRevision} --expected-observed-hash ${observedCanonicalHash} --request-id <unique-request-id> --request-payload-sha256 <sha256-of-this-adopt-request> --json`,
|
|
5246
5217
|
"or apply a reviewed draft:",
|
|
5247
|
-
`loop-agent task
|
|
5218
|
+
`loop-agent task advance ${input.taskId} --from-draft --input <reviewed-draft.json> --expected-revision ${expectedRevision} --expected-observed-hash ${observedCanonicalHash} --request-id <unique-request-id> --request-payload-sha256 <sha256-of-input-file> --json`,
|
|
5248
5219
|
].join(" ");
|
|
5249
5220
|
const err = new Error([
|
|
5250
5221
|
`dag generate requires managed Task Contract for task ${input.taskId} (effectiveStatus=${state.effectiveStatus})`,
|
|
5251
|
-
`Inspect current state: loop-agent task
|
|
5222
|
+
`Inspect current state: loop-agent task status ${input.taskId} --json`,
|
|
5252
5223
|
recovery,
|
|
5253
5224
|
"Do not auto-adopt or auto-apply before confirming the Task Contract facts and concurrency fields.",
|
|
5254
5225
|
].join("; "));
|
|
@@ -206,6 +206,29 @@ export function validateOutputProtocol(protocol, text) {
|
|
|
206
206
|
reason: `missing first non-empty line; expected one of: ${protocol.validLines.map((l) => JSON.stringify(l)).join(" or ")}`,
|
|
207
207
|
};
|
|
208
208
|
}
|
|
209
|
+
const isReviewVerdict = protocol.validLines.length === 2 && protocol.validLines.includes("VERDICT: pass") && protocol.validLines.includes("VERDICT: request-revision");
|
|
210
|
+
// Models frequently prepend a short explanation despite the protocol
|
|
211
|
+
// instruction. Recover only when there is exactly one unambiguous protocol
|
|
212
|
+
// line; conflicting or repeated verdicts remain fail-closed.
|
|
213
|
+
const candidates = isReviewVerdict ? text
|
|
214
|
+
.split("\n")
|
|
215
|
+
.map((line) => normalizeVerdictCandidateLine(line.trim()))
|
|
216
|
+
.filter((line) => protocol.validLines.includes(line)) : [];
|
|
217
|
+
const uniqueCandidates = [...new Set(candidates)];
|
|
218
|
+
if (uniqueCandidates.length > 1) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
failureCategory: "protocol-invalid",
|
|
222
|
+
reason: `conflicting protocol lines found: ${uniqueCandidates.map((line) => JSON.stringify(line)).join(" and ")}`,
|
|
223
|
+
firstNonEmptyLine: first,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (protocol.validLines.includes(first)) {
|
|
227
|
+
return { ok: true, matchedLine: first };
|
|
228
|
+
}
|
|
229
|
+
if (uniqueCandidates.length === 1) {
|
|
230
|
+
return { ok: true, matchedLine: uniqueCandidates[0] };
|
|
231
|
+
}
|
|
209
232
|
if (!protocol.validLines.includes(first)) {
|
|
210
233
|
return {
|
|
211
234
|
ok: false,
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
"artifacts/**"
|
|
225
225
|
],
|
|
226
226
|
"outputContract": "Run-owned reports/backend-test-pytest-collection-initial.md and contracts/backend-test-pytest-collection-initial.json with bounded diagnostics, asset hashes, collected item IDs and deterministic repair eligibility.",
|
|
227
|
-
"subtask_prompt": "
|
|
227
|
+
"subtask_prompt": "Resolve final Markdown-mapped scripts before any business test body execution. A safe deterministic mapped script that the pytest writer omitted is REPAIRABLE without starting pytest; otherwise run pytest collection only over existing mapped scripts. Materialize hash-bound PASS/REPAIRABLE/BLOCKED facts. Only missing mapped generated scripts and generated testcase-local syntax/import inconsistencies are repairable; dependency, plugin, production-module, environment, safety and unknown failures remain blocked.",
|
|
228
228
|
"shell": {
|
|
229
229
|
"commands": [],
|
|
230
230
|
"backendTestPipeline": "markdown-collection-assess",
|
|
@@ -266,7 +266,7 @@
|
|
|
266
266
|
"type": "implementation-outcome-v1"
|
|
267
267
|
},
|
|
268
268
|
"outputContract": "First non-empty line is IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked, followed by a concise repair summary. Modify only generated pytest scripts/helpers/factories and preserve every Markdown Case, Test Point, primary symbol and assertion meaning.",
|
|
269
|
-
"subtask_prompt": "Repair the generated backend pytest asset as one bounded program using the direct upstream collection assessment. This is the only repair attempt and happens before any business test body execution.\n\nFix only collection-proven generated testcase-local syntax, module path, missing symbol, circular import, fixture-name, decorator or parameterization inconsistencies. Inspect all affected importers and providers so the repair is cross-file consistent.\n\nPreserve final testcase/md/** semantics, every Case ID, Rule/Test Point binding, primary symbol, parameter ID, expected status/body/schema assertion, HTTP logging, redaction and truncation behavior.\n\nDo not read task source/** or reinterpret requirements. Do not modify Markdown, conftest, pytest config, production code or dependencies.\n\nDo not add skip/skipif/xfail, remove tests, reduce collected items, loosen assertions, swallow exceptions, use try/except ImportError fallback, mutate sys.path/PYTHONPATH, or replace the real API with mocks.\n\nDo not execute pytest; the deterministic effective collection gate owns the final collection attempt."
|
|
269
|
+
"subtask_prompt": "Repair the generated backend pytest asset as one bounded program using the direct upstream collection assessment. This is the only repair attempt and happens before any business test body execution.\n\nFix only collection-proven generated testcase-local defects: create the exact safe missing mapped test_*.py paths listed by the initial facts, or repair syntax, module path, missing symbol, circular import, fixture-name, decorator or parameterization inconsistencies. Inspect all affected importers and providers so the repair is cross-file consistent; do not create unrelated pytest scripts.\n\nPreserve final testcase/md/** semantics, every Case ID, Rule/Test Point binding, primary symbol, parameter ID, expected status/body/schema assertion, HTTP logging, redaction and truncation behavior.\n\nDo not read task source/** or reinterpret requirements. Do not modify Markdown, conftest, pytest config, production code or dependencies.\n\nDo not add skip/skipif/xfail, remove tests, reduce collected items, loosen assertions, swallow exceptions, use try/except ImportError fallback, mutate sys.path/PYTHONPATH, or replace the real API with mocks.\n\nDo not execute pytest; the deterministic effective collection gate owns the final collection attempt."
|
|
270
270
|
},
|
|
271
271
|
{
|
|
272
272
|
"id": "effective-backend-pytest-collection-gate-shell",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
- Shell 搜索优先 `rg`,按名找文件优先 `fd`;脚本用 Git Bash / 兼容 Bash。
|
|
25
25
|
- 用 loop-agent 迭代本仓库时,控制器必须来自已发布 npm 包(记录实际版本);启动后不要中途升级;不要用工作区 `npm link` / `npm run dev` 控制可能改 CLI/runtime/package 的任务。
|
|
26
26
|
- 反复出现的约束固化为文档、脚本、检查、测试或模板;禁止占位实现(除非 contract 标明脚手架)。
|
|
27
|
-
- 非微小实现:`exec-plan` 不能替代 Agent DAG;除非用户要求 one-shot 或计划记录 escape hatch,否则改实现前完成 `
|
|
27
|
+
- 非微小实现:`exec-plan` 不能替代 Agent DAG;除非用户要求 one-shot 或计划记录 escape hatch,否则改实现前完成 `task advance`(结构化路径 + writeSet 审查),不得主会话直接改实现。
|
|
28
28
|
- 不要自行引入外部 SDD/spec-first 等强制平行治理树;以本文件与 `docs/` 为准(ADR 0006)。
|
|
29
29
|
|
|
30
30
|
## 开始顺序
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
- 用 loop-agent 迭代本仓库时,控制器必须来自已发布的 npm 安装包;首次安装或有意升级可用 `@tea-agent/loop-agent@latest`,但一次自举任务启动后不要中途升级控制器,并记录 `npm list -g @tea-agent/loop-agent --depth=0` 显示的实际版本。不要用当前工作区的 `npm link` 或 `npm run dev` 控制可能改动 CLI、DAG runtime、executor、package metadata 或 build output 的任务。
|
|
28
28
|
- 反复出现的约束要固化为文档、脚本、检查项、测试或模板。
|
|
29
29
|
- 除非用户明确要求,不要自行引入外部 SDD、spec-first、brainstorming 等方法论的强制设计文档、审批门或专用目录;本仓库的工作流程以本文件和 `docs/` 中的治理规则为准。
|
|
30
|
-
- 对非微小的实现或修复,`exec-plan` 只负责记录 Contract、进度和验证证据,不能替代 Agent DAG。除非用户明确要求 one-shot,或在计划中记录了适用的 escape hatch 与理由,否则在改动实现文件前必须完成 `
|
|
30
|
+
- 对非微小的实现或修复,`exec-plan` 只负责记录 Contract、进度和验证证据,不能替代 Agent DAG。除非用户明确要求 one-shot,或在计划中记录了适用的 escape hatch 与理由,否则在改动实现文件前必须完成 `task advance`(含结构化 `allowedPaths` / `forbiddenPaths` 与 writeSet 审查);随后 `--approve-gate` 长跑,不得主会话直接改实现。
|
|
31
31
|
- 禁止占位实现;除非 contract 明确约定为脚手架且标出后续闭环。
|
|
32
32
|
|
|
33
33
|
## 开始顺序
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
9. 查看最近提交、相关执行计划、progress/report,确认当前上下文。
|
|
54
54
|
10. 检查 `git status --short --branch`。
|
|
55
55
|
11. 运行本次任务相关的最小基线验证。
|
|
56
|
-
12. 如果用户提到"后端测试"、"接口测试"、"pytest"、"自动化测试",在任务 `task.json` 中设置 `taskKind: "backend-test"` 再 `
|
|
56
|
+
12. 如果用户提到"后端测试"、"接口测试"、"pytest"、"自动化测试",在任务 `task.json` 中设置 `taskKind: "backend-test"` 再 `task advance`;不要用 `--profile backend-test`(CLI 不接受该值,专用模板只走 taskKind)。知识回写用 `taskKind: "knowledge-sync"`(须 `featureId`),图谱开荒用 `taskKind: "knowledge-graph-bootstrap"`。`--profile` 仅表示治理强度:`auto|minimal|standard|reviewed|supervised`。
|
|
57
57
|
13. 如果用户提到"看板"、"observe"、"监控面板"、"启动看板",使用 `agent-worker console` 启动统一 Operator Console(默认 repo=当前目录、port=8790;兼容入口 `agent-worker console serve --repo . --port 8790`);`/inspect/` 提供只读检视。`agent-worker observe serve --repo . --port 8787` 仅为兼容入口。
|
|
58
58
|
14. 如果用户要求“合并 `<source>` 到 `<target>`”或“合并 origin/main 到当前分支”,先阅读 `docs/operations/branch-merge-guideline.md`,按影响自动选择快速、标准或深度模式;始终冻结 source SHA、审查双方功能、运行 merge-tree、生成 source-SHA 合并报告,并在提交前再次 fetch 防止主干前进。
|
|
59
59
|
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
- 优先沿用现有 helper、目录边界和局部模式,再考虑新增抽象。
|
|
89
89
|
- 长期决策写入 `docs/`,不要只留在聊天里。
|
|
90
90
|
- 分支合并遵循 `docs/operations/branch-merge-guideline.md`;快速模式只用于可证明的低风险/no-op 合并,涉及冲突、init/package/runtime/release/public API 时必须升级为标准或深度模式。
|
|
91
|
-
- 后端测试、接口/API 测试、pytest 或明确的后端自动化测试,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `"backend-test"`,不得保留默认 `standard`。`backend-test` 是 `taskKind`,不是 `--profile` 的可选值;`
|
|
91
|
+
- 后端测试、接口/API 测试、pytest 或明确的后端自动化测试,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `"backend-test"`,不得保留默认 `standard`。`backend-test` 是 `taskKind`,不是 `--profile` 的可选值;`task advance` 继续使用 `--profile auto` 选择治理等级。仅说“自动化测试”且前后端不明时,先根据任务源和项目技术栈判断,禁止无条件路由。
|
|
92
92
|
- 本地 Operator Console:`agent-worker console`(默认 repo=当前目录、port=8790;兼容 `agent-worker console serve --repo . --port 8790`),访问 `http://127.0.0.1:8790/`;其中 `/inspect/` 为只读运行检视。默认绑定本机 `127.0.0.1`;可用 `--host 0.0.0.0` / `--debug`,不要直接暴露到公开网络。端口被占用时不会自动更换,请用 `--port <port>` 显式指定。
|
|
93
93
|
- 面向使用者的新增、修改、删除或修复,应同步更新根目录 `CHANGELOG.md`;保持版本级摘要即可,不写过细技术细节。
|
|
94
94
|
- 面向用户的中文更新日志、README 和说明文档应使用自然、结果导向的表达:先说明用户能获得什么或问题如何改善,保留必要的命令和产品术语,避免逐字翻译、内部实现细节和无意义的中英混杂。
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"requirements": { "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": false, "required": ["id", "expectedOutcome", "implementationTargets", "verificationTargetIds"], "properties": { "id": { "$ref": "#/$defs/requirementId" }, "expectedOutcome": { "type": "string", "minLength": 1 }, "implementationTargets": { "type": "array", "items": { "$ref": "#/$defs/path" } }, "verificationTargetIds": { "type": "array", "items": { "type": "string", "minLength": 1 } }, "evidenceGap": { "$ref": "#/$defs/gap" } } } },
|
|
14
14
|
"uiStates": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["name", "applicable"], "properties": { "name": { "type": "string", "minLength": 1 }, "applicable": { "type": "boolean" }, "expectedBehavior": { "type": "string", "minLength": 1 }, "implementationTargets": { "type": "array", "items": { "$ref": "#/$defs/path" } }, "verificationTargetIds": { "type": "array", "items": { "type": "string", "minLength": 1 } }, "notApplicableReason": { "type": "string", "minLength": 1 } }, "allOf": [{ "if": { "properties": { "applicable": { "const": true } }, "required": ["applicable"] }, "then": { "required": ["expectedBehavior", "implementationTargets", "verificationTargetIds"], "properties": { "implementationTargets": { "minItems": 1 }, "verificationTargetIds": { "minItems": 1 } } } }, { "if": { "properties": { "applicable": { "const": false } }, "required": ["applicable"] }, "then": { "required": ["notApplicableReason"] } }] } },
|
|
15
15
|
"interactions": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["name", "trigger", "expectedBehavior", "implementationTargets", "verificationTargetIds"], "properties": { "name": { "type": "string", "minLength": 1 }, "trigger": { "type": "string", "minLength": 1 }, "expectedBehavior": { "type": "string", "minLength": 1 }, "implementationTargets": { "type": "array", "items": { "$ref": "#/$defs/path" } }, "verificationTargetIds": { "type": "array", "items": { "type": "string" } } } } },
|
|
16
|
-
"mockApi": { "type": "object", "additionalProperties": false, "required": ["strategy", "productionDefaultOff", "activation", "endpoints"], "properties": { "strategy": { "enum": ["native", "browser-intercept", "request-adapter", "not-needed"] }, "productionDefaultOff": { "const": true }, "activation": { "type": "string", "minLength": 1 }, "endpoints": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["method", "path"], "properties": { "method": { "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] }, "path": { "type": "string", "pattern": "^/" }, "fixture": { "$ref": "#/$defs/path" }, "consumer": { "$ref": "#/$defs/path" } } } } } },
|
|
16
|
+
"mockApi": { "type": "object", "additionalProperties": false, "required": ["strategy", "productionDefaultOff", "activation", "endpoints"], "properties": { "strategy": { "enum": ["native", "browser-intercept", "request-adapter", "not-needed"] }, "productionDefaultOff": { "const": true }, "activation": { "type": ["string", "null"], "minLength": 1 }, "endpoints": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["method", "path"], "properties": { "method": { "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] }, "path": { "type": "string", "pattern": "^/" }, "fixture": { "anyOf": [{ "$ref": "#/$defs/path" }, { "type": "null" }] }, "consumer": { "anyOf": [{ "$ref": "#/$defs/path" }, { "type": "null" }] } } } } } },
|
|
17
17
|
"designEvidence": { "type": "object", "additionalProperties": false, "required": ["source", "paths", "conflicts"], "properties": { "source": { "type": "string", "minLength": 1 }, "paths": { "type": "array", "items": { "$ref": "#/$defs/path" } }, "conflicts": { "type": "array", "items": { "type": "string", "minLength": 1 } } } },
|
|
18
|
-
"verificationTargets": { "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": false, "required": ["id", "type", "commandLabel", "file", "requirementIds", "uiStates"], "properties": { "id": { "type": "string", "minLength": 1 }, "type": { "enum": ["static", "unit", "component", "integration", "mock"] }, "commandLabel": { "type": "string", "minLength": 1 }, "file": { "$ref": "#/$defs/path" }, "symbol": { "type": "string", "minLength": 1 }, "requirementIds": { "type": "array", "items": { "$ref": "#/$defs/requirementId" } }, "uiStates": { "type": "array", "items": { "type": "string", "minLength": 1 } } } } },
|
|
18
|
+
"verificationTargets": { "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": false, "required": ["id", "type", "commandLabel", "file", "requirementIds", "uiStates"], "properties": { "id": { "type": "string", "minLength": 1 }, "type": { "enum": ["static", "unit", "component", "integration", "mock"] }, "commandLabel": { "type": "string", "minLength": 1 }, "file": { "$ref": "#/$defs/path" }, "symbol": { "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }, "requirementIds": { "type": "array", "items": { "$ref": "#/$defs/requirementId" } }, "uiStates": { "type": "array", "items": { "type": "string", "minLength": 1 } } } } },
|
|
19
19
|
"evidenceGaps": { "type": "array", "items": { "$ref": "#/$defs/gap" } }
|
|
20
20
|
},
|
|
21
21
|
"allOf": [{ "if": { "properties": { "mockApi": { "properties": { "strategy": { "enum": ["native", "browser-intercept", "request-adapter"] } } } } }, "then": { "properties": { "mockApi": { "properties": { "endpoints": { "minItems": 1, "items": { "required": ["method", "path", "fixture", "consumer"] } } } } } } }],
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"default": "zh-CN"
|
|
64
64
|
},
|
|
65
65
|
"defaultEntry": {
|
|
66
|
-
"enum": ["
|
|
66
|
+
"enum": ["task advance", "dag execute"],
|
|
67
67
|
"description": "默认 DAG 入口,用于任务 DAG 生成或直接 DAG 执行。",
|
|
68
|
-
"default": "
|
|
68
|
+
"default": "task advance"
|
|
69
69
|
},
|
|
70
70
|
"profileRouting": {
|
|
71
71
|
"type": "object",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
| **允许** | 已发布 `loop-agent` / `agent-worker` CLI;只读 status/doctor/report/inspect/observe;准备 `source/*` 与 `task.json` 边界;human gate;shell 验证与 handoff。 |
|
|
34
34
|
| **禁止** | 绕过 CLI 用宿主 Edit/Write/ApplyPatch 直接改业务实现;CLI/DAG 失败后「救火改文件」;用聊天自述代替 shell 验证。 |
|
|
35
35
|
| **失败时** | `dag doctor` / `dag report` / `dag reconcile-run`(及适用 worker reconcile);修正任务源/`task.json`/DAG 后经 CLI 重跑。 |
|
|
36
|
-
| **实现写入** | 业务代码 **只** 经受治理 DAG writer(`implement-pi` / `repair-pi`)经 `
|
|
36
|
+
| **实现写入** | 业务代码 **只** 经受治理 DAG writer(`implement-pi` / `repair-pi`)经 `task advance`(批准 writeSet gate 后长跑)。 |
|
|
37
37
|
|
|
38
38
|
**永远不要**:`loop-agent` / `agent-worker` 失败 ⇒ 主会话直接改仓库实现。
|
|
39
39
|
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
| 初始化更新校验 / 检查初始化更新 / loop-agent 初始化更新校验 / loop agent初始化更新校验 | 更新校验 | 只读报告,不写入 |
|
|
46
46
|
| 初始化对齐 / 升级后对齐 / reconcile 初始化 / loop-agent 初始化对齐 | 升级对齐 | 自动应用确定性安全动作;surface 缺失、人工决策、活跃 DAG/Worker 或 Worker 状态无法确认时零写入 |
|
|
47
47
|
| 初始化安全更新 / 应用初始化更新 / loop-agent 初始化安全更新 / loop agent初始化安全更新 | 安全更新 | 先 check-update,再只执行确定性安全动作 |
|
|
48
|
-
| loop-agent 帮我完成 / 帮我实现 / 帮我修复 / 帮我开发 <需求>;使用 loop-agent 完成 <X>;按 loop-agent 流程处理 <X> | 通用需求实现 |
|
|
48
|
+
| loop-agent 帮我完成 / 帮我实现 / 帮我修复 / 帮我开发 <需求>;使用 loop-agent 完成 <X>;按 loop-agent 流程处理 <X> | 通用需求实现 | 先提供 PRD 与 `allowedPaths` / `forbiddenPaths` / verify,再 `task advance` → 审查 writeSet gate → `task advance --approve-gate`;主会话不得直接修改业务实现 |
|
|
49
49
|
|
|
50
50
|
**更新校验(只读)**:只读执行下面命令;**不得自动**执行 `apply-safe` 或模型合并。
|
|
51
51
|
|
|
@@ -85,33 +85,30 @@ pwd → `README.md` → `harness.json` → `__LOOP_AGENT_GOVERNANCE_ROOT__/READM
|
|
|
85
85
|
### Agent DAG 路径
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
loop-agent
|
|
89
|
-
|
|
90
|
-
loop-agent import-prd <task-id> --file <path-to-prd.md>
|
|
91
|
-
# 非微小或跨会话任务(推荐):loop-agent plan create <plan-id> "<title>"
|
|
92
|
-
loop-agent task source prepare <task-id> \
|
|
93
|
-
--use-imported-prd \
|
|
88
|
+
loop-agent task advance <task-id> "任务标题" \
|
|
89
|
+
--prd <path-to-prd.md> \
|
|
94
90
|
--allowed-path "<glob>" \
|
|
95
91
|
--forbidden-path ".harness/**" \
|
|
96
92
|
--verify "typecheck:npm run typecheck" \
|
|
97
|
-
--
|
|
98
|
-
|
|
99
|
-
loop-agent
|
|
100
|
-
loop-agent
|
|
93
|
+
--json
|
|
94
|
+
# 审查 writeSet gate digest 后:
|
|
95
|
+
loop-agent task advance <task-id> --approve-gate "write-set-review:<digest>" --json
|
|
96
|
+
loop-agent task status <task-id> --json
|
|
97
|
+
# 非微小或跨会话任务(推荐):loop-agent plan create <plan-id> "<title>"
|
|
101
98
|
# 有 plan 时收尾:loop-agent plan complete <plan-id> --summary "..."
|
|
102
99
|
```
|
|
103
100
|
|
|
104
|
-
`source/需求.md` 与 `source/执行约束.md` 仍必需(M8/M9),但默认由 `task
|
|
101
|
+
`source/需求.md` 与 `source/执行约束.md` 仍必需(M8/M9),但默认由 `task advance --prd` 投影生成,而不是主会话手写。`plan create` 不是 `task advance` 的硬依赖。写入前同步 `task.json.allowedPaths` / `task.json.forbiddenPaths` 并审查 writer `writeSet`。高级任意 DagSpec 才用 `dag validate|execute|report`,不进入标准 happy path。
|
|
105
102
|
|
|
106
|
-
|
|
103
|
+
凡是影响项目公共契约、执行入口、交付流水线、自动化/治理、数据模型、安全或权限模型、跨模块行为、用户可见工作流的改动,都必须在编辑实现文件前先通过 `task advance` 建立 managed contract / writeSet gate,并审查 gate digest。
|
|
107
104
|
|
|
108
105
|
### 任务类型路由(taskKind)
|
|
109
106
|
|
|
110
107
|
- 用户明确提出后端测试、接口/API 测试、pytest,或语境明确为后端的自动化测试时,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `"backend-test"`,不得保留默认 `standard`。
|
|
111
|
-
- `backend-test` 是 `taskKind`,不是 `--profile` 的可选值;运行 `
|
|
108
|
+
- `backend-test` 是 `taskKind`,不是 `--profile` 的可选值;运行 `task advance` 时继续使用 `--profile auto`。
|
|
112
109
|
- 仅出现“自动化测试”且无法判断前后端时,先阅读任务源与目标项目技术栈再决定,禁止无条件路由到 `backend-test`。
|
|
113
110
|
- 用户提示词明确是前端实现需求(例如前端页面、UI、组件或交互开发)时,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `"frontend-implementation"`,不得保留默认 `standard`。
|
|
114
|
-
- `frontend-implementation` 是 `taskKind`,不是 `--profile` 的可选值;运行 `
|
|
111
|
+
- `frontend-implementation` 是 `taskKind`,不是 `--profile` 的可选值;运行 `task advance` 时继续使用 `--profile auto`,也可按需显式选择 `minimal` / `standard` / `reviewed` / `supervised`,不要把业务模板名当作 profile。
|
|
115
112
|
- 前端自动化测试(浏览器/UI 自动化、Playwright、E2E)继续使用 `taskKind: "frontend-test"`,不得设置为 `frontend-implementation`。
|
|
116
113
|
|
|
117
114
|
### 运行看板(只读)
|