@tea-agent/loop-agent 0.35.1-beta.3 → 0.35.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 +0 -2
  2. package/CHANGELOG.md +25 -24
  3. package/bin/loop-agent.js +1 -37
  4. package/dist/application/dag/generate-task-dag.js +4 -1
  5. package/dist/application/task-lifecycle/advance.js +14 -0
  6. package/dist/cli/program.js +2 -2
  7. package/dist/commands/task-advance.js +1 -0
  8. package/dist/executors/dag-pi-executor.js +0 -44
  9. package/dist/shared/package-metadata.js +0 -42
  10. package/dist/task/config-types.js +2 -0
  11. package/dist/task/contract/project.js +3 -0
  12. package/dist/task/contract/schema.js +1 -0
  13. package/dist/task/source-prepare/build-draft.js +7 -0
  14. package/dist/task/source-prepare/semantic-intake.js +37 -10
  15. package/dist/task/task-demand-routing.js +10 -0
  16. package/dist/worker/console/operator-actions.js +72 -6
  17. package/dist/worker/console/prd-intake-bridge.js +10 -3
  18. package/dist/worker/console/prd-reference-discovery.js +124 -0
  19. package/dist/worker/console/static/assets/{index-hJqCPs_g.css → index-HX1pbOyl.css} +1 -1
  20. package/dist/worker/console/static/assets/{index-CvsQgALl.js → index-M0BLEBfh.js} +25 -25
  21. package/dist/worker/console/static/index.html +2 -2
  22. package/dist/worker/console/static-src/app/useOperatorActions.js +19 -1
  23. package/dist/worker/console/static-src/app/useRecoveryConsole.js +0 -5
  24. package/dist/worker/console/static-src/app/useTaskWizard.js +12 -0
  25. package/dist/worker/loop-agent/loop-agent-client.js +3 -17
  26. package/dist/worker/observability/read-model.js +0 -20
  27. package/dist/worker/preflight.js +1 -2
  28. package/dist/workflows/dag/backend-test-scenario-param.js +23 -33
  29. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
  30. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  31. package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
  32. package/dist/workflows/dag/frontend-repair.js +18 -219
  33. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  34. package/dist/workflows/dag/init-hybrid.js +26 -49
  35. package/dist/workflows/dag/node-execution.js +0 -89
  36. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  37. package/dist/workflows/dag/runner.js +11 -245
  38. package/dist/workflows/dag/scheduler.js +3 -257
  39. package/dist/workflows/dag/types.js +2 -130
  40. package/package.json +2 -2
  41. package/dist/build-stamp.json +0 -6
  42. package/dist/workflows/dag/contract-output-registry.js +0 -14
  43. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  44. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  45. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  46. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  47. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  48. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
@@ -17,6 +17,9 @@ import { toCanonicalDraftForCli, } from "./draft-store.js";
17
17
  import { normalizeConsoleWorkflowKind } from "./workflow-kinds.js";
18
18
  import { deriveTaskIdentityFromPrd, nextTaskIdRevision, } from "./prd-identity.js";
19
19
  import { appendEngineeringCliArgs, buildDraftFromTaskIntake, parseEngineeringBoundaryFromParams, } from "./prd-intake-bridge.js";
20
+ import { discoverPrdReferences } from "./prd-reference-discovery.js";
21
+ import { clearSemanticIntakeAttempt } from "../../task/source-prepare/semantic-intake.js";
22
+ import nodePath from "node:path";
20
23
  import { resolveSiblingAgentWorkerBin } from "./sibling-controller.js";
21
24
  import { projectOperationEventSummary, projectOperationForChat, } from "./chat/chat-event-store.js";
22
25
  import { MutationGateReceiptStore } from "./mutation-gate-receipt-store.js";
