@tea-agent/loop-agent 0.23.1 → 0.24.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 (71) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/README.md +1 -1
  3. package/bin/agent-worker.js +0 -0
  4. package/dist/commands/init.js +2 -2
  5. package/dist/executors/shell-executor.js +20 -7
  6. package/dist/shared/operator/capabilities.js +475 -2
  7. package/dist/task/dag-source-paths.js +50 -0
  8. package/dist/task/frontend-project-capability.js +316 -0
  9. package/dist/task/task-demand-routing.js +432 -0
  10. package/dist/worker/console/app-data.js +2 -0
  11. package/dist/worker/console/chat/artifact-card.js +23 -0
  12. package/dist/worker/console/chat/chat-event-store.js +495 -0
  13. package/dist/worker/console/chat/chat-ui-policy.js +25 -0
  14. package/dist/worker/console/chat/compaction-errors.js +64 -0
  15. package/dist/worker/console/chat/composer-draft-store.js +45 -0
  16. package/dist/worker/console/chat/context-panel.js +54 -0
  17. package/dist/worker/console/chat/contract-apply-receipt-store.js +174 -0
  18. package/dist/worker/console/chat/explore-tools.js +299 -0
  19. package/dist/worker/console/chat/human-gate-card.js +37 -0
  20. package/dist/worker/console/chat/interview-adapter.js +149 -0
  21. package/dist/worker/console/chat/operation-card.js +23 -0
  22. package/dist/worker/console/chat/pi-console-config.js +158 -0
  23. package/dist/worker/console/chat/pi-runtime.js +581 -43
  24. package/dist/worker/console/chat/repo-browser.js +140 -0
  25. package/dist/worker/console/chat/repo-walk.js +116 -0
  26. package/dist/worker/console/chat/resource-loader.js +18 -17
  27. package/dist/worker/console/chat/routes.js +1359 -65
  28. package/dist/worker/console/chat/runtime-context.js +24 -0
  29. package/dist/worker/console/chat/runtime-selection.js +37 -0
  30. package/dist/worker/console/chat/session-store.js +210 -11
  31. package/dist/worker/console/chat/shortcuts.js +15 -0
  32. package/dist/worker/console/chat/tool-adapter.js +81 -194
  33. package/dist/worker/console/chat/tools.js +72 -48
  34. package/dist/worker/console/chat/usage.js +37 -0
  35. package/dist/worker/console/chat/workspace-landing.js +56 -0
  36. package/dist/worker/console/dag-confirmation.js +42 -8
  37. package/dist/worker/console/human-gate-token.js +130 -0
  38. package/dist/worker/console/interview/grill-me.js +28 -5
  39. package/dist/worker/console/mutation-gate-receipt-store.js +184 -0
  40. package/dist/worker/console/operation-runner.js +6 -2
  41. package/dist/worker/console/operation-sse.js +26 -0
  42. package/dist/worker/console/operator-actions.js +420 -7
  43. package/dist/worker/console/server.js +14 -2
  44. package/dist/worker/console/static/assets/index-Cv68Ge5n.js +27 -0
  45. package/dist/worker/console/static/assets/index-Dp_2sKGk.css +1 -0
  46. package/dist/worker/console/static/index.html +2 -2
  47. package/dist/worker/observe/routes.js +8 -4
  48. package/dist/workflows/dag/backend-test-intake-context.js +10 -12
  49. package/dist/workflows/dag/backend-test-markdown-workflow.js +232 -47
  50. package/dist/workflows/dag/backend-test-result-contract.js +233 -0
  51. package/dist/workflows/dag/frontend-implementation-contract.js +23 -17
  52. package/dist/workflows/dag/frontend-lint-baseline.js +4 -4
  53. package/dist/workflows/dag/frontend-project-capability.js +1 -316
  54. package/dist/workflows/dag/frontend-worktree-diff.js +4 -1
  55. package/dist/workflows/dag/init-hybrid.js +46 -43
  56. package/dist/workflows/dag/rerun-plan.js +6 -10
  57. package/dist/workflows/dag/task-demand-routing.js +1 -383
  58. package/docs/README.md +1 -1
  59. package/docs/architecture/README.md +5 -5
  60. package/docs/architecture/evolution.md +4 -4
  61. package/docs/architecture/worker-and-feature.md +1 -1
  62. package/docs/templates/backend-test-dag.json +2 -2
  63. package/docs/templates/frontend-implementation-contract.schema.json +1 -1
  64. package/harness.json +1 -1
  65. package/package.json +1 -1
  66. package/skills/frontend-implementation/SKILL.md +7 -0
  67. package/skills/frontend-implementation/references/node-contracts.md +1 -1
  68. package/skills/frontend-verification/SKILL.md +4 -3
  69. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  70. package/dist/worker/console/static/assets/index-DVl7Jxt5.js +0 -25
  71. package/dist/worker/console/static/assets/index-lVcIr9Ju.css +0 -1
