@godv61/dsh-task-engine 0.24.0 → 0.25.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/lib/client.js CHANGED
@@ -4557,6 +4557,14 @@ var workbenchTheme = `
4557
4557
  .te-binding-tools .te-input { flex:1; width:100%; min-width:120px; padding:8px 10px; font-size:13px; }
4558
4558
  .te-binding-filter { display:flex; align-items:center; gap:5px; font-size:12px; white-space:nowrap; cursor:pointer; }
4559
4559
  .te-binding-list { max-height:280px; overflow-y:auto; overscroll-behavior:contain; display:flex; flex-direction:column; gap:4px; scrollbar-gutter:stable; }
4560
+ /* Search + filter row. The catalog grows with every install, so finding one skill
4561
+ has to be a lookup rather than a scroll. */
4562
+ .te-binding-controls { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin:0 0 6px; }
4563
+ .te-binding-search { flex:1; min-width:120px; padding:6px 9px; font-size:12px; color:var(--dsw-alias-label-primary); background:var(--dsw-alias-bg-layer-1); border:1px solid var(--dsw-alias-border-l2); border-radius:7px; }
4564
+ .te-binding-search:focus-visible { outline:2px solid var(--dsw-alias-state-business-primary); outline-offset:1px; }
4565
+ /* Only the open skill's rules render, and inside a bounded region: expanding a
4566
+ second skill must not push the first off screen or grow the page without limit. */
4567
+ .te-rule-list { display:flex; flex-direction:column; gap:4px; margin-top:6px; padding:8px; max-height:240px; overflow-y:auto; overscroll-behavior:contain; border:1px solid var(--dsw-alias-border-l2); border-radius:8px; background:var(--dsw-alias-bg-layer-1); scrollbar-gutter:stable; }
4560
4568
  .te-binding-option { display:flex; gap:9px; align-items:flex-start; padding:9px; border:1px solid transparent; border-radius:8px; cursor:pointer; }
4561
4569
  .te-binding-option:hover { background:var(--dsw-alias-bg-layer-2); }
4562
4570
  .te-binding-option.is-selected { background:var(--dsw-alias-bg-layer-2); border-color:var(--dsw-alias-border-l2); }
@@ -4695,6 +4703,10 @@ var styles = {
4695
4703
  },
4696
4704
  // ── flow preset picker ────────────────────────────────────────────────────
4697
4705
  flowGrid: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(230px, 1fr))", gap: 10 },
4706
+ // The adoption row is deliberately separated from the flow picker: choosing a
4707
+ // skeleton and adopting a recommended setup are different decisions, and the
4708
+ // button is the only thing that writes method into the user's config.
4709
+ adoptRow: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap", marginBottom: 4 },
4698
4710
  flowOption: {
4699
4711
  display: "flex",
4700
4712
  flexDirection: "column",
@@ -4871,19 +4883,23 @@ var STANDARD = {
4871
4883
  { from: "\u4EA4\u4ED8", to: "\u4EE3\u7801\u5BA1\u6838", requires: ["verified"] },
4872
4884
  { from: "\u4EE3\u7801\u5BA1\u6838", to: "\u5B8C\u6210", requires: ["review_passed", "artifacts_present"] }
4873
4885
  ],
4886
+ // No artifacts, no commit policy, no skills: the skeleton carries none of them.
4887
+ artifacts: [],
4888
+ commit: { policy: "task", message_pattern: "", message_hint: "\u65E0\u683C\u5F0F\u8981\u6C42", checkpoints: ["\u4EE3\u7801\u5BA1\u6838"], file_scope: true },
4889
+ high_risk_requires_verification: true
4890
+ };
4891
+ var STANDARD_RECOMMENDATION = {
4892
+ commit: {
4893
+ // Only the TEXT conventions. Policy, checkpoints and scope stay on the
4894
+ // skeleton: they decide when a gate fires, not how a message reads.
4895
+ message_pattern: "^\u3010(\\S+)\u3011\u3010(?:TASK|T\\d+)\u3011.+",
4896
+ message_hint: "\u3010<task_id>\u3011\u3010TASK/T1\u3011\u8BF4\u660E \u2014\u2014 \u7B2C\u4E00\u6BB5\u586B\u672C\u4EFB\u52A1 id\uFF08\u5982 GREET-001\uFF09\uFF0C\u5199\u7ED3\u679C\u4E0D\u5199\u7A7A\u6CDB\u52A8\u4F5C"
4897
+ },
4874
4898
  artifacts: [
4875
4899
  { stage: "\u9700\u6C42\u8BC4\u5BA1", id: "requirement", name: "\u9700\u6C42\u8BF4\u660E", fields: ["scope", "acceptance_criteria"] },
4876
4900
  { stage: "\u8BBE\u8BA1", id: "design", name: "\u8BBE\u8BA1\u6587\u6863", fields: ["approach", "risks", "impact"] },
4877
4901
  { stage: "\u4EE3\u7801\u5BA1\u6838", id: "review", name: "\u8BC4\u5BA1\u8BB0\u5F55", fields: ["conclusion", "issues"] }
4878
4902
  ],
4879
- commit: {
4880
- policy: "task",
4881
- message_pattern: "^\u3010(\\S+)\u3011\u3010(?:TASK|T\\d+)\u3011.+",
4882
- message_hint: "\u3010<task_id>\u3011\u3010TASK/T1\u3011\u8BF4\u660E \u2014\u2014 \u7B2C\u4E00\u6BB5\u586B\u672C\u4EFB\u52A1 id\uFF08\u5982 GREET-001\uFF09\uFF0C\u5199\u7ED3\u679C\u4E0D\u5199\u7A7A\u6CDB\u52A8\u4F5C",
4883
- checkpoints: ["\u4EE3\u7801\u5BA1\u6838"],
4884
- file_scope: true
4885
- },
4886
- high_risk_requires_verification: true,
4887
4903
  stage_bindings: {
4888
4904
  "\u9700\u6C42\u8BC4\u5BA1": { skills: [bundled("requirement-analysis", "security-redlines")] },
4889
4905
  "\u8BBE\u8BA1": { skills: [bundled("solution-design")] },
@@ -4903,30 +4919,38 @@ var AGILE = {
4903
4919
  transitions: [
4904
4920
  { from: "\u9700\u6C42", to: "\u5F00\u53D1", requires: ["requirement_confirmation"] },
4905
4921
  { from: "\u5F00\u53D1", to: "\u4EA4\u4ED8", requires: ["todos_done"] },
4922
+ // Entering 审查 is unconditional. A review guard on this edge would be
4923
+ // circular — it would demand the verdict BEFORE the stage that produces it —
4924
+ // and it would also block the commit at 交付, since a commit requires its
4925
+ // stage's outgoing guards. What 审查 requires is declared as completion_guards
4926
+ // instead, because a terminal stage has no outgoing edge to carry it.
4906
4927
  { from: "\u4EA4\u4ED8", to: "\u5BA1\u67E5", requires: [] }
4907
4928
  ],
4929
+ artifacts: [],
4930
+ commit: { policy: "item", message_pattern: "", message_hint: "\u65E0\u683C\u5F0F\u8981\u6C42", checkpoints: ["\u4EA4\u4ED8"], file_scope: true },
4931
+ high_risk_requires_verification: false,
4932
+ // 审查 is terminal and IS the review, so there is no outgoing edge to carry
4933
+ // the guard. Declaring it here is what makes a blocked review prevent
4934
+ // completion; without it the stage could be reached and finished with the
4935
+ // review failing, which is what the assessment found.
4936
+ completion_guards: ["review_passed"]
4937
+ };
4938
+ var AGILE_RECOMMENDATION = {
4939
+ commit: {
4940
+ // `item` policy emits two label shapes: `T<n>` mid-flow and `TASK` at the
4941
+ // closing checkpoint. The pattern accepts BOTH, so the label the engine hands
4942
+ // the model always satisfies the rule that validates it.
4943
+ message_pattern: "^\u3010(\\S+)\u3011\u3010(?:TASK|T\\d+)\u3011.+",
4944
+ message_hint: "\u3010<task_id>\u3011\u3010TASK/T1\u3011\u8BF4\u660E \u2014\u2014 \u7B2C\u4E00\u6BB5\u586B\u672C\u4EFB\u52A1 id\uFF1B\u5B9E\u65BD\u9879\u63D0\u4EA4\u7528 T1\u3001T2\uFF0C\u6536\u5C3E\u63D0\u4EA4\u7528 TASK"
4945
+ },
4908
4946
  artifacts: [
4909
4947
  { stage: "\u9700\u6C42", id: "requirement", name: "\u9700\u6C42\u8BF4\u660E", fields: ["scope"] },
4910
4948
  // The 审查 stage binds code-review, whose instructions write this artifact with
4911
- // `record artifact=review`. Declaring it here keeps that binding executable:
4912
- // the engine refuses an undeclared artifact, so a bound skill pointing at one
4913
- // would be a contract the preset itself made impossible to satisfy.
4949
+ // `record artifact=review`. Declaring it keeps that binding executable: the
4950
+ // engine refuses an undeclared artifact, so a bound skill pointing at one would
4951
+ // be a contract the recommendation made impossible to satisfy.
4914
4952
  { stage: "\u5BA1\u67E5", id: "review", name: "\u8BC4\u5BA1\u8BB0\u5F55", fields: ["conclusion", "issues"] }
4915
4953
  ],
4916
- commit: {
4917
- // `item` policy emits two label shapes: `T<n>` for a mid-flow item commit and
4918
- // `TASK` for the closing commit at the checkpoint. The pattern below accepts
4919
- // BOTH, so the label the engine hands the model always satisfies the preset's
4920
- // own rule. Previously it accepted only `T\d+` while the checkpoint stage
4921
- // returned `TASK`, which made the closing commit impossible to phrase: the
4922
- // status line and the validator disagreed about the contract.
4923
- policy: "item",
4924
- message_pattern: "^\u3010(\\S+)\u3011\u3010(?:TASK|T\\d+)\u3011.+",
4925
- message_hint: "\u3010<task_id>\u3011\u3010TASK/T1\u3011\u8BF4\u660E \u2014\u2014 \u7B2C\u4E00\u6BB5\u586B\u672C\u4EFB\u52A1 id\uFF1B\u5B9E\u65BD\u9879\u63D0\u4EA4\u7528 T1\u3001T2\uFF0C\u6536\u5C3E\u63D0\u4EA4\u7528 TASK",
4926
- checkpoints: ["\u4EA4\u4ED8"],
4927
- file_scope: true
4928
- },
4929
- high_risk_requires_verification: false,
4930
4954
  stage_bindings: {
4931
4955
  "\u9700\u6C42": { skills: [bundled("requirement-analysis", "security-redlines")] },
4932
4956
  "\u5F00\u53D1": { skills: [bundled("code-implement", "coding-conventions", "security-redlines")] },
@@ -4941,18 +4965,13 @@ var MINIMAL = {
4941
4965
  { from: "\u5F00\u53D1", to: "\u4EA4\u4ED8", requires: ["todos_done"] }
4942
4966
  ],
4943
4967
  artifacts: [],
4944
- commit: {
4945
- policy: "task",
4946
- message_pattern: "",
4947
- message_hint: "\u65E0\u683C\u5F0F\u8981\u6C42",
4948
- checkpoints: ["\u4EA4\u4ED8"],
4949
- file_scope: false
4950
- },
4951
- high_risk_requires_verification: false,
4968
+ commit: { policy: "task", message_pattern: "", message_hint: "\u65E0\u683C\u5F0F\u8981\u6C42", checkpoints: ["\u4EA4\u4ED8"], file_scope: false },
4969
+ high_risk_requires_verification: false
4970
+ };
4971
+ var MINIMAL_RECOMMENDATION = {
4952
4972
  // A fast-change flow checks each change once, not twice under two headings a
4953
4973
  // short change rarely distinguishes. The item still has to be reviewed — what
4954
- // drops is the duplicated verdict, which is where the weight actually was: this
4955
- // flow used to carry the same per-item audit burden as the full one.
4974
+ // drops is the duplicated verdict, which is where the weight actually was.
4956
4975
  review_depth: "single",
4957
4976
  stage_bindings: {
4958
4977
  "\u5F00\u53D1": { skills: [bundled("code-implement", "coding-conventions", "security-redlines")] },
@@ -4968,54 +4987,81 @@ function deriveCapabilities(config) {
4968
4987
  if (config.commit.file_scope) caps.push("file_scope");
4969
4988
  return caps;
4970
4989
  }
4971
- function preset(id, version, label2, description, config) {
4972
- return { id, version, label: label2, description, capabilities: deriveCapabilities(config), config };
4990
+ function preset(id, version, label2, description, config, recommendation) {
4991
+ return {
4992
+ id,
4993
+ version,
4994
+ label: label2,
4995
+ description,
4996
+ capabilities: deriveCapabilities(config),
4997
+ config,
4998
+ // exactOptionalPropertyTypes: omit rather than assign undefined.
4999
+ ...recommendation !== void 0 ? { recommendation } : {}
5000
+ };
4973
5001
  }
4974
5002
  var FLOW_PRESETS = {
4975
- standard: preset("standard", 2, "\u5B8C\u6574\u7814\u53D1", "\u65B0\u529F\u80FD\u3001\u67B6\u6784\u6216\u8DE8\u6A21\u5757\u6539\u52A8\u3001\u9AD8\u98CE\u9669\u4EFB\u52A1\uFF1A\u9700\u6C42\u786E\u8BA4 \u2192 \u65B9\u6848\u786E\u8BA4 \u2192 \u5B9E\u73B0 \u2192 \u9A8C\u8BC1 \u2192 \u5BA1\u6838 \u2192 \u63D0\u4EA4", STANDARD),
4976
- // Version 2: the closing-commit label shape was unified with the message pattern,
4977
- // and the `review` artifact the bound code-review skill writes was declared. Tasks
4978
- // created before this keep their frozen version 1 config and are unaffected.
4979
- agile: preset("agile", 2, "\u65E5\u5E38\u8FED\u4EE3", "\u76EE\u6807\u660E\u786E\u7684\u5E38\u89C4\u529F\u80FD\u4E0E\u7F3A\u9677\u4FEE\u590D\uFF1A\u76EE\u6807\u4E0E\u9A8C\u6536 \u2192 \u5B9E\u73B0 \u2192 \u9A8C\u6536\u4E0E\u5BA1\u67E5 \u2192 \u63D0\u4EA4", AGILE),
4980
- // Version 2: per-item review depth is declared as `single` instead of inheriting
4981
- // the full flow's two verdicts per item. Tasks created before this keep their
4982
- // frozen version 1 config, which has no `review_depth` and therefore reads as
4983
- // two-stage exactly as it behaved.
4984
- minimal: preset("minimal", 2, "\u5FEB\u901F\u4FEE\u6539", "\u5C40\u90E8\u3001\u4F4E\u98CE\u9669\u3001\u65B9\u6848\u660E\u786E\u7684\u6539\u52A8\uFF1A\u4FEE\u6539 \u2192 \u68C0\u67E5\u4E0E\u63D0\u4EA4\uFF0C\u6BCF\u9879\u4E00\u6B21\u68C0\u67E5", MINIMAL)
5003
+ standard: preset("standard", 3, "\u5B8C\u6574\u7814\u53D1", "\u65B0\u529F\u80FD\u3001\u67B6\u6784\u6216\u8DE8\u6A21\u5757\u6539\u52A8\u3001\u9AD8\u98CE\u9669\u4EFB\u52A1\uFF1A\u9700\u6C42\u786E\u8BA4 \u2192 \u65B9\u6848\u786E\u8BA4 \u2192 \u5B9E\u73B0 \u2192 \u9A8C\u8BC1 \u2192 \u5BA1\u6838 \u2192 \u63D0\u4EA4", STANDARD, STANDARD_RECOMMENDATION),
5004
+ // Version 4 for agile: 交付 → 审查 now requires review_passed, where it previously
5005
+ // required nothing. That is a gate-semantics change, so a task created before it
5006
+ // keeps its frozen version 3 config and behaves exactly as it did.
5007
+ //
5008
+ // Version 3 across all three: the preset is now a bare skeleton and its former
5009
+ // built-in bindings, commit rule and artifacts are a separate recommendation a
5010
+ // user adopts explicitly. Tasks created before this keep their frozen config and
5011
+ // are unaffected; a project's existing config is likewise left exactly as it is.
5012
+ agile: preset("agile", 4, "\u65E5\u5E38\u8FED\u4EE3", "\u76EE\u6807\u660E\u786E\u7684\u5E38\u89C4\u529F\u80FD\u4E0E\u7F3A\u9677\u4FEE\u590D\uFF1A\u76EE\u6807\u4E0E\u9A8C\u6536 \u2192 \u5B9E\u73B0 \u2192 \u9A8C\u6536\u4E0E\u5BA1\u67E5 \u2192 \u63D0\u4EA4", AGILE, AGILE_RECOMMENDATION),
5013
+ minimal: preset("minimal", 3, "\u5FEB\u901F\u4FEE\u6539", "\u5C40\u90E8\u3001\u4F4E\u98CE\u9669\u3001\u65B9\u6848\u660E\u786E\u7684\u6539\u52A8\uFF1A\u4FEE\u6539 \u2192 \u68C0\u67E5\u4E0E\u63D0\u4EA4\uFF0C\u6BCF\u9879\u4E00\u6B21\u68C0\u67E5", MINIMAL, MINIMAL_RECOMMENDATION)
4985
5014
  };
4986
5015
  function dedupe(names) {
4987
5016
  return [...new Set(names)];
4988
5017
  }
4989
- function mergeBindings(base, override) {
4990
- if (override === void 0) return base;
4991
- let changed = false;
4992
- const merged = { ...base.stage_bindings };
4993
- for (const [stage, addition] of Object.entries(override)) {
4994
- const existing = merged[stage];
4995
- const skills = [...existing?.skills ?? []];
4996
- const seen = new Set(skills.map((skill) => formatResourceRef(skill.skill)));
4997
- for (const skill of addition.skills ?? []) {
4998
- const key = formatResourceRef(skill.skill);
4999
- if (seen.has(key)) continue;
5000
- seen.add(key);
5001
- skills.push(skill);
5002
- }
5003
- const legacy = dedupe([...existing?.legacy_rules ?? [], ...addition.legacy_rules ?? []]);
5004
- const binding = {};
5005
- if (skills.length > 0) binding.skills = skills;
5006
- if (legacy.length > 0) binding.legacy_rules = legacy;
5007
- merged[stage] = binding;
5008
- changed = true;
5009
- }
5010
- if (!changed) return base;
5011
- return { ...base, stage_bindings: merged };
5018
+ function applyProjectConfig(base, project) {
5019
+ if (project === void 0) return base;
5020
+ const next = { ...base };
5021
+ if (project.stage_bindings !== void 0) {
5022
+ const bindings = {};
5023
+ for (const [stage, binding] of Object.entries(project.stage_bindings)) {
5024
+ const entry = {};
5025
+ if ((binding.skills ?? []).length > 0) entry.skills = [...binding.skills];
5026
+ const legacy = dedupe(binding.legacy_rules ?? []);
5027
+ if (legacy.length > 0) entry.legacy_rules = legacy;
5028
+ bindings[stage] = entry;
5029
+ }
5030
+ next.stage_bindings = bindings;
5031
+ }
5032
+ if (project.commit !== void 0) next.commit = project.commit;
5033
+ if (project.artifacts !== void 0) next.artifacts = project.artifacts;
5034
+ if (project.review_depth !== void 0) next.review_depth = project.review_depth;
5035
+ if (project.commit_required !== void 0) next.commit_required = project.commit_required;
5036
+ return next;
5037
+ }
5038
+ function adoptRecommendation(presetId, current) {
5039
+ const preset2 = FLOW_PRESETS[presetId];
5040
+ if (preset2 === void 0) return void 0;
5041
+ const rec = preset2.recommendation;
5042
+ if (rec === void 0) return { flow: presetId, ...current };
5043
+ const commit = {
5044
+ ...preset2.config.commit,
5045
+ ...rec.commit ?? {},
5046
+ ...current?.commit ?? {}
5047
+ };
5048
+ const config = {
5049
+ flow: presetId,
5050
+ stage_bindings: current?.stage_bindings ?? rec.stage_bindings ?? {},
5051
+ commit,
5052
+ artifacts: current?.artifacts ?? rec.artifacts ?? preset2.config.artifacts,
5053
+ ...current?.review_depth !== void 0 ? { review_depth: current.review_depth } : rec.review_depth !== void 0 ? { review_depth: rec.review_depth } : {},
5054
+ ...current?.commit_required !== void 0 ? { commit_required: current.commit_required } : {}
5055
+ };
5056
+ return config;
5012
5057
  }
5013
- function resolveFlow(flow, override) {
5058
+ function resolveFlow(flow, project) {
5014
5059
  const preset2 = FLOW_PRESETS[flow];
5015
5060
  if (preset2 === void 0) {
5016
5061
  return { ok: false, code: "UNKNOWN_FLOW", flow, knownFlows: Object.keys(FLOW_PRESETS) };
5017
5062
  }
5018
- return { ok: true, config: mergeBindings(preset2.config, override?.stage_bindings), preset: preset2 };
5063
+ const { flow: _ignored, ...rest } = project ?? { flow };
5064
+ return { ok: true, config: applyProjectConfig(preset2.config, { flow, ...rest }), preset: preset2 };
5019
5065
  }
5020
5066
 
5021
5067
  // src/client/shared.ts
@@ -5031,13 +5077,16 @@ function refFromText(text2) {
5031
5077
  return parseResourceRef(text2) ?? { source: "bundled", name: text2 };
5032
5078
  }
5033
5079
  function resolvedConfig(flow, stageBindings) {
5034
- const resolved = resolveFlow(flow, { stage_bindings: stageBindings });
5080
+ const resolved = resolveFlow(flow, { flow, stage_bindings: stageBindings });
5035
5081
  return resolved.ok ? resolved.config : FLOW_PRESETS.standard.config;
5036
5082
  }
5037
5083
  function TaskEngineSection(props) {
5038
5084
  const { remote, workspace } = props;
5039
5085
  const [flow, setFlow] = (0, import_react.useState)("standard");
5040
5086
  const [stageBindings, setStageBindings] = (0, import_react.useState)({});
5087
+ const [commitRule, setCommitRule] = (0, import_react.useState)(void 0);
5088
+ const [artifacts, setArtifacts] = (0, import_react.useState)(void 0);
5089
+ const [reviewDepth, setReviewDepth] = (0, import_react.useState)(void 0);
5041
5090
  const [source, setSource] = (0, import_react.useState)("default");
5042
5091
  const [savedAt, setSavedAt] = (0, import_react.useState)("");
5043
5092
  const [dirty, setDirty] = (0, import_react.useState)(false);
@@ -5059,6 +5108,9 @@ function TaskEngineSection(props) {
5059
5108
  if (result.ok) {
5060
5109
  setFlow(result.value.flow);
5061
5110
  setStageBindings(result.value.config.stage_bindings ?? {});
5111
+ setCommitRule(result.value.config.commit);
5112
+ setArtifacts(result.value.config.artifacts);
5113
+ setReviewDepth(result.value.config.review_depth);
5062
5114
  setSource(result.value.source);
5063
5115
  setConfigProblems(result.value.problems);
5064
5116
  setSavedAt("");
@@ -5098,7 +5150,17 @@ function TaskEngineSection(props) {
5098
5150
  if (id === flow) return;
5099
5151
  if (dirty && !window.confirm("\u5F53\u524D\u6D41\u7A0B\u914D\u7F6E\u6709\u672A\u4FDD\u5B58\u7684\u4FEE\u6539\uFF0C\u5207\u6362\u6D41\u7A0B\u4F1A\u4E22\u5F03\u5B83\u4EEC\u3002\u7EE7\u7EED\u5207\u6362\uFF1F")) return;
5100
5152
  setFlow(id);
5101
- setStageBindings({ ...FLOW_PRESETS[id]?.config.stage_bindings ?? {} });
5153
+ setDirty(true);
5154
+ setSavedAt("");
5155
+ };
5156
+ const adopt = () => {
5157
+ const adopted = adoptRecommendation(flow);
5158
+ if (adopted === void 0) return;
5159
+ if (dirty && !window.confirm("\u91C7\u7528\u63A8\u8350\u914D\u7F6E\u4F1A\u66FF\u6362\u5F53\u524D\u7684\u6280\u80FD\u4E0E\u89C4\u5219\u8BBE\u7F6E\u3002\u7EE7\u7EED\uFF1F")) return;
5160
+ setStageBindings({ ...adopted.stage_bindings ?? {} });
5161
+ setCommitRule(adopted.commit);
5162
+ setArtifacts(adopted.artifacts);
5163
+ setReviewDepth(adopted.review_depth);
5102
5164
  setDirty(true);
5103
5165
  setSavedAt("");
5104
5166
  };
@@ -5124,7 +5186,15 @@ function TaskEngineSection(props) {
5124
5186
  ] }),
5125
5187
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.head, children: [
5126
5188
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: styles.title, children: "\u5DE5\u7A0B\u6D41\u7A0B\u914D\u7F6E" }),
5127
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.muted, children: "\u9009\u4E00\u5957\u6D41\u7A0B\uFF0C\u518D\u7ED9\u6BCF\u4E2A\u8282\u70B9\u6302\u4E0A skill / rule\u3002\u9636\u6BB5\u6D41\u8F6C\u3001\u5B88\u536B\u3001\u63D0\u4EA4\u89C4\u5219\u90FD\u7531\u6D41\u7A0B\u9884\u8BBE\u56FA\u5316\uFF0C\u65E0\u9700\u624B\u5DE5\u914D\u7F6E\u3002" })
5189
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.muted, children: "\u6D41\u7A0B\u51B3\u5B9A\u5DE5\u4F5C\u600E\u4E48\u6D41\u8F6C\uFF08\u9636\u6BB5\u4E0E\u95E8\u7981\uFF09\uFF1B\u6280\u80FD\u3001\u89C4\u5219\u3001\u63D0\u4EA4\u683C\u5F0F\u548C\u4EA7\u7269\u5B57\u6BB5\u7531\u4F60\u81EA\u5DF1\u914D\u7F6E\u3002 \u9700\u8981\u4E00\u4EFD\u73B0\u6210\u7684\u8D77\u70B9\u65F6\uFF0C\u53EF\u4EE5\u300C\u91C7\u7528\u63A8\u8350\u914D\u7F6E\u300D\u2014\u2014\u91C7\u7528\u540E\u8FD9\u4E9B\u5C31\u5B8C\u5168\u5C5E\u4E8E\u4F60\uFF0C\u53EF\u968F\u610F\u4FEE\u6539\u6216\u5220\u9664\u3002" })
5190
+ ] }),
5191
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.adoptRow, children: [
5192
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dsh_client_ui_primitives.Button, { size: "sm", onClick: adopt, children: [
5193
+ "\u91C7\u7528\u300C",
5194
+ FLOW_PRESETS[flow]?.label ?? flow,
5195
+ "\u300D\u7684\u63A8\u8350\u914D\u7F6E"
5196
+ ] }),
5197
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.muted, children: "\u5199\u5165\u63A8\u8350\u6280\u80FD\u3001\u63D0\u4EA4\u4FE1\u606F\u683C\u5F0F\u4E0E\u4EA7\u7269\u5B57\u6BB5\u3002\u4EC5\u5728\u4F60\u70B9\u51FB\u65F6\u53D1\u751F\u4E00\u6B21\uFF1B\u5220\u9664\u540E\u4E0D\u4F1A\u81EA\u52A8\u8865\u56DE\u3002" })
5128
5198
  ] }),
5129
5199
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.sourceRow, children: [
5130
5200
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.StateDot, { state: source === "project" ? "done" : source === "invalid" ? "warning" : "ongoing" }),
@@ -5182,7 +5252,7 @@ function TaskEngineSection(props) {
5182
5252
  disabled: problems.length > 0 || saving,
5183
5253
  onClick: () => {
5184
5254
  setSaving(true);
5185
- void save(remote, flow, stageBindings, workspace, setSavedAt, setSource).then((ok) => {
5255
+ void save(remote, { flow, stage_bindings: stageBindings, ...commitRule !== void 0 ? { commit: commitRule } : {}, ...artifacts !== void 0 ? { artifacts } : {}, ...reviewDepth !== void 0 ? { review_depth: reviewDepth } : {} }, workspace, setSavedAt, setSource).then((ok) => {
5186
5256
  if (ok) {
5187
5257
  setConfigProblems([]);
5188
5258
  setDirty(false);
@@ -5234,10 +5304,36 @@ function SectionCard({ icon, title, hint, children }) {
5234
5304
  function BindingEditor({ stages, defaults, stageBindings, setStageBindings, skills, rules }) {
5235
5305
  const [stage, setStage] = (0, import_react.useState)(stages[0] ?? "");
5236
5306
  const [expanded, setExpanded] = (0, import_react.useState)(null);
5307
+ const [query, setQuery] = (0, import_react.useState)("");
5308
+ const [onlySelected, setOnlySelected] = (0, import_react.useState)(false);
5309
+ const [ruleQuery, setRuleQuery] = (0, import_react.useState)("");
5310
+ const [ruleOnlySelected, setRuleOnlySelected] = (0, import_react.useState)(false);
5237
5311
  const currentStage = stages.includes(stage) ? stage : stages[0] ?? "";
5238
5312
  const bindingFor = (name) => stageBindings[name] ?? {};
5239
5313
  const presetSkills = (name) => defaults[name]?.skills ?? [];
5240
5314
  const isPresetSkill = (name, raw) => presetSkills(name).some((entry) => formatResourceRef(entry.skill) === raw);
5315
+ const visibleSkills = () => {
5316
+ const bound = bindingFor(currentStage).skills ?? [];
5317
+ const boundKeys = new Set(bound.map((entry) => formatResourceRef(entry.skill)));
5318
+ const needle = query.trim().toLowerCase();
5319
+ return skills.filter((entry) => {
5320
+ const raw = formatResourceRef(entry.ref);
5321
+ const inForce = boundKeys.has(raw) || isPresetSkill(currentStage, raw);
5322
+ if (onlySelected && !inForce) return false;
5323
+ if (needle === "") return true;
5324
+ return inForce || entry.name.toLowerCase().includes(needle) || entry.description.toLowerCase().includes(needle) || entry.sourceLabel.toLowerCase().includes(needle);
5325
+ });
5326
+ };
5327
+ const visibleRulesFor = (bound) => {
5328
+ const chosen = new Set(bound.rules.map((rule) => formatResourceRef(rule)));
5329
+ const needle = ruleQuery.trim().toLowerCase();
5330
+ return rules.filter((rule) => {
5331
+ const raw = formatResourceRef(rule.ref);
5332
+ if (ruleOnlySelected && !chosen.has(raw)) return false;
5333
+ if (needle === "") return true;
5334
+ return chosen.has(raw) || rule.name.toLowerCase().includes(needle) || rule.sourceLabel.toLowerCase().includes(needle);
5335
+ });
5336
+ };
5241
5337
  const setSkills = (name, next) => {
5242
5338
  setStageBindings({ ...stageBindings, [name]: { ...bindingFor(name), skills: next } });
5243
5339
  };
@@ -5284,12 +5380,32 @@ function BindingEditor({ stages, defaults, stageBindings, setStageBindings, skil
5284
5380
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "te-binding-count", children: [
5285
5381
  "\u5DF2\u9009 ",
5286
5382
  boundSkills.length,
5287
- presetCount > 0 ? `\uFF08\u542B ${presetCount} \u9879\u9884\u8BBE\uFF09` : ""
5383
+ presetCount > 0 ? `\uFF08\u542B ${presetCount} \u9879\u9884\u8BBE\uFF09` : "",
5384
+ visibleSkills().length !== skills.length ? ` \xB7 \u663E\u793A ${visibleSkills().length}/${skills.length}` : ""
5385
+ ] })
5386
+ ] }),
5387
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "te-binding-controls", children: [
5388
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
5389
+ "input",
5390
+ {
5391
+ className: "te-binding-search",
5392
+ type: "search",
5393
+ value: query,
5394
+ placeholder: "\u641C\u7D22\u6280\u80FD\u540D\u6216\u8BF4\u660E\u2026",
5395
+ "aria-label": "\u641C\u7D22\u6280\u80FD",
5396
+ onChange: (event) => setQuery(event.target.value)
5397
+ }
5398
+ ),
5399
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "te-binding-filter", children: [
5400
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked: onlySelected, onChange: () => setOnlySelected((value) => !value) }),
5401
+ " ",
5402
+ "\u4EC5\u770B\u5DF2\u9009"
5288
5403
  ] })
5289
5404
  ] }),
5290
5405
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "te-binding-list", role: "group", "aria-label": "\u6280\u80FD\u5217\u8868", tabIndex: 0, children: [
5291
5406
  skills.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "te-binding-empty", children: "\u6682\u65E0\u6280\u80FD\uFF0C\u8BF7\u5728\u4E0A\u65B9\u201C\u6280\u80FD\u201D\u9875\u6DFB\u52A0\u3002" }) : null,
5292
- skills.map((entry) => {
5407
+ skills.length > 0 && visibleSkills().length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "te-binding-empty", children: "\u6CA1\u6709\u5339\u914D\u7684\u6280\u80FD\u3002" }) : null,
5408
+ visibleSkills().map((entry) => {
5293
5409
  const raw = formatResourceRef(entry.ref);
5294
5410
  const bound = boundSkills.find((candidate) => formatResourceRef(candidate.skill) === raw);
5295
5411
  const preset2 = isPresetSkill(currentStage, raw);
@@ -5317,8 +5433,29 @@ function BindingEditor({ stages, defaults, stageBindings, setStageBindings, skil
5317
5433
  ] }),
5318
5434
  bound !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "te-link", onClick: () => setExpanded(open ? null : raw), children: open ? "\u6536\u8D77\u89C4\u5219" : "\u89C4\u5219\u8BBE\u7F6E" }) : null,
5319
5435
  open && bound !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "te-rule-list", children: [
5436
+ ruleQuery !== "" || ruleOnlySelected ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "te-binding-meta", children: [
5437
+ rules.length,
5438
+ " \u6761\u53EF\u7528\u89C4\u5219"
5439
+ ] }),
5440
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
5441
+ "input",
5442
+ {
5443
+ className: "te-binding-search",
5444
+ type: "search",
5445
+ value: ruleQuery,
5446
+ placeholder: "\u8FC7\u6EE4\u89C4\u5219\u2026",
5447
+ "aria-label": `\u8FC7\u6EE4 ${entry.name} \u7684\u89C4\u5219`,
5448
+ onChange: (event) => setRuleQuery(event.target.value)
5449
+ }
5450
+ ),
5451
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "te-binding-filter", children: [
5452
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked: ruleOnlySelected, onChange: () => setRuleOnlySelected((value) => !value) }),
5453
+ " ",
5454
+ "\u4EC5\u770B\u5DF2\u9009"
5455
+ ] }),
5320
5456
  rules.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "te-binding-meta", children: "\u89C4\u5219\u5E93\u4E3A\u7A7A\u3002" }) : null,
5321
- rules.map((rule) => {
5457
+ rules.length > 0 && visibleRulesFor(bound).length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "te-binding-meta", children: "\u6CA1\u6709\u5339\u914D\u7684\u89C4\u5219\u3002" }) : null,
5458
+ visibleRulesFor(bound).map((rule) => {
5322
5459
  const ruleRaw = formatResourceRef(rule.ref);
5323
5460
  const on = bound.rules.some((candidate) => formatResourceRef(candidate) === ruleRaw);
5324
5461
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "te-binding-filter", children: [
@@ -5340,9 +5477,9 @@ function BindingEditor({ stages, defaults, stageBindings, setStageBindings, skil
5340
5477
  ] }) }, currentStage)
5341
5478
  ] });
5342
5479
  }
5343
- async function save(remote, flow, stageBindings, workspace, setSavedAt, setSource) {
5480
+ async function save(remote, config, workspace, setSavedAt, setSource) {
5344
5481
  try {
5345
- const result = await remote.write({ path: workspace, flow, stage_bindings: stageBindings });
5482
+ const result = await remote.write({ path: workspace, ...config });
5346
5483
  if (!result.ok) {
5347
5484
  setSavedAt("\u4FDD\u5B58\u5931\u8D25\uFF1A" + describeError(result.error));
5348
5485
  return false;