@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.
Files changed (48) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +73 -15
  3. package/dist/commands/client-recovery.js +56 -1
  4. package/dist/commands/init-upgrade.js +186 -21
  5. package/dist/commands/init.js +1 -1
  6. package/dist/executors/dag-pi-executor.js +52 -3
  7. package/dist/executors/pi-playwright-cli-tool.js +14 -8
  8. package/dist/executors/shell-executor.js +288 -0
  9. package/dist/task/config-types.js +21 -0
  10. package/dist/worker/console/app-data.js +132 -11
  11. package/dist/worker/console/chat/pi-runtime.js +24 -42
  12. package/dist/worker/console/chat/resource-loader.js +11 -20
  13. package/dist/worker/console/chat/routes.js +7 -8
  14. package/dist/worker/console/chat/runtime-context.js +1 -1
  15. package/dist/worker/console/chat/tools.js +67 -54
  16. package/dist/worker/console/operation-runner.js +15 -1
  17. package/dist/worker/console/operation-store.js +70 -49
  18. package/dist/worker/console/operator-actions.js +57 -1
  19. package/dist/worker/console/static/assets/index-Cwx-ZVEQ.js +29 -0
  20. package/dist/worker/console/static/favicon.svg +37 -0
  21. package/dist/worker/console/static/index.html +2 -1
  22. package/dist/workflows/dag/backend-test-scenario-param.js +846 -0
  23. package/dist/workflows/dag/backend-test-writer-completeness.js +418 -0
  24. package/dist/workflows/dag/frontend-test-case-checklist.js +94 -15
  25. package/dist/workflows/dag/frontend-test-case-manifest.js +104 -0
  26. package/dist/workflows/dag/frontend-test-html-report.js +106 -24
  27. package/dist/workflows/dag/frontend-test-result-contract.js +3 -0
  28. package/dist/workflows/dag/init-hybrid.js +187 -108
  29. package/dist/workflows/dag/node-execution.js +31 -2
  30. package/dist/workflows/dag/retry-policy.js +55 -18
  31. package/dist/workflows/dag/types.js +41 -0
  32. package/dist/workflows/dag/validate.js +42 -4
  33. package/docs/operations/README.md +1 -1
  34. package/docs/templates/README.md +2 -1
  35. package/docs/templates/agent-dag.schema.json +9 -4
  36. package/docs/templates/backend-test-dag.json +36 -11
  37. package/docs/templates/frontend-test-case-checklist.md +1 -1
  38. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +9 -1
  39. package/docs/templates/frontend-test-dag.json +125 -267
  40. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +7 -1
  41. package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
  42. package/docs/templates/frontend-test-standard-scenarios.v1.json +114 -0
  43. package/docs/templates/init-managed-agents.md +1 -1
  44. package/harness.json +2 -2
  45. package/package.json +2 -1
  46. package/skills/playwright-cli/SKILL.md +1 -1
  47. package/skills/playwright-cli-case-generator/SKILL.md +1 -1
  48. 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
- const indexPath = requestIndexFile(this.appData, clientRequestId);
30
- const existingIndex = await readJsonIfExists(indexPath);
31
- if (existingIndex) {
32
- const existing = await this.get(existingIndex.operationId);
33
- if (existing) {
34
- if (existing.payloadHash === payloadHash) {
35
- return { kind: "idempotent", operation: existing };
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
- const operationId = newOperationId();
46
- const now = new Date().toISOString();
47
- const operation = {
48
- schemaVersion: 1,
49
- operationId,
50
- clientRequestId,
51
- action: input.action,
52
- actionParams: input.actionParams,
53
- payloadHash,
54
- state: "queued",
55
- createdAt: now,
56
- taskId: input.taskId,
57
- cliArgs: input.cliArgs,
58
- ...(input.externalCommand ? { externalCommand: input.externalCommand } : {}),
59
- };
60
- await writeSecureJson(operationFile(this.appData, operationId), operation);
61
- await writeSecureJson(indexPath, { operationId, payloadHash });
62
- return { kind: "created", operation };
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
- await writeSecureJson(operationFile(this.appData, operation.operationId), operation);
69
- return operation;
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
- const current = await this.get(operationId);
73
- if (!current) {
74
- throw new Error(`operation not found: ${operationId}`);
75
- }
76
- const next = {
77
- ...current,
78
- ...patch,
79
- operationId: current.operationId,
80
- schemaVersion: 1,
81
- clientRequestId: current.clientRequestId,
82
- payloadHash: current.payloadHash,
83
- action: current.action,
84
- createdAt: current.createdAt,
85
- };
86
- return this.save(next);
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: result.stderr || `exit ${result.exitCode}`,
187
+ message,
132
188
  });
133
189
  }
134
190
  catch (error) {