@tea-agent/loop-agent 0.34.0 → 0.34.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/CHANGELOG.md +30 -0
- package/dist/application/task-lifecycle/advance.js +57 -7
- package/dist/application/task-lifecycle/plan-transitions.js +13 -21
- package/dist/executors/dag-pi-executor.js +18 -3
- package/dist/shared/operator/capabilities.js +26 -2
- package/dist/task/source-prepare/prepare.js +49 -2
- package/dist/worker/console/interview/grill-me.js +7 -6
- package/dist/worker/console/operator-actions.js +231 -22
- package/dist/worker/console/prd-intake-bridge.js +393 -0
- package/dist/worker/console/recovery-cta.js +35 -6
- package/dist/worker/console/static/assets/index-BpuHmlSP.js +29 -0
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/console/static-src/app/console-types.js +1 -0
- package/dist/worker/console/static-src/app/usePrdImport.js +2 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +24 -1
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +19 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +57 -2
- package/dist/worker/observability/read-model.js +3 -0
- package/dist/workflows/dag/failure-category.js +3 -0
- package/dist/workflows/dag/init-hybrid.js +15 -9
- package/dist/workflows/dag/node-execution.js +3 -2
- package/dist/workflows/dag/retry-policy.js +44 -11
- package/dist/workflows/dag/runner.js +6 -0
- package/dist/workflows/dag/scheduler.js +49 -1
- package/dist/workflows/dag/validate.js +16 -2
- package/docs/templates/agent-dag.schema.json +4 -4
- package/package.json +1 -1
- package/dist/worker/console/static/assets/index-CMHovlqG.js +0 -32
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.34.1] - 2026-08-11
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- Console PRD Intake Bridge:统一粘贴、单文件与多文件 PRD 导入流程,复用确定性解析与 Semantic Intake,并回写真实 Console Draft
|
|
10
|
+
- 引入工程边界表单,工程路径与验证命令仅接受人工输入,杜绝使用模板内容冒充需求事实
|
|
11
|
+
- DAG 中断恢复增强:支持协作式 abortSignal,未启动节点明确标记为 controller-interrupted,避免粗暴终止
|
|
12
|
+
- Console Recovery 提供直观的 reconcile CTA,按运行状态精准展示恢复入口,破坏性操作需经浏览器 Human Gate 确认
|
|
13
|
+
|
|
14
|
+
### 新增
|
|
15
|
+
|
|
16
|
+
- Console PRD 导入桥接:支持将粘贴、单文件与多文件导入统一接入 task advance 的确定性解析,并映射产物至 DraftStore
|
|
17
|
+
- Console 工程边界表单:在 PRD 导入时支持可选的 allowed/forbidden/verify 字段,并作为标志传递至 bootstrap intake
|
|
18
|
+
- Console Recovery CTA:基于 effective status 与 liveness 展示 dag reconcile-run(supersede/abandon)入口
|
|
19
|
+
- DAG runner 协作式 abortSignal:主控停止时允许已运行节点按 checkpoint 收尾,未启动节点标记 controller-interrupted
|
|
20
|
+
|
|
21
|
+
### 改进
|
|
22
|
+
|
|
23
|
+
- dag reconcile-run 增加 --json 与 --action 参数,避免 operation runner 因期待 JSON 失败,并明确拒绝非法 action
|
|
24
|
+
- task advance 生命周期优化:PRD 导入后默认进入仅导入阶段,确保多文件 bootstrap 完成后再执行 Semantic Intake
|
|
25
|
+
- Console Recovery 状态映射细化:将 interrupted/orphaned 等状态映射为 needs-reconcile,suspected-stall 映射为 observe/doctor
|
|
26
|
+
- task advance writeSet gate 输出附确定性 delta,清晰展示边界扩张来源,且不破坏既有 approve 流程
|
|
27
|
+
|
|
28
|
+
### 修复
|
|
29
|
+
|
|
30
|
+
- 修复追加 PRD 后两阶段 intake 未重新投影契约、Console 可能回读旧 Semantic Intake artifact 的问题
|
|
31
|
+
- 修复 implement-pi 在 provider clean timeout 且明确零写入时直接终止的问题,现对标准 bounded writer 自动重试一次
|
|
32
|
+
- 修复 grill-me 模板被误当作已解析需求的问题
|
|
33
|
+
- 修复窄屏(<1100px)下 CHANGELOG 文档中尖括号导致的 MDX JSX 解析错误
|
|
34
|
+
|
|
5
35
|
## [0.34.0] - 2026-08-11
|
|
6
36
|
|
|
7
37
|
### 重点更新
|
|
@@ -42,7 +42,8 @@ function hasEngineeringBoundary(input) {
|
|
|
42
42
|
}
|
|
43
43
|
/** Inputs that must produce a new managed-contract revision when explicit. */
|
|
44
44
|
function hasContractChangingInput(input) {
|
|
45
|
-
return Boolean(
|
|
45
|
+
return Boolean((input.prdPaths?.length ?? 0) > 0 ||
|
|
46
|
+
hasEngineeringBoundary(input) ||
|
|
46
47
|
input.title ||
|
|
47
48
|
input.taskKind ||
|
|
48
49
|
input.featureId);
|
|
@@ -339,7 +340,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
339
340
|
verifyCommands: applied.verifyCommands ?? input.verifyCommands,
|
|
340
341
|
};
|
|
341
342
|
const hasBoundary = hasEngineeringBoundary(effectiveInput);
|
|
342
|
-
let refreshManagedContractThisCall = Boolean(hasContractChangingInput(effectiveInput) &&
|
|
343
|
+
let refreshManagedContractThisCall = Boolean((hasContractChangingInput(effectiveInput) || record.pendingPrdRefresh) &&
|
|
343
344
|
(snapshot.contract?.effectiveStatus === "managed" ||
|
|
344
345
|
Boolean(snapshot.contract?.revision && snapshot.contract.revision > 0)));
|
|
345
346
|
if (applied.accepted) {
|
|
@@ -515,6 +516,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
515
516
|
continue;
|
|
516
517
|
}
|
|
517
518
|
if (action.type === "import-prd") {
|
|
519
|
+
const importedMaterializedPaths = [];
|
|
518
520
|
for (const prd of input.prdPaths ?? []) {
|
|
519
521
|
let markdown;
|
|
520
522
|
try {
|
|
@@ -528,7 +530,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
528
530
|
explicitRole: prd.role,
|
|
529
531
|
markdown,
|
|
530
532
|
});
|
|
531
|
-
await importPrdDocument({
|
|
533
|
+
const imported = await importPrdDocument({
|
|
532
534
|
repoRoot: input.repoRoot,
|
|
533
535
|
taskId: input.taskId,
|
|
534
536
|
filePath: prd.filePath,
|
|
@@ -536,15 +538,48 @@ export async function advanceTaskLifecycle(input) {
|
|
|
536
538
|
// Prefer basename-aligned names for multi-file product suites.
|
|
537
539
|
name: path.basename(prd.filePath).replace(/\.(md|markdown|txt)$/iu, "") || undefined,
|
|
538
540
|
});
|
|
541
|
+
importedMaterializedPaths.push(imported.materializedRelativePath);
|
|
539
542
|
}
|
|
540
543
|
// Latch even when prdPaths is empty so a mis-planned import cannot loop.
|
|
541
544
|
importedPrdThisCall = true;
|
|
542
545
|
if ((input.prdPaths?.length ?? 0) > 0) {
|
|
543
|
-
record =
|
|
546
|
+
record = {
|
|
547
|
+
...appendTransition(record, "prd-imported"),
|
|
548
|
+
pendingPrdRefresh: true,
|
|
549
|
+
pendingPrdRefreshPaths: Array.from(new Set([
|
|
550
|
+
...(record.pendingPrdRefreshPaths ?? []),
|
|
551
|
+
...importedMaterializedPaths,
|
|
552
|
+
])),
|
|
553
|
+
};
|
|
544
554
|
completed.push("prd-imported");
|
|
545
555
|
await saveLifecycleRecord(input.repoRoot, record);
|
|
546
556
|
}
|
|
547
557
|
snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
|
|
558
|
+
// Two-phase Console bridge: when this call only had --prd and no
|
|
559
|
+
// engineering boundary, stop after archive. Re-plan would otherwise
|
|
560
|
+
// immediately prepare (hasPrdInput latched false + alreadyImported)
|
|
561
|
+
// and burn one-shot Semantic Intake before multi-file imports finish.
|
|
562
|
+
if (!hasBoundary && (input.prdPaths?.length ?? 0) > 0) {
|
|
563
|
+
return {
|
|
564
|
+
taskId: input.taskId,
|
|
565
|
+
lifecycleState: "intake-incomplete",
|
|
566
|
+
completedTransitions: uniqueTransitions(record.completedTransitions, completed),
|
|
567
|
+
gate: null,
|
|
568
|
+
blockers: [],
|
|
569
|
+
warnings: [
|
|
570
|
+
...warnings,
|
|
571
|
+
{
|
|
572
|
+
code: "BOUNDARY_REQUIRED",
|
|
573
|
+
message: "PRD imported; re-run task advance (optionally with --allowed-path / --verify) to prepare contract / semantic intake",
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
next: {
|
|
577
|
+
kind: "advance",
|
|
578
|
+
command: `loop-agent task advance ${input.taskId} --json`,
|
|
579
|
+
},
|
|
580
|
+
recommendations,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
548
583
|
continue;
|
|
549
584
|
}
|
|
550
585
|
if (action.type === "prepare-contract") {
|
|
@@ -580,6 +615,8 @@ export async function advanceTaskLifecycle(input) {
|
|
|
580
615
|
: {
|
|
581
616
|
kind: "facts",
|
|
582
617
|
useImportedPrd: hasPrd || (snapshot.source?.importedPrdCount ?? 0) > 0,
|
|
618
|
+
replaceManagedRequirement: record.pendingPrdRefresh === true,
|
|
619
|
+
importedPrdPaths: record.pendingPrdRefreshPaths,
|
|
583
620
|
text: input.fromText,
|
|
584
621
|
textLabel: input.fromText ? "from-text" : undefined,
|
|
585
622
|
},
|
|
@@ -675,7 +712,11 @@ export async function advanceTaskLifecycle(input) {
|
|
|
675
712
|
refreshManagedContractThisCall = false;
|
|
676
713
|
regenerateDagThisCall = true;
|
|
677
714
|
}
|
|
678
|
-
record =
|
|
715
|
+
record = {
|
|
716
|
+
...appendTransition(record, "contract-managed"),
|
|
717
|
+
pendingPrdRefresh: false,
|
|
718
|
+
pendingPrdRefreshPaths: [],
|
|
719
|
+
};
|
|
679
720
|
completed.push("contract-managed");
|
|
680
721
|
await saveLifecycleRecord(input.repoRoot, record);
|
|
681
722
|
snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
|
|
@@ -706,7 +747,12 @@ export async function advanceTaskLifecycle(input) {
|
|
|
706
747
|
const softIntakeAfter = Boolean(prepareResult.error?.details?.softIntake) ||
|
|
707
748
|
(gapCodesAfter.length > 0 &&
|
|
708
749
|
gapCodesAfter.every((code) => isIntakeSoftGapCode(code)));
|
|
709
|
-
|
|
750
|
+
// Soft-stop when PRD is on disk (this call or prior import).
|
|
751
|
+
// Console two-phase bootstrap: import first, then advance intake
|
|
752
|
+
// without re-passing --prd — still must soft-stop, not hard-fail.
|
|
753
|
+
const hasImportedPrdOnDisk = completed.includes("prd-imported") ||
|
|
754
|
+
(snapshot.source?.importedPrdCount ?? 0) > 0;
|
|
755
|
+
if (softIntakeAfter && hasImportedPrdOnDisk) {
|
|
710
756
|
warnings.push({
|
|
711
757
|
code: "INTAKE_INCOMPLETE",
|
|
712
758
|
message: prepareResult.error?.message ??
|
|
@@ -750,7 +796,11 @@ export async function advanceTaskLifecycle(input) {
|
|
|
750
796
|
refreshManagedContractThisCall = false;
|
|
751
797
|
regenerateDagThisCall = true;
|
|
752
798
|
}
|
|
753
|
-
record =
|
|
799
|
+
record = {
|
|
800
|
+
...appendTransition(record, "contract-managed"),
|
|
801
|
+
pendingPrdRefresh: false,
|
|
802
|
+
pendingPrdRefreshPaths: [],
|
|
803
|
+
};
|
|
754
804
|
completed.push("contract-managed");
|
|
755
805
|
await saveLifecycleRecord(input.repoRoot, record);
|
|
756
806
|
snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
|
|
@@ -62,17 +62,21 @@ export function planTransitions(input) {
|
|
|
62
62
|
expectedTransitions.push("contract-managed");
|
|
63
63
|
}
|
|
64
64
|
else if (!sourceReady || !contractManaged) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
// Console PRD Intake Bridge (2026-08-11), two-phase:
|
|
66
|
+
// 1) hasPrdInput without engineering boundary → import only (archive
|
|
67
|
+
// references). Do NOT prepare yet so multi-file bootstrap can finish
|
|
68
|
+
// all imports before the one-shot Semantic Intake burns.
|
|
69
|
+
// 2) alreadyImportedPrd without new --prd (and/or with boundary) →
|
|
70
|
+
// prepare-contract so deterministic parse + Semantic Intake run.
|
|
71
|
+
// prepare soft-stops on EMPTY_ALLOWED_PATHS etc. and never opens the
|
|
72
|
+
// writeSet gate without a complete managed contract.
|
|
73
|
+
if (shouldImportPrd && !input.hasEngineeringBoundary) {
|
|
74
|
+
actions.push({ type: "import-prd" });
|
|
75
|
+
expectedTransitions.push("prd-imported");
|
|
72
76
|
actions.push({ type: "stop", reason: "blocker" });
|
|
73
77
|
blockers.push({
|
|
74
78
|
code: "BOUNDARY_REQUIRED",
|
|
75
|
-
message: "PRD imported;
|
|
79
|
+
message: "PRD imported; re-run task advance (optionally with --allowed-path / --verify) to prepare contract / semantic intake",
|
|
76
80
|
});
|
|
77
81
|
return {
|
|
78
82
|
actions,
|
|
@@ -80,7 +84,7 @@ export function planTransitions(input) {
|
|
|
80
84
|
blockers,
|
|
81
85
|
next: {
|
|
82
86
|
kind: "advance",
|
|
83
|
-
command: `loop-agent task advance ${snapshot.taskId} --
|
|
87
|
+
command: `loop-agent task advance ${snapshot.taskId} --json`,
|
|
84
88
|
},
|
|
85
89
|
expectedTransitions,
|
|
86
90
|
};
|
|
@@ -218,18 +222,6 @@ export function planTransitions(input) {
|
|
|
218
222
|
expectedTransitions,
|
|
219
223
|
};
|
|
220
224
|
}
|
|
221
|
-
function stopWith(actions, expectedTransitions, blockers, snapshot, reason) {
|
|
222
|
-
return {
|
|
223
|
-
actions: [...actions, { type: "stop", reason }],
|
|
224
|
-
gate: snapshot.gate ?? null,
|
|
225
|
-
blockers,
|
|
226
|
-
next: {
|
|
227
|
-
kind: "advance",
|
|
228
|
-
command: `loop-agent task advance ${snapshot.taskId} --json`,
|
|
229
|
-
},
|
|
230
|
-
expectedTransitions,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
225
|
function deriveNext(snapshot, gate, approveGate) {
|
|
234
226
|
if (gate && !approveGate) {
|
|
235
227
|
return { kind: "approve-gate", command: gate.approveCommand };
|
|
@@ -8,7 +8,7 @@ import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND
|
|
|
8
8
|
import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
|
|
9
9
|
import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
|
|
10
10
|
import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
11
|
-
import { isWriterEmptyDiffRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
|
|
11
|
+
import { isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
|
|
12
12
|
import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
|
|
13
13
|
import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
|
|
14
14
|
/**
|
|
@@ -831,6 +831,18 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
831
831
|
if (meta.writeGuardAttribution === "best-effort") {
|
|
832
832
|
stderrParts.push("write guard note: concurrent rank writers use best-effort per-node attribution; keep same-rank writeSet entries disjoint");
|
|
833
833
|
}
|
|
834
|
+
// Pure provider stall/timeout with zero workspace side effects is eligible
|
|
835
|
+
// for a single writer transport retry (implement-pi flake resilience).
|
|
836
|
+
// Partial writes, tool calls, or write-guard failures keep raw `timeout`.
|
|
837
|
+
const writerCleanTimeout = !mapped.ok &&
|
|
838
|
+
mapped.failureCategory === "timeout" &&
|
|
839
|
+
writeGuardOk &&
|
|
840
|
+
!completenessFailure &&
|
|
841
|
+
!writerThinkingExhausted &&
|
|
842
|
+
isWriterTransportRetryCandidate(input.task) &&
|
|
843
|
+
changeManifestChangedFiles !== undefined &&
|
|
844
|
+
changeManifestChangedFiles.length === 0 &&
|
|
845
|
+
readWriterThinkingExhaustionEvidence(result).writeToolCallCount === 0;
|
|
834
846
|
return {
|
|
835
847
|
...mapped,
|
|
836
848
|
ok: false,
|
|
@@ -851,7 +863,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
851
863
|
// the continuation is retryable and carries the embedded target paths.
|
|
852
864
|
// Provider/transport failures (quota, auth, network, timeout,
|
|
853
865
|
// rate-limit, unavailable) keep their original category so a provider
|
|
854
|
-
// outage is never masked as a recoverable local write problem
|
|
866
|
+
// outage is never masked as a recoverable local write problem — except
|
|
867
|
+
// clean timeouts with zero attributed writes, remapped below.
|
|
855
868
|
writeGuardOk &&
|
|
856
869
|
completenessFailure?.failureCategory ===
|
|
857
870
|
INCOMPLETE_WRITE_SET_RETRY_CATEGORY &&
|
|
@@ -867,7 +880,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
867
880
|
// upgrade to incomplete-write-set above.
|
|
868
881
|
writerThinkingExhausted
|
|
869
882
|
? WRITER_THINKING_EXHAUSTED_CATEGORY
|
|
870
|
-
:
|
|
883
|
+
: writerCleanTimeout
|
|
884
|
+
? WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY
|
|
885
|
+
: mapped.failureCategory,
|
|
871
886
|
durationMs: mapped.durationMs || Date.now() - started,
|
|
872
887
|
};
|
|
873
888
|
}
|
|
@@ -586,13 +586,13 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
586
586
|
},
|
|
587
587
|
{
|
|
588
588
|
action: "importPrd",
|
|
589
|
-
cli: "loop-agent task advance <id> --prd <server-staged-path> --json",
|
|
589
|
+
cli: "loop-agent task advance <id> --prd <server-staged-path> --json (+ intake bridge)",
|
|
590
590
|
kind: "mutation",
|
|
591
591
|
inputSchemaVersion: 1,
|
|
592
592
|
resultSchemaVersion: 1,
|
|
593
593
|
envelopeSchemaVersion: 1,
|
|
594
594
|
requiredErrorCodes: [...COMMON_MUTATION_ERRORS, "PATH_OUTSIDE_REPO"],
|
|
595
|
-
description: "Archive
|
|
595
|
+
description: "Archive PRD via task advance, run intake bridge (deterministic/semantic), rewrite Console draft.",
|
|
596
596
|
inputParams: [
|
|
597
597
|
{
|
|
598
598
|
name: "taskId",
|
|
@@ -606,6 +606,30 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
606
606
|
required: true,
|
|
607
607
|
description: "PRD markdown content",
|
|
608
608
|
},
|
|
609
|
+
{
|
|
610
|
+
name: "taskKind",
|
|
611
|
+
type: "string",
|
|
612
|
+
required: false,
|
|
613
|
+
description: "workflow kind (human-selected)",
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
name: "allowedPaths",
|
|
617
|
+
type: "array",
|
|
618
|
+
required: false,
|
|
619
|
+
description: "optional engineering allowedPaths (human-confirmed)",
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
name: "forbiddenPaths",
|
|
623
|
+
type: "array",
|
|
624
|
+
required: false,
|
|
625
|
+
description: "optional engineering forbiddenPaths",
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
name: "verifyCommands",
|
|
629
|
+
type: "array",
|
|
630
|
+
required: false,
|
|
631
|
+
description: "optional verify commands (label:command or shell)",
|
|
632
|
+
},
|
|
609
633
|
],
|
|
610
634
|
modelCallable: "always",
|
|
611
635
|
humanConfirmation: "none",
|
|
@@ -331,7 +331,19 @@ export async function prepareTaskSource(input) {
|
|
|
331
331
|
constraintsPath: contractPaths.constraintsPath,
|
|
332
332
|
});
|
|
333
333
|
if (projection.ok) {
|
|
334
|
-
baseDraft =
|
|
334
|
+
baseDraft = input.intent.replaceManagedRequirement
|
|
335
|
+
? {
|
|
336
|
+
...projection.draft,
|
|
337
|
+
requirement: {
|
|
338
|
+
objective: "",
|
|
339
|
+
scope: [],
|
|
340
|
+
nonGoals: [],
|
|
341
|
+
acceptanceCriteria: [],
|
|
342
|
+
},
|
|
343
|
+
openQuestions: [],
|
|
344
|
+
assumptions: [],
|
|
345
|
+
}
|
|
346
|
+
: projection.draft;
|
|
335
347
|
}
|
|
336
348
|
else {
|
|
337
349
|
managedProjectionError = projection.message;
|
|
@@ -396,7 +408,30 @@ export async function prepareTaskSource(input) {
|
|
|
396
408
|
taskId,
|
|
397
409
|
requireParseableRequirement: false,
|
|
398
410
|
});
|
|
399
|
-
|
|
411
|
+
const requestedPaths = new Set((input.intent.importedPrdPaths ?? [])
|
|
412
|
+
.map((value) => value.replace(/\\/g, "/").trim())
|
|
413
|
+
.filter(Boolean));
|
|
414
|
+
if (requestedPaths.size > 0) {
|
|
415
|
+
const selectedDocuments = sourceIntegrity.documents.filter((document) => requestedPaths.has(document.materializedPath.replace(/\\/g, "/")));
|
|
416
|
+
const selectedPaths = new Set(selectedDocuments.map((document) => document.materializedPath.replace(/\\/g, "/")));
|
|
417
|
+
for (const requestedPath of requestedPaths) {
|
|
418
|
+
if (selectedPaths.has(requestedPath))
|
|
419
|
+
continue;
|
|
420
|
+
sourceIntegrity.issues.push({
|
|
421
|
+
code: "REFERENCE_NOT_IN_MANIFEST",
|
|
422
|
+
level: "blocking",
|
|
423
|
+
message: `current import batch reference not found in source-manifest: ${requestedPath}`,
|
|
424
|
+
path: requestedPath,
|
|
425
|
+
});
|
|
426
|
+
sourceIntegrity.ok = false;
|
|
427
|
+
}
|
|
428
|
+
sourceIntegrity = {
|
|
429
|
+
...sourceIntegrity,
|
|
430
|
+
documents: selectedDocuments,
|
|
431
|
+
parseableDocuments: selectedDocuments.filter((document) => document.parseable),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
hasImportedPrd = sourceIntegrity.documents.length > 0;
|
|
400
435
|
if (sourceIntegrity.ok) {
|
|
401
436
|
references = draftReferencesFromManifest(sourceIntegrity.documents);
|
|
402
437
|
let acStart = 1;
|
|
@@ -517,6 +552,7 @@ export async function prepareTaskSource(input) {
|
|
|
517
552
|
"PRODUCT_CLARIFICATION_NOT_EXECUTABLE",
|
|
518
553
|
"PRODUCT_REQUIREMENT_PENDING",
|
|
519
554
|
].includes(gap.code));
|
|
555
|
+
const hasEmptyAcceptance = gaps.some((gap) => gap.code === "EMPTY_ACCEPTANCE");
|
|
520
556
|
for (const message of validation.errors) {
|
|
521
557
|
const isAcceptanceSchema = /acceptance criterion|acceptanceCriteria/i.test(message);
|
|
522
558
|
// Prefer product-artifact diagnostics over generic schema AC errors.
|
|
@@ -528,6 +564,17 @@ export async function prepareTaskSource(input) {
|
|
|
528
564
|
});
|
|
529
565
|
continue;
|
|
530
566
|
}
|
|
567
|
+
// Soft-intake path: completeness already has EMPTY_ACCEPTANCE (soft).
|
|
568
|
+
// Do not add a hard DRAFT_VALIDATION twin that flips softIntake=false
|
|
569
|
+
// and blocks one-shot Semantic Intake for free-form PRDs.
|
|
570
|
+
if (hasEmptyAcceptance && isAcceptanceSchema) {
|
|
571
|
+
gaps.push({
|
|
572
|
+
code: "DRAFT_VALIDATION",
|
|
573
|
+
level: "warning",
|
|
574
|
+
message: `${message} (soft intake; EMPTY_ACCEPTANCE primary)`,
|
|
575
|
+
});
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
531
578
|
gaps.push({
|
|
532
579
|
code: "DRAFT_VALIDATION",
|
|
533
580
|
level: "blocking",
|
|
@@ -82,7 +82,8 @@ export function buildQuestionForGap(gapId, draft, context = {}) {
|
|
|
82
82
|
id: `q-${gapId}`,
|
|
83
83
|
gapId,
|
|
84
84
|
text: "本轮允许改动的范围(scope)是哪些路径/模块?用逗号或换行列出。",
|
|
85
|
-
|
|
85
|
+
// No product-template default — must come from PRD intake or human override.
|
|
86
|
+
recommendation: "(请按 PRD / 仓库结构填写;勿套用 Console 模板路径)",
|
|
86
87
|
affectsFields: ["requirement.scope"],
|
|
87
88
|
risk: "high",
|
|
88
89
|
},
|
|
@@ -90,7 +91,7 @@ export function buildQuestionForGap(gapId, draft, context = {}) {
|
|
|
90
91
|
id: `q-${gapId}`,
|
|
91
92
|
gapId,
|
|
92
93
|
text: "明确非目标(non-goals)是什么?本轮刻意不做哪些事?",
|
|
93
|
-
recommendation: "
|
|
94
|
+
recommendation: "(本轮明确不做的事项;可写「暂无」)",
|
|
94
95
|
affectsFields: ["requirement.nonGoals"],
|
|
95
96
|
risk: "medium",
|
|
96
97
|
},
|
|
@@ -98,7 +99,7 @@ export function buildQuestionForGap(gapId, draft, context = {}) {
|
|
|
98
99
|
id: `q-${gapId}`,
|
|
99
100
|
gapId,
|
|
100
101
|
text: "验收标准有哪些?可用分号或换行列出(将编号为 AC-1…)。",
|
|
101
|
-
recommendation: "
|
|
102
|
+
recommendation: "(请写可观测验收标准;勿套用产品模板 AC)",
|
|
102
103
|
affectsFields: ["requirement.acceptanceCriteria"],
|
|
103
104
|
risk: "high",
|
|
104
105
|
},
|
|
@@ -114,7 +115,7 @@ export function buildQuestionForGap(gapId, draft, context = {}) {
|
|
|
114
115
|
id: `q-${gapId}`,
|
|
115
116
|
gapId,
|
|
116
117
|
text: "结构化 allowedPaths(写入白名单)?逗号/换行分隔 glob。",
|
|
117
|
-
recommendation: "src
|
|
118
|
+
recommendation: "(工程边界须人类确认;示例:src/**, test/**)",
|
|
118
119
|
affectsFields: ["constraints.allowedPaths"],
|
|
119
120
|
risk: "high",
|
|
120
121
|
},
|
|
@@ -122,7 +123,7 @@ export function buildQuestionForGap(gapId, draft, context = {}) {
|
|
|
122
123
|
id: `q-${gapId}`,
|
|
123
124
|
gapId,
|
|
124
125
|
text: "forbiddenPaths(禁止写入)?逗号/换行分隔。",
|
|
125
|
-
recommendation: "
|
|
126
|
+
recommendation: ".harness/**, node_modules/**, package.json",
|
|
126
127
|
affectsFields: ["constraints.forbiddenPaths"],
|
|
127
128
|
risk: "high",
|
|
128
129
|
},
|
|
@@ -130,7 +131,7 @@ export function buildQuestionForGap(gapId, draft, context = {}) {
|
|
|
130
131
|
id: `q-${gapId}`,
|
|
131
132
|
gapId,
|
|
132
133
|
text: "验证命令(verifyCommands)?可给一条或多条 shell(换行分隔)。",
|
|
133
|
-
recommendation: "
|
|
134
|
+
recommendation: "npm run typecheck",
|
|
134
135
|
affectsFields: ["verification.commands"],
|
|
135
136
|
risk: "medium",
|
|
136
137
|
},
|