@tea-agent/loop-agent 0.12.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 +50 -2
- package/README.md +30 -2
- package/dist/application/dag/generate-task-dag.js +30 -0
- package/dist/cli/command-definitions.js +10 -3
- package/dist/commands/knowledge.js +129 -31
- package/dist/governance/manifest-types.js +3 -0
- package/dist/task/config-types.js +5 -1
- package/dist/worker/cli.js +96 -1
- 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/read-model.js +246 -41
- package/dist/worker/observe/routes.js +158 -15
- package/dist/worker/observe/spec-evidence.js +281 -0
- package/dist/worker/observe/static/api.js +19 -0
- package/dist/worker/observe/static/app.js +2 -2
- package/dist/worker/observe/static/relations.js +17 -12
- package/dist/worker/observe/static/router.js +8 -0
- package/dist/worker/observe/static/styles.css +12 -0
- package/dist/worker/observe/static/views/batch.js +3 -2
- package/dist/worker/observe/static/views/dag-inspector.js +123 -4
- package/dist/worker/observe/static/views/dashboard.js +8 -5
- package/dist/worker/observe/static/views/feature.js +43 -4
- package/dist/worker/observe/static/views/pool.js +5 -2
- package/dist/worker/observe/static/views/run.js +1 -1
- package/dist/worker/observe/static/views/task.js +69 -15
- 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/convergence/controller.js +16 -8
- package/dist/workflows/dag/failure-routing.js +12 -1
- package/dist/workflows/dag/init-hybrid.js +837 -8
- package/dist/workflows/dag/types.js +1 -0
- package/docs/README.md +1 -1
- package/docs/agent-dag-recovery-playbook.md +9 -0
- package/docs/architecture/evolution.md +4 -3
- package/docs/architecture/facts-and-state.md +14 -1
- package/docs/architecture/worker-and-feature.md +6 -2
- package/docs/decisions/README.md +3 -0
- package/docs/design/README.md +8 -0
- package/docs/exec-plans/active/README.md +2 -0
- package/docs/exec-plans/completed/README.md +3 -2
- package/docs/feature-workflow.md +80 -2
- package/docs/loop-agent-harness.md +15 -4
- package/docs/progress/README.md +4 -0
- package/docs/reports/README.md +6 -0
- package/docs/templates/backend-test-dag.json +12 -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 +1 -0
|
@@ -773,7 +773,10 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
773
773
|
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
774
774
|
: []),
|
|
775
775
|
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
776
|
-
"
|
|
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.",
|
|
777
780
|
"frontend-implementation DAGs must complete deterministic static verification and behavior verification before final review.",
|
|
778
781
|
"frontend review must block closeout unless review verdict is exactly VERDICT: pass.",
|
|
779
782
|
];
|
|
@@ -881,7 +884,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
881
884
|
].join("\n\n"),
|
|
882
885
|
},
|
|
883
886
|
{
|
|
884
|
-
id: "frontend-design-gate-shell",
|
|
887
|
+
id: "frontend-first-design-gate-shell",
|
|
885
888
|
depends_on: ["frontend-design-gate-pi"],
|
|
886
889
|
role: "verifier",
|
|
887
890
|
executor: "shell",
|
|
@@ -889,14 +892,81 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
889
892
|
writePolicy: "read-only",
|
|
890
893
|
allowedPaths: readOnlyPaths,
|
|
891
894
|
forbiddenPaths,
|
|
892
|
-
outputContract: "Deterministic
|
|
893
|
-
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.",
|
|
894
897
|
shell: {
|
|
895
898
|
commands: [],
|
|
896
899
|
verdictGate: {
|
|
897
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",
|
|
898
968
|
accept: ["VERDICT: pass"],
|
|
899
|
-
label: "frontend design gate",
|
|
969
|
+
label: "frontend final design gate",
|
|
900
970
|
lineMode: "first-verdict-line",
|
|
901
971
|
},
|
|
902
972
|
cwd: ".",
|
|
@@ -905,7 +975,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
905
975
|
},
|
|
906
976
|
{
|
|
907
977
|
id: implementId,
|
|
908
|
-
depends_on: ["frontend-design-gate-shell"],
|
|
978
|
+
depends_on: ["frontend-final-design-gate-shell", "frontend-plan-revision-pi", "frontend-final-design-review-pi"],
|
|
909
979
|
role: "implementer",
|
|
910
980
|
executor: "pi",
|
|
911
981
|
toolProfile: "write",
|
|
@@ -919,8 +989,9 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
919
989
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
920
990
|
outputContract: "Markdown summary with Changed Files, Implemented Behavior, UI States Covered, Styling / Component Notes, Verification Attempted, and Residual Risks.",
|
|
921
991
|
subtask_prompt: [
|
|
922
|
-
"Implement the approved frontend plan with minimal focused changes.",
|
|
923
|
-
"
|
|
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.",
|
|
924
995
|
sourceContext,
|
|
925
996
|
].join("\n\n"),
|
|
926
997
|
},
|
|
@@ -1382,6 +1453,18 @@ function buildBackendTestHybridDag(sources) {
|
|
|
1382
1453
|
objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
|
|
1383
1454
|
successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
|
|
1384
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
|
+
},
|
|
1385
1468
|
defaults: {
|
|
1386
1469
|
...BACKEND_TEST_DEFAULTS,
|
|
1387
1470
|
contextProfile: taskConfig.contextProfile,
|
|
@@ -1403,6 +1486,744 @@ function buildBackendTestHybridDag(sources) {
|
|
|
1403
1486
|
assertValidDagSpec(spec);
|
|
1404
1487
|
return spec;
|
|
1405
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
|
+
};
|
|
2223
|
+
parseDagSpec(spec);
|
|
2224
|
+
assertValidDagSpec(spec);
|
|
2225
|
+
return spec;
|
|
2226
|
+
}
|
|
1406
2227
|
export function buildHybridDagFromTask(sources, options = {}) {
|
|
1407
2228
|
if (sources.taskConfig.taskKind === "frontend-implementation" ||
|
|
1408
2229
|
options.template === "frontend-implementation") {
|
|
@@ -1412,6 +2233,14 @@ export function buildHybridDagFromTask(sources, options = {}) {
|
|
|
1412
2233
|
options.template === "backend-test-dag") {
|
|
1413
2234
|
return buildBackendTestHybridDag(sources);
|
|
1414
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
|
+
}
|
|
1415
2244
|
const standard = buildStandardHybridDagFromTask(sources);
|
|
1416
2245
|
const template = options.template ?? "standard-dag";
|
|
1417
2246
|
if (template === "standard-dag")
|