@tea-agent/loop-agent 0.11.0 → 0.13.0-alpha.0
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 +74 -1
- package/README.md +33 -4
- package/dist/application/dag/generate-task-dag.js +45 -0
- package/dist/application/dag/run-dag.js +10 -0
- package/dist/application/dag/validate-dag.js +11 -0
- package/dist/cli/command-definitions.js +10 -3
- package/dist/commands/init.js +74 -7
- package/dist/commands/knowledge.js +129 -31
- package/dist/governance/manifest-types.js +3 -0
- package/dist/shared/package-metadata.js +135 -0
- package/dist/task/config-types.js +6 -1
- package/dist/worker/cli.js +99 -2
- package/dist/worker/delivery/package.js +3 -3
- package/dist/worker/feature/decision-loader.js +37 -6
- package/dist/worker/feature/next-action.js +10 -2
- package/dist/worker/feature/ready-plan-projection.js +81 -0
- package/dist/worker/feature/reducer.js +2 -1
- package/dist/worker/feature/review.js +19 -2
- package/dist/worker/feature/run.js +27 -2
- package/dist/worker/follow-up/approve.js +5 -2
- package/dist/worker/follow-up/factory.js +1 -1
- package/dist/worker/observability/event-history.js +216 -0
- package/dist/worker/observability/read-model.js +552 -118
- package/dist/worker/observe/paths.js +17 -0
- package/dist/worker/observe/routes.js +310 -23
- package/dist/worker/observe/server.js +59 -1
- package/dist/worker/observe/spec-evidence.js +281 -0
- package/dist/worker/observe/static/api.js +46 -0
- package/dist/worker/observe/static/app.js +120 -2598
- package/dist/worker/observe/static/constants.js +148 -0
- package/dist/worker/observe/static/copy.js +67 -0
- package/dist/worker/observe/static/dag-helpers.js +172 -0
- package/dist/worker/observe/static/dag-model.js +72 -0
- package/dist/worker/observe/static/dom.js +61 -0
- package/dist/worker/observe/static/format-pool.js +67 -0
- package/dist/worker/observe/static/format.js +292 -0
- package/dist/worker/observe/static/index.html +300 -82
- package/dist/worker/observe/static/kpi.js +94 -0
- package/dist/worker/observe/static/relations.js +133 -0
- package/dist/worker/observe/static/router.js +93 -0
- package/dist/worker/observe/static/run-processing.js +148 -0
- package/dist/worker/observe/static/shell-chrome.js +68 -0
- package/dist/worker/observe/static/state.js +253 -0
- package/dist/worker/observe/static/styles.css +1731 -495
- package/dist/worker/observe/static/views/batch.js +227 -0
- package/dist/worker/observe/static/views/dag-graph.js +172 -0
- package/dist/worker/observe/static/views/dag-inspector.js +596 -0
- package/dist/worker/observe/static/views/dag.js +362 -0
- package/dist/worker/observe/static/views/dashboard.js +445 -0
- package/dist/worker/observe/static/views/failures.js +143 -0
- package/dist/worker/observe/static/views/feature.js +492 -0
- package/dist/worker/observe/static/views/pool.js +350 -0
- package/dist/worker/observe/static/views/run.js +453 -0
- package/dist/worker/observe/static/views/session-timeline.js +205 -0
- package/dist/worker/observe/static/views/shell.js +7 -0
- package/dist/worker/observe/static/views/task.js +314 -0
- package/dist/worker/observe/static/views/timeline.js +163 -0
- package/dist/worker/pool/doctor.js +165 -0
- package/dist/worker/pool/migrate-state.js +303 -0
- package/dist/worker/pool/run-store.js +205 -17
- package/dist/worker/pool/types.js +17 -1
- package/dist/worker/pool/validation.js +100 -15
- package/dist/worker/report/morning-report.js +12 -2
- package/dist/worker/runner/run-ready.js +41 -26
- package/dist/worker/task-graph/ready-planner.js +136 -0
- package/dist/workflows/dag/controller-identity.js +104 -0
- package/dist/workflows/dag/convergence/controller.js +16 -8
- package/dist/workflows/dag/failure-routing.js +12 -1
- package/dist/workflows/dag/init-hybrid.js +1233 -11
- package/dist/workflows/dag/node-execution.js +123 -29
- package/dist/workflows/dag/repair-artifact.js +91 -0
- package/dist/workflows/dag/report.js +50 -0
- package/dist/workflows/dag/retry-policy.js +138 -0
- package/dist/workflows/dag/runner.js +32 -0
- package/dist/workflows/dag/runtime-contract.js +87 -0
- package/dist/workflows/dag/skill-snapshot.js +2 -0
- package/dist/workflows/dag/types.js +45 -1
- package/dist/workflows/dag/validate.js +68 -4
- package/docs/README.md +1 -1
- package/docs/agent-dag-recovery-playbook.md +9 -0
- package/docs/agent-dag-runner.md +26 -1
- package/docs/architecture/dag-execution.md +6 -0
- package/docs/architecture/evolution.md +7 -5
- package/docs/architecture/facts-and-state.md +15 -2
- package/docs/architecture/worker-and-feature.md +6 -2
- package/docs/decisions/README.md +3 -0
- package/docs/design/README.md +12 -3
- package/docs/exec-plans/active/README.md +2 -2
- package/docs/exec-plans/completed/README.md +12 -0
- package/docs/feature-workflow.md +108 -2
- package/docs/loop-agent-harness.md +15 -4
- package/docs/progress/README.md +22 -0
- package/docs/reports/README.md +14 -2
- package/docs/templates/agent-dag-report.schema.json +17 -0
- package/docs/templates/agent-dag.schema.json +69 -1
- package/docs/templates/agent-dag.supervised-implementation.json +8 -2
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
- package/docs/templates/backend-test-dag.json +288 -0
- package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
- package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
- package/docs/templates/knowledge-sync-dag.json +177 -0
- package/docs/templates/knowledge-sync-draft.schema.json +71 -0
- package/docs/verification-matrix.md +2 -1
- package/package.json +8 -2
- package/scripts/kb-bootstrap-init-skeleton.sh +239 -0
- package/scripts/kb-graph-incremental-prepare.mjs +372 -0
- package/scripts/kb-graph-incremental-prepare.sh +5 -0
- package/scripts/kb-graph-materialize.mjs +105 -0
- package/scripts/kb-graph-materialize.sh +4 -0
- package/scripts/kb-graph-promote.mjs +153 -0
- package/scripts/kb-graph-promote.sh +4 -0
- package/scripts/kb-query.mjs +554 -0
- package/scripts/kb-query.sh +5 -0
- package/skills/agent-worker/SKILL.md +3 -1
- package/skills/agent-worker/references/agent-worker-operator.md +18 -1
- package/skills/frontend-design-review/SKILL.md +26 -24
- package/skills/frontend-implementation/SKILL.md +29 -26
- package/skills/frontend-implementation/references/node-contracts.md +50 -19
- package/skills/frontend-review/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +2 -0
- package/skills/loop-agent/references/hybrid-dag.md +22 -3
- package/skills/loop-agent/references/verification-and-failure-handling.md +6 -0
|
@@ -2,9 +2,10 @@ import { access, readdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { assertValidDagSpec } from "./validate.js";
|
|
5
|
-
import { DEFAULT_DAG_OUTPUT_LANGUAGE, DEFAULT_DAG_EXECUTOR_MODELS, parseDagSpec, } from "./types.js";
|
|
5
|
+
import { DAG_AGENT_RUNTIME_PI_ONLY, DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1, DAG_RUNTIME_CONTRACT_SCHEMA_VERSION, DEFAULT_DAG_OUTPUT_LANGUAGE, DEFAULT_DAG_EXECUTOR_MODELS, parseDagSpec, } from "./types.js";
|
|
6
6
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
7
7
|
import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
|
|
8
|
+
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate } from "./retry-policy.js";
|
|
8
9
|
import { resolveAdapter } from "../../adapters/index.js";
|
|
9
10
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
10
11
|
import { buildAuthoritySurfaceAuditNode, buildAuthoritySurfaceGateNode, resolveAuthoritySurfaceAudit, } from "./authority-surface.js";
|
|
@@ -77,6 +78,18 @@ const FRONTEND_DESIGN_REVIEW_SKILLS = ["frontend-design-review"];
|
|
|
77
78
|
const FRONTEND_REVIEW_SKILLS = ["frontend-review"];
|
|
78
79
|
const FRONTEND_VERIFICATION_SKILLS = ["frontend-verification"];
|
|
79
80
|
const IMPLEMENT_WRITESET_PLACEHOLDER = "REPLACE/WITH/NARROW/IMPLEMENT/PATHS/**";
|
|
81
|
+
/**
|
|
82
|
+
* Runtime contract stamped on every newly generated DAG. Capability fields are
|
|
83
|
+
* authoritative and are the primary compatibility gate. `minimumControllerVersion`
|
|
84
|
+
* is intentionally omitted here: it is an optional diagnostic hint, and hardcoding
|
|
85
|
+
* a future version would make a controller reject the very DAGs it generates.
|
|
86
|
+
* Operators can still pin a minimum version on hand-authored specs when needed.
|
|
87
|
+
*/
|
|
88
|
+
export const GENERATED_DAG_RUNTIME_CONTRACT = {
|
|
89
|
+
schemaVersion: DAG_RUNTIME_CONTRACT_SCHEMA_VERSION,
|
|
90
|
+
agentRuntime: DAG_AGENT_RUNTIME_PI_ONLY,
|
|
91
|
+
repairWriterProtocol: DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1,
|
|
92
|
+
};
|
|
80
93
|
function resolveEnabledExecutors(manifestExecutors) {
|
|
81
94
|
return Object.entries(manifestExecutors)
|
|
82
95
|
.filter(([, config]) => config.enabled !== false)
|
|
@@ -605,8 +618,9 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
605
618
|
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
606
619
|
];
|
|
607
620
|
const spec = {
|
|
608
|
-
version:
|
|
621
|
+
version: 3,
|
|
609
622
|
title: `Hybrid DAG: ${taskConfig.title}`,
|
|
623
|
+
runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
|
|
610
624
|
outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
|
|
611
625
|
objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
|
|
612
626
|
successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
|
|
@@ -739,6 +753,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
739
753
|
],
|
|
740
754
|
};
|
|
741
755
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
756
|
+
applyDefaultReadOnlyRetryPolicy(spec);
|
|
742
757
|
parseDagSpec(spec);
|
|
743
758
|
assertValidDagSpec(spec);
|
|
744
759
|
return spec;
|
|
@@ -758,7 +773,10 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
758
773
|
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
759
774
|
: []),
|
|
760
775
|
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
761
|
-
"
|
|
776
|
+
"Frontend implementation DAGs must pass the final design verdict gate before any write node executes; the first design gate also accepts request-revision for plan revision only.",
|
|
777
|
+
"Final design gate pass is the only authorization for frontend implementation writes.",
|
|
778
|
+
"Plan revision remains read-only and never edits business code.",
|
|
779
|
+
"Design revision failures route to replan-and-rerun, never dev-fix.",
|
|
762
780
|
"frontend-implementation DAGs must complete deterministic static verification and behavior verification before final review.",
|
|
763
781
|
"frontend review must block closeout unless review verdict is exactly VERDICT: pass.",
|
|
764
782
|
];
|
|
@@ -778,8 +796,9 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
778
796
|
adapterCommands: sources.verifyCommands?.final,
|
|
779
797
|
});
|
|
780
798
|
const spec = {
|
|
781
|
-
version:
|
|
799
|
+
version: 3,
|
|
782
800
|
title: `Frontend implementation DAG: ${taskConfig.title}`,
|
|
801
|
+
runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
|
|
783
802
|
objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
|
|
784
803
|
successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
|
|
785
804
|
globalConstraints,
|
|
@@ -865,7 +884,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
865
884
|
].join("\n\n"),
|
|
866
885
|
},
|
|
867
886
|
{
|
|
868
|
-
id: "frontend-design-gate-shell",
|
|
887
|
+
id: "frontend-first-design-gate-shell",
|
|
869
888
|
depends_on: ["frontend-design-gate-pi"],
|
|
870
889
|
role: "verifier",
|
|
871
890
|
executor: "shell",
|
|
@@ -873,14 +892,81 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
873
892
|
writePolicy: "read-only",
|
|
874
893
|
allowedPaths: readOnlyPaths,
|
|
875
894
|
forbiddenPaths,
|
|
876
|
-
outputContract: "Deterministic
|
|
877
|
-
subtask_prompt: "Deterministic gate:
|
|
895
|
+
outputContract: "Deterministic first design verdict gate: exit 0 when frontend-design-gate-pi emits VERDICT: pass or VERDICT: request-revision. Does not authorize code writes.",
|
|
896
|
+
subtask_prompt: "Deterministic gate: validate frontend-design-gate-pi first-line VERDICT is pass or request-revision; block downstream only on malformed/unexpected verdict.",
|
|
878
897
|
shell: {
|
|
879
898
|
commands: [],
|
|
880
899
|
verdictGate: {
|
|
881
900
|
fromNodeId: "frontend-design-gate-pi",
|
|
901
|
+
accept: ["VERDICT: pass", "VERDICT: request-revision"],
|
|
902
|
+
label: "frontend first design gate",
|
|
903
|
+
lineMode: "first-verdict-line",
|
|
904
|
+
},
|
|
905
|
+
cwd: ".",
|
|
906
|
+
timeoutMs: 60000,
|
|
907
|
+
},
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
id: "frontend-plan-revision-pi",
|
|
911
|
+
depends_on: ["frontend-first-design-gate-shell", "frontend-plan-pi", "frontend-design-gate-pi"],
|
|
912
|
+
role: "planner",
|
|
913
|
+
executor: "pi",
|
|
914
|
+
complexity: "MED",
|
|
915
|
+
writePolicy: "read-only",
|
|
916
|
+
allowedPaths: readOnlyPaths,
|
|
917
|
+
forbiddenPaths,
|
|
918
|
+
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
919
|
+
outputContract: "Markdown revision plan (pass case: first line PASS_NO_REVISION_NEEDED with confirmation of original plan; request-revision case: complete revised implementation plan with corrections from design findings). No file writes.",
|
|
920
|
+
subtask_prompt: [
|
|
921
|
+
"Consume frontend-plan-pi (original plan) and frontend-design-gate-pi (first design review findings).",
|
|
922
|
+
"If the first design gate passed (VERDICT: pass from frontend-design-gate-pi), output exactly:",
|
|
923
|
+
"PASS_NO_REVISION_NEEDED",
|
|
924
|
+
"The original plan from frontend-plan-pi is confirmed and does not require changes.",
|
|
925
|
+
"",
|
|
926
|
+
"If the first design gate requested revision (VERDICT: request-revision), produce a complete revised implementation plan that addresses every Required Plan Correction from the design findings.",
|
|
927
|
+
"The revised plan must include Implementation Steps, Target Files, UI State Handling, Styling / Component Strategy, Interaction Notes, Dependency Policy, Verification Plan, and Residual Risks.",
|
|
928
|
+
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
929
|
+
sourceContext,
|
|
930
|
+
].join("\n\n"),
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
id: "frontend-final-design-review-pi",
|
|
934
|
+
depends_on: ["frontend-plan-revision-pi"],
|
|
935
|
+
role: "reviewer",
|
|
936
|
+
executor: "pi",
|
|
937
|
+
complexity: "MED",
|
|
938
|
+
writePolicy: "read-only",
|
|
939
|
+
allowedPaths: readOnlyPaths,
|
|
940
|
+
forbiddenPaths,
|
|
941
|
+
skills: FRONTEND_DESIGN_REVIEW_SKILLS,
|
|
942
|
+
outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings and Checked Items. No file writes.",
|
|
943
|
+
subtask_prompt: [
|
|
944
|
+
"Audit the revised (or confirmed) frontend plan from frontend-plan-revision-pi before implementation.",
|
|
945
|
+
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
946
|
+
"If frontend-plan-revision-pi returned PASS_NO_REVISION_NEEDED, confirm the original plan against all design constraints and task requirements. Re-verify that all applicable UI states are covered, dependencies are authorized, and deterministic verification commands are present.",
|
|
947
|
+
"If frontend-plan-revision-pi revised the plan, verify that every Required Plan Correction from the first design review has been fully addressed.",
|
|
948
|
+
"Request revision if any design gap remains, if corrections are incomplete, or if the revised plan introduces new unaddressed issues.",
|
|
949
|
+
"Read-only: do not modify repository files.",
|
|
950
|
+
sourceContext,
|
|
951
|
+
].join("\n\n"),
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
id: "frontend-final-design-gate-shell",
|
|
955
|
+
depends_on: ["frontend-final-design-review-pi"],
|
|
956
|
+
role: "verifier",
|
|
957
|
+
executor: "shell",
|
|
958
|
+
complexity: "LOW",
|
|
959
|
+
writePolicy: "read-only",
|
|
960
|
+
allowedPaths: readOnlyPaths,
|
|
961
|
+
forbiddenPaths,
|
|
962
|
+
outputContract: "Deterministic final design verdict gate: exit 0 only when frontend-final-design-review-pi emits VERDICT: pass. This is the sole authorization for frontend implementation writes.",
|
|
963
|
+
subtask_prompt: "Deterministic gate: block frontend implementation unless frontend-final-design-review-pi emitted VERDICT: pass.",
|
|
964
|
+
shell: {
|
|
965
|
+
commands: [],
|
|
966
|
+
verdictGate: {
|
|
967
|
+
fromNodeId: "frontend-final-design-review-pi",
|
|
882
968
|
accept: ["VERDICT: pass"],
|
|
883
|
-
label: "frontend design gate",
|
|
969
|
+
label: "frontend final design gate",
|
|
884
970
|
lineMode: "first-verdict-line",
|
|
885
971
|
},
|
|
886
972
|
cwd: ".",
|
|
@@ -889,7 +975,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
889
975
|
},
|
|
890
976
|
{
|
|
891
977
|
id: implementId,
|
|
892
|
-
depends_on: ["frontend-design-gate-shell"],
|
|
978
|
+
depends_on: ["frontend-final-design-gate-shell", "frontend-plan-revision-pi", "frontend-final-design-review-pi"],
|
|
893
979
|
role: "implementer",
|
|
894
980
|
executor: "pi",
|
|
895
981
|
toolProfile: "write",
|
|
@@ -903,8 +989,9 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
903
989
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
904
990
|
outputContract: "Markdown summary with Changed Files, Implemented Behavior, UI States Covered, Styling / Component Notes, Verification Attempted, and Residual Risks.",
|
|
905
991
|
subtask_prompt: [
|
|
906
|
-
"Implement the approved frontend plan with minimal focused changes.",
|
|
907
|
-
"
|
|
992
|
+
"Implement the final approved frontend plan (from frontend-plan-revision-pi) with minimal focused changes.",
|
|
993
|
+
"The frontend-final-design-review-pi verdict confirmed the plan is ready. Stay within writeSet and preserve unrelated files.",
|
|
994
|
+
"Do not write root artifacts/** unless explicitly included in writeSet.",
|
|
908
995
|
sourceContext,
|
|
909
996
|
].join("\n\n"),
|
|
910
997
|
},
|
|
@@ -1028,6 +1115,1111 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1028
1115
|
},
|
|
1029
1116
|
],
|
|
1030
1117
|
};
|
|
1118
|
+
applyDefaultReadOnlyRetryPolicy(spec);
|
|
1119
|
+
parseDagSpec(spec);
|
|
1120
|
+
assertValidDagSpec(spec);
|
|
1121
|
+
return spec;
|
|
1122
|
+
}
|
|
1123
|
+
// ---------------------------------------------------------------------------
|
|
1124
|
+
// Backend test DAG template
|
|
1125
|
+
// ---------------------------------------------------------------------------
|
|
1126
|
+
function buildAnalyzeInputsNode(sources) {
|
|
1127
|
+
return {
|
|
1128
|
+
id: "analyze-inputs-pi",
|
|
1129
|
+
depends_on: [],
|
|
1130
|
+
role: "planner",
|
|
1131
|
+
executor: "pi",
|
|
1132
|
+
complexity: "MED",
|
|
1133
|
+
writePolicy: "read-only",
|
|
1134
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
1135
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1136
|
+
outputContract: "Structured Markdown extracting core content from source documents. No file writes.",
|
|
1137
|
+
subtask_prompt: [
|
|
1138
|
+
"Read the task source materials and extract the following structured content for downstream test generation.",
|
|
1139
|
+
"",
|
|
1140
|
+
"## Required Output Sections:",
|
|
1141
|
+
"",
|
|
1142
|
+
"### 1. API Endpoints",
|
|
1143
|
+
"List all API endpoints: Method, Path, Description, Request params, Response format.",
|
|
1144
|
+
"",
|
|
1145
|
+
"### 2. Data Model",
|
|
1146
|
+
"For each table/collection: fields, types, constraints, descriptions.",
|
|
1147
|
+
"",
|
|
1148
|
+
"### 3. Business Logic",
|
|
1149
|
+
"Core business rules, validation rules, calculation formulas.",
|
|
1150
|
+
"",
|
|
1151
|
+
"### 4. State Transitions",
|
|
1152
|
+
"State machines (e.g. order status: pending → paid → shipped → completed).",
|
|
1153
|
+
"",
|
|
1154
|
+
"### 5. Error Scenarios & Error Codes",
|
|
1155
|
+
"All error codes, error messages, and when they occur.",
|
|
1156
|
+
"",
|
|
1157
|
+
"### 6. External Dependencies",
|
|
1158
|
+
"Third-party services, databases, message queues. Include timeout settings if documented.",
|
|
1159
|
+
"",
|
|
1160
|
+
"### 7. Acceptance Criteria",
|
|
1161
|
+
"Extract ALL acceptance criteria from 需求.md. Number them AC-001, AC-002, etc. If not explicitly listed, derive from functional requirements.",
|
|
1162
|
+
"",
|
|
1163
|
+
"### 8. Risk Areas",
|
|
1164
|
+
"High-risk areas requiring extra test coverage.",
|
|
1165
|
+
"",
|
|
1166
|
+
"## Conditional Sections (include ONLY if mentioned in requirements):",
|
|
1167
|
+
"- Authentication & Authorization: include ONLY if requirements mention auth mechanism (JWT, OAuth2, API Key, etc.)",
|
|
1168
|
+
"- Timeout Handling: include ONLY if requirements mention timeout configuration or degradation strategy",
|
|
1169
|
+
"- Concurrency & Idempotency: include ONLY if requirements mention concurrency, idempotency rules, or locking mechanisms",
|
|
1170
|
+
"- State Transitions: include ONLY if requirements mention business state machines",
|
|
1171
|
+
"- If not mentioned in requirements, do NOT include these sections",
|
|
1172
|
+
"",
|
|
1173
|
+
"This output will be used directly by downstream nodes. Be thorough and structured.",
|
|
1174
|
+
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
1175
|
+
buildSourceContextBlock(sources),
|
|
1176
|
+
].join("\n\n"),
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
function buildGenerateBackendFunctionalCasesNode(sources) {
|
|
1180
|
+
return {
|
|
1181
|
+
id: "generate-backend-functional-cases-pi",
|
|
1182
|
+
depends_on: ["analyze-inputs-pi"],
|
|
1183
|
+
role: "implementer",
|
|
1184
|
+
executor: "pi",
|
|
1185
|
+
toolProfile: "write",
|
|
1186
|
+
complexity: "MED",
|
|
1187
|
+
writePolicy: "exclusive",
|
|
1188
|
+
writeSet: ["testcase/md/**"],
|
|
1189
|
+
allowedPaths: ["testcase/md/**"],
|
|
1190
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1191
|
+
// 注意:Pi 节点超时由 executor 层控制(默认 30 分钟)
|
|
1192
|
+
// 如需调整,在 harness.json 的 executors.pi 中配置 modelConfig.timeoutMs
|
|
1193
|
+
subtask_prompt: [
|
|
1194
|
+
"Based on the upstream analyze-inputs-pi output, generate structured backend functional test cases.",
|
|
1195
|
+
"",
|
|
1196
|
+
"## Output Steps (do in order):",
|
|
1197
|
+
"1. First, output a brief summary: how many modules, how many cases planned per module",
|
|
1198
|
+
"2. Then write each test case file under testcase/md/",
|
|
1199
|
+
"",
|
|
1200
|
+
"## Format Rules:",
|
|
1201
|
+
"- Each test case ID: BE-<MODULE>-<NNN> (e.g. BE-ORDER-001)",
|
|
1202
|
+
"- Each file covers one module",
|
|
1203
|
+
"- Case structure: ID, Title, Precondition, Steps, Expected Result",
|
|
1204
|
+
"- Map each case to acceptance criteria (AC-xxx)",
|
|
1205
|
+
"",
|
|
1206
|
+
"## Coverage Requirements:",
|
|
1207
|
+
"- Positive paths: happy path for each acceptance criterion",
|
|
1208
|
+
"- Negative paths: error scenarios (invalid input, not found, state violations)",
|
|
1209
|
+
"- Boundary conditions: empty input, max length, edge values",
|
|
1210
|
+
"",
|
|
1211
|
+
"## Conditional Coverage (include ONLY if mentioned in upstream analysis):",
|
|
1212
|
+
"- State transitions: include ONLY if upstream analyze-inputs-pi mentions state machine",
|
|
1213
|
+
"- Authentication scenarios: include ONLY if upstream analyze-inputs-pi mentions auth mechanism",
|
|
1214
|
+
"- Timeout scenarios: include ONLY if upstream analyze-inputs-pi mentions timeout handling",
|
|
1215
|
+
"- Concurrency scenarios: include ONLY if upstream analyze-inputs-pi mentions concurrency/idempotency rules",
|
|
1216
|
+
"- If not mentioned, do NOT generate these test cases",
|
|
1217
|
+
"",
|
|
1218
|
+
"## Constraints:",
|
|
1219
|
+
"- Stay within writeSet: testcase/md/**",
|
|
1220
|
+
"- Do NOT re-read source documents — use the upstream analyze-inputs-pi output only",
|
|
1221
|
+
"- Do not write root artifacts/**",
|
|
1222
|
+
].join("\n\n"),
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function buildReviewBackendCasesNode(sources) {
|
|
1226
|
+
return {
|
|
1227
|
+
id: "review-backend-cases-pi",
|
|
1228
|
+
depends_on: ["generate-backend-functional-cases-pi"],
|
|
1229
|
+
role: "reviewer",
|
|
1230
|
+
executor: "pi",
|
|
1231
|
+
complexity: "HIGH",
|
|
1232
|
+
writePolicy: "read-only",
|
|
1233
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
1234
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1235
|
+
outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; followed by Findings and Coverage Assessment. No file writes.",
|
|
1236
|
+
subtask_prompt: [
|
|
1237
|
+
"Review the generated backend functional test cases under testcase/md/.",
|
|
1238
|
+
"",
|
|
1239
|
+
"## Mandatory First Line:",
|
|
1240
|
+
"First non-empty line must be exactly: VERDICT: pass or VERDICT: request-revision",
|
|
1241
|
+
"",
|
|
1242
|
+
"## Review Checklist:",
|
|
1243
|
+
"- ID format: every case uses BE-<MODULE>-<NNN>",
|
|
1244
|
+
"- Positive coverage: each acceptance criterion (AC-xxx) has happy-path case",
|
|
1245
|
+
"- Negative coverage: error scenarios (invalid input, not found, state violations)",
|
|
1246
|
+
"- Boundary coverage: edge cases (empty, max length, edge values)",
|
|
1247
|
+
"- Traceability: each AC maps to at least one case ID",
|
|
1248
|
+
"- Case structure: ID, Title, Precondition, Steps, Expected Result",
|
|
1249
|
+
"- No duplicate IDs across files",
|
|
1250
|
+
"",
|
|
1251
|
+
"## Conditional Coverage (check ONLY if mentioned in upstream analysis):",
|
|
1252
|
+
"- State transition coverage: check ONLY if analyze-inputs-pi mentions state machine",
|
|
1253
|
+
"- Authentication coverage: check ONLY if analyze-inputs-pi mentions auth mechanism",
|
|
1254
|
+
"- Timeout coverage: check ONLY if analyze-inputs-pi mentions timeout handling",
|
|
1255
|
+
"- Concurrency coverage: check ONLY if analyze-inputs-pi mentions concurrency/idempotency rules",
|
|
1256
|
+
"- If not mentioned, do NOT flag as missing",
|
|
1257
|
+
"",
|
|
1258
|
+
"## Verdict Rules:",
|
|
1259
|
+
"- All Critical checks pass + Important findings ≤ 2 → VERDICT: pass",
|
|
1260
|
+
"- Any Critical fails OR Important > 2 → VERDICT: request-revision",
|
|
1261
|
+
"",
|
|
1262
|
+
"## Output After Verdict:",
|
|
1263
|
+
"1. Coverage Assessment table (AC → case IDs)",
|
|
1264
|
+
"2. Findings list (Critical/Important/Informational)",
|
|
1265
|
+
"3. Statistics (total cases, positive/negative/boundary breakdown)",
|
|
1266
|
+
"",
|
|
1267
|
+
"## Constraints:",
|
|
1268
|
+
"- Read-only: do not modify files",
|
|
1269
|
+
"- Do NOT re-read source documents — use upstream analyze-inputs-pi output for acceptance criteria",
|
|
1270
|
+
"- Use testcase/md/ files for case review",
|
|
1271
|
+
].join("\n\n"),
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function buildReviewBackendCasesGateNode(sources) {
|
|
1275
|
+
return {
|
|
1276
|
+
id: "review-backend-cases-gate-shell",
|
|
1277
|
+
depends_on: ["review-backend-cases-pi"],
|
|
1278
|
+
role: "verifier",
|
|
1279
|
+
executor: "shell",
|
|
1280
|
+
complexity: "LOW",
|
|
1281
|
+
writePolicy: "read-only",
|
|
1282
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
1283
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1284
|
+
outputContract: "Deterministic backend case review gate: exit 0 only when review-backend-cases-pi emits VERDICT: pass.",
|
|
1285
|
+
subtask_prompt: "Deterministic gate: block pytest generation unless backend case review emitted VERDICT: pass.",
|
|
1286
|
+
shell: {
|
|
1287
|
+
commands: [],
|
|
1288
|
+
verdictGate: {
|
|
1289
|
+
fromNodeId: "review-backend-cases-pi",
|
|
1290
|
+
accept: ["VERDICT: pass"],
|
|
1291
|
+
label: "backend case review",
|
|
1292
|
+
lineMode: "first-verdict-line",
|
|
1293
|
+
},
|
|
1294
|
+
cwd: ".",
|
|
1295
|
+
timeoutMs: 60000,
|
|
1296
|
+
},
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
function buildGenerateBackendPytestNode(sources) {
|
|
1300
|
+
return {
|
|
1301
|
+
id: "generate-backend-pytest-pi",
|
|
1302
|
+
depends_on: ["review-backend-cases-gate-shell"],
|
|
1303
|
+
role: "implementer",
|
|
1304
|
+
executor: "pi",
|
|
1305
|
+
toolProfile: "write",
|
|
1306
|
+
complexity: "HIGH",
|
|
1307
|
+
writePolicy: "exclusive",
|
|
1308
|
+
writeSet: ["testcase/**/test_*.py"],
|
|
1309
|
+
allowedPaths: ["testcase/**/test_*.py"],
|
|
1310
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1311
|
+
// 注意:Pi 节点超时由 executor 层控制(默认 30 分钟)
|
|
1312
|
+
// 如需调整,在 harness.json 的 executors.pi 中配置 modelConfig.timeoutMs
|
|
1313
|
+
subtask_prompt: [
|
|
1314
|
+
"Convert the reviewed test cases under testcase/md/ into pytest automation code.",
|
|
1315
|
+
"",
|
|
1316
|
+
"## Output Steps (do in order):",
|
|
1317
|
+
"1. First, output a brief summary: how many files, how many test functions planned",
|
|
1318
|
+
"2. Then write each test file under testcase/",
|
|
1319
|
+
"",
|
|
1320
|
+
"## Format Rules:",
|
|
1321
|
+
"- File prefix: test_<module>.py",
|
|
1322
|
+
"- Function name: test_BE_<MODULE>_<NNN>_<description>",
|
|
1323
|
+
"- Docstring first line: BE-<MODULE>-<NNN>: <Case Title>",
|
|
1324
|
+
"- 1:1 mapping: each functional case → one pytest function",
|
|
1325
|
+
"",
|
|
1326
|
+
"## Implementation Rules:",
|
|
1327
|
+
"- Use assert statements, not unittest assertions",
|
|
1328
|
+
"- Assert specific values, not just 'no exception'",
|
|
1329
|
+
"- Use @pytest.mark.parametrize for boundary cases",
|
|
1330
|
+
"- Use markers: @pytest.mark.positive, @pytest.mark.negative, @pytest.mark.boundary",
|
|
1331
|
+
"",
|
|
1332
|
+
"## Conditional Implementation (include ONLY if test cases exist):",
|
|
1333
|
+
"- Authentication tests: implement ONLY if testcase/md/ contains auth-related cases",
|
|
1334
|
+
"- Timeout tests: implement ONLY if testcase/md/ contains timeout-related cases",
|
|
1335
|
+
"- Use @pytest.mark.auth for auth tests, @pytest.mark.timeout for timeout tests",
|
|
1336
|
+
"- If no such cases exist, do NOT add these tests",
|
|
1337
|
+
"",
|
|
1338
|
+
"## Constraints:",
|
|
1339
|
+
"- Only create NEW files, do NOT modify existing framework files (conftest.py, pytest.ini, pyproject.toml)",
|
|
1340
|
+
"- If filename exists, add suffix: test_order.py → test_order_01.py",
|
|
1341
|
+
"- Stay within writeSet: testcase/**/test_*.py",
|
|
1342
|
+
"- Do NOT re-read source documents — use the reviewed cases under testcase/md/ only",
|
|
1343
|
+
"- Read existing conftest.py/pytest.ini to understand conventions, but do NOT modify them",
|
|
1344
|
+
].join("\n\n"),
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
function buildExecuteBackendPytestNode(sources) {
|
|
1348
|
+
return {
|
|
1349
|
+
id: "execute-backend-pytest-shell",
|
|
1350
|
+
depends_on: ["generate-backend-pytest-pi"],
|
|
1351
|
+
role: "verifier",
|
|
1352
|
+
executor: "shell",
|
|
1353
|
+
complexity: "LOW",
|
|
1354
|
+
writePolicy: "read-only",
|
|
1355
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
1356
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1357
|
+
outputContract: "Archived pytest stdout/stderr with exit codes and HTML report path; no source or test file modifications.",
|
|
1358
|
+
subtask_prompt: "Run pytest for the backend test suite and capture results.",
|
|
1359
|
+
shell: {
|
|
1360
|
+
commands: [
|
|
1361
|
+
"python -m pytest testcase/ --html=reports/backend-test-report.html -v",
|
|
1362
|
+
],
|
|
1363
|
+
verifyEvidence: buildVerifyEvidence({
|
|
1364
|
+
phase: "final",
|
|
1365
|
+
quota: "full",
|
|
1366
|
+
commandSource: "inline",
|
|
1367
|
+
fallbackCommands: [
|
|
1368
|
+
"python -m pytest testcase/ --html=reports/backend-test-report.html -v",
|
|
1369
|
+
],
|
|
1370
|
+
finalFullRequired: true,
|
|
1371
|
+
}),
|
|
1372
|
+
cwd: ".",
|
|
1373
|
+
timeoutMs: 300000,
|
|
1374
|
+
},
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
function buildTestRetrospectNode(sources) {
|
|
1378
|
+
return {
|
|
1379
|
+
id: "test-retrospect-pi",
|
|
1380
|
+
depends_on: ["execute-backend-pytest-shell"],
|
|
1381
|
+
role: "closeout",
|
|
1382
|
+
executor: "pi",
|
|
1383
|
+
toolProfile: "write",
|
|
1384
|
+
complexity: "MED",
|
|
1385
|
+
writePolicy: "exclusive",
|
|
1386
|
+
writeSet: ["docs/test-reports/**"],
|
|
1387
|
+
allowedPaths: ["docs/test-reports/**"],
|
|
1388
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1389
|
+
subtask_prompt: [
|
|
1390
|
+
"Read upstream outputs (review report + pytest results) and generate a test retrospective report.",
|
|
1391
|
+
"",
|
|
1392
|
+
"## Output Steps (do in order):",
|
|
1393
|
+
"1. First, output the maturity rating on the first line: Rating: A/B/C/D",
|
|
1394
|
+
"2. Then write the full report under docs/test-reports/",
|
|
1395
|
+
"",
|
|
1396
|
+
"## Report Structure:",
|
|
1397
|
+
"1. Maturity Rating with rationale",
|
|
1398
|
+
"2. Test Coverage Summary (total cases, pass rate, failed case analysis)",
|
|
1399
|
+
"3. Review Findings and resolution status",
|
|
1400
|
+
"4. Failed Test Analysis (if any)",
|
|
1401
|
+
"5. Recommendations for improvement",
|
|
1402
|
+
"",
|
|
1403
|
+
"## Rating Criteria:",
|
|
1404
|
+
"- A: 100% acceptance criteria covered + 100% pytest pass + no Critical findings",
|
|
1405
|
+
"- B: ≥80% coverage + ≥90% pass + Low findings only",
|
|
1406
|
+
"- C: ≥60% coverage + ≥70% pass + no Critical findings",
|
|
1407
|
+
"- D: below C thresholds",
|
|
1408
|
+
"",
|
|
1409
|
+
"## Constraints:",
|
|
1410
|
+
"- Stay within writeSet: docs/test-reports/**",
|
|
1411
|
+
"- Do NOT re-read source documents — use upstream outputs only",
|
|
1412
|
+
"- Do not write root artifacts/**",
|
|
1413
|
+
].join("\n\n"),
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
const BACKEND_TEST_DEFAULTS = {
|
|
1417
|
+
...HYBRID_DEFAULTS,
|
|
1418
|
+
writePolicy: "read-only",
|
|
1419
|
+
};
|
|
1420
|
+
const BACKEND_TEST_SKILLS_BY_ROLE = {
|
|
1421
|
+
planner: ["loop-agent"],
|
|
1422
|
+
scout: [],
|
|
1423
|
+
implementer: ["test-driven-development", "verification-before-completion"],
|
|
1424
|
+
reviewer: ["requesting-code-review", "code-review-core"],
|
|
1425
|
+
verifier: ["verification-before-completion", "systematic-debugging"],
|
|
1426
|
+
closeout: ["loop-agent", "verification-before-completion"],
|
|
1427
|
+
};
|
|
1428
|
+
function buildBackendTestHybridDag(sources) {
|
|
1429
|
+
const { taskConfig } = sources;
|
|
1430
|
+
const sourceContext = buildSourceContextBlock(sources);
|
|
1431
|
+
const readOnlyPaths = commonReadOnlyPaths(sources);
|
|
1432
|
+
const forbiddenPaths = commonForbiddenPaths(sources);
|
|
1433
|
+
const globalConstraints = [
|
|
1434
|
+
...taskConfig.hardConstraints,
|
|
1435
|
+
...(sources.constraintMarkdown
|
|
1436
|
+
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
1437
|
+
: []),
|
|
1438
|
+
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
1439
|
+
"backend-test-dag nodes must maintain traceability from requirements to functional cases to pytest automation.",
|
|
1440
|
+
"Functional test case IDs must use BE-<MODULE>-<NNN> format.",
|
|
1441
|
+
"pytest execution must produce HTML reports under reports/.",
|
|
1442
|
+
"pytest automation scripts must use test_ filename prefix for pytest discovery.",
|
|
1443
|
+
"generate-backend-pytest-pi must only create new test files under testcase/; modifying existing framework files (conftest.py, pytest.ini, pyproject.toml) is forbidden.",
|
|
1444
|
+
"review-backend-cases-gate-shell must block pytest generation unless the review verdict is exactly VERDICT: pass.",
|
|
1445
|
+
"If a target test filename already exists under testcase/, add a numeric suffix (_01, _02, ...); never overwrite or append to existing files.",
|
|
1446
|
+
"execute-backend-pytest-shell must not modify test assertions or production code to make tests pass; test failures indicate potential implementation issues and must be reported honestly.",
|
|
1447
|
+
];
|
|
1448
|
+
const spec = {
|
|
1449
|
+
version: 3,
|
|
1450
|
+
title: `Backend test DAG: ${taskConfig.title}`,
|
|
1451
|
+
runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
|
|
1452
|
+
outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
|
|
1453
|
+
objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
|
|
1454
|
+
successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
|
|
1455
|
+
globalConstraints,
|
|
1456
|
+
convergence: {
|
|
1457
|
+
enabled: true,
|
|
1458
|
+
maxPasses: 3,
|
|
1459
|
+
stopOnVerdictPass: true,
|
|
1460
|
+
stopOnHardVerifyPass: true,
|
|
1461
|
+
pauseOnRegression: true,
|
|
1462
|
+
chainNodeIds: [
|
|
1463
|
+
"generate-backend-functional-cases-pi",
|
|
1464
|
+
"review-backend-cases-pi",
|
|
1465
|
+
"review-backend-cases-gate-shell",
|
|
1466
|
+
],
|
|
1467
|
+
},
|
|
1468
|
+
defaults: {
|
|
1469
|
+
...BACKEND_TEST_DEFAULTS,
|
|
1470
|
+
contextProfile: taskConfig.contextProfile,
|
|
1471
|
+
},
|
|
1472
|
+
skillsByRole: BACKEND_TEST_SKILLS_BY_ROLE,
|
|
1473
|
+
executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
|
|
1474
|
+
tasks: [
|
|
1475
|
+
buildAnalyzeInputsNode(sources),
|
|
1476
|
+
buildGenerateBackendFunctionalCasesNode(sources),
|
|
1477
|
+
buildReviewBackendCasesNode(sources),
|
|
1478
|
+
buildReviewBackendCasesGateNode(sources),
|
|
1479
|
+
buildGenerateBackendPytestNode(sources),
|
|
1480
|
+
buildExecuteBackendPytestNode(sources),
|
|
1481
|
+
buildTestRetrospectNode(sources),
|
|
1482
|
+
],
|
|
1483
|
+
};
|
|
1484
|
+
applyDefaultReadOnlyRetryPolicy(spec);
|
|
1485
|
+
parseDagSpec(spec);
|
|
1486
|
+
assertValidDagSpec(spec);
|
|
1487
|
+
return spec;
|
|
1488
|
+
}
|
|
1489
|
+
// ---------------------------------------------------------------------------
|
|
1490
|
+
// Knowledge-sync DAG template
|
|
1491
|
+
// ---------------------------------------------------------------------------
|
|
1492
|
+
const KNOWLEDGE_SYNC_DEFAULTS = {
|
|
1493
|
+
...HYBRID_DEFAULTS,
|
|
1494
|
+
writePolicy: "read-only",
|
|
1495
|
+
};
|
|
1496
|
+
const KNOWLEDGE_SYNC_SKILLS_BY_ROLE = {
|
|
1497
|
+
planner: ["loop-agent"],
|
|
1498
|
+
scout: [],
|
|
1499
|
+
implementer: ["verification-before-completion"],
|
|
1500
|
+
reviewer: ["requesting-code-review"],
|
|
1501
|
+
verifier: ["verification-before-completion", "systematic-debugging"],
|
|
1502
|
+
closeout: ["loop-agent", "verification-before-completion"],
|
|
1503
|
+
};
|
|
1504
|
+
/** Safe Feature directory id: F-… without path separators. */
|
|
1505
|
+
const KNOWLEDGE_SYNC_FEATURE_ID_RE = /^F-[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
1506
|
+
export function assertSafeKnowledgeSyncFeatureId(featureId) {
|
|
1507
|
+
const trimmed = featureId.trim();
|
|
1508
|
+
if (!KNOWLEDGE_SYNC_FEATURE_ID_RE.test(trimmed)) {
|
|
1509
|
+
throw new Error(`knowledge-sync featureId must match F-<id> (letters/digits/._- only); got ${JSON.stringify(featureId)}`);
|
|
1510
|
+
}
|
|
1511
|
+
if (trimmed.includes("..") || trimmed.includes("/") || trimmed.includes("\\")) {
|
|
1512
|
+
throw new Error(`knowledge-sync featureId must not contain path segments: ${featureId}`);
|
|
1513
|
+
}
|
|
1514
|
+
return trimmed;
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Resolve featureId for knowledge-sync writeSet binding (fail-closed).
|
|
1518
|
+
* Order: task.json featureId → hardConstraints key → requirement body F-YYYY-… → taskId if F-*.
|
|
1519
|
+
*/
|
|
1520
|
+
export function resolveKnowledgeSyncFeatureId(sources) {
|
|
1521
|
+
const candidates = [];
|
|
1522
|
+
const fromConfig = sources.taskConfig.featureId?.trim();
|
|
1523
|
+
if (fromConfig)
|
|
1524
|
+
candidates.push(fromConfig);
|
|
1525
|
+
for (const line of sources.taskConfig.hardConstraints ?? []) {
|
|
1526
|
+
const match = line.match(/(?:featureId|feature_id)\s*[=:]\s*(F-[A-Za-z0-9][A-Za-z0-9._-]*)/i);
|
|
1527
|
+
if (match?.[1])
|
|
1528
|
+
candidates.push(match[1]);
|
|
1529
|
+
}
|
|
1530
|
+
const fromRequirement = sources.requirementMarkdown?.match(/\b(F-\d{4}-\d{2,})\b/);
|
|
1531
|
+
if (fromRequirement?.[1])
|
|
1532
|
+
candidates.push(fromRequirement[1]);
|
|
1533
|
+
if (KNOWLEDGE_SYNC_FEATURE_ID_RE.test(sources.taskId)) {
|
|
1534
|
+
candidates.push(sources.taskId);
|
|
1535
|
+
}
|
|
1536
|
+
for (const candidate of candidates) {
|
|
1537
|
+
try {
|
|
1538
|
+
return assertSafeKnowledgeSyncFeatureId(candidate);
|
|
1539
|
+
}
|
|
1540
|
+
catch {
|
|
1541
|
+
// try next candidate
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
throw new Error([
|
|
1545
|
+
"knowledge-sync requires an explicit featureId bound to features/<featureId>/.",
|
|
1546
|
+
'Set task.json "featureId": "F-YYYY-NNN", or hardConstraints entry featureId=F-…,',
|
|
1547
|
+
"or put F-YYYY-NNN in 需求.md, or use a taskId that is already an F-* id.",
|
|
1548
|
+
].join(" "));
|
|
1549
|
+
}
|
|
1550
|
+
function knowledgeSyncWriteSet(featureId) {
|
|
1551
|
+
const id = assertSafeKnowledgeSyncFeatureId(featureId);
|
|
1552
|
+
return [
|
|
1553
|
+
`features/${id}/testing/**`,
|
|
1554
|
+
`features/${id}/requirement-delta.md`,
|
|
1555
|
+
"docs/test-reports/**",
|
|
1556
|
+
];
|
|
1557
|
+
}
|
|
1558
|
+
function knowledgeSyncPendingWriteSet(featureId) {
|
|
1559
|
+
const id = assertSafeKnowledgeSyncFeatureId(featureId);
|
|
1560
|
+
return [`features/${id}/testing/sync/pending/**`];
|
|
1561
|
+
}
|
|
1562
|
+
function knowledgeSyncPointerWriteSet(featureId) {
|
|
1563
|
+
const id = assertSafeKnowledgeSyncFeatureId(featureId);
|
|
1564
|
+
return [
|
|
1565
|
+
`features/${id}/testing/runs/**`,
|
|
1566
|
+
`features/${id}/testing/sync/applied/**`,
|
|
1567
|
+
];
|
|
1568
|
+
}
|
|
1569
|
+
function knowledgeSyncDraftRelPath(featureId) {
|
|
1570
|
+
const id = assertSafeKnowledgeSyncFeatureId(featureId);
|
|
1571
|
+
return `features/${id}/testing/sync/pending/knowledge-sync-draft.json`;
|
|
1572
|
+
}
|
|
1573
|
+
function buildKnowledgeSyncCollectNode(sources, featureId) {
|
|
1574
|
+
return {
|
|
1575
|
+
id: "knowledge-sync-collect-pi",
|
|
1576
|
+
depends_on: [],
|
|
1577
|
+
role: "planner",
|
|
1578
|
+
executor: "pi",
|
|
1579
|
+
complexity: "MED",
|
|
1580
|
+
writePolicy: "read-only",
|
|
1581
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
1582
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1583
|
+
outputContract: "Plain Markdown inventory of final-verification evidence, acceptance criteria, cases, defects, and requirement-delta candidates; no file writes.",
|
|
1584
|
+
subtask_prompt: [
|
|
1585
|
+
"Collect inputs for knowledge-sync after final verification.",
|
|
1586
|
+
`Bound featureId for this run: ${featureId} (directory features/${featureId}/).`,
|
|
1587
|
+
`Read task source (需求.md, 执行约束.md, references), features/${featureId}/ testing assets when present, and upstream final verification / pytest / retrospect evidence paths.`,
|
|
1588
|
+
"Produce a concise inventory covering: featureId/taskId/runId/headSha, AC list, reviewed cases, automation bindings, open defects, requirement-delta risk, and missing evidence.",
|
|
1589
|
+
"Do not claim final verification passed without shell evidence. Read-only: do not modify repository files.",
|
|
1590
|
+
buildSourceContextBlock(sources),
|
|
1591
|
+
].join("\n\n"),
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
function buildKnowledgeSyncDraftNode(sources, featureId) {
|
|
1595
|
+
const pending = knowledgeSyncPendingWriteSet(featureId);
|
|
1596
|
+
const draftPath = knowledgeSyncDraftRelPath(featureId);
|
|
1597
|
+
return {
|
|
1598
|
+
id: "knowledge-sync-draft-pi",
|
|
1599
|
+
depends_on: ["knowledge-sync-collect-pi"],
|
|
1600
|
+
role: "implementer",
|
|
1601
|
+
executor: "pi",
|
|
1602
|
+
toolProfile: "write",
|
|
1603
|
+
complexity: "HIGH",
|
|
1604
|
+
writePolicy: "exclusive",
|
|
1605
|
+
writeSet: pending,
|
|
1606
|
+
allowedPaths: pending,
|
|
1607
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1608
|
+
subtask_prompt: [
|
|
1609
|
+
"Using knowledge-sync-collect-pi inventory and final verification evidence, write the pending knowledge-sync draft for this Feature only.",
|
|
1610
|
+
`featureId MUST be exactly ${featureId}. Write only under features/${featureId}/testing/sync/pending/.`,
|
|
1611
|
+
`Create ${draftPath} (schemaVersion 1) and features/${featureId}/testing/sync/pending/knowledge-sync-draft.md human summary.`,
|
|
1612
|
+
"Draft payload must include: acceptanceVerdict, coverageMatrix, caseIndex, automationMap, defects, requirementDelta (or null), retrospectiveRef, evidencePointers, and operations[] with op/target/risk/reason.",
|
|
1613
|
+
`Every operations[].target path must stay under features/${featureId}/ or docs/test-reports/.`,
|
|
1614
|
+
"Mark requirement/acceptance body edits as risk=high. Keep raw shell logs as path pointers only.",
|
|
1615
|
+
"If final verification is not pass, still write draft but set gates.blockIfFinalVerificationNotPass accordingly and do not invent pass results.",
|
|
1616
|
+
"Stay within writeSet. Do not write other features/**, src/**, .harness/**, or knowledge/testing/standards/**.",
|
|
1617
|
+
buildSourceContextBlock(sources),
|
|
1618
|
+
].join("\n\n"),
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
function buildKnowledgeSyncValidateNode(sources, featureId) {
|
|
1622
|
+
const draftPath = knowledgeSyncDraftRelPath(featureId);
|
|
1623
|
+
const validateScript = [
|
|
1624
|
+
"node",
|
|
1625
|
+
"-e",
|
|
1626
|
+
JSON.stringify([
|
|
1627
|
+
"const fs=require('fs');",
|
|
1628
|
+
"const path=require('path');",
|
|
1629
|
+
`const featureId=${JSON.stringify(featureId)};`,
|
|
1630
|
+
`const file=${JSON.stringify(draftPath)};`,
|
|
1631
|
+
"if(!fs.existsSync(file)){ console.error('knowledge-sync validate: missing draft at '+file); process.exit(1); }",
|
|
1632
|
+
"let draft; try { draft=JSON.parse(fs.readFileSync(file,'utf8')); } catch(e){ console.error('invalid JSON',file,e.message); process.exit(1); }",
|
|
1633
|
+
"let failed=false;",
|
|
1634
|
+
"if(draft.schemaVersion!==1){ console.error(file+': schemaVersion must be 1'); failed=true; }",
|
|
1635
|
+
"for (const key of ['featureId','taskId','runId','finalVerification','operations','payload','gates']) {",
|
|
1636
|
+
" if(draft[key]===undefined||draft[key]===null){ console.error(file+': missing '+key); failed=true; }",
|
|
1637
|
+
"}",
|
|
1638
|
+
"if(draft.featureId!==featureId){ console.error(file+': featureId must be '+featureId+' got '+draft.featureId); failed=true; }",
|
|
1639
|
+
"if(!Array.isArray(draft.operations)||draft.operations.length===0){ console.error(file+': operations must be non-empty array'); failed=true; }",
|
|
1640
|
+
"const prefix='features/'+featureId+'/';",
|
|
1641
|
+
"for (const op of draft.operations||[]) {",
|
|
1642
|
+
" const t=op&&op.target; if(typeof t!=='string'){ console.error('op missing target'); failed=true; continue; }",
|
|
1643
|
+
" const ok=t===prefix.slice(0,-1)||t.startsWith(prefix)||t.startsWith('docs/test-reports/');",
|
|
1644
|
+
" if(!ok){ console.error('op.target outside feature bind: '+t); failed=true; }",
|
|
1645
|
+
"}",
|
|
1646
|
+
"const highRisk=(draft.operations||[]).some((op)=>op && op.risk==='high');",
|
|
1647
|
+
"if(draft.gates && draft.gates.blockIfFinalVerificationNotPass && draft.finalVerification!=='pass' && draft.finalVerification!=='pass-with-waivers'){",
|
|
1648
|
+
" console.error(file+': finalVerification is not pass'); failed=true;",
|
|
1649
|
+
"}",
|
|
1650
|
+
"if(highRisk && draft.gates && draft.gates.requireHumanIfHighRisk){",
|
|
1651
|
+
" console.log(file+': high-risk ops present; human approval required before/while apply');",
|
|
1652
|
+
"}",
|
|
1653
|
+
"if(failed) process.exit(1);",
|
|
1654
|
+
"console.log('knowledge-sync validate ok '+file);",
|
|
1655
|
+
].join("")),
|
|
1656
|
+
].join(" ");
|
|
1657
|
+
return {
|
|
1658
|
+
id: "knowledge-sync-validate-shell",
|
|
1659
|
+
depends_on: ["knowledge-sync-draft-pi"],
|
|
1660
|
+
role: "verifier",
|
|
1661
|
+
executor: "shell",
|
|
1662
|
+
complexity: "LOW",
|
|
1663
|
+
writePolicy: "read-only",
|
|
1664
|
+
allowedPaths: [
|
|
1665
|
+
...commonReadOnlyPaths(sources),
|
|
1666
|
+
`features/${featureId}/testing/sync/pending/**`,
|
|
1667
|
+
],
|
|
1668
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1669
|
+
outputContract: `Deterministic validation of ${draftPath} (schema, featureId bind, operation targets, finalVerification gate); no worktree writes.`,
|
|
1670
|
+
subtask_prompt: `Validate only ${draftPath} for featureId=${featureId} before apply.`,
|
|
1671
|
+
shell: {
|
|
1672
|
+
commands: [validateScript],
|
|
1673
|
+
verifyEvidence: buildVerifyEvidence({
|
|
1674
|
+
phase: "final",
|
|
1675
|
+
quota: "full",
|
|
1676
|
+
commandSource: "inline",
|
|
1677
|
+
fallbackCommands: [validateScript],
|
|
1678
|
+
finalFullRequired: true,
|
|
1679
|
+
}),
|
|
1680
|
+
cwd: ".",
|
|
1681
|
+
timeoutMs: 120000,
|
|
1682
|
+
},
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
function buildKnowledgeSyncApplyNode(sources, featureId) {
|
|
1686
|
+
const writeSet = knowledgeSyncWriteSet(featureId);
|
|
1687
|
+
const draftPath = knowledgeSyncDraftRelPath(featureId);
|
|
1688
|
+
return {
|
|
1689
|
+
id: "knowledge-sync-apply-pi",
|
|
1690
|
+
depends_on: ["knowledge-sync-validate-shell"],
|
|
1691
|
+
role: "implementer",
|
|
1692
|
+
executor: "pi",
|
|
1693
|
+
toolProfile: "write",
|
|
1694
|
+
complexity: "HIGH",
|
|
1695
|
+
writePolicy: "exclusive",
|
|
1696
|
+
writeSet,
|
|
1697
|
+
allowedPaths: writeSet,
|
|
1698
|
+
forbiddenPaths: [
|
|
1699
|
+
...commonForbiddenPaths(sources),
|
|
1700
|
+
"src/**",
|
|
1701
|
+
"knowledge/testing/standards/**",
|
|
1702
|
+
"conftest.py",
|
|
1703
|
+
"pytest.ini",
|
|
1704
|
+
"pyproject.toml",
|
|
1705
|
+
],
|
|
1706
|
+
subtask_prompt: [
|
|
1707
|
+
"Apply the validated knowledge-sync draft into the Feature testing knowledge base (L1).",
|
|
1708
|
+
`Bound featureId: ${featureId}. Only touch features/${featureId}/ and docs/test-reports/** (writeSet is exclusive).`,
|
|
1709
|
+
`From ${draftPath}, upsert under features/${featureId}/:`,
|
|
1710
|
+
"- testing/acceptance-verdict.yaml",
|
|
1711
|
+
"- testing/coverage-matrix.yaml",
|
|
1712
|
+
"- testing/cases/** and index.yaml when payload includes caseIndex",
|
|
1713
|
+
"- testing/automation-map.yaml",
|
|
1714
|
+
"- testing/defects/registry.yaml and open.md",
|
|
1715
|
+
"- testing/closeout-testing.md and retrospectives when provided",
|
|
1716
|
+
`- features/${featureId}/requirement-delta.md only as draft/proposal unless operations mark approved product change`,
|
|
1717
|
+
"Never modify other features/**, knowledge/testing/standards/**, src/**, or pytest framework files.",
|
|
1718
|
+
"Do not invent AC pass without evidencePointers. Keep raw logs as pointers only.",
|
|
1719
|
+
"If high-risk requirement changes lack approval, write requirement-delta.md as draft and do not merge into requirement.md/acceptance.yaml.",
|
|
1720
|
+
"Stay within writeSet.",
|
|
1721
|
+
buildSourceContextBlock(sources),
|
|
1722
|
+
].join("\n\n"),
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
function buildKnowledgeSyncPointerNode(sources, featureId) {
|
|
1726
|
+
const writeSet = knowledgeSyncPointerWriteSet(featureId);
|
|
1727
|
+
return {
|
|
1728
|
+
id: "knowledge-sync-pointer-pi",
|
|
1729
|
+
depends_on: ["knowledge-sync-apply-pi"],
|
|
1730
|
+
role: "closeout",
|
|
1731
|
+
executor: "pi",
|
|
1732
|
+
toolProfile: "write",
|
|
1733
|
+
complexity: "MED",
|
|
1734
|
+
writePolicy: "exclusive",
|
|
1735
|
+
writeSet,
|
|
1736
|
+
allowedPaths: writeSet,
|
|
1737
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1738
|
+
subtask_prompt: [
|
|
1739
|
+
"Write knowledge-sync audit pointers after apply.",
|
|
1740
|
+
`Update features/${featureId}/testing/runs/latest.md with runId, headSha, finalVerification, maturity, evidence paths, and applied artifact refs.`,
|
|
1741
|
+
`Write features/${featureId}/testing/sync/applied/KS-<timestamp>.json summarizing applied operations and source draft hash/path.`,
|
|
1742
|
+
"Do not rewrite acceptance-verdict or cases here unless only adding pointer fields already applied.",
|
|
1743
|
+
"Stay within writeSet. Do not touch other features. Read-only regarding src/** and .harness/**.",
|
|
1744
|
+
buildSourceContextBlock(sources),
|
|
1745
|
+
].join("\n\n"),
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
function buildKnowledgeSyncHybridDag(sources) {
|
|
1749
|
+
const { taskConfig } = sources;
|
|
1750
|
+
const featureId = resolveKnowledgeSyncFeatureId(sources);
|
|
1751
|
+
const globalConstraints = [
|
|
1752
|
+
...taskConfig.hardConstraints,
|
|
1753
|
+
...(sources.constraintMarkdown
|
|
1754
|
+
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
1755
|
+
: []),
|
|
1756
|
+
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
1757
|
+
`knowledge-sync-dag is bound to featureId=${featureId}; writes only features/${featureId}/testing/**, features/${featureId}/requirement-delta.md, and docs/test-reports/**.`,
|
|
1758
|
+
"knowledge-sync must not modify other features/**, src/**, .harness/**, knowledge/testing/standards/**, or pytest framework files.",
|
|
1759
|
+
"Apply is blocked unless knowledge-sync-validate-shell passes; final verification evidence remains the completion authority.",
|
|
1760
|
+
"High-risk requirement/acceptance body changes require human approval via requirement-delta; do not silently rewrite requirement.md.",
|
|
1761
|
+
"Raw shell logs stay as path pointers; knowledge base stores stable facts only.",
|
|
1762
|
+
"Prefer structured YAML/Markdown L1 knowledge pack over vector-store-only writes.",
|
|
1763
|
+
];
|
|
1764
|
+
const spec = {
|
|
1765
|
+
version: 2,
|
|
1766
|
+
title: `Knowledge-sync DAG (${featureId}): ${taskConfig.title}`,
|
|
1767
|
+
outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
|
|
1768
|
+
objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
|
|
1769
|
+
successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
|
|
1770
|
+
globalConstraints,
|
|
1771
|
+
defaults: {
|
|
1772
|
+
...KNOWLEDGE_SYNC_DEFAULTS,
|
|
1773
|
+
contextProfile: taskConfig.contextProfile,
|
|
1774
|
+
},
|
|
1775
|
+
skillsByRole: KNOWLEDGE_SYNC_SKILLS_BY_ROLE,
|
|
1776
|
+
executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
|
|
1777
|
+
tasks: [
|
|
1778
|
+
buildKnowledgeSyncCollectNode(sources, featureId),
|
|
1779
|
+
buildKnowledgeSyncDraftNode(sources, featureId),
|
|
1780
|
+
buildKnowledgeSyncValidateNode(sources, featureId),
|
|
1781
|
+
buildKnowledgeSyncApplyNode(sources, featureId),
|
|
1782
|
+
buildKnowledgeSyncPointerNode(sources, featureId),
|
|
1783
|
+
],
|
|
1784
|
+
};
|
|
1785
|
+
parseDagSpec(spec);
|
|
1786
|
+
assertValidDagSpec(spec);
|
|
1787
|
+
return spec;
|
|
1788
|
+
}
|
|
1789
|
+
// ---------------------------------------------------------------------------
|
|
1790
|
+
// Knowledge-graph bootstrap DAG template (AI-assisted graph initialization)
|
|
1791
|
+
// ---------------------------------------------------------------------------
|
|
1792
|
+
const KG_BOOTSTRAP_DEFAULTS = {
|
|
1793
|
+
...HYBRID_DEFAULTS,
|
|
1794
|
+
writePolicy: "read-only",
|
|
1795
|
+
};
|
|
1796
|
+
const KG_BOOTSTRAP_SKILLS_BY_ROLE = {
|
|
1797
|
+
planner: ["loop-agent"],
|
|
1798
|
+
scout: ["codebase-scout"],
|
|
1799
|
+
implementer: ["verification-before-completion"],
|
|
1800
|
+
reviewer: ["requesting-code-review"],
|
|
1801
|
+
verifier: ["verification-before-completion", "systematic-debugging"],
|
|
1802
|
+
closeout: ["loop-agent", "verification-before-completion"],
|
|
1803
|
+
};
|
|
1804
|
+
function buildKgBootstrapInlineNodeScript(lines) {
|
|
1805
|
+
return ["node", "-e", JSON.stringify(lines.join(""))].join(" ");
|
|
1806
|
+
}
|
|
1807
|
+
function buildKgBootstrapPreflightNode(sources) {
|
|
1808
|
+
const script = buildKgBootstrapInlineNodeScript([
|
|
1809
|
+
"const fs=require('fs');",
|
|
1810
|
+
"const path=require('path');",
|
|
1811
|
+
"const root=process.cwd();",
|
|
1812
|
+
"const scope=path.join(root,'knowledge','bootstrap','scope.yaml');",
|
|
1813
|
+
"const status=path.join(root,'knowledge','bootstrap','status.yaml');",
|
|
1814
|
+
"const staging=path.join(root,'knowledge','bootstrap','staging');",
|
|
1815
|
+
"function ensureDir(p){fs.mkdirSync(p,{recursive:true});}",
|
|
1816
|
+
"ensureDir(path.join(root,'knowledge','bootstrap','runs'));",
|
|
1817
|
+
"ensureDir(staging);",
|
|
1818
|
+
"ensureDir(path.join(root,'knowledge','graph'));",
|
|
1819
|
+
"if(!fs.existsSync(scope)){",
|
|
1820
|
+
" console.error('kg-bootstrap preflight: missing knowledge/bootstrap/scope.yaml — complete B0/B1 skeleton first');",
|
|
1821
|
+
" process.exit(1);",
|
|
1822
|
+
"}",
|
|
1823
|
+
"if(!fs.existsSync(status)){",
|
|
1824
|
+
" console.error('kg-bootstrap preflight: missing knowledge/bootstrap/status.yaml');",
|
|
1825
|
+
" process.exit(1);",
|
|
1826
|
+
"}",
|
|
1827
|
+
"console.log('kg-bootstrap preflight ok');",
|
|
1828
|
+
]);
|
|
1829
|
+
return {
|
|
1830
|
+
id: "kg-bootstrap-preflight-shell",
|
|
1831
|
+
depends_on: [],
|
|
1832
|
+
role: "verifier",
|
|
1833
|
+
executor: "shell",
|
|
1834
|
+
complexity: "LOW",
|
|
1835
|
+
writePolicy: "read-only",
|
|
1836
|
+
allowedPaths: ["knowledge/bootstrap/**", "knowledge/graph/**"],
|
|
1837
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1838
|
+
outputContract: "Preflight: require knowledge/bootstrap/scope.yaml and status.yaml; ensure staging/runs/graph dirs exist.",
|
|
1839
|
+
subtask_prompt: "Fail if bootstrap skeleton is missing. Do not invent business entities.",
|
|
1840
|
+
shell: {
|
|
1841
|
+
commands: [script],
|
|
1842
|
+
verifyEvidence: buildVerifyEvidence({
|
|
1843
|
+
phase: "intermediate",
|
|
1844
|
+
quota: "full",
|
|
1845
|
+
commandSource: "inline",
|
|
1846
|
+
fallbackCommands: [script],
|
|
1847
|
+
}),
|
|
1848
|
+
cwd: ".",
|
|
1849
|
+
timeoutMs: 60000,
|
|
1850
|
+
},
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
function buildKgBootstrapInventoryNode(sources) {
|
|
1854
|
+
const script = buildKgBootstrapInlineNodeScript([
|
|
1855
|
+
"const fs=require('fs');",
|
|
1856
|
+
"const path=require('path');",
|
|
1857
|
+
"const root=process.cwd();",
|
|
1858
|
+
"const out=path.join(root,'knowledge','bootstrap','inventory.json');",
|
|
1859
|
+
"function listDir(rel){const p=path.join(root,rel);if(!fs.existsSync(p))return[];return fs.readdirSync(p,{withFileTypes:true}).map(e=>({name:e.name,dir:e.isDirectory()}));}",
|
|
1860
|
+
"const top=listDir('.').filter(e=>e.dir).map(e=>e.name).filter(n=>!['node_modules','.git','dist','.harness'].includes(n));",
|
|
1861
|
+
"const features=listDir('features').filter(e=>e.dir&&/^F-/.test(e.name)).map(e=>e.name);",
|
|
1862
|
+
"const services=[];",
|
|
1863
|
+
"for (const base of ['services','apps','packages']) {",
|
|
1864
|
+
" for (const e of listDir(base)) if(e.dir) services.push({candidateName:e.name,paths:[base+'/'+e.name+'/**'],evidence:[base+'/'+e.name]});",
|
|
1865
|
+
"}",
|
|
1866
|
+
"const manifests=['package.json','go.mod','pyproject.toml','pom.xml','Cargo.toml'].filter(f=>fs.existsSync(path.join(root,f)));",
|
|
1867
|
+
"const inventory={schemaVersion:1,generatedAt:new Date().toISOString(),signals:{packageManifests:manifests,topLevelDirs:top,featureIds:features,likelyServices:services,docHits:[],openApiOrProto:[],existingKnowledge:{hasDomains:fs.existsSync(path.join(root,'knowledge','domains')),hasServices:fs.existsSync(path.join(root,'knowledge','services')),hasGraph:fs.existsSync(path.join(root,'knowledge','graph','edges.yaml'))}},limits:{maxFilesScanned:0,truncated:false}};",
|
|
1868
|
+
"fs.mkdirSync(path.dirname(out),{recursive:true});",
|
|
1869
|
+
"fs.writeFileSync(out,JSON.stringify(inventory,null,2)+'\\n');",
|
|
1870
|
+
"console.log('wrote '+out+' features='+features.length+' serviceCandidates='+services.length);",
|
|
1871
|
+
]);
|
|
1872
|
+
return {
|
|
1873
|
+
id: "kg-bootstrap-inventory-shell",
|
|
1874
|
+
depends_on: ["kg-bootstrap-preflight-shell"],
|
|
1875
|
+
role: "verifier",
|
|
1876
|
+
executor: "shell",
|
|
1877
|
+
complexity: "LOW",
|
|
1878
|
+
writePolicy: "exclusive",
|
|
1879
|
+
writeSet: ["knowledge/bootstrap/inventory.json", "knowledge/bootstrap/status.yaml"],
|
|
1880
|
+
allowedPaths: ["knowledge/bootstrap/**", "./**"],
|
|
1881
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1882
|
+
outputContract: "Deterministic inventory.json under knowledge/bootstrap/ from directory and feature signals.",
|
|
1883
|
+
subtask_prompt: "Scan repository structure into knowledge/bootstrap/inventory.json (B2).",
|
|
1884
|
+
shell: {
|
|
1885
|
+
commands: [script],
|
|
1886
|
+
verifyEvidence: buildVerifyEvidence({
|
|
1887
|
+
phase: "intermediate",
|
|
1888
|
+
quota: "full",
|
|
1889
|
+
commandSource: "inline",
|
|
1890
|
+
fallbackCommands: [script],
|
|
1891
|
+
}),
|
|
1892
|
+
cwd: ".",
|
|
1893
|
+
timeoutMs: 120000,
|
|
1894
|
+
},
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
function buildKgBootstrapProposeNode(sources) {
|
|
1898
|
+
return {
|
|
1899
|
+
id: "kg-bootstrap-propose-pi",
|
|
1900
|
+
depends_on: ["kg-bootstrap-inventory-shell"],
|
|
1901
|
+
role: "implementer",
|
|
1902
|
+
executor: "pi",
|
|
1903
|
+
toolProfile: "write",
|
|
1904
|
+
complexity: "HIGH",
|
|
1905
|
+
writePolicy: "exclusive",
|
|
1906
|
+
writeSet: [
|
|
1907
|
+
"knowledge/bootstrap/staging/**",
|
|
1908
|
+
"knowledge/bootstrap/runs/**",
|
|
1909
|
+
],
|
|
1910
|
+
allowedPaths: [
|
|
1911
|
+
"knowledge/bootstrap/**",
|
|
1912
|
+
"knowledge/**",
|
|
1913
|
+
"features/**",
|
|
1914
|
+
"docs/**",
|
|
1915
|
+
"README.md",
|
|
1916
|
+
"services/**",
|
|
1917
|
+
"apps/**",
|
|
1918
|
+
"packages/**",
|
|
1919
|
+
"src/**",
|
|
1920
|
+
],
|
|
1921
|
+
forbiddenPaths: [
|
|
1922
|
+
...commonForbiddenPaths(sources),
|
|
1923
|
+
"knowledge/domains/**",
|
|
1924
|
+
"knowledge/services/**",
|
|
1925
|
+
"knowledge/modules/**",
|
|
1926
|
+
"knowledge/graph/edges.yaml",
|
|
1927
|
+
"knowledge/graph/entities-index.yaml",
|
|
1928
|
+
],
|
|
1929
|
+
subtask_prompt: [
|
|
1930
|
+
"B3: Generate AI proposals for the business knowledge graph ONLY under knowledge/bootstrap/staging/**.",
|
|
1931
|
+
"Read knowledge/bootstrap/scope.yaml and knowledge/bootstrap/inventory.json first.",
|
|
1932
|
+
"If scope.yaml has update_mode: incremental (or status.yaml update_mode: incremental), treat this as a narrow incremental update:",
|
|
1933
|
+
"- Prefer seed_features / seed_services / seed_domains and include_paths only; do not re-propose the entire monorepo.",
|
|
1934
|
+
"- Do not invent entities outside the incremental scope; note skipped inventory candidates in coverage-notes.md.",
|
|
1935
|
+
"Create domain/service/module drafts with meta.yaml + overview.md (and interfaces/dependencies/pitfalls when evidence exists).",
|
|
1936
|
+
"Write staging/edges.proposed.yaml and optional staging/features-links.proposed/<F-id>.yaml for seed features.",
|
|
1937
|
+
"Write staging/coverage-notes.md explaining which inventory candidates were covered or skipped.",
|
|
1938
|
+
"Every entity/edge MUST include evidence paths. confidence may be inferred|proposed only — NEVER asserted.",
|
|
1939
|
+
"Do not invent production API details without file evidence; use unknown/TODO instead.",
|
|
1940
|
+
"Do NOT write knowledge/domains|services formal trees or graph indexes — promotion is a later node.",
|
|
1941
|
+
"Stay within writeSet.",
|
|
1942
|
+
buildSourceContextBlock(sources),
|
|
1943
|
+
].join("\n\n"),
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
function buildKgBootstrapValidateNode(sources) {
|
|
1947
|
+
const script = buildKgBootstrapInlineNodeScript([
|
|
1948
|
+
"const fs=require('fs');",
|
|
1949
|
+
"const path=require('path');",
|
|
1950
|
+
"const staging=path.join(process.cwd(),'knowledge','bootstrap','staging');",
|
|
1951
|
+
"if(!fs.existsSync(staging)){console.error('missing staging');process.exit(1);}",
|
|
1952
|
+
"function walk(dir,acc=[]){if(!fs.existsSync(dir))return acc;for(const ent of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,ent.name);if(ent.isDirectory())walk(p,acc);else acc.push(p);}return acc;}",
|
|
1953
|
+
"const files=walk(staging);",
|
|
1954
|
+
"if(files.length===0){console.error('staging empty');process.exit(1);}",
|
|
1955
|
+
"const ids=new Set(); let failed=false;",
|
|
1956
|
+
"for(const f of files){",
|
|
1957
|
+
" if(!f.endsWith('meta.yaml')&&!f.endsWith('edges.proposed.yaml')) continue;",
|
|
1958
|
+
" const text=fs.readFileSync(f,'utf8');",
|
|
1959
|
+
" if(/confidence:\\s*asserted/i.test(text)&&f.includes('staging')){console.error('agent must not self-assert: '+f);failed=true;}",
|
|
1960
|
+
" const m=text.match(/^id:\\s*([A-Za-z0-9_-]+)/m);",
|
|
1961
|
+
" if(m){ if(ids.has(m[1])){console.error('duplicate id '+m[1]);failed=true;} ids.add(m[1]); }",
|
|
1962
|
+
"}",
|
|
1963
|
+
"const edgesPath=path.join(staging,'edges.proposed.yaml');",
|
|
1964
|
+
"if(!fs.existsSync(edgesPath)){console.error('missing staging/edges.proposed.yaml');failed=true;}",
|
|
1965
|
+
"if(!fs.existsSync(path.join(staging,'coverage-notes.md'))){console.error('missing staging/coverage-notes.md');failed=true;}",
|
|
1966
|
+
"if(failed) process.exit(1);",
|
|
1967
|
+
"console.log('kg-bootstrap validate ok files='+files.length+' ids='+ids.size);",
|
|
1968
|
+
]);
|
|
1969
|
+
return {
|
|
1970
|
+
id: "kg-bootstrap-validate-shell",
|
|
1971
|
+
depends_on: ["kg-bootstrap-propose-pi"],
|
|
1972
|
+
role: "verifier",
|
|
1973
|
+
executor: "shell",
|
|
1974
|
+
complexity: "LOW",
|
|
1975
|
+
writePolicy: "read-only",
|
|
1976
|
+
allowedPaths: ["knowledge/bootstrap/staging/**"],
|
|
1977
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1978
|
+
outputContract: "Validate staging proposals: non-empty, edges.proposed + coverage-notes present, no self-asserted confidence, unique ids.",
|
|
1979
|
+
subtask_prompt: "B4 deterministic validation of knowledge/bootstrap/staging.",
|
|
1980
|
+
shell: {
|
|
1981
|
+
commands: [script],
|
|
1982
|
+
verifyEvidence: buildVerifyEvidence({
|
|
1983
|
+
phase: "final",
|
|
1984
|
+
quota: "full",
|
|
1985
|
+
commandSource: "inline",
|
|
1986
|
+
fallbackCommands: [script],
|
|
1987
|
+
finalFullRequired: true,
|
|
1988
|
+
}),
|
|
1989
|
+
cwd: ".",
|
|
1990
|
+
timeoutMs: 120000,
|
|
1991
|
+
},
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
function buildKgBootstrapReviewNode(sources) {
|
|
1995
|
+
return {
|
|
1996
|
+
id: "kg-bootstrap-review-pi",
|
|
1997
|
+
depends_on: ["kg-bootstrap-validate-shell"],
|
|
1998
|
+
role: "reviewer",
|
|
1999
|
+
executor: "pi",
|
|
2000
|
+
complexity: "HIGH",
|
|
2001
|
+
writePolicy: "read-only",
|
|
2002
|
+
allowedPaths: ["knowledge/bootstrap/**", "knowledge/**", "features/**", "docs/**"],
|
|
2003
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2004
|
+
outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; human still promotes asserted entities.",
|
|
2005
|
+
subtask_prompt: [
|
|
2006
|
+
"Review staging knowledge-graph proposals for evidence quality, over-claiming, missing inventory coverage, and unsafe edges.",
|
|
2007
|
+
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
2008
|
+
"pass means proposals are ready for human promotion consideration — it does NOT mark entities asserted.",
|
|
2009
|
+
"request-revision if critical entities lack evidence, ids collide conceptually, or formal trees were written outside staging.",
|
|
2010
|
+
"Read-only: do not modify files.",
|
|
2011
|
+
buildSourceContextBlock(sources),
|
|
2012
|
+
].join("\n\n"),
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
function buildKgBootstrapReviewGateNode(sources) {
|
|
2016
|
+
return {
|
|
2017
|
+
id: "kg-bootstrap-review-gate-shell",
|
|
2018
|
+
depends_on: ["kg-bootstrap-review-pi"],
|
|
2019
|
+
role: "verifier",
|
|
2020
|
+
executor: "shell",
|
|
2021
|
+
complexity: "LOW",
|
|
2022
|
+
writePolicy: "read-only",
|
|
2023
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
2024
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2025
|
+
outputContract: "Deterministic gate: exit 0 only when kg-bootstrap-review-pi first verdict line is VERDICT: pass.",
|
|
2026
|
+
subtask_prompt: "Block promote/materialize unless review verdict is pass.",
|
|
2027
|
+
shell: {
|
|
2028
|
+
commands: [],
|
|
2029
|
+
verdictGate: {
|
|
2030
|
+
fromNodeId: "kg-bootstrap-review-pi",
|
|
2031
|
+
accept: ["VERDICT: pass"],
|
|
2032
|
+
label: "kg-bootstrap-review",
|
|
2033
|
+
lineMode: "first-verdict-line",
|
|
2034
|
+
},
|
|
2035
|
+
cwd: ".",
|
|
2036
|
+
timeoutMs: 60000,
|
|
2037
|
+
},
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
function buildKgBootstrapPromoteNode(sources) {
|
|
2041
|
+
const script = buildKgBootstrapInlineNodeScript([
|
|
2042
|
+
"const fs=require('fs');",
|
|
2043
|
+
"const path=require('path');",
|
|
2044
|
+
"const root=process.cwd();",
|
|
2045
|
+
"const staging=path.join(root,'knowledge','bootstrap','staging');",
|
|
2046
|
+
"function copyDir(src,dest){",
|
|
2047
|
+
" if(!fs.existsSync(src)) return 0;",
|
|
2048
|
+
" let n=0;",
|
|
2049
|
+
" fs.mkdirSync(dest,{recursive:true});",
|
|
2050
|
+
" for(const ent of fs.readdirSync(src,{withFileTypes:true})){",
|
|
2051
|
+
" const s=path.join(src,ent.name), d=path.join(dest,ent.name);",
|
|
2052
|
+
" if(ent.isDirectory()) n+=copyDir(s,d);",
|
|
2053
|
+
" else { if(!fs.existsSync(d)){ fs.mkdirSync(path.dirname(d),{recursive:true}); fs.copyFileSync(s,d); n++; } }",
|
|
2054
|
+
" }",
|
|
2055
|
+
" return n;",
|
|
2056
|
+
"}",
|
|
2057
|
+
"let copied=0;",
|
|
2058
|
+
"copied+=copyDir(path.join(staging,'domains'), path.join(root,'knowledge','domains'));",
|
|
2059
|
+
"copied+=copyDir(path.join(staging,'services'), path.join(root,'knowledge','services'));",
|
|
2060
|
+
"copied+=copyDir(path.join(staging,'modules'), path.join(root,'knowledge','modules'));",
|
|
2061
|
+
"const edgesSrc=path.join(staging,'edges.proposed.yaml');",
|
|
2062
|
+
"const edgesManual=path.join(root,'knowledge','graph','edges.manual.yaml');",
|
|
2063
|
+
"if(fs.existsSync(edgesSrc)){ fs.mkdirSync(path.dirname(edgesManual),{recursive:true}); if(!fs.existsSync(edgesManual)) fs.copyFileSync(edgesSrc,edgesManual); }",
|
|
2064
|
+
"const linksDir=path.join(staging,'features-links.proposed');",
|
|
2065
|
+
"if(fs.existsSync(linksDir)){",
|
|
2066
|
+
" for(const f of fs.readdirSync(linksDir)){",
|
|
2067
|
+
" if(!f.endsWith('.yaml')&&!f.endsWith('.yml')) continue;",
|
|
2068
|
+
" const id=f.replace(/\\.ya?ml$/,'');",
|
|
2069
|
+
" const dest=path.join(root,'features',id,'knowledge-links.yaml');",
|
|
2070
|
+
" if(!fs.existsSync(path.join(root,'features',id))) continue;",
|
|
2071
|
+
" if(!fs.existsSync(dest)){ fs.mkdirSync(path.dirname(dest),{recursive:true}); fs.copyFileSync(path.join(linksDir,f),dest); copied++; }",
|
|
2072
|
+
" }",
|
|
2073
|
+
"}",
|
|
2074
|
+
"console.log('kg-bootstrap promote copied_new_files='+copied+' (existing asserted targets skipped)');",
|
|
2075
|
+
]);
|
|
2076
|
+
return {
|
|
2077
|
+
id: "kg-bootstrap-promote-shell",
|
|
2078
|
+
depends_on: ["kg-bootstrap-review-gate-shell"],
|
|
2079
|
+
role: "verifier",
|
|
2080
|
+
executor: "shell",
|
|
2081
|
+
complexity: "LOW",
|
|
2082
|
+
writePolicy: "exclusive",
|
|
2083
|
+
writeSet: [
|
|
2084
|
+
"knowledge/domains/**",
|
|
2085
|
+
"knowledge/services/**",
|
|
2086
|
+
"knowledge/modules/**",
|
|
2087
|
+
"knowledge/graph/edges.manual.yaml",
|
|
2088
|
+
"features/**/knowledge-links.yaml",
|
|
2089
|
+
],
|
|
2090
|
+
allowedPaths: [
|
|
2091
|
+
"knowledge/bootstrap/staging/**",
|
|
2092
|
+
"knowledge/domains/**",
|
|
2093
|
+
"knowledge/services/**",
|
|
2094
|
+
"knowledge/modules/**",
|
|
2095
|
+
"knowledge/graph/**",
|
|
2096
|
+
"features/**",
|
|
2097
|
+
],
|
|
2098
|
+
forbiddenPaths: [
|
|
2099
|
+
...commonForbiddenPaths(sources),
|
|
2100
|
+
"src/**",
|
|
2101
|
+
"testcase/**",
|
|
2102
|
+
],
|
|
2103
|
+
outputContract: "Promote staging → formal knowledge trees without overwriting existing files; copy edges.manual.yaml if absent.",
|
|
2104
|
+
subtask_prompt: "B5 promote: merge new files only (no overwrite of existing asserted content).",
|
|
2105
|
+
shell: {
|
|
2106
|
+
commands: [script],
|
|
2107
|
+
verifyEvidence: buildVerifyEvidence({
|
|
2108
|
+
phase: "final",
|
|
2109
|
+
quota: "full",
|
|
2110
|
+
commandSource: "inline",
|
|
2111
|
+
fallbackCommands: [script],
|
|
2112
|
+
finalFullRequired: true,
|
|
2113
|
+
}),
|
|
2114
|
+
cwd: ".",
|
|
2115
|
+
timeoutMs: 120000,
|
|
2116
|
+
},
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
function buildKgBootstrapMaterializeNode(sources) {
|
|
2120
|
+
const script = buildKgBootstrapInlineNodeScript([
|
|
2121
|
+
"const fs=require('fs');",
|
|
2122
|
+
"const path=require('path');",
|
|
2123
|
+
"const root=process.cwd();",
|
|
2124
|
+
"const entities=[];",
|
|
2125
|
+
"function walk(dir,acc=[]){if(!fs.existsSync(dir))return acc;for(const ent of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,ent.name);if(ent.isDirectory())walk(p,acc);else acc.push(p);}return acc;}",
|
|
2126
|
+
"for (const base of ['knowledge/domains','knowledge/services','knowledge/modules']) {",
|
|
2127
|
+
" for (const f of walk(path.join(root,base))) {",
|
|
2128
|
+
" if(!f.endsWith('meta.yaml')&&!f.endsWith('.md')) continue;",
|
|
2129
|
+
" const rel=path.relative(root,f).split(path.sep).join('/');",
|
|
2130
|
+
" const text=fs.existsSync(f)&&f.endsWith('meta.yaml')?fs.readFileSync(f,'utf8'):'';",
|
|
2131
|
+
" const id=(text.match(/^id:\\s*([A-Za-z0-9_-]+)/m)||[])[1];",
|
|
2132
|
+
" const kind=(text.match(/^kind:\\s*([A-Za-z0-9_-]+)/m)||[])[1];",
|
|
2133
|
+
" if(id) entities.push({kind:kind||'unknown',id,path:rel});",
|
|
2134
|
+
" }",
|
|
2135
|
+
"}",
|
|
2136
|
+
"const featRoot=path.join(root,'features');",
|
|
2137
|
+
"if(fs.existsSync(featRoot)){",
|
|
2138
|
+
" for(const name of fs.readdirSync(featRoot)){",
|
|
2139
|
+
" if(!/^F-/.test(name)) continue;",
|
|
2140
|
+
" entities.push({kind:'feature',id:name,path:'features/'+name+'/'} );",
|
|
2141
|
+
" const acc=path.join(featRoot,name,'acceptance.yaml');",
|
|
2142
|
+
" if(fs.existsSync(acc)) entities.push({kind:'ac-file',id:name+':acceptance',path:'features/'+name+'/acceptance.yaml',feature_id:name});",
|
|
2143
|
+
" }",
|
|
2144
|
+
"}",
|
|
2145
|
+
"const graphDir=path.join(root,'knowledge','graph');",
|
|
2146
|
+
"fs.mkdirSync(graphDir,{recursive:true});",
|
|
2147
|
+
"const index={schema_version:1,generated_at:new Date().toISOString(),entities};",
|
|
2148
|
+
"fs.writeFileSync(path.join(graphDir,'entities-index.yaml'),'# generated by kg-bootstrap-materialize\\nschema_version: 1\\ngenerated_at: '+index.generated_at+'\\nentities:\\n'+entities.map(e=>' - kind: '+e.kind+'\\n id: '+e.id+'\\n path: '+e.path+'\\n').join(''));",
|
|
2149
|
+
"const manual=path.join(graphDir,'edges.manual.yaml');",
|
|
2150
|
+
"const edgesOut=path.join(graphDir,'edges.yaml');",
|
|
2151
|
+
"if(fs.existsSync(manual)) fs.copyFileSync(manual,edgesOut);",
|
|
2152
|
+
"else fs.writeFileSync(edgesOut,'schema_version: 1\\nupdated_at: '+index.generated_at+'\\nedges: []\\n');",
|
|
2153
|
+
"console.log('materialized entities='+entities.length);",
|
|
2154
|
+
]);
|
|
2155
|
+
return {
|
|
2156
|
+
id: "kg-bootstrap-materialize-shell",
|
|
2157
|
+
depends_on: ["kg-bootstrap-promote-shell"],
|
|
2158
|
+
role: "closeout",
|
|
2159
|
+
executor: "shell",
|
|
2160
|
+
complexity: "LOW",
|
|
2161
|
+
writePolicy: "exclusive",
|
|
2162
|
+
writeSet: [
|
|
2163
|
+
"knowledge/graph/entities-index.yaml",
|
|
2164
|
+
"knowledge/graph/edges.yaml",
|
|
2165
|
+
],
|
|
2166
|
+
allowedPaths: ["knowledge/**", "features/**"],
|
|
2167
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2168
|
+
outputContract: "Write knowledge/graph/entities-index.yaml and edges.yaml from formal trees + edges.manual.yaml.",
|
|
2169
|
+
subtask_prompt: "B6 materialize graph indexes for kb-query.",
|
|
2170
|
+
shell: {
|
|
2171
|
+
commands: [script],
|
|
2172
|
+
verifyEvidence: buildVerifyEvidence({
|
|
2173
|
+
phase: "final",
|
|
2174
|
+
quota: "full",
|
|
2175
|
+
commandSource: "inline",
|
|
2176
|
+
fallbackCommands: [script],
|
|
2177
|
+
finalFullRequired: true,
|
|
2178
|
+
}),
|
|
2179
|
+
cwd: ".",
|
|
2180
|
+
timeoutMs: 120000,
|
|
2181
|
+
},
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
function buildKnowledgeGraphBootstrapHybridDag(sources) {
|
|
2185
|
+
const { taskConfig } = sources;
|
|
2186
|
+
const globalConstraints = [
|
|
2187
|
+
...taskConfig.hardConstraints,
|
|
2188
|
+
...(sources.constraintMarkdown
|
|
2189
|
+
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
2190
|
+
: []),
|
|
2191
|
+
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
2192
|
+
"knowledge-graph-bootstrap-dag: AI may write only knowledge/bootstrap/staging/** and runs/** until promote.",
|
|
2193
|
+
"AI must never set confidence: asserted; only human promotion marks formal knowledge.",
|
|
2194
|
+
"Do not modify src/**, testcase/**, or Feature testing verdict/cases via bootstrap.",
|
|
2195
|
+
"Promote must not overwrite existing formal files (merge-new-only).",
|
|
2196
|
+
"Require knowledge/bootstrap/scope.yaml before propose (B0/B1 skeleton).",
|
|
2197
|
+
"Graph indexes are written only by materialize-shell, not by propose-pi.",
|
|
2198
|
+
];
|
|
2199
|
+
const spec = {
|
|
2200
|
+
version: 2,
|
|
2201
|
+
title: `Knowledge-graph bootstrap DAG: ${taskConfig.title}`,
|
|
2202
|
+
outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
|
|
2203
|
+
objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
|
|
2204
|
+
successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
|
|
2205
|
+
globalConstraints,
|
|
2206
|
+
defaults: {
|
|
2207
|
+
...KG_BOOTSTRAP_DEFAULTS,
|
|
2208
|
+
contextProfile: taskConfig.contextProfile,
|
|
2209
|
+
},
|
|
2210
|
+
skillsByRole: KG_BOOTSTRAP_SKILLS_BY_ROLE,
|
|
2211
|
+
executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
|
|
2212
|
+
tasks: [
|
|
2213
|
+
buildKgBootstrapPreflightNode(sources),
|
|
2214
|
+
buildKgBootstrapInventoryNode(sources),
|
|
2215
|
+
buildKgBootstrapProposeNode(sources),
|
|
2216
|
+
buildKgBootstrapValidateNode(sources),
|
|
2217
|
+
buildKgBootstrapReviewNode(sources),
|
|
2218
|
+
buildKgBootstrapReviewGateNode(sources),
|
|
2219
|
+
buildKgBootstrapPromoteNode(sources),
|
|
2220
|
+
buildKgBootstrapMaterializeNode(sources),
|
|
2221
|
+
],
|
|
2222
|
+
};
|
|
1031
2223
|
parseDagSpec(spec);
|
|
1032
2224
|
assertValidDagSpec(spec);
|
|
1033
2225
|
return spec;
|
|
@@ -1037,6 +2229,18 @@ export function buildHybridDagFromTask(sources, options = {}) {
|
|
|
1037
2229
|
options.template === "frontend-implementation") {
|
|
1038
2230
|
return buildFrontendHybridDagFromTask(sources);
|
|
1039
2231
|
}
|
|
2232
|
+
if (sources.taskConfig.taskKind === "backend-test" ||
|
|
2233
|
+
options.template === "backend-test-dag") {
|
|
2234
|
+
return buildBackendTestHybridDag(sources);
|
|
2235
|
+
}
|
|
2236
|
+
if (sources.taskConfig.taskKind === "knowledge-sync" ||
|
|
2237
|
+
options.template === "knowledge-sync-dag") {
|
|
2238
|
+
return buildKnowledgeSyncHybridDag(sources);
|
|
2239
|
+
}
|
|
2240
|
+
if (sources.taskConfig.taskKind === "knowledge-graph-bootstrap" ||
|
|
2241
|
+
options.template === "knowledge-graph-bootstrap-dag") {
|
|
2242
|
+
return buildKnowledgeGraphBootstrapHybridDag(sources);
|
|
2243
|
+
}
|
|
1040
2244
|
const standard = buildStandardHybridDagFromTask(sources);
|
|
1041
2245
|
const template = options.template ?? "standard-dag";
|
|
1042
2246
|
if (template === "standard-dag")
|
|
@@ -1048,6 +2252,21 @@ export function buildHybridDagFromTask(sources, options = {}) {
|
|
|
1048
2252
|
function cloneTask(task, patch = {}) {
|
|
1049
2253
|
return { ...task, ...patch };
|
|
1050
2254
|
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Apply the default read-only Pi retry policy to safe planner/scout/reviewer/
|
|
2257
|
+
* verifier/closeout Pi nodes in the generated DAG. Writers, supervisors,
|
|
2258
|
+
* dynamic, shell, static, and decision-gate nodes are skipped. Idempotent:
|
|
2259
|
+
* never overwrites an explicit retryPolicy a task already declares.
|
|
2260
|
+
*/
|
|
2261
|
+
function applyDefaultReadOnlyRetryPolicy(spec) {
|
|
2262
|
+
for (const task of spec.tasks) {
|
|
2263
|
+
if (task.retryPolicy !== undefined)
|
|
2264
|
+
continue;
|
|
2265
|
+
if (isSafeReadOnlyPiRetryCandidate(task)) {
|
|
2266
|
+
task.retryPolicy = DEFAULT_READ_ONLY_PI_RETRY_POLICY;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
1051
2270
|
function getTaskOrThrow(spec, id) {
|
|
1052
2271
|
const task = spec.tasks.find((item) => item.id === id);
|
|
1053
2272
|
if (!task)
|
|
@@ -1135,6 +2354,7 @@ function buildReviewGatedHybridDag(standard, sources) {
|
|
|
1135
2354
|
replaceTask(spec, cloneTask(closeout, { depends_on: ["review-gate-shell"] }));
|
|
1136
2355
|
spec.tasks.splice(spec.tasks.length - 1, 0, buildReviewNode(sources), buildReviewGateNode(sources));
|
|
1137
2356
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
2357
|
+
applyDefaultReadOnlyRetryPolicy(spec);
|
|
1138
2358
|
parseDagSpec(spec);
|
|
1139
2359
|
assertValidDagSpec(spec);
|
|
1140
2360
|
return spec;
|
|
@@ -1263,6 +2483,7 @@ function buildProcessGateNode(sources) {
|
|
|
1263
2483
|
},
|
|
1264
2484
|
repairArtifactGate: {
|
|
1265
2485
|
fromNodeId: "process-supervisor-pi",
|
|
2486
|
+
repairNodeId: repairNodeId(),
|
|
1266
2487
|
},
|
|
1267
2488
|
cwd: ".",
|
|
1268
2489
|
timeoutMs: 60000,
|
|
@@ -1405,6 +2626,7 @@ function buildSupervisedHybridDag(standard, sources) {
|
|
|
1405
2626
|
],
|
|
1406
2627
|
};
|
|
1407
2628
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
2629
|
+
applyDefaultReadOnlyRetryPolicy(spec);
|
|
1408
2630
|
parseDagSpec(spec);
|
|
1409
2631
|
assertValidDagSpec(spec);
|
|
1410
2632
|
return spec;
|