@tea-agent/loop-agent 0.24.11-beta.0 → 0.25.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 (97) hide show
  1. package/CHANGELOG.md +53 -4
  2. package/README.md +4 -4
  3. package/dist/application/dag/generate-task-dag.js +25 -8
  4. package/dist/commands/dag-init-hybrid.js +3 -2
  5. package/dist/commands/delegate.js +11 -11
  6. package/dist/commands/init.js +29 -14
  7. package/dist/commands/instructions.js +1 -2
  8. package/dist/executors/dag-pi-executor.js +74 -13
  9. package/dist/executors/pi-executor.js +25 -7
  10. package/dist/executors/pi-prompt-transport.js +198 -0
  11. package/dist/executors/pi-sdk-executor.js +1 -1
  12. package/dist/executors/process-tree.js +33 -0
  13. package/dist/executors/shell-executor.js +362 -24
  14. package/dist/executors/shell-verification.js +4 -2
  15. package/dist/infrastructure/harness/task-store.js +31 -0
  16. package/dist/shared/operator/capabilities.js +6 -0
  17. package/dist/task/read-model.js +3 -7
  18. package/dist/task/runtime.js +12 -3
  19. package/dist/verification/maven/cache.js +142 -0
  20. package/dist/verification/maven/index.js +120 -0
  21. package/dist/verification/maven/plan-commands.js +421 -0
  22. package/dist/verification/maven/pom-static.js +136 -0
  23. package/dist/verification/maven/scope-resolve.js +153 -0
  24. package/dist/verification/maven/stale.js +130 -0
  25. package/dist/verification/maven/types.js +23 -0
  26. package/dist/verification/maven/workspace-graph.js +322 -0
  27. package/dist/worker/cli.js +236 -21
  28. package/dist/worker/delivery/final-verification.js +12 -0
  29. package/dist/worker/delivery/git-transaction.js +32 -7
  30. package/dist/worker/delivery/package.js +9 -5
  31. package/dist/worker/delivery/verification-bundle.js +26 -5
  32. package/dist/worker/feature/advance.js +301 -0
  33. package/dist/worker/feature/doctor.js +223 -0
  34. package/dist/worker/feature/next-action.js +11 -3
  35. package/dist/worker/feature/scaffold.js +798 -0
  36. package/dist/worker/observe/dag-run-artifacts.js +90 -0
  37. package/dist/worker/observe/node-input.js +444 -0
  38. package/dist/worker/observe/routes.js +17 -0
  39. package/dist/worker/observe/static/api.js +9 -0
  40. package/dist/worker/observe/static/constants.js +9 -0
  41. package/dist/worker/observe/static/state.js +14 -0
  42. package/dist/worker/observe/static/styles.css +74 -0
  43. package/dist/worker/observe/static/views/dag-inspector.js +371 -15
  44. package/dist/worker/outcomes/projector.js +5 -1
  45. package/dist/worker/run-task/run-task.js +18 -2
  46. package/dist/worker/runner/run-ready.js +41 -1
  47. package/dist/workflows/dag/backend-test-markdown-workflow.js +300 -9
  48. package/dist/workflows/dag/backend-test-result-contract.js +103 -0
  49. package/dist/workflows/dag/failure-category.js +5 -1
  50. package/dist/workflows/dag/frontend-implementation-contract.js +4 -0
  51. package/dist/workflows/dag/frontend-prewrite-gate.js +0 -17
  52. package/dist/workflows/dag/frontend-test-result-contract.js +106 -41
  53. package/dist/workflows/dag/init-hybrid.js +224 -39
  54. package/dist/workflows/dag/node-execution.js +3 -2
  55. package/dist/workflows/dag/reconcile-run.js +24 -0
  56. package/dist/workflows/dag/types.js +57 -0
  57. package/dist/workflows/dag/validate.js +19 -2
  58. package/dist/workflows/loop/actions/dag-action.js +2 -1
  59. package/docs/architecture/dag-execution.md +5 -1
  60. package/docs/architecture/runtime-boundaries.md +11 -1
  61. package/docs/architecture/worker-and-feature.md +2 -0
  62. package/docs/init-surface.manifest.json +3 -0
  63. package/docs/templates/backend-test-dag.json +3 -3
  64. package/docs/templates/product-line/README.md +17 -1
  65. package/docs/templates/product-line/feature-scaffold-batch.example.yaml +31 -0
  66. package/docs/templates/product-line/scaffold-samples/README.md +36 -0
  67. package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +13 -0
  68. package/docs/templates/product-line/scaffold-samples/backend-only/design.md +14 -0
  69. package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +3 -0
  70. package/docs/templates/product-line/scaffold-samples/backend-only/requirement.md +6 -0
  71. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/BE-IMPL-001.yaml +83 -0
  72. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/task-graph.yaml +14 -0
  73. package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +15 -0
  74. package/docs/templates/product-line/scaffold-samples/fe-with-api/design.md +18 -0
  75. package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +3 -0
  76. package/docs/templates/product-line/scaffold-samples/fe-with-api/requirement.md +6 -0
  77. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/BE-IMPL-001.yaml +84 -0
  78. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/CONTRACT-001.yaml +84 -0
  79. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/FE-IMPL-001.yaml +86 -0
  80. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/task-graph.yaml +40 -0
  81. package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +13 -0
  82. package/docs/templates/product-line/scaffold-samples/frontend-only/design.md +14 -0
  83. package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +3 -0
  84. package/docs/templates/product-line/scaffold-samples/frontend-only/requirement.md +6 -0
  85. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/FE-IMPL-001.yaml +85 -0
  86. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/task-graph.yaml +14 -0
  87. package/examples/l5-report-coms-process-definition.html +322 -0
  88. package/package.json +1 -1
  89. package/skills/agent-worker/references/agent-worker-operator.md +84 -0
  90. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  91. package/skills/loop-agent/SKILL.md +14 -16
  92. package/skills/loop-agent/references/command-reference.md +33 -12
  93. package/skills/loop-agent/references/harness-policy.md +28 -6
  94. package/skills/loop-agent/references/hybrid-dag.md +8 -7
  95. package/skills/loop-agent/references/post-implementation-and-patterns.md +19 -9
  96. package/skills/loop-agent/references/source-and-plan-practice.md +161 -0
  97. package/skills/loop-agent/references/task-workflow.md +13 -5
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { lstat, readFile, realpath } from "node:fs/promises";
2
+ import { lstat, readFile, realpath, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { z } from "zod";
5
5
  import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
@@ -68,6 +68,111 @@ export const frontendTestResultContractSchema = z.object({
68
68
  ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["integrationMode"], message: "real integration requires passed outcome" });
69
69
  }
