@tea-agent/loop-agent 0.20.1 → 0.22.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/CHANGELOG.md +72 -0
- package/bin/agent-worker.js +0 -0
- package/dist/adapters/loop-agent.js +52 -0
- package/dist/commands/init.js +104 -0
- package/dist/executors/dag-pi-executor.js +26 -0
- package/dist/executors/pi-executor.js +111 -36
- package/dist/executors/pi-sdk-executor.js +105 -29
- package/dist/executors/shell-executor.js +215 -29
- package/dist/shared/openspec-spec.js +49 -0
- package/dist/worker/loop-agent/loop-agent-client.js +43 -9
- package/dist/worker/observability/read-model.js +28 -2
- package/dist/worker/observe/spec-evidence.js +12 -15
- package/dist/worker/observe/static/constants.js +5 -0
- package/dist/worker/observe/static/dag-helpers.js +22 -0
- package/dist/worker/observe/static/format-pool.js +22 -3
- package/dist/worker/observe/static/styles.css +32 -3
- package/dist/worker/observe/static/views/dag-inspector.js +2 -2
- package/dist/worker/observe/static/views/dag.js +5 -0
- package/dist/worker/run-task/run-task.js +16 -6
- package/dist/workflows/dag/backend-test-markdown-workflow.js +328 -97
- package/dist/workflows/dag/backend-test-result-contract.js +10 -4
- package/dist/workflows/dag/frontend-implementation-contract.js +141 -32
- package/dist/workflows/dag/frontend-lint-baseline.js +471 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +79 -16
- package/dist/workflows/dag/frontend-project-capability.js +11 -8
- package/dist/workflows/dag/frontend-repair.js +6 -4
- package/dist/workflows/dag/frontend-review-context.js +67 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +105 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +71 -66
- package/dist/workflows/dag/frontend-verification-trace.js +31 -1
- package/dist/workflows/dag/frontend-worktree-diff.js +81 -6
- package/dist/workflows/dag/init-hybrid.js +370 -79
- package/dist/workflows/dag/lifecycle.js +60 -4
- package/dist/workflows/dag/liveness-policy.js +250 -0
- package/dist/workflows/dag/node-execution.js +49 -0
- package/dist/workflows/dag/runner.js +21 -1
- package/dist/workflows/dag/types.js +67 -1
- package/docs/README.md +5 -6
- package/docs/architecture/dag-execution.md +11 -0
- package/docs/architecture/facts-and-state.md +1 -0
- package/docs/architecture/worker-and-feature.md +10 -0
- package/docs/templates/agent-dag.schema.json +15 -5
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -2
- package/docs/templates/backend-test-dag.json +15 -15
- package/docs/templates/frontend-implementation-contract.schema.json +4 -3
- package/docs/templates/frontend-test-case-checklist.md +6 -2
- package/docs/templates/frontend-test-dag.json +2 -2
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/frontend-design-review/SKILL.md +12 -10
- package/skills/frontend-design-review/references/review-checklist.md +4 -4
- package/skills/frontend-implementation/SKILL.md +2 -2
- package/skills/frontend-implementation/references/code-standards.md +4 -3
- package/skills/frontend-implementation/references/design-spec.md +19 -14
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/frontend-review/SKILL.md +15 -28
- package/skills/frontend-review/references/review-findings.md +16 -18
- package/skills/frontend-verification/SKILL.md +16 -13
- package/skills/frontend-verification/references/verification-checklist.md +18 -30
- package/skills/loop-agent/references/command-reference.md +2 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
|
@@ -231,6 +231,12 @@ export function parseJunitXml(xml) {
|
|
|
231
231
|
const failureTag = body.match(/<failure\b([^>]*)>([\s\S]*?)<\/failure>|<failure\b([^>]*)\/>/i);
|
|
232
232
|
const errorTag = body.match(/<error\b([^>]*)>([\s\S]*?)<\/error>|<error\b([^>]*)\/>/i);
|
|
233
233
|
const skippedTag = /<skipped\b/i.test(body);
|
|
234
|
+
const caseStdout = body.match(/<system-out\b[^>]*>([\s\S]*?)<\/system-out>/i)?.[1];
|
|
235
|
+
const caseStderr = body.match(/<system-err\b[^>]*>([\s\S]*?)<\/system-err>/i)?.[1];
|
|
236
|
+
const capturedOutput = {
|
|
237
|
+
...(caseStdout ? { stdout: decodeXmlEntities(caseStdout).trim() } : {}),
|
|
238
|
+
...(caseStderr ? { stderr: decodeXmlEntities(caseStderr).trim() } : {}),
|
|
239
|
+
};
|
|
234
240
|
if (failureTag) {
|
|
235
241
|
caseFailed += 1;
|
|
236
242
|
const fAttrs = failureTag[1] ?? failureTag[3] ?? "";
|
|
@@ -243,7 +249,7 @@ export function parseJunitXml(xml) {
|
|
|
243
249
|
message: summary,
|
|
244
250
|
kind: "failure",
|
|
245
251
|
});
|
|
246
|
-
cases.push({ classname, name, durationMs: caseDurationMs, status: "failure", message: summary, details: fBody || summary });
|
|
252
|
+
cases.push({ classname, name, durationMs: caseDurationMs, status: "failure", message: summary, details: fBody || summary, ...capturedOutput });
|
|
247
253
|
}
|
|
248
254
|
else if (errorTag) {
|
|
249
255
|
caseErrors += 1;
|
|
@@ -257,16 +263,16 @@ export function parseJunitXml(xml) {
|
|
|
257
263
|
message: summary,
|
|
258
264
|
kind: "error",
|
|
259
265
|
});
|
|
260
|
-
cases.push({ classname, name, durationMs: caseDurationMs, status: "error", message: summary, details: eBody || summary });
|
|
266
|
+
cases.push({ classname, name, durationMs: caseDurationMs, status: "error", message: summary, details: eBody || summary, ...capturedOutput });
|
|
261
267
|
}
|
|
262
268
|
else if (skippedTag) {
|
|
263
269
|
caseSkipped += 1;
|
|
264
270
|
const skippedAttrs = body.match(/<skipped\b([^>]*)/i)?.[1] ?? "";
|
|
265
271
|
const message = attr(skippedAttrs, "message");
|
|
266
|
-
cases.push({ classname, name, durationMs: caseDurationMs, status: "skipped", ...(message ? { message } : {}) });
|
|
272
|
+
cases.push({ classname, name, durationMs: caseDurationMs, status: "skipped", ...(message ? { message } : {}), ...capturedOutput });
|
|
267
273
|
}
|
|
268
274
|
else {
|
|
269
|
-
cases.push({ classname, name, durationMs: caseDurationMs, status: "passed" });
|
|
275
|
+
cases.push({ classname, name, durationMs: caseDurationMs, status: "passed", ...capturedOutput });
|
|
270
276
|
}
|
|
271
277
|
match = caseRe.exec(trimmed);
|
|
272
278
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
8
8
|
import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
9
|
+
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
9
10
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
10
11
|
/**
|
|
11
12
|
* Load the canonical frontend-implementation-contract-v1 JSON Schema from the
|
|
@@ -122,7 +123,7 @@ export const frontendImplementationContractSchema = z
|
|
|
122
123
|
verificationTargetIds: z.array(z.string().min(1)),
|
|
123
124
|
evidenceGap: gap.optional(),
|
|
124
125
|
})
|
|
125
|
-
.strict()),
|
|
126
|
+
.strict()).min(1),
|
|
126
127
|
uiStates: z.array(z
|
|
127
128
|
.object({
|
|
128
129
|
name: z.string().min(1),
|
|
@@ -132,7 +133,7 @@ export const frontendImplementationContractSchema = z
|
|
|
132
133
|
verificationTargetIds: z.array(z.string().min(1)).optional(),
|
|
133
134
|
notApplicableReason: z.string().min(1).optional(),
|
|
134
135
|
})
|
|
135
|
-
.strict()),
|
|
136
|
+
.strict()).min(1),
|
|
136
137
|
interactions: z.array(z
|
|
137
138
|
.object({
|
|
138
139
|
name: z.string().min(1),
|
|
@@ -185,7 +186,7 @@ export const frontendImplementationContractSchema = z
|
|
|
185
186
|
requirementIds: z.array(id),
|
|
186
187
|
uiStates: z.array(z.string().min(1)),
|
|
187
188
|
})
|
|
188
|
-
.strict()),
|
|
189
|
+
.strict()).min(1),
|
|
189
190
|
evidenceGaps: z.array(gap),
|
|
190
191
|
})
|
|
191
192
|
.strict()
|
|
@@ -198,6 +199,30 @@ export const frontendImplementationContractSchema = z
|
|
|
198
199
|
path: ["verificationTargets"],
|
|
199
200
|
});
|
|
200
201
|
const known = new Set(value.sourceBinding.requirementIds);
|
|
202
|
+
const stateNames = new Set(value.uiStates.map((state) => state.name));
|
|
203
|
+
for (const target of value.verificationTargets) {
|
|
204
|
+
// Verification evidence may point to read-only project files such as
|
|
205
|
+
// tsconfig.json or a test entrypoint; implementation targets remain
|
|
206
|
+
// governed by targets.files and the writer writeSet.
|
|
207
|
+
for (const requirementId of target.requirementIds) {
|
|
208
|
+
if (!known.has(requirementId)) {
|
|
209
|
+
ctx.addIssue({
|
|
210
|
+
code: "custom",
|
|
211
|
+
message: `verification target references unknown requirement ${requirementId}`,
|
|
212
|
+
path: ["verificationTargets"],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const stateName of target.uiStates) {
|
|
217
|
+
if (!stateNames.has(stateName)) {
|
|
218
|
+
ctx.addIssue({
|
|
219
|
+
code: "custom",
|
|
220
|
+
message: `verification target references unknown UI state ${stateName}`,
|
|
221
|
+
path: ["verificationTargets"],
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
201
226
|
for (const requirement of value.requirements) {
|
|
202
227
|
if (!known.has(requirement.id))
|
|
203
228
|
ctx.addIssue({
|
|
@@ -238,7 +263,70 @@ export const frontendImplementationContractSchema = z
|
|
|
238
263
|
path: ["uiStates"],
|
|
239
264
|
});
|
|
240
265
|
}
|
|
266
|
+
if (value.mockApi.strategy !== "not-needed") {
|
|
267
|
+
if (value.mockApi.endpoints.length === 0) {
|
|
268
|
+
ctx.addIssue({
|
|
269
|
+
code: "custom",
|
|
270
|
+
message: "Mock strategy requires at least one endpoint",
|
|
271
|
+
path: ["mockApi", "endpoints"],
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
value.mockApi.endpoints.forEach((endpoint, index) => {
|
|
275
|
+
if (!endpoint.fixture) {
|
|
276
|
+
ctx.addIssue({
|
|
277
|
+
code: "custom",
|
|
278
|
+
message: "Mock endpoint requires a fixture path",
|
|
279
|
+
path: ["mockApi", "endpoints", index, "fixture"],
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (!endpoint.consumer) {
|
|
283
|
+
ctx.addIssue({
|
|
284
|
+
code: "custom",
|
|
285
|
+
message: "Mock endpoint requires a consumer path",
|
|
286
|
+
path: ["mockApi", "endpoints", index, "consumer"],
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
for (const [field, file] of [["fixture", endpoint.fixture], ["consumer", endpoint.consumer]]) {
|
|
290
|
+
if (file && !value.targets.files.some((pattern) => pathMatchesPattern(file, pattern))) {
|
|
291
|
+
ctx.addIssue({
|
|
292
|
+
code: "custom",
|
|
293
|
+
message: `Mock ${field} is outside contract targets: ${file}`,
|
|
294
|
+
path: ["mockApi", "endpoints", index, field],
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
241
300
|
});
|
|
301
|
+
export async function assertFrontendSourceBindingFresh(input) {
|
|
302
|
+
for (const source of input.binding.sources) {
|
|
303
|
+
// DAG source bindings are task-relative (for example source/需求.md),
|
|
304
|
+
// while older bindings may already contain the repository-relative
|
|
305
|
+
// .harness/tasks/<taskId>/ prefix. Resolve both forms before hashing.
|
|
306
|
+
const taskDir = path.resolve(input.workspaceRoot, ".harness", "tasks", input.binding.taskId);
|
|
307
|
+
const absolute = source.path.startsWith(".harness/")
|
|
308
|
+
? path.resolve(input.workspaceRoot, source.path)
|
|
309
|
+
: path.resolve(taskDir, source.path);
|
|
310
|
+
const relative = path.relative(input.workspaceRoot, absolute);
|
|
311
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
312
|
+
throw new Error(`frontend source binding escapes workspace: ${source.path}`);
|
|
313
|
+
}
|
|
314
|
+
let content;
|
|
315
|
+
try {
|
|
316
|
+
content = await readFile(absolute);
|
|
317
|
+
const info = await stat(absolute);
|
|
318
|
+
if (!info.isFile())
|
|
319
|
+
throw new Error("not a regular file");
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
throw new Error(`frontend source binding file unavailable: ${source.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
323
|
+
}
|
|
324
|
+
const actual = createHash("sha256").update(content).digest("hex");
|
|
325
|
+
if (actual !== source.sha256) {
|
|
326
|
+
throw new Error(`frontend source binding is stale: ${source.path}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
242
330
|
const SECRET_KEY = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential|authorization)/i;
|
|
243
331
|
const SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b)/;
|
|
244
332
|
function secretIssues(value, at = "$", issues = []) {
|
|
@@ -314,9 +402,42 @@ function looksLikeStrictFrontendContract(value) {
|
|
|
314
402
|
* Near-schema payloads are left untouched so unknown-key fail-closed still holds.
|
|
315
403
|
*/
|
|
316
404
|
export function coerceFrontendImplementationContractInput(value, canonicalBinding) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
405
|
+
const rawRecord = asRecord(value);
|
|
406
|
+
const verificationTargetIds = rawRecord && Array.isArray(rawRecord.verificationTargets)
|
|
407
|
+
? new Set(rawRecord.verificationTargets
|
|
408
|
+
.map((item) => asString(asRecord(item)?.id))
|
|
409
|
+
.filter(Boolean))
|
|
410
|
+
: undefined;
|
|
411
|
+
const normalizedValue = rawRecord
|
|
412
|
+
? {
|
|
413
|
+
...rawRecord,
|
|
414
|
+
requirements: verificationTargetIds && Array.isArray(rawRecord.requirements)
|
|
415
|
+
? rawRecord.requirements.map((item) => {
|
|
416
|
+
const requirement = asRecord(item);
|
|
417
|
+
if (!requirement || !Array.isArray(requirement.verificationTargetIds))
|
|
418
|
+
return item;
|
|
419
|
+
return {
|
|
420
|
+
...requirement,
|
|
421
|
+
verificationTargetIds: requirement.verificationTargetIds.filter((id) => typeof id === "string" && verificationTargetIds.has(id)),
|
|
422
|
+
};
|
|
423
|
+
})
|
|
424
|
+
: rawRecord.requirements,
|
|
425
|
+
uiStates: Array.isArray(rawRecord.uiStates)
|
|
426
|
+
? rawRecord.uiStates.map((item) => {
|
|
427
|
+
const state = asRecord(item);
|
|
428
|
+
if (!state ||
|
|
429
|
+
state.applicable === false ||
|
|
430
|
+
(state.notApplicableReason !== "" && state.notApplicableReason !== null))
|
|
431
|
+
return item;
|
|
432
|
+
const { notApplicableReason: _emptyReason, ...withoutEmptyReason } = state;
|
|
433
|
+
return withoutEmptyReason;
|
|
434
|
+
})
|
|
435
|
+
: rawRecord.uiStates,
|
|
436
|
+
}
|
|
437
|
+
: value;
|
|
438
|
+
if (looksLikeStrictFrontendContract(normalizedValue))
|
|
439
|
+
return normalizedValue;
|
|
440
|
+
const record = asRecord(normalizedValue);
|
|
320
441
|
if (!record)
|
|
321
442
|
return value;
|
|
322
443
|
const implementation = asRecord(record.implementation);
|
|
@@ -333,7 +454,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
333
454
|
...asStringArray(asRecord(component?.paths)?.domHelper ? [asRecord(component?.paths)?.domHelper] : []),
|
|
334
455
|
].filter((item, index, arr) => arr.indexOf(item) === index);
|
|
335
456
|
if (targetFiles.length === 0) {
|
|
336
|
-
|
|
457
|
+
throw new Error("frontend contract compatibility input must declare target files");
|
|
337
458
|
}
|
|
338
459
|
const riskRaw = asString(record.riskLevel) ||
|
|
339
460
|
asString(record.risk) ||
|
|
@@ -383,21 +504,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
383
504
|
});
|
|
384
505
|
}
|
|
385
506
|
if (verificationTargets.length === 0) {
|
|
386
|
-
|
|
387
|
-
id: "VT-STATIC",
|
|
388
|
-
type: "static",
|
|
389
|
-
commandLabel: "npm run typecheck",
|
|
390
|
-
file: targetFiles[0],
|
|
391
|
-
requirementIds: [...canonicalBinding.requirementIds],
|
|
392
|
-
uiStates: ["success"],
|
|
393
|
-
}, {
|
|
394
|
-
id: "VT-UNIT",
|
|
395
|
-
type: "unit",
|
|
396
|
-
commandLabel: "npm run test:unit:fe",
|
|
397
|
-
file: targetFiles.find((p) => p.includes("__tests__")) || targetFiles[0],
|
|
398
|
-
requirementIds: [...canonicalBinding.requirementIds],
|
|
399
|
-
uiStates: ["success", "error"],
|
|
400
|
-
});
|
|
507
|
+
throw new Error("frontend contract compatibility input must declare verification targets");
|
|
401
508
|
}
|
|
402
509
|
const defaultVerificationIds = verificationTargets.map((item) => String(item.id));
|
|
403
510
|
const requirements = [];
|
|
@@ -440,11 +547,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
440
547
|
}
|
|
441
548
|
for (const id of canonicalBinding.requirementIds) {
|
|
442
549
|
if (!requirements.some((item) => item.id === id)) {
|
|
443
|
-
|
|
444
|
-
id,
|
|
445
|
-
implementationTargets: targetFiles,
|
|
446
|
-
verificationTargetIds: defaultVerificationIds,
|
|
447
|
-
});
|
|
550
|
+
throw new Error(`frontend contract compatibility input does not cover ${id}`);
|
|
448
551
|
}
|
|
449
552
|
}
|
|
450
553
|
const uiStates = [];
|
|
@@ -511,14 +614,20 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
511
614
|
});
|
|
512
615
|
}
|
|
513
616
|
const strategyRaw = asString(mockApiIn.strategy) || "not-needed";
|
|
514
|
-
const
|
|
617
|
+
const allowedStrategies = [
|
|
515
618
|
"native",
|
|
516
619
|
"browser-intercept",
|
|
517
620
|
"request-adapter",
|
|
518
621
|
"not-needed",
|
|
519
|
-
]
|
|
520
|
-
|
|
521
|
-
:
|
|
622
|
+
];
|
|
623
|
+
if (!allowedStrategies.includes(strategyRaw)) {
|
|
624
|
+
throw new Error(`frontend contract compatibility input has unsupported Mock strategy: ${strategyRaw}`);
|
|
625
|
+
}
|
|
626
|
+
const strategy = strategyRaw;
|
|
627
|
+
const productionDefaultOff = mockApiIn.productionDefaultOff === true || mockApiIn.productionMockOff === true;
|
|
628
|
+
if (!productionDefaultOff) {
|
|
629
|
+
throw new Error("frontend contract compatibility input must prove production Mock is off");
|
|
630
|
+
}
|
|
522
631
|
const activation = asString(mockApiIn.activation) ||
|
|
523
632
|
(strategy === "not-needed"
|
|
524
633
|
? "production remains real fetch; unit tests may inject fetchImpl only"
|