@tea-agent/loop-agent 0.35.1-beta.2 → 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.
- package/AGENTS.md +0 -2
- package/CHANGELOG.md +25 -24
- package/bin/loop-agent.js +1 -37
- package/dist/application/dag/generate-task-dag.js +4 -1
- package/dist/application/task-lifecycle/advance.js +14 -0
- package/dist/cli/program.js +2 -2
- package/dist/commands/task-advance.js +1 -0
- package/dist/executors/dag-pi-executor.js +0 -44
- package/dist/shared/package-metadata.js +0 -42
- package/dist/task/config-types.js +2 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +1 -0
- package/dist/task/source-prepare/build-draft.js +7 -0
- package/dist/task/source-prepare/semantic-intake.js +37 -10
- package/dist/task/task-demand-routing.js +10 -0
- package/dist/worker/console/operator-actions.js +72 -6
- package/dist/worker/console/prd-intake-bridge.js +10 -3
- package/dist/worker/console/prd-reference-discovery.js +124 -0
- package/dist/worker/console/static/assets/{index-hJqCPs_g.css → index-HX1pbOyl.css} +1 -1
- package/dist/worker/console/static/assets/{index-CvsQgALl.js → index-M0BLEBfh.js} +25 -25
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +19 -1
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +0 -5
- package/dist/worker/console/static-src/app/useTaskWizard.js +12 -0
- package/dist/worker/loop-agent/loop-agent-client.js +3 -17
- package/dist/worker/observability/read-model.js +0 -20
- package/dist/worker/preflight.js +1 -2
- package/dist/workflows/dag/backend-test-scenario-param.js +23 -33
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
- package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
- package/dist/workflows/dag/frontend-repair.js +18 -219
- package/dist/workflows/dag/frontend-verification-trace.js +32 -47
- package/dist/workflows/dag/init-hybrid.js +26 -41
- package/dist/workflows/dag/node-execution.js +0 -89
- package/dist/workflows/dag/recovery-recommendation.js +0 -58
- package/dist/workflows/dag/runner.js +11 -245
- package/dist/workflows/dag/scheduler.js +3 -257
- package/dist/workflows/dag/types.js +2 -130
- package/package.json +2 -2
- package/dist/build-stamp.json +0 -6
- package/dist/workflows/dag/contract-output-registry.js +0 -14
- package/dist/workflows/dag/contract-validator-registrations.js +0 -8
- package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
- package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
- package/dist/workflows/dag/frontend-recovery-run.js +0 -539
- package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
- 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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
+
}
|