@@ -1364,7 +1367,9 @@ export async function dispatchOperatorAction(ctx, req) {
1364
1367
  if (!taskId || !content) {
1365
1368
  return invalid(action, "taskId and content are required");
1366
1369
  }
1367
- const taskKind = normalizeConsoleWorkflowKind(str(p.taskKind), "standard");
1370
+ const taskKindRaw = str(p.taskKind)?.trim();
1371
+ const taskKind = normalizeConsoleWorkflowKind(taskKindRaw, "standard");
1372
+ const taskKindExplicit = Boolean(taskKindRaw);
1368
1373
  const engineering = parseEngineeringBoundaryFromParams(p);
1369
1374
  const identity = deriveTaskIdentityFromPrd(content);
1370
1375
  const title = str(p.title) ?? identity.title;
@@ -1407,7 +1412,7 @@ export async function dispatchOperatorAction(ctx, req) {
1407
1412
  };
1408
1413
  }
1409
1414
  const intakeArgs = ["task", "advance", taskId, "--json"];
1410
- if (taskKind && taskKind !== "standard") {
1415
+ if (taskKindExplicit) {
1411
1416
  intakeArgs.push("--task-kind", taskKind);
1412
1417
  }
1413
1418
  appendEngineeringCliArgs(intakeArgs, engineering);
@@ -1424,6 +1429,7 @@ export async function dispatchOperatorAction(ctx, req) {
1424
1429
  taskId,
1425
1430
  title,
1426
1431
  taskKind,
1432
+ taskKindExplicit,
1427
1433
  advanceJson: intakeJson,
1428
1434
  engineering,
1429
1435
  });
@@ -2495,7 +2501,38 @@ async function dispatchBootstrapFromPrd(ctx, p) {
2495
2501
  return invalid("bootstrapFromPrd", parsedDocs.message);
2496
2502
  }
2497
2503
  const documents = parsedDocs.documents;
2498
- const taskKind = normalizeConsoleWorkflowKind(str(p.taskKind), "standard");
2504
+ // B1: free-form prose that references repo-local markdown paths (e.g.
2505
+ // "请参照这三个prd路径 docs/product-analysis/foo.md …") only materializes
2506
+ // the sentence itself. Discover the referenced files and import them as
2507
+ // additional documents so intake has real requirement facts to structure.
2508
+ let autoImportedReferences = [];
2509
+ let unresolvedReferences = [];
2510
+ const autoDocs = [];
2511
+ if (!documents) {
2512
+ const discovery = await discoverPrdReferences({
2513
+ prose: content,
2514
+ repoRoot: ctx.repoRoot,
2515
+ });
2516
+ autoImportedReferences = discovery.references.map((r) => ({
2517
+ repoRelative: r.repoRelative,
2518
+ role: r.role,
2519
+ }));
2520
+ unresolvedReferences = discovery.unresolved;
2521
+ for (const ref of discovery.references) {
2522
+ const absolute = nodePath.resolve(ctx.repoRoot, ref.repoRelative);
2523
+ const refContent = await readFile(absolute, "utf-8").catch(() => "");
2524
+ if (!refContent.trim())
2525
+ continue;
2526
+ autoDocs.push({
2527
+ name: ref.repoRelative.split("/").pop() ?? ref.repoRelative,
2528
+ content: refContent,
2529
+ fileName: ref.repoRelative,
2530
+ });
2531
+ }
2532
+ }
2533
+ const taskKindRaw = str(p.taskKind)?.trim();
2534
+ const taskKind = normalizeConsoleWorkflowKind(taskKindRaw, "standard");
2535
+ const taskKindExplicit = Boolean(taskKindRaw);
2499
2536
  const engineering = parseEngineeringBoundaryFromParams(p);
2500
2537
  const identity = deriveTaskIdentityFromPrd(content);
2501
2538
  let taskId;
@@ -2547,7 +2584,7 @@ async function dispatchBootstrapFromPrd(ctx, p) {
2547
2584
  }
2548
2585
  const succeededImports = [];
2549
2586
  const importResults = [];
