@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
package/AGENTS.md
CHANGED
|
@@ -94,8 +94,6 @@ npm run build
|
|
|
94
94
|
node bin/loop-agent.js --help
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
- 改 `src/**` 后必须先 `npm run build` 再用 `bin/loop-agent.js`:bin 内置 stale-dist 守卫(src 比 dist 新即硬失败,`--ignore-stale-dist` 逃生),`--version` 会显示构建 git SHA,发布/自测前用它确认跑的是目标改动。
|
|
98
|
-
|
|
99
97
|
文档站变更:`npm run docs:build`。init / architecture / skill entry / pack 定向验证见 verification-matrix。
|
|
100
98
|
|
|
101
99
|
## 交接
|
package/CHANGELOG.md
CHANGED
|
@@ -1,38 +1,55 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## [0.35.2] - 2026-08-15
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### 重点更新
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- Operator Chat 处理详情对齐 pi-web:回合尾部的思考内容与工具调用摘要切分为可折叠「处理详情」,流式中思考可见,回合结束后自动折叠,最终回答单独成气泡
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
### 新增
|
|
12
|
+
|
|
13
|
+
- 对话时间线新增内联可折叠「处理详情」区块,集中展示模型思考与工具调用摘要,回合结束后自动折叠
|
|
14
|
+
- turn-process 保留 SDK thinking blocks,流式传输与持久化均携带思考内容
|
|
12
15
|
|
|
13
16
|
### 改进
|
|
14
17
|
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
18
|
+
- 命令面板 shortcut 优化:Operator 组单行展示,/task、/contract、/dag 分别跳转到任务与恢复页面
|
|
19
|
+
|
|
20
|
+
### 修复
|
|
21
|
+
|
|
22
|
+
- 修复「处理详情」思考折叠块背景引用未定义样式变量 --panel 的问题,改为确定性透明背景,消除 Console 样式变量完整性门禁对发布的拦截(视觉无变化)
|
|
23
|
+
- 移除自指的 /help 命令项:输入 / 已弹出命令面板,不再单独提供重复的帮助入口
|
|
24
|
+
|
|
25
|
+
## [0.35.1] - 2026-08-15
|
|
26
|
+
|
|
27
|
+
### 重点更新
|
|
28
|
+
|
|
29
|
+
- taskKind 显式契约全链路生效:页面显式选择的任务类型(含「后端研发(默认 DAG)」= standard)不再被 intake 语义识别或 DAG 生成阶段的语义分类改写;taskKindExplicit 从 CLI 标志 → 语义草稿 → task.json → DAG 路由全链路透传,显式 standard 跳过语义分类且不回写 taskKind
|
|
30
|
+
- Console 任务向导提交反馈:导入/访谈/契约等长动作点击后立即显示进度文案与按钮 spinner,消除数十秒无反馈的「页面卡死」观感;导入结果新增自动发现引用文档与未解析引用的透明提示
|
|
31
|
+
- Console intake 链路修复:任务导航上传 PRD 三件套后契约落地不再因工程边界缺失卡死;semantic intake 在缺失型边界缺口时接管并基于仓库布局推荐 allowedPaths/verify;文字录入「请参照这三个prd路径…」自动发现并导入引用文档;新的 PRD 导入重置 semantic intake 一次性 marker,失败后可重试
|
|
32
|
+
|
|
33
|
+
### 新增
|
|
34
|
+
|
|
35
|
+
- PRD 引用自动发现:无上传文档时从文字录入内容中提取 Unicode 路径 token,经 URL 剥离、存在性/大小/排除过滤与角色推断后按角色自动导入引用文件,并透出自动导入与未解析引用结果
|
|
36
|
+
- 任务向导长动作(bootstrap/import/interview/contract)点击后立即在 flash 区域显示动作进度文案(含全链路阶段说明与预期时长),结束时被结果消息覆盖
|
|
37
|
+
- PRD 导入主按钮增加 busy 态 spinner、「导入中…」文案与 aria-busy,新增旋转动画样式
|
|
38
|
+
- 导入结果渲染自动导入的引用文档(路径:角色)与未解析引用警告
|
|
39
|
+
|
|
40
|
+
### 改进
|
|
41
|
+
|
|
42
|
+
- semantic intake 资格判定拆分缺失型(工程边界/taskKind 缺失时允许模型按仓库布局推荐)与非法型(路径策略/安全/重叠等仍维持拒绝),回归其 repo-layout-aware fallback 的设计意图
|
|
43
|
+
- semantic intake 提示词增补正式 AC 标题逐字复制指引
|
|
44
|
+
- task advance 对模型未推荐边界场景增加 SEMANTIC_INTAKE_BOUNDARY_UNRESOLVED 可操作提示,明确告知用户当前状态
|
|
45
|
+
|
|
46
|
+
### 修复
|
|
47
|
+
|
|
48
|
+
- 修复 Console 显式选择的 taskKind 被丢弃的问题:此前「未选择」与「显式选 standard」被混为一谈,仅在非 standard 时传递任务类型标志,导致 AI 语义识别(如推荐 backend-test)覆盖用户选择、DAG 被改道
|
|
49
|
+
- 修复 DAG 生成阶段对显式 standard 仍执行语义分类并回写 taskKind 的问题:显式信息此前在契约落盘时丢失,现显式指定时跳过语义分类且不回写
|
|
50
|
+
- 修复 PRD 三件套上传时工程边界缺失以 EMPTY_ALLOWED_PATHS blocking 卡死、semantic intake 拒绝接管、契约永不 apply 的问题
|
|
51
|
+
- 修复文字录入「请参照这三个prd路径XXX」只物化一句话、引用文件从未导入的问题
|
|
52
|
+
- 修复 semantic intake attempt marker 一次性失败后永不重试的问题:新的 PRD 导入会重置 marker,失败可随新证据重试
|
|
36
53
|
|
|
37
54
|
## [0.35.0] - 2026-08-13
|
|
38
55
|
|
package/bin/loop-agent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
|
|
@@ -13,42 +13,6 @@ if (!existsSync(cliEntry)) {
|
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Stale-dist guard: when running from a source checkout (src/ present), refuse
|
|
18
|
-
* to silently execute a dist/ that predates the latest source edit. The
|
|
19
|
-
* classic failure mode is editing src/** and running the bin without
|
|
20
|
-
* `npm run build`, which executes the previous build. Suppress with
|
|
21
|
-
* --ignore-stale-dist when intentionally running a stale build.
|
|
22
|
-
*/
|
|
23
|
-
const srcDir = join(packageRoot, "src");
|
|
24
|
-
const ignoreStale = process.argv.includes("--ignore-stale-dist");
|
|
25
|
-
if (ignoreStale) {
|
|
26
|
-
process.argv = process.argv.filter((arg) => arg !== "--ignore-stale-dist");
|
|
27
|
-
}
|
|
28
|
-
if (!ignoreStale && existsSync(srcDir)) {
|
|
29
|
-
const distCliMtime = statSync(cliEntry).mtimeMs;
|
|
30
|
-
let newestSrcMtime = 0;
|
|
31
|
-
const walk = (dir) => {
|
|
32
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
33
|
-
const full = join(dir, entry.name);
|
|
34
|
-
if (entry.isDirectory()) {
|
|
35
|
-
walk(full);
|
|
36
|
-
} else {
|
|
37
|
-
const mtime = statSync(full).mtimeMs;
|
|
38
|
-
if (mtime > newestSrcMtime) newestSrcMtime = mtime;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
walk(srcDir);
|
|
43
|
-
if (newestSrcMtime > distCliMtime) {
|
|
44
|
-
console.error(
|
|
45
|
-
`loop-agent: dist/ is stale relative to src/ (a source file is newer than ${cliEntry}).\n` +
|
|
46
|
-
` Run \`npm run build\` before using the bin, or pass --ignore-stale-dist to run the stale build intentionally.`,
|
|
47
|
-
);
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
16
|
try {
|
|
53
17
|
await import(pathToFileURL(cliEntry).href);
|
|
54
18
|
} catch (error) {
|
|
@@ -308,7 +308,10 @@ export async function generateTaskDagUseCase(input) {
|
|
|
308
308
|
if (hasAutomaticTaskSourceRoute &&
|
|
309
309
|
candidateResult.template === "frontend-implementation") {
|
|
310
310
|
const taskConfig = await loadTaskConfig(repoRoot, parsed.taskId);
|
|
311
|
-
|
|
311
|
+
// Human-explicit taskKind (incl. explicit standard) is a hard contract:
|
|
312
|
+
// never rewrite task.json taskKind via semantic classification.
|
|
313
|
+
if (!taskConfig.taskKindExplicit &&
|
|
314
|
+
(!taskConfig.taskKind || taskConfig.taskKind === "standard")) {
|
|
312
315
|
await writeTaskConfig(repoRoot, parsed.taskId, {
|
|
313
316
|
...taskConfig,
|
|
314
317
|
taskKind: "frontend-implementation",
|
|
@@ -627,6 +627,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
627
627
|
flags: {
|
|
628
628
|
title: input.title,
|
|
629
629
|
taskKind: input.taskKind,
|
|
630
|
+
taskKindExplicit: input.taskKindExplicit,
|
|
630
631
|
featureId: input.featureId,
|
|
631
632
|
allowedPaths: effectiveInput.allowedPaths,
|
|
632
633
|
forbiddenPaths: input.forbiddenPaths,
|
|
@@ -681,6 +682,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
681
682
|
flags: {
|
|
682
683
|
title: input.title,
|
|
683
684
|
taskKind: input.taskKind,
|
|
685
|
+
taskKindExplicit: input.taskKindExplicit,
|
|
684
686
|
featureId: input.featureId,
|
|
685
687
|
allowedPaths: effectiveInput.allowedPaths,
|
|
686
688
|
forbiddenPaths: input.forbiddenPaths,
|
|
@@ -700,6 +702,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
700
702
|
flags: {
|
|
701
703
|
title: input.title,
|
|
702
704
|
taskKind: input.taskKind,
|
|
705
|
+
taskKindExplicit: input.taskKindExplicit,
|
|
703
706
|
featureId: input.featureId,
|
|
704
707
|
allowedPaths: effectiveInput.allowedPaths,
|
|
705
708
|
forbiddenPaths: input.forbiddenPaths,
|
|
@@ -762,6 +765,17 @@ export async function advanceTaskLifecycle(input) {
|
|
|
762
765
|
message: prepareResult.error?.message ??
|
|
763
766
|
"PRD imported but intake incomplete; DAG not generated",
|
|
764
767
|
});
|
|
768
|
+
// B2: semantic intake ran but the model declined to recommend an
|
|
769
|
+
// engineering boundary — surface an actionable hint instead of a
|
|
770
|
+
// bare EMPTY_ALLOWED_PATHS so the Console operator knows the fix.
|
|
771
|
+
const gapCodesNow = new Set((prepareResult.gaps ?? []).map((gap) => gap.code));
|
|
772
|
+
if (semanticIntakeAttemptedThisCall &&
|
|
773
|
+
gapCodesNow.has("EMPTY_ALLOWED_PATHS")) {
|
|
774
|
+
warnings.push({
|
|
775
|
+
code: "SEMANTIC_INTAKE_BOUNDARY_UNRESOLVED",
|
|
776
|
+
message: "semantic intake structured the requirement but no engineering boundary was recommended; fill allowedPaths in the Console wizard (工程边界) or pass --allowed-path",
|
|
777
|
+
});
|
|
778
|
+
}
|
|
765
779
|
for (const gap of prepareResult.gaps ?? []) {
|
|
766
780
|
if (gap.level === "blocking") {
|
|
767
781
|
warnings.push({ code: gap.code, message: gap.message });
|
package/dist/cli/program.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
-
import {
|
|
4
|
+
import { readPackageVersion } from "../shared/package-metadata.js";
|
|
5
5
|
import { resolveAdapter } from "../adapters/index.js";
|
|
6
6
|
import { runCoverageAudit } from "../commands/coverage-audit.js";
|
|
7
7
|
import { runCoverageReport } from "../commands/coverage-report.js";
|
|
@@ -547,7 +547,7 @@ export function buildLoopAgentProgram(options) {
|
|
|
547
547
|
program
|
|
548
548
|
.name("loop-agent")
|
|
549
549
|
.description("Loop Agent task and DAG automation CLI")
|
|
550
|
-
.version(
|
|
550
|
+
.version(readPackageVersion(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))) ?? "0.0.0", "-V, --version", "display version")
|
|
551
551
|
.showHelpAfterError()
|
|
552
552
|
.option("--repo-root <path>", "repository root")
|
|
553
553
|
.option("--adapter <name>", "harness adapter name")
|
|
@@ -252,6 +252,7 @@ function toUseCaseInput(repoRoot, taskId, options) {
|
|
|
252
252
|
fromText: options.fromText,
|
|
253
253
|
fromDraftPath: options.fromDraft,
|
|
254
254
|
taskKind: options.taskKind,
|
|
255
|
+
taskKindExplicit: Boolean(options.taskKind?.trim()),
|
|
255
256
|
featureId: options.featureId,
|
|
256
257
|
allowedPaths: options.allowedPaths,
|
|
257
258
|
forbiddenPaths: options.forbiddenPaths,
|
|
@@ -520,33 +520,6 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
520
520
|
}
|
|
521
521
|
let result;
|
|
522
522
|
try {
|
|
523
|
-
// Phase 5 (P1-9): capture the writeSet baseline BEFORE the writer provider
|
|
524
|
-
// call so a transient partial write can be rolled back and recovered via a
|
|
525
|
-
// staged child. Only frontend-implementation writers; a failed capture is a
|
|
526
|
-
// hard fail (no baseline → no rollback → no recovery). Dynamic import avoids
|
|
527
|
-
// an executor ↔ scheduler static cycle.
|
|
528
|
-
if (isWriteTask &&
|
|
529
|
-
(input.task.id === "frontend-implement-pi" ||
|
|
530
|
-
input.task.id === "frontend-repair-pi") &&
|
|
531
|
-
(input.task.writeSet?.length ?? 0) > 0) {
|
|
532
|
-
try {
|
|
533
|
-
const { captureFrontendWriterAttemptIntent } = await import("../workflows/dag/frontend-writer-recovery.js");
|
|
534
|
-
await captureFrontendWriterAttemptIntent({
|
|
535
|
-
cwd: input.cwd,
|
|
536
|
-
runDir: meta.runDir,
|
|
537
|
-
writeSet: input.task.writeSet,
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
catch (error) {
|
|
541
|
-
return {
|
|
542
|
-
ok: false,
|
|
543
|
-
stdout: "",
|
|
544
|
-
stderr: `frontend writer attempt baseline capture failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
545
|
-
failureCategory: "write-guard",
|
|
546
|
-
durationMs: Date.now() - started,
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
523
|
result = await piStepFn({
|
|
551
524
|
attachedFiles: [],
|
|
552
525
|
modelConfig: resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined),
|
|
@@ -718,23 +691,6 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
718
691
|
const changedFiles = pathsChangedDuringRun(snapshotGitStatusPorcelain(beforeStatus), afterSnapshot, beforePathFingerprints, afterPathFingerprints);
|
|
719
692
|
changeManifestAfterStatus = afterStatus;
|
|
720
693
|
changeManifestChangedFiles = changedFiles;
|
|
721
|
-
// Phase 5: record the attempt changed paths/hashes into the rollback
|
|
722
|
-
// intent so a transient partial write can be CAS-restored.
|
|
723
|
-
if ((input.task.id === "frontend-implement-pi" ||
|
|
724
|
-
input.task.id === "frontend-repair-pi") &&
|
|
725
|
-
(input.task.writeSet?.length ?? 0) > 0) {
|
|
726
|
-
try {
|
|
727
|
-
const { recordFrontendWriterAttempt } = await import("../workflows/dag/frontend-writer-recovery.js");
|
|
728
|
-
await recordFrontendWriterAttempt({
|
|
729
|
-
cwd: input.cwd,
|
|
730
|
-
runDir: meta.runDir,
|
|
731
|
-
changedPaths: changedFiles,
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
catch {
|
|
735
|
-
// best-effort: a missing intent (no baseline captured) is a no-op
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
694
|
const guard = validateShellWriteGuardFromDiff({
|
|
739
695
|
changedFiles,
|
|
740
696
|
task: input.task,
|
|
@@ -45,48 +45,6 @@ export function readPackageName(packageDir) {
|
|
|
45
45
|
}
|
|
46
46
|
return undefined;
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Read the build stamp (git SHA + build time) written by the build pipeline
|
|
50
|
-
* into `<packageDir>/dist/build-stamp.json`. `undefined` when absent (e.g.
|
|
51
|
-
* running from source via tsx, or a hand-assembled package), which callers
|
|
52
|
-
* treat as "no stamp available".
|
|
53
|
-
*/
|
|
54
|
-
export function readBuildStamp(packageDir) {
|
|
55
|
-
try {
|
|
56
|
-
const raw = readFileSync(path.join(packageDir, "dist", "build-stamp.json"), "utf-8");
|
|
57
|
-
const parsed = JSON.parse(raw);
|
|
58
|
-
if (parsed.schemaVersion !== 1 ||
|
|
59
|
-
typeof parsed.version !== "string" ||
|
|
60
|
-
typeof parsed.builtAt !== "string")
|
|
61
|
-
return undefined;
|
|
62
|
-
return parsed;
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Human-readable CLI version label: plain version, enriched with the build
|
|
70
|
-
* stamp (short git SHA + build date) when available so a stale dist/ that was
|
|
71
|
-
* built from an old commit is immediately recognizable in `--version`.
|
|
72
|
-
*/
|
|
73
|
-
export function readCliVersionLabel(packageDir) {
|
|
74
|
-
const version = readPackageVersion(packageDir) ?? "0.0.0";
|
|
75
|
-
const stamp = readBuildStamp(packageDir);
|
|
76
|
-
if (!stamp?.gitSha)
|
|
77
|
-
return version;
|
|
78
|
-
return `${version} (build ${stamp.gitSha.slice(0, 8)} ${stamp.builtAt.slice(0, 10)})`;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Strip the build-stamp suffix from a CLI `--version` label, returning the bare
|
|
82
|
-
* SemVer. Controller-identity/preflight comparisons must use this rather than
|
|
83
|
-
* the raw label so a build-stamped `0.35.0 (build 7f8c5b5b …)` still matches
|
|
84
|
-
* `packageVersion === "0.35.0"`.
|
|
85
|
-
*/
|
|
86
|
-
export function normalizeCliVersionLabel(label) {
|
|
87
|
-
const match = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/.exec(label.trim());
|
|
88
|
-
return match ? match[0] : label.trim();
|
|
89
|
-
}
|
|
90
48
|
/**
|
|
91
49
|
* Compute the SHA-256 hash of a file on disk.
|
|
92
50
|
*
|
|
@@ -118,6 +118,8 @@ const taskConfigObjectSchema = z.object({
|
|
|
118
118
|
sourceFiles: z.array(z.string()),
|
|
119
119
|
/** standard: 本仓库需求实现;feature-study: 参考外部代码特性并在目标仓库落地;frontend-implementation: 前端实现 DAG;backend-test: 后端测试 DAG;knowledge-sync: 最终验证后回写测试知识库 DAG;knowledge-graph-bootstrap: AI 辅助业务知识图谱初始化 */
|
|
120
120
|
taskKind: taskKindSchema.optional().default("standard"),
|
|
121
|
+
/** true = taskKind 由人类显式选择(含显式 standard);DAG 路由必须尊重,不做语义改写。 */
|
|
122
|
+
taskKindExplicit: z.boolean().optional(),
|
|
121
123
|
/** Feature 目录 id(如 F-2026-004)。knowledge-sync 必填(也可从 hardConstraints/需求正文/taskId 解析);用于收窄 writeSet */
|
|
122
124
|
featureId: z.string().min(1).optional(),
|
|
123
125
|
referenceRepos: z.array(referenceRepoConfigSchema).optional().default([]),
|
|
@@ -152,6 +152,9 @@ export function mergeManagedTaskConfigFields(existing, draft) {
|
|
|
152
152
|
...existing,
|
|
153
153
|
title: draft.title,
|
|
154
154
|
taskKind: draft.taskKind,
|
|
155
|
+
...(draft.taskKindExplicit !== undefined
|
|
156
|
+
? { taskKindExplicit: draft.taskKindExplicit }
|
|
157
|
+
: {}),
|
|
155
158
|
allowedPaths: canonicalizePathArray(draft.constraints.allowedPaths),
|
|
156
159
|
...(draft.constraints.allowedRoots !== undefined
|
|
157
160
|
? { allowedRoots: canonicalizePathArray(draft.constraints.allowedRoots) }
|
|
@@ -57,6 +57,7 @@ export const taskContractDraftSchema = z
|
|
|
57
57
|
taskId: z.string().min(1),
|
|
58
58
|
title: z.string().min(1),
|
|
59
59
|
taskKind: taskKindSchema,
|
|
60
|
+
taskKindExplicit: z.boolean().optional(),
|
|
60
61
|
featureId: z.string().min(1).optional(),
|
|
61
62
|
requirement: taskContractDraftRequirementSchema,
|
|
62
63
|
constraints: taskContractDraftConstraintsSchema,
|
|
@@ -17,6 +17,11 @@ function resolveTaskKind(flags, baseDraft, existing) {
|
|
|
17
17
|
const parsed = taskKindSchema.safeParse(candidate);
|
|
18
18
|
return parsed.success ? parsed.data : String(candidate);
|
|
19
19
|
}
|
|
20
|
+
function resolveTaskKindExplicit(flags, baseDraft) {
|
|
21
|
+
if (flags.taskKindExplicit !== undefined)
|
|
22
|
+
return flags.taskKindExplicit;
|
|
23
|
+
return baseDraft?.taskKindExplicit ?? false;
|
|
24
|
+
}
|
|
20
25
|
function resolveTitle(taskId, flags, facts, baseDraft, existing) {
|
|
21
26
|
return (trimNonEmpty(flags.title) ??
|
|
22
27
|
trimNonEmpty(baseDraft?.title) ??
|
|
@@ -105,6 +110,7 @@ export function buildPrepareDraft(input) {
|
|
|
105
110
|
const { taskId, existingTaskConfig, baseDraft, facts, flags, references } = input;
|
|
106
111
|
const title = resolveTitle(taskId, flags, facts, baseDraft, existingTaskConfig);
|
|
107
112
|
const taskKind = resolveTaskKind(flags, baseDraft, existingTaskConfig);
|
|
113
|
+
const taskKindExplicit = resolveTaskKindExplicit(flags, baseDraft);
|
|
108
114
|
const featureId = trimNonEmpty(flags.featureId) ??
|
|
109
115
|
trimNonEmpty(baseDraft?.featureId) ??
|
|
110
116
|
trimNonEmpty(existingTaskConfig?.featureId) ??
|
|
@@ -170,6 +176,7 @@ export function buildPrepareDraft(input) {
|
|
|
170
176
|
taskId,
|
|
171
177
|
title,
|
|
172
178
|
taskKind: taskKind,
|
|
179
|
+
taskKindExplicit,
|
|
173
180
|
requirement: {
|
|
174
181
|
objective,
|
|
175
182
|
scope,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* optional model recommendation (repo-layout-aware fallback for Console
|
|
8
8
|
* web-wizard flows that never collect human paths).
|
|
9
9
|
*/
|
|
10
|
-
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { executePiStep } from "../../executors/pi-executor.js";
|
|
@@ -92,31 +92,39 @@ export function evaluateSemanticIntakeEligibility(input) {
|
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
// Invalid-value engineering gaps are NOT fixed by semantic intake (the
|
|
96
|
+
// model cannot repair a rejected path policy or an illegal overlap).
|
|
97
|
+
// Missing-value engineering gaps (EMPTY_ALLOWED_PATHS / EMPTY_TASK_KIND)
|
|
98
|
+
// ARE the documented Console web-wizard case: the wizard never collects
|
|
99
|
+
// human paths, so the repo-layout-aware model recommendation is the
|
|
100
|
+
// designed fallback (see module header). Keep them eligible.
|
|
101
|
+
const invalidEngineering = blocking.length > 0 &&
|
|
102
|
+
blocking.every((g) => g.code === "MISSING_FEATURE_ID" ||
|
|
100
103
|
g.code === "PATH_POLICY" ||
|
|
101
104
|
g.code.startsWith("UNSAFE_") ||
|
|
102
105
|
g.code === "ALLOWED_FORBIDDEN_OVERLAP");
|
|
103
|
-
if (
|
|
106
|
+
if (invalidEngineering) {
|
|
104
107
|
return {
|
|
105
108
|
eligible: false,
|
|
106
|
-
reason: "
|
|
109
|
+
reason: "engineering boundary gaps carry invalid values; fix the flags/paths",
|
|
107
110
|
};
|
|
108
111
|
}
|
|
109
112
|
const structural = codes.has("SEMANTIC_INTAKE_RECOMMENDED") ||
|
|
110
113
|
blockingCodes.has("EMPTY_ACCEPTANCE") ||
|
|
111
114
|
blockingCodes.has("EMPTY_OBJECTIVE") ||
|
|
112
|
-
blockingCodes.has("NO_PARSEABLE_REQUIREMENT")
|
|
115
|
+
blockingCodes.has("NO_PARSEABLE_REQUIREMENT") ||
|
|
116
|
+
blockingCodes.has("EMPTY_ALLOWED_PATHS") ||
|
|
117
|
+
blockingCodes.has("EMPTY_TASK_KIND");
|
|
113
118
|
if (!structural && !input.force) {
|
|
114
119
|
return {
|
|
115
120
|
eligible: false,
|
|
116
121
|
reason: "no structural requirement gaps for semantic intake",
|
|
117
122
|
};
|
|
118
123
|
}
|
|
119
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
eligible: true,
|
|
126
|
+
reason: "structural or engineering-boundary gaps with imported sources",
|
|
127
|
+
};
|
|
120
128
|
}
|
|
121
129
|
export function extractJsonObject(text) {
|
|
122
130
|
const fenced = /```(?:json)?\s*([\s\S]*?)```/iu.exec(text);
|
|
@@ -206,6 +214,9 @@ export function buildDraftFromSemanticIntake(input) {
|
|
|
206
214
|
taskId: input.taskId,
|
|
207
215
|
title: input.semantic.title?.trim() || input.title,
|
|
208
216
|
taskKind,
|
|
217
|
+
...(explicitTaskKind || input.flags.taskKindExplicit
|
|
218
|
+
? { taskKindExplicit: true }
|
|
219
|
+
: {}),
|
|
209
220
|
requirement: {
|
|
210
221
|
objective: input.semantic.objective.trim(),
|
|
211
222
|
scope: input.semantic.scope.map((s) => s.trim()).filter(Boolean),
|
|
@@ -374,6 +385,7 @@ async function buildPrompt(input) {
|
|
|
374
385
|
"Prefer smallest correct write boundary; do not invent product paths that are not in the layout hint.",
|
|
375
386
|
"Do NOT rewrite or suggest editing the original PRD files.",
|
|
376
387
|
"Prefer explicit text; use provenance derived for rephrasing; use inferred only when clearly implied — if you must invent ACs, leave acceptanceCriteria empty so the system can stop for clarification.",
|
|
388
|
+
"When documents embed formal AC headings (e.g. #### AC-FE-001 …), copy each AC verbatim under the SAME id with provenance explicit; do not rephrase or renumber them.",
|
|
377
389
|
"Return exactly one JSON object, no markdown prose outside optional fence:",
|
|
378
390
|
JSON.stringify({
|
|
379
391
|
title: "optional string",
|
|
@@ -416,6 +428,21 @@ export async function readSemanticIntakeAttempted(repoRoot, taskId) {
|
|
|
416
428
|
return false;
|
|
417
429
|
}
|
|
418
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* C: a new PRD import is new evidence — clear the one-shot attempt marker
|
|
433
|
+
* so semantic intake may run again for the refreshed sources.
|
|
434
|
+
*/
|
|
435
|
+
export async function clearSemanticIntakeAttempt(repoRoot, taskId) {
|
|
436
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
437
|
+
const marker = path.join(paths.taskDir, SEMANTIC_INTAKE_ATTEMPT_MARKER);
|
|
438
|
+
try {
|
|
439
|
+
await unlink(marker);
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
419
446
|
async function writeAttemptMarker(repoRoot, taskId, payload) {
|
|
420
447
|
const paths = getTaskPaths(repoRoot, taskId);
|
|
421
448
|
const dir = path.join(paths.taskDir, "artifacts", "intake");
|
|
@@ -402,6 +402,16 @@ export function resolveTaskDagTemplateSelection(input) {
|
|
|
402
402
|
reasons: [],
|
|
403
403
|
};
|
|
404
404
|
}
|
|
405
|
+
// Human-explicit standard (e.g. Console 后端研发默认 DAG) is a hard
|
|
406
|
+
// contract: semantic task-source classification must not rewrite it to a
|
|
407
|
+
// specialized template (which would also rewrite task.json taskKind).
|
|
408
|
+
if (input.taskKindExplicit) {
|
|
409
|
+
return {
|
|
410
|
+
template: requestedTemplate,
|
|
411
|
+
source: "taskKind",
|
|
412
|
+
reasons: ["taskKind standard was explicitly selected by human; semantic classification skipped"],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
405
415
|
const classification = classifyTaskDemand(input);
|
|
406
416
|
if (classification.kind === "frontend") {
|
|
407
417
|
const frontendSignals = classification.signals
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assistant content-block helpers shared by Pi event mapping and Operator Chat UI.
|
|
3
|
+
* Mirrors pi-web `lib/message-display.ts` (process vs answer split) without deferred fetch.
|
|
4
|
+
*/
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
if (!value || typeof value !== "object")
|
|
7
|
+
return undefined;
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function readString(value) {
|
|
11
|
+
return typeof value === "string" ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Parse a persisted/SSE content array; unknown shapes are dropped. */
|
|
14
|
+
export function parseAssistantContent(value) {
|
|
15
|
+
if (!Array.isArray(value))
|
|
16
|
+
return undefined;
|
|
17
|
+
const blocks = [];
|
|
18
|
+
for (const item of value) {
|
|
19
|
+
const rec = asRecord(item);
|
|
20
|
+
if (!rec)
|
|
21
|
+
continue;
|
|
22
|
+
const type = readString(rec.type) ?? "";
|
|
23
|
+
if (type === "thinking" || type === "reasoning") {
|
|
24
|
+
const thinking = readString(rec.thinking) ?? readString(rec.text) ?? "";
|
|
25
|
+
blocks.push({ type: "thinking", thinking });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (type === "toolCall" ||
|
|
29
|
+
type === "tool_call" ||
|
|
30
|
+
type === "toolUse" ||
|
|
31
|
+
type === "tool_use") {
|
|
32
|
+
const toolCallId = readString(rec.toolCallId) ?? readString(rec.id) ?? "";
|
|
33
|
+
if (!toolCallId)
|
|
34
|
+
continue;
|
|
35
|
+
const toolName = readString(rec.toolName) ?? readString(rec.name);
|
|
36
|
+
blocks.push({
|
|
37
|
+
type: "toolCall",
|
|
38
|
+
toolCallId,
|
|
39
|
+
...(toolName ? { toolName } : {}),
|
|
40
|
+
});
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (type === "text" || type === "output_text" || type === "") {
|
|
44
|
+
const text = readString(rec.text);
|
|
45
|
+
if (text !== undefined)
|
|
46
|
+
blocks.push({ type: "text", text });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return blocks;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Extract ordered assistant content blocks from a Pi SDK message object.
|
|
53
|
+
* Thinking/reasoning blocks keep their own type so they never leak into answer text.
|
|
54
|
+
*/
|
|
55
|
+
export function extractAssistantContentBlocks(message) {
|
|
56
|
+
const rec = asRecord(message);
|
|
57
|
+
if (!rec)
|
|
58
|
+
return [];
|
|
59
|
+
const contents = Array.isArray(rec.content) ? rec.content : [];
|
|
60
|
+
const parts = Array.isArray(rec.parts) ? rec.parts : [];
|
|
61
|
+
const fromArrays = parseAssistantContent([...contents, ...parts]) ?? [];
|
|
62
|
+
if (fromArrays.length > 0)
|
|
63
|
+
return fromArrays;
|
|
64
|
+
const top = readString(rec.text);
|
|
65
|
+
return top ? [{ type: "text", text: top }] : [];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Latest assistant answer text (text blocks only). Thinking is excluded.
|
|
69
|
+
* Preserves the historical `{ text }` / `{ parts: [{ text }] }` fallbacks.
|
|
70
|
+
*/
|
|
71
|
+
export function extractAssistantText(message) {
|
|
72
|
+
return extractAssistantContentBlocks(message)
|
|
73
|
+
.filter((block) => block.type === "text")
|
|
74
|
+
.map((block) => block.text)
|
|
75
|
+
.join("");
|
|
76
|
+
}
|
|
77
|
+
export function isEmptyThinkingBlock(block, options = {}) {
|
|
78
|
+
return (block.type === "thinking" &&
|
|
79
|
+
!options.isStreaming &&
|
|
80
|
+
block.thinking.trim() === "");
|
|
81
|
+
}
|
|
82
|
+
export function getDisplayableAssistantBlocks(blocks, options = {}) {
|
|
83
|
+
return blocks.filter((block) => !isEmptyThinkingBlock(block, options));
|
|
84
|
+
}
|
|
85
|
+
function isFinalAnswerBlock(block) {
|
|
86
|
+
return block.type === "text";
|
|
87
|
+
}
|
|
88
|
+
export function splitFinalAssistantBlocks(blocks, options = {}) {
|
|
89
|
+
const displayable = getDisplayableAssistantBlocks(blocks, options);
|
|
90
|
+
let lastProcessIndex = -1;
|
|
91
|
+
for (let i = displayable.length - 1; i >= 0; i--) {
|
|
92
|
+
if (!isFinalAnswerBlock(displayable[i])) {
|
|
93
|
+
lastProcessIndex = i;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (lastProcessIndex === -1) {
|
|
98
|
+
return { answerBlocks: displayable, processBlocks: [] };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
answerBlocks: displayable.slice(lastProcessIndex + 1),
|
|
102
|
+
processBlocks: displayable.slice(0, lastProcessIndex + 1),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function joinAssistantText(blocks) {
|
|
106
|
+
return blocks
|
|
107
|
+
.filter((block) => block.type === "text")
|
|
108
|
+
.map((block) => block.text)
|
|
109
|
+
.join("");
|
|
110
|
+
}
|