@tea-agent/loop-agent 0.35.1-beta.4 → 0.35.2
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 +43 -26
- 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/chat/assistant-content.js +110 -0
- package/dist/worker/console/chat/pi-runtime.js +13 -32
- package/dist/worker/console/chat/routes.js +9 -4
- package/dist/worker/console/chat/session-store.js +3 -0
- package/dist/worker/console/chat/shortcuts.js +9 -7
- package/dist/worker/console/chat/turn-process.js +62 -15
- 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-DRqZiQ7J.css +1 -0
- package/dist/worker/console/static/assets/index-gVHrlqI9.js +56 -0
- 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/console/static-src/operator-chat/chat-sse-events.js +10 -1
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +11 -6
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +11 -6
- package/dist/worker/console/static-src/operator-chat/useComposer.js +15 -1
- package/dist/worker/loop-agent/loop-agent-client.js +3 -17
- package/dist/worker/observability/read-model.js +0 -20
- package/dist/worker/observe/spec-evidence.js +8 -3
- package/dist/worker/observe/static/views/dag-inspector.js +71 -6
- 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 -49
- 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/harness.json +1 -1
- package/package.json +2 -2
- package/dist/build-stamp.json +0 -6
- package/dist/worker/console/static/assets/index-CvsQgALl.js +0 -56
- package/dist/worker/console/static/assets/index-hJqCPs_g.css +0 -1
- 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
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { createHash, randomBytes } from "node:crypto";
|
|
29
29
|
import { access } from "node:fs/promises";
|
|
30
30
|
import path from "node:path";
|
|
31
|
+
import { extractAssistantContentBlocks, extractAssistantText, } from "./assistant-content.js";
|
|
31
32
|
import { projectCompactSnapshot, } from "./chat-event-store.js";
|
|
32
33
|
import { extractUsageSample } from "./usage.js";
|
|
33
34
|
import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
|
|
@@ -563,28 +564,7 @@ export function projectRuntimeSnapshot(input) {
|
|
|
563
564
|
hasBash: input.hasBash,
|
|
564
565
|
};
|
|
565
566
|
}
|
|
566
|
-
|
|
567
|
-
* Extract the latest assistant text fragment from an SDK message object.
|
|
568
|
-
* Tolerant of multiple message shapes the SDK emits.
|
|
569
|
-
*/
|
|
570
|
-
export function extractAssistantText(message) {
|
|
571
|
-
if (!message || typeof message !== "object")
|
|
572
|
-
return "";
|
|
573
|
-
const m = message;
|
|
574
|
-
if (typeof m.text === "string")
|
|
575
|
-
return m.text;
|
|
576
|
-
const contents = Array.isArray(m.content) ? m.content : [];
|
|
577
|
-
const parts = Array.isArray(m.parts) ? m.parts : [];
|
|
578
|
-
const segments = [];
|
|
579
|
-
for (const c of [...contents, ...parts]) {
|
|
580
|
-
if (!c || typeof c !== "object")
|
|
581
|
-
continue;
|
|
582
|
-
const seg = c;
|
|
583
|
-
if (typeof seg.text === "string")
|
|
584
|
-
segments.push(seg.text);
|
|
585
|
-
}
|
|
586
|
-
return segments.join("");
|
|
587
|
-
}
|
|
567
|
+
export { extractAssistantContentBlocks, extractAssistantText };
|
|
588
568
|
function readSessionManagerId(manager) {
|
|
589
569
|
if (!manager || typeof manager !== "object")
|
|
590
570
|
return undefined;
|
|
@@ -2116,6 +2096,15 @@ export class ConsolePiRuntime {
|
|
|
2116
2096
|
this.dispose(id);
|
|
2117
2097
|
}
|
|
2118
2098
|
}
|
|
2099
|
+
function mapAssistantMessageEvent(type, sessionId, message) {
|
|
2100
|
+
const content = extractAssistantContentBlocks(message);
|
|
2101
|
+
return {
|
|
2102
|
+
type,
|
|
2103
|
+
sessionId,
|
|
2104
|
+
text: extractAssistantText(message),
|
|
2105
|
+
...(content.length > 0 ? { content } : {}),
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2119
2108
|
function mapSdkEvent(sessionId, event) {
|
|
2120
2109
|
switch (event.type) {
|
|
2121
2110
|
case "agent_start":
|
|
@@ -2127,17 +2116,9 @@ function mapSdkEvent(sessionId, event) {
|
|
|
2127
2116
|
willRetry: Boolean(event.willRetry),
|
|
2128
2117
|
};
|
|
2129
2118
|
case "message_update":
|
|
2130
|
-
return
|
|
2131
|
-
type: "message_update",
|
|
2132
|
-
sessionId,
|
|
2133
|
-
text: extractAssistantText(event.message),
|
|
2134
|
-
};
|
|
2119
|
+
return mapAssistantMessageEvent("message_update", sessionId, event.message);
|
|
2135
2120
|
case "message_end":
|
|
2136
|
-
return
|
|
2137
|
-
type: "message_end",
|
|
2138
|
-
sessionId,
|
|
2139
|
-
text: extractAssistantText(event.message),
|
|
2140
|
-
};
|
|
2121
|
+
return mapAssistantMessageEvent("message_end", sessionId, event.message);
|
|
2141
2122
|
case "turn_end":
|
|
2142
2123
|
// Usage is emitted from message_end (see prompt subscribe). turn_end
|
|
2143
2124
|
// carries the same final assistant message and would double-count.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* Mutation routes go through the Console mutation gate (boot token + Origin).
|
|
13
13
|
* Read routes (GET capabilities / sessions) are open to loopback.
|
|
14
14
|
*/
|
|
15
|
+
import { parseAssistantContent } from "./assistant-content.js";
|
|
15
16
|
import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
|
|
16
17
|
import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
17
18
|
import { cleanChatTitle } from "./session-store.js";
|
|
@@ -83,12 +84,16 @@ function turnEventToStorePartial(event) {
|
|
|
83
84
|
case "agent_start":
|
|
84
85
|
return { kind: "agent_start", data: {} };
|
|
85
86
|
case "message_update":
|
|
87
|
+
case "message_end": {
|
|
88
|
+
const content = parseAssistantContent(event.content);
|
|
86
89
|
return {
|
|
87
|
-
kind:
|
|
88
|
-
data: {
|
|
90
|
+
kind: event.type,
|
|
91
|
+
data: {
|
|
92
|
+
text: String(event.text ?? ""),
|
|
93
|
+
...(content ? { content } : {}),
|
|
94
|
+
},
|
|
89
95
|
};
|
|
90
|
-
|
|
91
|
-
return { kind: "message_end", data: { text: String(event.text ?? "") } };
|
|
96
|
+
}
|
|
92
97
|
case "usage":
|
|
93
98
|
return { kind: "usage", data: (event.usage ?? {}) };
|
|
94
99
|
case "tool_call":
|
|
@@ -368,6 +368,9 @@ export async function runChatTurn(options) {
|
|
|
368
368
|
pending.push(store.appendMessage(sessionId, {
|
|
369
369
|
role: "assistant",
|
|
370
370
|
text: event.text,
|
|
371
|
+
...(event.content && event.content.length > 0
|
|
372
|
+
? { content: event.content }
|
|
373
|
+
: {}),
|
|
371
374
|
}));
|
|
372
375
|
}
|
|
373
376
|
break;
|
|
@@ -41,13 +41,6 @@ export const SHORTCUTS = [
|
|
|
41
41
|
action: "reload",
|
|
42
42
|
source: "operator",
|
|
43
43
|
},
|
|
44
|
-
{
|
|
45
|
-
command: "/help",
|
|
46
|
-
label: "Shortcuts",
|
|
47
|
-
description: "显示可用 shortcut",
|
|
48
|
-
action: "show-help",
|
|
49
|
-
source: "operator",
|
|
50
|
-
},
|
|
51
44
|
];
|
|
52
45
|
/** Pi Web commands with real browser-equivalent behavior (UI-11). */
|
|
53
46
|
export const WEB_SLASH_COMMANDS = [
|
|
@@ -103,6 +96,15 @@ export function resolveShortcut(input) {
|
|
|
103
96
|
return { command: local.command, action: local.action };
|
|
104
97
|
return { command: token, action: "unknown" };
|
|
105
98
|
}
|
|
99
|
+
/** Map Operator panel shortcuts onto Console workspace routes. */
|
|
100
|
+
export function workspaceForShortcut(action) {
|
|
101
|
+
if (action === "focus-panel:task" || action === "focus-panel:contract") {
|
|
102
|
+
return "tasks";
|
|
103
|
+
}
|
|
104
|
+
if (action === "focus-panel:dag")
|
|
105
|
+
return "recovery";
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
106
108
|
/**
|
|
107
109
|
* Merge Operator, Web, and dynamic Session commands with conflict dedupe.
|
|
108
110
|
* Priority: operator/web > extension > skill > prompt. /reload is single-card.
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Turn grouping for Operator Chat inline process UX (pi-web ProcessDetails mind).
|
|
3
3
|
*/
|
|
4
|
+
import { joinAssistantText, parseAssistantContent, splitFinalAssistantBlocks, } from "./assistant-content.js";
|
|
5
|
+
function emptyGroup(key) {
|
|
6
|
+
return {
|
|
7
|
+
key,
|
|
8
|
+
assistants: [],
|
|
9
|
+
tools: [],
|
|
10
|
+
processItems: [],
|
|
11
|
+
live: false,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
4
14
|
/**
|
|
5
15
|
* Restore chat bubbles + tool cards from durable session messages.
|
|
6
16
|
* Tool rows often appear before the final assistant `message_end` message;
|
|
@@ -36,11 +46,13 @@ export function hydrateSessionThread(raw) {
|
|
|
36
46
|
if (item.role === "assistant") {
|
|
37
47
|
if (!turnId)
|
|
38
48
|
turnId = `restored-${item.id}`;
|
|
49
|
+
const content = parseAssistantContent(item.content);
|
|
39
50
|
messages.push({
|
|
40
51
|
id: item.id,
|
|
41
52
|
role: "assistant",
|
|
42
53
|
text: item.text,
|
|
43
54
|
createdAt,
|
|
55
|
+
...(content && content.length > 0 ? { content } : {}),
|
|
44
56
|
});
|
|
45
57
|
flushPendingToAssistant(item.id);
|
|
46
58
|
continue;
|
|
@@ -64,6 +76,45 @@ export function hydrateSessionThread(raw) {
|
|
|
64
76
|
}
|
|
65
77
|
return { messages, toolCalls };
|
|
66
78
|
}
|
|
79
|
+
function fillProcessDetails(group) {
|
|
80
|
+
const finalAssistant = group.assistants[group.assistants.length - 1];
|
|
81
|
+
const blocks = finalAssistant?.content ?? [];
|
|
82
|
+
const split = splitFinalAssistantBlocks(blocks, { isStreaming: group.live });
|
|
83
|
+
const usedToolIds = new Set();
|
|
84
|
+
const processItems = [];
|
|
85
|
+
for (const block of split.processBlocks) {
|
|
86
|
+
if (block.type === "thinking") {
|
|
87
|
+
processItems.push({ kind: "thinking", thinking: block.thinking });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (block.type === "text") {
|
|
91
|
+
processItems.push({ kind: "text", text: block.text });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const tool = group.tools.find((entry) => entry.id === block.toolCallId);
|
|
95
|
+
if (tool) {
|
|
96
|
+
processItems.push({ kind: "tool", tool });
|
|
97
|
+
usedToolIds.add(tool.id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const tool of group.tools) {
|
|
101
|
+
if (!usedToolIds.has(tool.id))
|
|
102
|
+
processItems.push({ kind: "tool", tool });
|
|
103
|
+
}
|
|
104
|
+
group.processItems = processItems;
|
|
105
|
+
if (!finalAssistant)
|
|
106
|
+
return;
|
|
107
|
+
if (blocks.length === 0) {
|
|
108
|
+
group.answer = finalAssistant;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const answerText = joinAssistantText(split.answerBlocks);
|
|
112
|
+
group.answer = {
|
|
113
|
+
...finalAssistant,
|
|
114
|
+
text: answerText,
|
|
115
|
+
content: split.answerBlocks,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
67
118
|
/**
|
|
68
119
|
* Build user → process → answer groups for the message timeline.
|
|
69
120
|
*/
|
|
@@ -72,12 +123,7 @@ export function buildTurnProcessGroups(input) {
|
|
|
72
123
|
const groups = [];
|
|
73
124
|
let current = null;
|
|
74
125
|
const startGroup = (key) => {
|
|
75
|
-
const group =
|
|
76
|
-
key,
|
|
77
|
-
assistants: [],
|
|
78
|
-
tools: [],
|
|
79
|
-
live: false,
|
|
80
|
-
};
|
|
126
|
+
const group = emptyGroup(key);
|
|
81
127
|
groups.push(group);
|
|
82
128
|
return group;
|
|
83
129
|
};
|
|
@@ -92,12 +138,7 @@ export function buildTurnProcessGroups(input) {
|
|
|
92
138
|
current.assistants.push(message);
|
|
93
139
|
}
|
|
94
140
|
if (groups.length === 0 && input.toolCalls.length > 0) {
|
|
95
|
-
groups.push(
|
|
96
|
-
key: "orphan-tools",
|
|
97
|
-
assistants: [],
|
|
98
|
-
tools: [],
|
|
99
|
-
live: false,
|
|
100
|
-
});
|
|
141
|
+
groups.push(emptyGroup("orphan-tools"));
|
|
101
142
|
}
|
|
102
143
|
const assigned = new Set();
|
|
103
144
|
const assistantToGroup = new Map();
|
|
@@ -162,17 +203,23 @@ export function buildTurnProcessGroups(input) {
|
|
|
162
203
|
input.streaming &&
|
|
163
204
|
input.activeAssistantId !== null &&
|
|
164
205
|
group.assistants.some((a) => a.id === input.activeAssistantId);
|
|
206
|
+
fillProcessDetails(group);
|
|
165
207
|
}
|
|
166
208
|
return groups;
|
|
167
209
|
}
|
|
168
|
-
export function processGroupSummary(tools) {
|
|
210
|
+
export function processGroupSummary(tools, options) {
|
|
169
211
|
const count = tools.length;
|
|
212
|
+
const thinkingCount = options?.thinkingCount ?? 0;
|
|
213
|
+
const textCount = options?.textCount ?? 0;
|
|
214
|
+
const stepCount = thinkingCount + textCount + count;
|
|
170
215
|
const errorCount = tools.filter((t) => t.status === "error").length;
|
|
171
216
|
const runningCount = tools.filter((t) => t.status === "running").length;
|
|
172
|
-
const parts = [
|
|
217
|
+
const parts = [`处理详情 · ${stepCount} 步`];
|
|
218
|
+
if (count > 0)
|
|
219
|
+
parts.push(`${count} 工具`);
|
|
173
220
|
if (errorCount > 0)
|
|
174
221
|
parts.push(`${errorCount} 出错`);
|
|
175
222
|
else if (runningCount > 0)
|
|
176
223
|
parts.push("运行中");
|
|
177
|
-
return { count, errorCount, runningCount, label: parts.join(" · ") };
|
|
224
|
+
return { count, errorCount, runningCount, stepCount, label: parts.join(" · ") };
|
|
178
225
|
}
|
|
@@ -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
|
+
}
|