2550
- const runImportViaAdvance = async (label, text, name) => {
2587
+ const runImportViaAdvance = async (label, text, name, role = "requirement") => {
2551
2588
  const staged = await stageUtf8Text(ctx.appData, text, {
2552
2589
  extension: ".md",
2553
2590
  });
@@ -2558,7 +2595,7 @@ async function dispatchBootstrapFromPrd(ctx, p) {
2558
2595
  "--prd",
2559
2596
  staged.path,
2560
2597
  "--role",
2561
- "requirement",
2598
+ role,
2562
2599
  "--json",
2563
2600
  ], {
2564
2601
  cwd: ctx.repoRoot,
@@ -2599,6 +2636,23 @@ async function dispatchBootstrapFromPrd(ctx, p) {
2599
2636
  if (composedFailure)
2600
2637
  return composedFailure;
2601
2638
  }
2639
+ else if (autoDocs.length > 0) {
2640
+ // B1: discovered repo-local references become explicit imports (role
2641
+ // inferred per document), then the prose itself imports as composed.
2642
+ const roleByBase = new Map(autoImportedReferences.map((r) => {
2643
+ const base = r.repoRelative.split("/").pop() ?? r.repoRelative;
2644
+ return [base, r.role];
2645
+ }));
2646
+ for (const doc of autoDocs) {
2647
+ const role = roleByBase.get(doc.name) ?? "requirement";
2648
+ const failure = await runImportViaAdvance(doc.name, doc.content, doc.name, role);
2649
+ if (failure)
2650
+ return failure;
2651
+ }
2652
+ const composedFailure = await runImportViaAdvance("composed", content, "composed");
2653
+ if (composedFailure)
2654
+ return composedFailure;
2655
+ }
2602
2656
  else {
2603
2657
  const staged = await stageUtf8Text(ctx.appData, content, {
2604
2658
  extension: ".md",
@@ -2625,11 +2679,16 @@ async function dispatchBootstrapFromPrd(ctx, p) {
2625
2679
  }
2626
2680
  importResults.push(imported.json ?? null);
2627
2681
  }
2682
+ // C: a fresh import (manual or discovered) is new evidence; clear the
2683
+ // one-shot semantic-intake attempt marker so intake may run again.
2684
+ if (succeededImports.length > 0) {
2685
+ await clearSemanticIntakeAttempt(ctx.repoRoot, taskId);
2686
+ }
2628
2687
  // Phase 2 (design 2026-08-11): drive task advance intake after import so
2629
2688
  // deterministic parse + one-shot Semantic Intake run on archived references.
2630
2689
  // Timeout is longer than import: Pi MED may take minutes.
2631
2690
  const intakeArgs = ["task", "advance", taskId, "--json"];
2632
- if (taskKind && taskKind !== "standard") {
2691
+ if (taskKindExplicit) {
2633
2692
  intakeArgs.push("--task-kind", taskKind);
2634
2693
  }
2635
2694
  appendEngineeringCliArgs(intakeArgs, engineering);
@@ -2646,6 +2705,7 @@ async function dispatchBootstrapFromPrd(ctx, p) {
2646
2705
  taskId,
2647
2706
  title,
2648
2707
  taskKind,
2708
+ taskKindExplicit,
2649
2709
  advanceJson: intakeJson,
2650
2710
  engineering,
2651
2711
  });
@@ -2679,7 +2739,13 @@ async function dispatchBootstrapFromPrd(ctx, p) {
2679
2739
  imports: importResults,
2680
2740
  documentNames: documents && documents.length > 0
2681
2741
  ? [...documents.map((d) => d.name), "composed"]
2742
+ : autoDocs.length > 0
2743
+ ? [...autoDocs.map((d) => d.name), "composed"]
2744
+ : undefined,
2745
+ autoImportedReferences: autoImportedReferences.length > 0
2746
+ ? autoImportedReferences
2682
2747
  : undefined,
2748
+ unresolvedReferences: unresolvedReferences.length > 0 ? unresolvedReferences : undefined,
2683
2749
  draftPreview: {
2684
2750
  objective: draftSaved.draft.requirement?.objective ?? null,
2685
2751
  scope: draftSaved.draft.requirement?.scope ?? [],
@@ -215,6 +215,7 @@ export function applyEngineeringBoundaryToDraft(draft, boundary) {
215
215
  }
216
216
  export async function buildDraftFromTaskIntake(input) {
217
217
  const taskKind = normalizeConsoleWorkflowKind(input.taskKind, "standard");
218
+ const taskKindExplicit = Boolean(input.taskKindExplicit);
218
219
  const paths = getTaskPaths(input.repoRoot, input.taskId);
219
220
  const warnings = warningList(input.advanceJson);
220
221
  const lifecycleState = lifecycleFromAdvanceJson(input.advanceJson);
@@ -262,6 +263,14 @@ export async function buildDraftFromTaskIntake(input) {
262
263
  const semDraftConstraints = artifact?.draft?.constraints;
263
264
  const semDraftVerification = artifact?.draft?.verification;
264
265
  const semTaskKind = artifact?.draft?.taskKind?.trim();
266
+ // User-explicit Console taskKind is a hard contract: semantic intake
267
+ // must never overwrite it (it may recommend backend-test for prose like
268
+ // "削减单元测试数量" even when the operator picked standard/后端研发).
269
+ const resolvedTaskKind = taskKindExplicit
270
+ ? taskKind
271
+ : semTaskKind
272
+ ? normalizeConsoleWorkflowKind(semTaskKind, taskKind)
273
+ : taskKind;
265
274
  const mergedAllowed = filterPlaceholderPaths(semDraftConstraints?.allowedPaths);
266
275
  const mergedForbidden = filterPlaceholderPaths(semDraftConstraints?.forbiddenPaths);
267
276
  const mergedVerify = Array.isArray(semDraftVerification?.commands)
@@ -280,9 +289,7 @@ export async function buildDraftFromTaskIntake(input) {
280
289
  draft = {
281
290
  ...draft,
282
291
  title,
283
- ...(semTaskKind
284
- ? { taskKind: normalizeConsoleWorkflowKind(semTaskKind, taskKind) }
285
- : {}),
292
+ taskKind: resolvedTaskKind,
286
293
  requirement: {
287
294
  objective: semanticReq.objective.trim(),
288
295
  scope: asStringList(semanticReq.scope),
@@ -0,0 +1,124 @@
1
+ /**
2
+ * B1: discover repo-local markdown references inside free-form PRD prose.
3
+ *
4
+ * Console "文字录入" flows often paste one sentence like
5
+ * "请参照这三个prd路径 docs/product-analysis/foo/*.md 完成业务需求".
6
+ * The deterministic import only materializes the sentence itself; the
7
+ * referenced files never reach source/references/, so semantic intake has
8
+ * no requirement facts to structure. This module extracts candidate
9
+ * repo-relative .md paths from the prose, filters exclusions, and resolves
10
+ * each to an importable document (path + inferred role) so the caller can
11
+ * auto-import them before intake.
12
+ */
13
+ import { readFile, stat } from "node:fs/promises";
14
+ import path from "node:path";
15
+ import { resolvePrdImportRole } from "../../task/source-prepare/artifact-meta.js";
16
+ /** Repo paths that must never be auto-imported as PRD references. */
17
+ const EXCLUDED_PREFIXES = [
18
+ ".harness/",
19
+ "node_modules/",
20
+ ".git/",
21
+ "ai_workspace/",
22
+ "dist/",
23
+ "build/",
24
+ "coverage/",
25
+ ];
26
+ /** Derived contract files are never valid PRD inputs. */
27
+ const EXCLUDED_BASENAMES = new Set(["需求.md", "执行约束.md"]);
28
+ /** Markdown path tokens with unicode path segments (supports Chinese dirs/files). */
29
+ const MD_PATH_TOKEN_RE = /(?:[\w\u4e00-\u9fff.-]+\/)+[\w\u4e00-\u9fff.-]+\.(?:md|markdown)\b/gu;
30
+ /** Strip URLs so their path fragments are never treated as repo paths. */
31
+ const URL_RE = /(?:https?|ftp|file):\/\/[^\s\u4e00-\u9fff\uff08\uff09]+/gu;
32
+ const MAX_AUTO_IMPORT_BYTES = 512 * 1024;
33
+ const MAX_AUTO_IMPORT_COUNT = 8;
34
+ function isExcluded(repoRelative) {
35
+ const posix = repoRelative.replace(/\\/g, "/");
36
+ for (const prefix of EXCLUDED_PREFIXES) {
37
+ if (posix === prefix.slice(0, -1) || posix.startsWith(prefix)) {
38
+ return `excluded prefix ${prefix}`;
39
+ }
40
+ }
41
+ const base = posix.split("/").pop() ?? posix;
42
+ if (EXCLUDED_BASENAMES.has(base)) {
43
+ return `derived contract file ${base}`;
44
+ }
45
+ return null;
46
+ }
47
+ /**
48
+ * Extract candidate markdown path tokens from free-form prose.
49
+ * Returns unique tokens in first-occurrence order.
50
+ */
51
+ export function extractPrdPathTokens(prose) {
52
+ const withoutUrls = prose.replace(URL_RE, " ");
53
+ const seen = new Set();
54
+ const out = [];
55
+ for (const match of withoutUrls.matchAll(MD_PATH_TOKEN_RE)) {
56
+ const token = match[0];
57
+ if (!token || token.length <= "x.md".length)
58
+ continue;
59
+ if (seen.has(token))
60
+ continue;
61
+ seen.add(token);
62
+ out.push(token);
63
+ if (out.length >= 32)
64
+ break;
65
+ }
66
+ return out;
67
+ }
68
+ /**
69
+ * Resolve prose path tokens against the repo root. Existing, non-excluded,
70
+ * size-bounded markdown files become importable references.
71
+ */
72
+ export async function discoverPrdReferences(input) {
73
+ const tokens = extractPrdPathTokens(input.prose);
74
+ const references = [];
75
+ const unresolved = [];
76
+ for (const token of tokens) {
77
+ if (references.length >= MAX_AUTO_IMPORT_COUNT) {
78
+ unresolved.push({ token, reason: "auto-import limit reached" });
79
+ continue;
80
+ }
81
+ const repoRelative = token.replace(/\\/g, "/");
82
+ const excludedReason = isExcluded(repoRelative);
83
+ if (excludedReason) {
84
+ unresolved.push({ token, reason: excludedReason });
85
+ continue;
86
+ }
87
+ const absolute = path.resolve(input.repoRoot, repoRelative);
88
+ // Defense in depth: resolved candidates must stay under repoRoot.
89
+ const rel = path.relative(input.repoRoot, absolute);
90
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
91
+ unresolved.push({ token, reason: "outside repo root" });
92
+ continue;
93
+ }
94
+ let content;
95
+ try {
96
+ const fileStat = await stat(absolute);
97
+ if (!fileStat.isFile()) {
98
+ unresolved.push({ token, reason: "not a regular file" });
99
+ continue;
100
+ }
101
+ if (fileStat.size > MAX_AUTO_IMPORT_BYTES) {
102
+ unresolved.push({
103
+ token,
104
+ reason: `exceeds ${MAX_AUTO_IMPORT_BYTES} bytes`,
105
+ });
106
+ continue;
107
+ }
108
+ content = await readFile(absolute, "utf-8");
109
+ }
110
+ catch {
111
+ unresolved.push({ token, reason: "not found in repo" });
112
+ continue;
113
+ }
114
+ references.push({
115
+ repoRelative,
116
+ role: resolvePrdImportRole({
117
+ filePath: absolute,
118
+ markdown: content,
119
+ }),
120
+ sizeBytes: Buffer.byteLength(content, "utf-8"),
121
+ });
122
+ }
123
+ return { references, unresolved };
124
+ }