70
70
  });
71
+ async function isNonEmptyFile(filePath) {
72
+ try {
73
+ const info = await stat(filePath);
74
+ return info.isFile() && info.size > 0;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ function isSafeEvidenceShellPath(value) {
81
+ return (typeof value === "string" &&
82
+ value.length > 0 &&
83
+ !path.isAbsolute(value) &&
84
+ !path.win32.isAbsolute(value) &&
85
+ !value.includes(".."));
86
+ }
87
+ /**
88
+ * Validate frontend browser evidence without going through a shell command.
89
+ * Keeping this in the Node executor avoids CMD/Git Bash/PowerShell quoting and
90
+ * backslash interpretation for the former long `node -e` command.
91
+ */
92
+ export async function validateFrontendCaseEvidence(input) {
93
+ const manifestPath = path.join(input.workspaceRoot, "testcase/frontend/cases/manifest.json");
94
+ try {
95
+ await stat(manifestPath);
96
+ }
97
+ catch {
98
+ throw new Error("missing testcase/frontend/cases/manifest.json");
99
+ }
100
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
101
+ if (!Array.isArray(manifest.cases))
102
+ throw new Error("invalid frontend case manifest");
103
+ const statuses = new Set(["passed", "failed", "blocked"]);
104
+ const issues = [];
105
+ let hardFail = false;
106
+ for (const rawCase of manifest.cases) {
107
+ const item = rawCase;
108
+ const id = typeof item?.caseId === "string" ? item.caseId : "?";
109
+ const dir = item?.evidenceDir;
110
+ const prefix = `testcase/frontend/evidence/${id}`;
111
+ if (!item ||
112
+ typeof item.caseId !== "string" ||
113
+ typeof dir !== "string" ||
114
+ !isSafeEvidenceShellPath(dir) ||
115
+ !(dir === prefix || dir.startsWith(`${prefix}/`))) {
116
+ hardFail = true;
117
+ issues.push({ ruleId: "unsafe-evidence-dir", caseId: id, detail: String(dir) });
118
+ continue;
119
+ }
120
+ const execution = path.join(input.workspaceRoot, dir, "execution.md");
121
+ const resultPath = path.join(input.workspaceRoot, dir, "case-result.json");
122
+ if (!(await isNonEmptyFile(execution))) {
123
+ issues.push({ ruleId: "missing-execution", caseId: id, detail: "execution.md is missing or empty" });
124
+ }
125
+ let result = null;
126
+ if (!(await isNonEmptyFile(resultPath))) {
127
+ issues.push({ ruleId: "missing-case-result", caseId: id, detail: "case-result.json is missing" });
128
+ continue;
129
+ }
130
+ try {
131
+ result = JSON.parse(await readFile(resultPath, "utf8"));
132
+ }
133
+ catch {
134
+ issues.push({ ruleId: "invalid-case-result", caseId: id, detail: "case-result.json is malformed" });
135
+ continue;
136
+ }
137
+ if (!result || result.caseId !== id) {
138
+ issues.push({ ruleId: "case-result-identity", caseId: id, detail: "case-result caseId does not match manifest" });
139
+ }
140
+ if (!statuses.has(String(result?.status))) {
141
+ issues.push({ ruleId: "invalid-case-status", caseId: id, detail: "status must be passed, failed, or blocked" });
142
+ }
143
+ if (!Array.isArray(result?.evidencePaths)) {
144
+ issues.push({ ruleId: "invalid-evidence-paths", caseId: id, detail: "evidencePaths must be an array" });
145
+ }
146
+ else {
147
+ for (const evidencePath of result.evidencePaths) {
148
+ if (!isSafeEvidenceShellPath(evidencePath)) {
149
+ hardFail = true;
150
+ issues.push({ ruleId: "unsafe-evidence-path", caseId: id, detail: String(evidencePath) });
151
+ continue;
152
+ }
153
+ const currentPrefix = `${prefix}/`;
154
+ if (evidencePath.startsWith("testcase/frontend/evidence/") && !evidencePath.startsWith(currentPrefix)) {
155
+ hardFail = true;
156
+ issues.push({ ruleId: "cross-case-evidence-path", caseId: id, detail: evidencePath });
157
+ continue;
158
+ }
159
+ const target = evidencePath.startsWith("testcase/")
160
+ ? path.join(input.workspaceRoot, evidencePath)
161
+ : path.join(input.workspaceRoot, dir, evidencePath);
162
+ if (!(await isNonEmptyFile(target))) {
163
+ issues.push({ ruleId: "missing-evidence", caseId: id, detail: `${evidencePath} is missing or empty` });
164
+ }
165
+ }
166
+ }
167
+ if (result?.status === "passed" && (!Array.isArray(result.evidencePaths) || !result.evidencePaths.some((entry) => /\.(png|jpg|jpeg|webp|zip|har|webm|mp4)$/i.test(String(entry))))) {
168
+ issues.push({ ruleId: "passed-without-browser-evidence", caseId: id, detail: "passed case has no screenshot, HAR, video, or equivalent browser artifact" });
169
+ }
170
+ if (result?.status === "blocked" && (typeof result.blockedReason !== "string" || !result.blockedReason.trim())) {
171
+ issues.push({ ruleId: "blocked-reason", caseId: id, detail: "blocked result has no usable reason" });
172
+ }
173
+ }
174
+ return { cases: manifest.cases.length, issues, hardFail };
175
+ }
71
176
  function sha256(content) {
72
177
  return createHash("sha256").update(content).digest("hex");
73
178
  }
@@ -214,43 +319,3 @@ export function buildFrontendTestOutcomeGateShellSnippet(options) {
214
319
  `node -e 'const fs=require("fs");const r=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));const ok=r.outcome==="passed"&&r.integrationMode==="real"&&Number(r.totals?.failed||0)===0&&Number(r.totals?.blocked||0)===0&&Array.isArray(r.acceptanceCoverage?.missing)&&r.acceptanceCoverage.missing.length===0;console.log("frontend-test outcome="+r.outcome+" integrationMode="+r.integrationMode);if(!ok)process.exit(1);' "\${RESULT}"`,
215
320
  ].join("; ");
216
321
  }
217
- /**
218
- * Shared frontend-test evidence advisory check for map children + node 7.
219
- * Hard-fails only for unsafe evidence directories or evidence paths. Missing,
220
- * malformed, or empty evidence is reported without mutating case outputs.
221
- */
222
- export function buildFrontendCaseEvidenceValidateShellSnippet() {
223
- const body = [
224
- "const fs=require('fs'),path=require('path');",
225
- "const manifestPath='testcase/frontend/cases/manifest.json';",
226
- "if(!fs.existsSync(manifestPath))throw new Error('missing '+manifestPath);",
227
- "const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
228
- "if(!Array.isArray(manifest.cases))throw new Error('invalid frontend case manifest');",
229
- "const statuses=new Set(['passed','failed','blocked']);",
230
- "const issues=[];",
231
- "let hardFail=false;",
232
- "function isSafeRel(p){return typeof p==='string'&&p.length>0&&!path.isAbsolute(p)&&!path.win32.isAbsolute(p)&&!p.includes('..');}",
233
- "for(const c of manifest.cases){",
234
- " const id=c&&typeof c.caseId==='string'?c.caseId:'?';",
235
- " const dir=c&&c.evidenceDir;",
236
- " const prefix='testcase/frontend/evidence/'+id;",
237
- " if(!c||typeof c.caseId!=='string'||typeof dir!=='string'||!isSafeRel(dir)||!(dir===prefix||dir.startsWith(prefix+'/'))){",
238
- " hardFail=true; issues.push({ruleId:'unsafe-evidence-dir',caseId:id,detail:String(dir)}); continue;",
239
- " }",
240
- " const execution=path.join(dir,'execution.md');",
241
- " const resultPath=path.join(dir,'case-result.json');",
242
- " if(!fs.existsSync(execution)||!fs.statSync(execution).isFile()||fs.statSync(execution).size===0)issues.push({ruleId:'missing-execution',caseId:id,detail:'execution.md is missing or empty'});",
243
- " let result=null;",
244
- " if(!fs.existsSync(resultPath)){issues.push({ruleId:'missing-case-result',caseId:id,detail:'case-result.json is missing'});continue;}",
245
- " try{result=JSON.parse(fs.readFileSync(resultPath,'utf8'));}catch(e){issues.push({ruleId:'invalid-case-result',caseId:id,detail:'case-result.json is malformed'});continue;}",
246
- " if(!result||result.caseId!==id)issues.push({ruleId:'case-result-identity',caseId:id,detail:'case-result caseId does not match manifest'});",
247
- " if(!statuses.has(result&&result.status))issues.push({ruleId:'invalid-case-status',caseId:id,detail:'status must be passed, failed, or blocked'});",
248
- " if(!Array.isArray(result&&result.evidencePaths)){issues.push({ruleId:'invalid-evidence-paths',caseId:id,detail:'evidencePaths must be an array'});}else{for(const p of result.evidencePaths){if(!isSafeRel(p)){hardFail=true;issues.push({ruleId:'unsafe-evidence-path',caseId:id,detail:String(p)});continue;}const currentPrefix=prefix+'/';if(p.startsWith('testcase/frontend/evidence/')&&!p.startsWith(currentPrefix)){hardFail=true;issues.push({ruleId:'cross-case-evidence-path',caseId:id,detail:p});continue;}const target=p.startsWith('testcase/')?p:path.join(dir,p);if(!fs.existsSync(target)||!fs.statSync(target).isFile()||fs.statSync(target).size===0)issues.push({ruleId:'missing-evidence',caseId:id,detail:p+' is missing or empty'});}}",
249
- " if(result&&result.status==='passed'&&(!Array.isArray(result.evidencePaths)||!result.evidencePaths.some(p=>/\\.(png|jpg|jpeg|webp|zip|har|webm|mp4)$/i.test(p))))issues.push({ruleId:'passed-without-browser-evidence',caseId:id,detail:'passed case has no screenshot, HAR, video, or equivalent browser artifact'});",
250
- " if(result&&result.status==='blocked'&&(typeof result.blockedReason!=='string'||!result.blockedReason.trim()))issues.push({ruleId:'blocked-reason',caseId:id,detail:'blocked result has no usable reason'});",
251
- "}",
252
- "if(hardFail){console.error('frontend-test evidence hard-fail: '+JSON.stringify(issues)); process.exit(1);}",
253
- "console.log('frontend case evidence advisory validation ok cases='+manifest.cases.length+' findings='+issues.length+(issues.length?(' issues='+JSON.stringify(issues)):''));",
254
- ].join("");
255
- return ["node -e", JSON.stringify(body)].join(" ");
256
- }