@@ -7,6 +7,7 @@ import { z } from "zod";
7
7
  import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
8
8
  import { findPackageRoot } from "../../shared/package-metadata.js";
9
9
  import { pathMatchesPattern } from "../../shared/git-progress.js";
10
+ import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
10
11
  export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
11
12
  /**
12
13
  * Load the canonical frontend-implementation-contract-v1 JSON Schema from the
@@ -133,7 +134,7 @@ export const frontendImplementationContractSchema = z
133
134
  verificationTargetIds: z.array(z.string().min(1)).optional(),
134
135
  notApplicableReason: z.string().min(1).optional(),
135
136
  })
136
- .strict()).min(1),
137
+ .strict()),
137
138
  interactions: z.array(z
138
139
  .object({
139
140
  name: z.string().min(1),
@@ -300,17 +301,11 @@ export const frontendImplementationContractSchema = z
300
301
  });
301
302
  export async function assertFrontendSourceBindingFresh(input) {
302
303
  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
- }
304
+ const absolute = resolveDagTaskSourcePath({
305
+ workspaceRoot: input.workspaceRoot,
306
+ taskId: input.binding.taskId,
307
+ sourcePath: source.path,
308
+ });
314
309
  let content;
315
310
  try {
316
311
  content = await readFile(absolute);
@@ -425,12 +420,23 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
425
420
  uiStates: Array.isArray(rawRecord.uiStates)
426
421
  ? rawRecord.uiStates.map((item) => {
427
422
  const state = asRecord(item);
428
- if (!state ||
429
- state.applicable === false ||
430
- (state.notApplicableReason !== "" && state.notApplicableReason !== null))
423
+ if (!state)
431
424
  return item;
432
- const { notApplicableReason: _emptyReason, ...withoutEmptyReason } = state;
433
- return withoutEmptyReason;
425
+ if (state.applicable === true &&
426
+ (state.notApplicableReason === null ||
427
+ (typeof state.notApplicableReason === "string" &&
428
+ state.notApplicableReason.trim() === ""))) {
429
+ const { notApplicableReason: _emptyReason, ...withoutEmptyReason } = state;
430
+ return withoutEmptyReason;
431
+ }
432
+ if (state.applicable === false &&
433
+ (state.expectedBehavior === null ||
434
+ (typeof state.expectedBehavior === "string" &&
435
+ state.expectedBehavior.trim() === ""))) {
436
+ const { expectedBehavior: _emptyBehavior, ...withoutEmptyBehavior } = state;
437
+ return withoutEmptyBehavior;
438
+ }
439
+ return item;
434
440
  })
435
441
  : rawRecord.uiStates,
436
442
  }
@@ -87,14 +87,14 @@ function stripAnsi(value) {
87
87
  function normalizeRepoPath(workspaceRoot, candidate) {
88
88
  const clean = stripAnsi(candidate).trim().replace(/^file:\/\//, "");
89
89
  const windowsAbsolute = /^[A-Za-z]:[\\/]/.test(clean);
90
- const windowsRoot = /^[A-Za-z]:[\\/]/.test(workspaceRoot);
90
+ const posixAbsolute = path.posix.isAbsolute(clean);
91
91
  let relative;
92
- if (windowsAbsolute || windowsRoot) {
93
- if (!windowsAbsolute || !windowsRoot)
92
+ if (windowsAbsolute) {
93
+ if (!/^[A-Za-z]:[\\/]/.test(workspaceRoot))
94
94
  return null;
95
95
  relative = path.win32.relative(workspaceRoot, clean);
96
96
  }
97
- else if (path.posix.isAbsolute(clean)) {
97
+ else if (posixAbsolute) {
98
98
  relative = path.posix.relative(workspaceRoot.replaceAll("\\", "/"), clean.replaceAll("\\", "/"));
99
99
  }
100
100
  else {
@@ -1,316 +1 @@
1
- /**
2
- * Generation-time frontend project capability discovery (M5/M6).
3
- * Strong evidence only: package.json deps, config files, source imports.
4
- * Lockfile-only or directory-name-only must NOT activate adapters.
5
- */
6
- import { readFile, readdir, stat } from "node:fs/promises";
7
- import path from "node:path";
8
- import { OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE } from "../../shared/openspec-spec.js";
9
- async function exists(filePath) {
10
- try {
11
- await stat(filePath);
12
- return true;
13
- }
14
- catch {
15
- return false;
16
- }
17
- }
18
- async function readJson(filePath) {
19
- try {
20
- return JSON.parse(await readFile(filePath, "utf8"));
21
- }
22
- catch {
23
- return null;
24
- }
25
- }
26
- function allDeps(pkg) {
27
- return {
28
- ...(pkg.dependencies ?? {}),
29
- ...(pkg.devDependencies ?? {}),
30
- ...(pkg.peerDependencies ?? {}),
31
- };
32
- }
33
- function hasDep(deps, name) {
34
- return Object.hasOwn(deps, name);
35
- }
36
- async function listOpenspec(repoRoot) {
37
- const out = [];
38
- for (const specDir of OPENSPEC_SPEC_DIRS) {
39
- const root = path.join(repoRoot, specDir);
40
- if (!(await exists(root)))
41
- continue;
42
- await walk(root, 0);
43
- }
44
- return out.slice(0, 40);
45
- async function walk(dir, depth) {
46
- if (depth > 4)
47
- return;
48
- let entries = [];
49
- try {
50
- entries = await readdir(dir);
51
- }
52
- catch {
53
- return;
54
- }
55
- for (const name of entries) {
56
- const full = path.join(dir, name);
57
- try {
58
- const info = await stat(full);
59
- if (info.isDirectory())
60
- await walk(full, depth + 1);
61
- else if (OPENSPEC_SPEC_EXT_RE.test(name)) {
62
- out.push(path.relative(repoRoot, full).replace(/\\/g, "/"));
63
- }
64
- }
65
- catch {
66
- // skip
67
- }
68
- }
69
- }
70
- }
71
- export function buildAdapterGuidance(capability) {
72
- const lines = [
73
- "## Frontend project capability (generation-time)",
74
- `Framework: ${capability.framework}${capability.frameworkVersion ? `@${capability.frameworkVersion}` : ""}`,
75
- `Evidence: ${capability.evidencePaths.join(", ") || "(none)"}`,
76
- "Rules: openspec specs and task sources outrank adapter tips; do not invent APIs for unknown versions; lockfile-only is not enough.",
77
- ];
78
- if (capability.framework === "react") {
79
- lines.push("React adapter: prefer function components + hooks; reuse existing Testing Library / Vitest patterns; do not introduce new state libs without authorization.");
80
- }
81
- else if (capability.framework === "next") {
82
- lines.push("Next.js adapter: respect App vs Pages router from evidence; keep server/client boundaries explicit; avoid guessing cache/navigation APIs across major versions.");
83
- }
84
- else if (capability.framework === "vue") {
85
- lines.push("Vue adapter: prefer Composition API if project already uses it; reuse Pinia/Vue Router/Vue Query only when evidenced; Vitest + Vue Test Utils when present.");
86
- }
87
- else {
88
- lines.push("Unknown framework: use generic frontend-implementation skill only; do not assume React/Vue/Next APIs.");
89
- }
90
- if (capability.a11y.status === "present") {
91
- lines.push(`A11y tools present: ${capability.a11y.tools.join(", ")}. Report static/component-level evidence only; Browser a11y remains not-run.`);
92
- }
93
- else {
94
- lines.push("A11y tools: absent/unknown — mark a11y checks unavailable/not-run; do not install tools.");
95
- }
96
- if (capability.designEvidence.normativePaths.length) {
97
- lines.push(`Normative design paths: ${capability.designEvidence.normativePaths.slice(0, 8).join(", ")}`);
98
- }
99
- return lines.join("\n");
100
- }
101
- /**
102
- * Discover frontend project capability from repo root.
103
- * Strong evidence: package.json direct deps + config/bootstrap files.
104
- */
105
- export async function discoverFrontendProjectCapability(repoRoot) {
106
- const evidencePaths = [];
107
- const reasons = [];
108
- const pkgPath = path.join(repoRoot, "package.json");
109
- const pkgRaw = await readJson(pkgPath);
110
- if (!pkgRaw || typeof pkgRaw !== "object") {
111
- const openspec = await listOpenspec(repoRoot);
112
- const base = {
113
- schemaVersion: 1,
114
- framework: "unknown",
115
- state: [],
116
- dataFetching: [],
117
- componentLibrary: [],
118
- style: [],
119
- testRunner: [],
120
- mock: [],
121
- a11y: { status: "unknown", tools: [], evidencePaths: [] },
122
- evidencePaths: openspec.slice(0, 5),
123
- reasons: ["package.json missing or unreadable"],
124
- designEvidence: {
125
- normativePaths: openspec,
126
- advisoryPaths: [],
127
- conflicts: [],
128
- },
129
- };
130
- if (openspec.length) {
131
- base.reasons.push(`openspec spec files discovered: ${openspec.slice(0, 5).join(", ")}`);
132
- }
133
- return { ...base, adapterGuidance: buildAdapterGuidance(base) };
134
- }
135
- evidencePaths.push("package.json");
136
- const deps = allDeps(pkgRaw);
137
- // Lockfile alone does not activate
138
- for (const lock of ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"]) {
139
- if (await exists(path.join(repoRoot, lock))) {
140
- reasons.push(`${lock} present but not used as sole activation evidence`);
141
- }
142
- }
143
- let framework = "unknown";
144
- let frameworkVersion;
145
- const configHits = [];
146
- const nextConfig = ["next.config.js", "next.config.mjs", "next.config.ts"];
147
- for (const name of nextConfig) {
148
- if (await exists(path.join(repoRoot, name))) {
149
- configHits.push(name);
150
- evidencePaths.push(name);
151
- }
152
- }
153
- if (hasDep(deps, "next") && configHits.some((c) => c.startsWith("next"))) {
154
- framework = "next";
155
- frameworkVersion = deps.next?.replace(/^[^\d]*/, "");
156
- reasons.push("next dependency + next.config*");
157
- }
158
- else if (hasDep(deps, "next") && configHits.length === 0) {
159
- // dep without config: still weak if only transitive — require direct dep
160
- framework = "next";
161
- frameworkVersion = deps.next?.replace(/^[^\d]*/, "");
162
- reasons.push("next direct dependency (config not found)");
163
- evidencePaths.push("package.json#dependencies.next");
164
- }
165
- if (framework === "unknown") {
166
- const vueConfig = ["vite.config.ts", "vite.config.js", "vue.config.js"];
167
- let vueCfg = false;
168
- for (const name of vueConfig) {
169
- if (await exists(path.join(repoRoot, name))) {
170
- vueCfg = true;
171
- evidencePaths.push(name);
172
- }
173
- }
174
- if (hasDep(deps, "vue") && (vueCfg || hasDep(deps, "nuxt"))) {
175
- framework = "vue";
176
- frameworkVersion = deps.vue?.replace(/^[^\d]*/, "");
177
- reasons.push("vue direct dependency + config/tooling");
178
- }
179
- }
180
- if (framework === "unknown" && hasDep(deps, "react")) {
181
- // Need config or jsx tooling — not lockfile-only
182
- const reactCfg = (await exists(path.join(repoRoot, "vite.config.ts"))) ||
183
- (await exists(path.join(repoRoot, "vite.config.js"))) ||
184
- (await exists(path.join(repoRoot, "webpack.config.js"))) ||
185
- hasDep(deps, "react-scripts") ||
186
- hasDep(deps, "vite");
187
- if (reactCfg) {
188
- framework = "react";
189
- frameworkVersion = deps.react?.replace(/^[^\d]*/, "");
190
- reasons.push("react direct dependency + bundler/tooling evidence");
191
- evidencePaths.push("package.json#dependencies.react");
192
- }
193
- else {
194
- reasons.push("react listed but no strong config/bundler evidence — framework=unknown");
195
- }
196
- }
197
- const state = [];
198
- for (const lib of [
199
- "redux",
200
- "@reduxjs/toolkit",
201
- "zustand",
202
- "jotai",
203
- "recoil",
204
- "pinia",
205
- "vuex",
206
- ]) {
207
- if (hasDep(deps, lib))
208
- state.push(lib);
209
- }
210
- const dataFetching = [];
211
- for (const lib of [
212
- "@tanstack/react-query",
213
- "swr",
214
- "axios",
215
- "@tanstack/vue-query",
216
- "ofetch",
217
- ]) {
218
- if (hasDep(deps, lib))
219
- dataFetching.push(lib);
220
- }
221
- const componentLibrary = [];
222
- for (const lib of [
223
- "antd",
224
- "@mui/material",
225
- "element-plus",
226
- "vuetify",
227
- "@chakra-ui/react",
228
- ]) {
229
- if (hasDep(deps, lib))
230
- componentLibrary.push(lib);
231
- }
232
- const style = [];
233
- for (const lib of [
234
- "tailwindcss",
235
- "sass",
236
- "less",
237
- "styled-components",
238
- "@emotion/react",
239
- ]) {
240
- if (hasDep(deps, lib))
241
- style.push(lib);
242
- }
243
- const testRunner = [];
244
- for (const lib of [
245
- "vitest",
246
- "jest",
247
- "@testing-library/react",
248
- "@testing-library/vue",
249
- "@playwright/test",
250
- ]) {
251
- if (hasDep(deps, lib))
252
- testRunner.push(lib);
253
- }
254
- const mock = [];
255
- for (const lib of ["msw", "nock", "miragejs"]) {
256
- if (hasDep(deps, lib))
257
- mock.push(lib);
258
- }
259
- const a11yTools = [];
260
- const a11yEvidence = [];
261
- for (const lib of [
262
- "eslint-plugin-jsx-a11y",
263
- "@axe-core/react",
264
- "jest-axe",
265
- "vitest-axe",
266
- "axe-core",
267
- ]) {
268
- if (hasDep(deps, lib)) {
269
- a11yTools.push(lib);
270
- a11yEvidence.push(`package.json#${lib}`);
271
- }
272
- }
273
- const a11y = {
274
- status: (a11yTools.length > 0 ? "present" : "absent"),
275
- tools: a11yTools,
276
- evidencePaths: a11yEvidence,
277
- };
278
- let router;
279
- if (hasDep(deps, "react-router-dom") || hasDep(deps, "react-router")) {
280
- router = "react-router";
281
- }
282
- if (framework === "next")
283
- router = router ?? "next-router";
284
- if (hasDep(deps, "vue-router"))
285
- router = "vue-router";
286
- const openspec = await listOpenspec(repoRoot);
287
- const designEvidence = {
288
- normativePaths: openspec,
289
- advisoryPaths: [],
290
- conflicts: [],
291
- };
292
- if (openspec.length)
293
- evidencePaths.push(...openspec.slice(0, 5));
294
- const base = {
295
- schemaVersion: 1,
296
- framework,
297
- frameworkVersion,
298
- router,
299
- state,
300
- dataFetching,
301
- componentLibrary,
302
- style,
303
- testRunner,
304
- mock,
305
- a11y,
306
- packageManager: (await exists(path.join(repoRoot, "pnpm-lock.yaml")))
307
- ? "pnpm"
308
- : (await exists(path.join(repoRoot, "yarn.lock")))
309
- ? "yarn"
310
- : "npm",
311
- evidencePaths: [...new Set(evidencePaths)],
312
- reasons,
313
- designEvidence,
314
- };
315
- return { ...base, adapterGuidance: buildAdapterGuidance(base) };
316
- }
1
+ export * from "../../task/frontend-project-capability.js";
@@ -33,7 +33,10 @@ function splitLines(text) {
33
33
  .filter(Boolean);
34
34
  }
35
35
  function statusPaths(status) {
36
- return splitLines(status)
36
+ return status
37
+ .split(/\r?\n/)
38
+ .map((line) => line.trimEnd())
39
+ .filter((line) => line.length >= 4)
37
40
  .map((line) => line.slice(3).split(" -> ").at(-1) ?? "")
38
41
  .filter(Boolean)
39
42
  .sort();