@tea-agent/loop-agent 0.28.13 → 0.29.1
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 +73 -15
- package/dist/commands/client-recovery.js +56 -1
- package/dist/commands/init-upgrade.js +186 -21
- package/dist/commands/init.js +1 -1
- package/dist/executors/dag-pi-executor.js +52 -3
- package/dist/executors/pi-playwright-cli-tool.js +14 -8
- package/dist/executors/shell-executor.js +288 -0
- package/dist/task/config-types.js +21 -0
- package/dist/worker/console/app-data.js +132 -11
- package/dist/worker/console/chat/pi-runtime.js +24 -42
- package/dist/worker/console/chat/resource-loader.js +11 -20
- package/dist/worker/console/chat/routes.js +7 -8
- package/dist/worker/console/chat/runtime-context.js +1 -1
- package/dist/worker/console/chat/tools.js +67 -54
- package/dist/worker/console/operation-runner.js +15 -1
- package/dist/worker/console/operation-store.js +70 -49
- package/dist/worker/console/operator-actions.js +57 -1
- package/dist/worker/console/static/assets/index-Cwx-ZVEQ.js +29 -0
- package/dist/worker/console/static/favicon.svg +37 -0
- package/dist/worker/console/static/index.html +2 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +846 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +418 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +94 -15
- package/dist/workflows/dag/frontend-test-case-manifest.js +104 -0
- package/dist/workflows/dag/frontend-test-html-report.js +106 -24
- package/dist/workflows/dag/frontend-test-result-contract.js +3 -0
- package/dist/workflows/dag/init-hybrid.js +187 -108
- package/dist/workflows/dag/node-execution.js +31 -2
- package/dist/workflows/dag/retry-policy.js +55 -18
- package/dist/workflows/dag/types.js +41 -0
- package/dist/workflows/dag/validate.js +42 -4
- package/docs/operations/README.md +1 -1
- package/docs/templates/README.md +2 -1
- package/docs/templates/agent-dag.schema.json +9 -4
- package/docs/templates/backend-test-dag.json +36 -11
- package/docs/templates/frontend-test-case-checklist.md +1 -1
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +9 -1
- package/docs/templates/frontend-test-dag.json +125 -267
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +7 -1
- package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
- package/docs/templates/frontend-test-standard-scenarios.v1.json +114 -0
- package/docs/templates/init-managed-agents.md +1 -1
- package/harness.json +2 -2
- package/package.json +2 -1
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/playwright-cli-case-generator/SKILL.md +1 -1
- package/dist/worker/console/static/assets/index-BfRgtLF4.js +0 -29
|
@@ -17,73 +17,94 @@ export function newOperationId() {
|
|
|
17
17
|
}
|
|
18
18
|
export class OperationStore {
|
|
19
19
|
appData;
|
|
20
|
+
/**
|
|
21
|
+
* Per-key write lock. Concurrent update/save/create on the same operation
|
|
22
|
+
* or clientRequestId must not race rename() into the same JSON path
|
|
23
|
+
* (Windows EPERM) or last-write-wins lost patches.
|
|
24
|
+
*/
|
|
25
|
+
writeChains = new Map();
|
|
20
26
|
constructor(appData) {
|
|
21
27
|
this.appData = appData;
|
|
22
28
|
}
|
|
29
|
+
serialize(key, op) {
|
|
30
|
+
const prev = this.writeChains.get(key) ?? Promise.resolve();
|
|
31
|
+
const next = prev.then(op, op);
|
|
32
|
+
this.writeChains.set(key, next.then(() => undefined, () => undefined));
|
|
33
|
+
return next;
|
|
34
|
+
}
|
|
23
35
|
async create(input) {
|
|
24
36
|
const clientRequestId = input.clientRequestId.trim();
|
|
25
37
|
if (!clientRequestId) {
|
|
26
38
|
throw new Error("clientRequestId is required for mutation operations");
|
|
27
39
|
}
|
|
28
40
|
const payloadHash = computeActionPayloadHash(input.action, input.actionParams);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (existing
|
|
35
|
-
|
|
41
|
+
return this.serialize(`req:${clientRequestId}`, async () => {
|
|
42
|
+
const indexPath = requestIndexFile(this.appData, clientRequestId);
|
|
43
|
+
const existingIndex = await readJsonIfExists(indexPath);
|
|
44
|
+
if (existingIndex) {
|
|
45
|
+
const existing = await this.get(existingIndex.operationId);
|
|
46
|
+
if (existing) {
|
|
47
|
+
if (existing.payloadHash === payloadHash) {
|
|
48
|
+
return { kind: "idempotent", operation: existing };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
kind: "conflict",
|
|
52
|
+
code: "REQUEST_ID_REUSE_CONFLICT",
|
|
53
|
+
message: `clientRequestId ${clientRequestId} was reused with a different payload`,
|
|
54
|
+
existing,
|
|
55
|
+
};
|
|
36
56
|
}
|
|
37
|
-
return {
|
|
38
|
-
kind: "conflict",
|
|
39
|
-
code: "REQUEST_ID_REUSE_CONFLICT",
|
|
40
|
-
message: `clientRequestId ${clientRequestId} was reused with a different payload`,
|
|
41
|
-
existing,
|
|
42
|
-
};
|
|
43
57
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
const operationId = newOperationId();
|
|
59
|
+
const now = new Date().toISOString();
|
|
60
|
+
const operation = {
|
|
61
|
+
schemaVersion: 1,
|
|
62
|
+
operationId,
|
|
63
|
+
clientRequestId,
|
|
64
|
+
action: input.action,
|
|
65
|
+
actionParams: input.actionParams,
|
|
66
|
+
payloadHash,
|
|
67
|
+
state: "queued",
|
|
68
|
+
createdAt: now,
|
|
69
|
+
taskId: input.taskId,
|
|
70
|
+
cliArgs: input.cliArgs,
|
|
71
|
+
...(input.externalCommand
|
|
72
|
+
? { externalCommand: input.externalCommand }
|
|
73
|
+
: {}),
|
|
74
|
+
};
|
|
75
|
+
await writeSecureJson(operationFile(this.appData, operationId), operation);
|
|
76
|
+
await writeSecureJson(indexPath, { operationId, payloadHash });
|
|
77
|
+
return { kind: "created", operation };
|
|
78
|
+
});
|
|
63
79
|
}
|
|
64
80
|
async get(operationId) {
|
|
65
81
|
return readJsonIfExists(operationFile(this.appData, operationId));
|
|
66
82
|
}
|
|
67
83
|
async save(operation) {
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
return this.serialize(operation.operationId, async () => {
|
|
85
|
+
await writeSecureJson(operationFile(this.appData, operation.operationId), operation);
|
|
86
|
+
return operation;
|
|
87
|
+
});
|
|
70
88
|
}
|
|
71
89
|
async update(operationId, patch) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
return this.serialize(operationId, async () => {
|
|
91
|
+
const current = await this.get(operationId);
|
|
92
|
+
if (!current) {
|
|
93
|
+
throw new Error(`operation not found: ${operationId}`);
|
|
94
|
+
}
|
|
95
|
+
const next = {
|
|
96
|
+
...current,
|
|
97
|
+
...patch,
|
|
98
|
+
operationId: current.operationId,
|
|
99
|
+
schemaVersion: 1,
|
|
100
|
+
clientRequestId: current.clientRequestId,
|
|
101
|
+
payloadHash: current.payloadHash,
|
|
102
|
+
action: current.action,
|
|
103
|
+
createdAt: current.createdAt,
|
|
104
|
+
};
|
|
105
|
+
await writeSecureJson(operationFile(this.appData, operationId), next);
|
|
106
|
+
return next;
|
|
107
|
+
});
|
|
87
108
|
}
|
|
88
109
|
async list() {
|
|
89
110
|
const files = await listJsonFiles(this.appData.operations);
|
|
@@ -72,6 +72,49 @@ function readWriteSetGateToken(reviewPacket) {
|
|
|
72
72
|
const token = reviewPacket.writeSetGateToken;
|
|
73
73
|
return typeof token === "string" && token.includes(":") ? token : undefined;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Prefer structured stdout JSON failure semantics (findings / message) over bare
|
|
77
|
+
* `exit N` when stderr is empty — spine audit and similar read CLIs write details
|
|
78
|
+
* to stdout JSON and exit non-zero with empty stderr.
|
|
79
|
+
*/
|
|
80
|
+
function summarizeReadCliFailure(result) {
|
|
81
|
+
const fromJson = summarizeJsonFailure(result.json);
|
|
82
|
+
if (fromJson)
|
|
83
|
+
return fromJson;
|
|
84
|
+
const stderr = (result.stderr ?? "").trim();
|
|
85
|
+
if (stderr)
|
|
86
|
+
return stderr;
|
|
87
|
+
return `exit ${result.exitCode}`;
|
|
88
|
+
}
|
|
89
|
+
function summarizeJsonFailure(json) {
|
|
90
|
+
if (!json || typeof json !== "object")
|
|
91
|
+
return undefined;
|
|
92
|
+
const raw = json;
|
|
93
|
+
if (Array.isArray(raw.findings)) {
|
|
94
|
+
const messages = raw.findings
|
|
95
|
+
.map((finding) => {
|
|
96
|
+
if (!finding || typeof finding !== "object")
|
|
97
|
+
return "";
|
|
98
|
+
const message = finding.message;
|
|
99
|
+
return typeof message === "string" ? message.trim() : "";
|
|
100
|
+
})
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
if (messages.length > 0) {
|
|
103
|
+
const head = messages.slice(0, 6).join("; ");
|
|
104
|
+
return messages.length > 6 ? `${head}…` : head;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const nestedError = raw.error;
|
|
108
|
+
if (nestedError && typeof nestedError === "object") {
|
|
109
|
+
const message = nestedError.message;
|
|
110
|
+
if (typeof message === "string" && message.trim())
|
|
111
|
+
return message.trim();
|
|
112
|
+
}
|
|
113
|
+
if (typeof raw.message === "string" && raw.message.trim()) {
|
|
114
|
+
return raw.message.trim();
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
75
118
|
async function runReadCli(ctx, args, command) {
|
|
76
119
|
const run = ctx.runCommand ?? ((a, o) => ctx.client.run(a, o));
|
|
77
120
|
try {
|
|
@@ -124,11 +167,24 @@ async function runReadCli(ctx, args, command) {
|
|
|
124
167
|
raw: result.json,
|
|
125
168
|
});
|
|
126
169
|
}
|
|
170
|
+
const message = summarizeReadCliFailure(result);
|
|
171
|
+
if (result.json && typeof result.json === "object") {
|
|
172
|
+
return buildOperatorResult({
|
|
173
|
+
command,
|
|
174
|
+
ok: false,
|
|
175
|
+
outcome: "rejected",
|
|
176
|
+
result: result.json,
|
|
177
|
+
error: {
|
|
178
|
+
code: "INTERNAL_ERROR",
|
|
179
|
+
message,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
127
183
|
return operatorFailed({
|
|
128
184
|
command,
|
|
129
185
|
outcome: "rejected",
|
|
130
186
|
code: "INTERNAL_ERROR",
|
|
131
|
-
message
|
|
187
|
+
message,
|
|
132
188
|
});
|
|
133
189
|
}
|
|
134
190
|
catch (error) {
|