diff --git a/backend/api/server.py b/backend/api/server.py index 390a1e4..0ddf49e 100644 --- a/backend/api/server.py +++ b/backend/api/server.py @@ -1751,12 +1751,11 @@ def create_app(state: AgentState) -> FastAPI: if isinstance(run_id, str) and run_id.strip(): rid = run_id.strip() per_round = _resolve_run_dic_per_round(state_entries) - # augment 处理的是「上一轮评测的错例」,产物按 R{n-1} 的 runDic 命名 - # (data_clean_/、augment_.jsonl ...),不是本轮 eval workflow + # augment 产物的 runDic = resolve_run_ids.sh 在当轮 eval 落盘后取到的 + # max(已落盘 workflow) = 当轮 eval 自身的 workflow id,即 per_round[R{n}] + # (不是 R{n-1},脚本取的是 eval 之后的 max,此时 max 已经包含当轮 eval) if step == 'augment': - m = re.match(r'^[Rr](\d+)$', rid) - if m and int(m.group(1)) >= 1: - run_dic = per_round.get(f'R{int(m.group(1)) - 1}') + pass # 直接走下面 per_round.get(rid) 取本轮 runDic if run_dic is None: run_dic = per_round.get(rid) if run_dic is None: @@ -6378,9 +6377,20 @@ def _build_pipeline_items( for c in sec['cards']: c.pop('_order', None) - # 4. Build round items in display order. - # Order: by run_index ascending, then within same round: train before analysis. - section_order = {'baseline': 0, 'train': 0, 'analysis': 1} + # 3b. If a gate is running for a given run_id, downgrade running cards + # in that run to 'waiting' (the step is blocked on human review). + runs_with_gate_running: set[str] = set() + for entry in latest_gate_by_key.values(): + if entry.get('status') == 'running': + runs_with_gate_running.add(entry['_run_id']) + if runs_with_gate_running: + for sec in sections.values(): + if sec['run_id'] in runs_with_gate_running: + for card in sec['cards']: + if card['status'] == 'running': + card['status'] = 'waiting' + + # 4. Build round items in display order (chronological by first_ts). section_items: list[dict[str, Any]] = [] for (run_id, section_type), sec in sections.items(): run_index = _parse_run_index(run_id) @@ -6398,7 +6408,7 @@ def _build_pipeline_items( 'description': _SECTION_DESCRIPTIONS[section_type], 'status': round_status, 'cards': sec['cards'], - '_order_key': (run_index, section_order.get(section_type, 9), sec.get('first_ts', '')), + '_order_key': sec.get('first_ts', ''), }) # 5. Build gate items. @@ -6482,7 +6492,9 @@ def _compute_in_flight( seen_keys: set[str] = set() for idx in range(len(state_entries) - 1, -1, -1): entry = state_entries[idx] - if entry.get('kpi') or entry.get('log'): + if entry.get('kpi'): + continue + if entry.get('log') and not entry.get('step'): continue step = entry.get('step') if not isinstance(step, str) or not step: @@ -6514,9 +6526,22 @@ def _compute_in_flight( if not target_run_id or not target_section: return None + # Check if a gate (human-check/human-review) is running for the target run_id + gate_running_for_run = False + for entry in state_entries: + step = entry.get('step') + if not isinstance(step, str) or not step: + continue + if _STEP_KIND.get(step) == 'gate' and entry.get('status') == 'running': + entry_run_id = entry.get('run_id') + if entry_run_id == target_run_id: + gate_running_for_run = True + latest_per_step: dict[str, dict[str, Any]] = {} for idx, entry in enumerate(state_entries): - if entry.get('kpi') or entry.get('log'): + if entry.get('kpi'): + continue + if entry.get('log') and not entry.get('step'): continue step = entry.get('step') if not isinstance(step, str) or not step: @@ -6542,6 +6567,8 @@ def _compute_in_flight( status = entry.get('status') if status not in ('complete', 'running'): continue + if status == 'running' and gate_running_for_run: + status = 'waiting' meta = _CARD_META.get((step, target_section), { 'icon': 'clock', 'title': step, @@ -6617,7 +6644,9 @@ def _apply_program_state( # whenever a running section is hidden. latest_per_step: dict[str, str] = {} for entry in state_entries: - if entry.get('kpi') or entry.get('log'): + if entry.get('kpi'): + continue + if entry.get('log') and not entry.get('step'): continue step = entry.get('step') status = entry.get('status') @@ -6638,15 +6667,19 @@ def _apply_program_state( statuses = list(latest_per_step.values()) if not statuses: payload['status']['state'] = 'pending' - elif non_gate_running: + elif non_gate_running and not gate_running: payload['status']['state'] = 'running' elif gate_running: - # Only HiTL gates are 'running' — agent has handed control back to the - # user. Surface as 'waiting' so the header pill says "等待人工" instead - # of the spinning "Running" badge. + # HiTL gate is running — agent has handed control back to the user. + # Even if a parent step (e.g. augment) is still technically 'running', + # the gate blocks progress. Surface as 'waiting' so the header pill + # says "等待人工" instead of the spinning "Running" badge. payload['status']['state'] = 'waiting' elif all(s == 'complete' for s in statuses): - payload['status']['state'] = 'complete' + if run_active: + payload['status']['state'] = 'running' + else: + payload['status']['state'] = 'complete' elif 'complete' in statuses: payload['status']['state'] = 'running' elif 'failed' in statuses: @@ -6685,6 +6718,7 @@ def _apply_program_state( and payload['status']['state'] == 'running' and running_steps and not all_watched + and not gate_running ): session_failure_status = 'failed' diff --git a/skills/label-master/SKILL.md b/skills/label-master/SKILL.md index 515385a..14df348 100644 --- a/skills/label-master/SKILL.md +++ b/skills/label-master/SKILL.md @@ -4,6 +4,7 @@ description: 标签大师:读取和使用中控标签知识库,辅助标签 when_to_use: 当用户要求判断 query 应该归属哪个标签、解释标签边界、校验数据生成目标中的 target、整理标签知识或分析标签混淆时使用。 aliases: label-knowledge, 标签大师, 标签知识, label, 标签体系 allowed_tools: read_file, write_file, edit_file, grep_search, glob_search, ask_user_question, python_exec, bash +repeatable: true --- 使用这个 skill 作为“标签知识理解和标签边界分析”的入口。它不直接把标签判断收敛成一个黑盒分类工具,而是指导 Agent 逐步阅读知识、比较候选、解释依据,并在不确定时向用户确认。 diff --git a/skills/model-iteration/SKILL.md b/skills/model-iteration/SKILL.md index d0b5f28..614e056 100644 --- a/skills/model-iteration/SKILL.md +++ b/skills/model-iteration/SKILL.md @@ -65,6 +65,13 @@ when_to_use: | - [ ] **`step:"hypothesis"` entry 写在当前 round(R{n})名下**——不要写成 R{n+1}。后端把 hypothesis 归类为 analysis 类,是 R{n}·Baseline / R{n}·Analysis 的最后一张卡,不是 R{n+1}·Train 的开头。这样 gate(Human Check / Review)会插在 hypothesis 卡之后、R{n+1}·Train 之前,**用户看到假设内容再拍板是否进 train**。 - [ ] **`hypothesis=complete` 之前,必须 append `iteration_log.jsonl` 一条 R{n} entry**,schema 至少包含 `iteration:n / runDic:<本轮编号> / timestamp / results:{<指标 key>:...} / hypothesis`。**前端 metrics 折线图唯一数据源就是这个文件**,没写 → 图表彻底空白(state file 写得再全也不行)。已踩坑:R0 走完 dist-analysis + hypothesis 但漏了 iteration_log,前端 metrics 卡看不到任何点,指标空了一整轮。`results` 必须用 program.md 「§iteration_log schema」里固定的 metric key(`req_set_car / dapan_car / specific_test / triage_err_rate / bvt_nav / talkable_controllable` 等),key 拼错前端按缺失处理。 +### Step 3 → Step 4 / gate 边界(**CRITICAL:hypothesis=complete 后禁止空手结束 turn**) +- [ ] **`hypothesis=complete` 写入 program-state.jsonl 之后,当前 turn 绝对不允许结束**——必须在同一轮里做下面两件之一(二选一,不能都不做): + - (A) 如果命中 HiTL 信号(§HiTL 阈值):写 `human-check` gate entry(running)→ chat reply 给用户选项 → 然后才可以让 turn 自然结束(gate=running 会让 UI 显示 "waiting" 卡片) + - (B) 如果不命中 HiTL(候选量 ≤ 50 全自动路径):直接写 `augment=running` → 开始执行增强 +- [ ] **"turn 预算不够"不是合法理由**——50 turns 里跑完 hypothesis 最多用 15~20 turns,剩余空间远够写一条 gate entry 或启动 augment。如果你真的接近上限,也必须**先写 gate entry 再结束**,不能空手结束。 +- [ ] **"下一轮 wake 继续"不存在**——没有自动 wake 机制。你结束了就是结束了,session 不会被再次调起,用户只看到 pipeline 卡在 hypothesis=complete 不动。 + ### Step 1 → Step 3 边界(**NEVER STOP 硬连接**,R0 / R1+ regression 都适用) - [ ] dist-analysis 算完 delta(new_fail / new_fix / persistent / 子集分布)那一刻起,**同一轮 bash 不许结束**:紧接着写 `results/workflow.md`(包含完整指标表 + 病灶定位 + 假设 + 回滚/继续建议)→ append `iteration_log.jsonl` R{n} entry(`results` 字段填本轮 metric,`hypothesis` 字段填下一动作) - [ ] **regression 场景同样适用**:哪怕 R1 出现导航bvt 纯劣化、可聊可控大幅 -25 这种"必须回滚"信号,**先把分析落到 workflow.md 和 iteration_log,再用 chat reply 给人回滚选项**。文件先落、聊天再发——顺序不能反。 @@ -81,6 +88,7 @@ when_to_use: | - [ ] §4.0.1 Step C:备份 `.bak` 文件 - [ ] §4.1 输入准备 / §4.2 GPT 调用 / §4.3 sanity check / §4.4 写入 - [ ] **§4.5 label-master 标签复核(落盘后必做,强制)**:H1 `modified_samples.jsonl` 只跑层 1(格式)—— 语义已在 §4.0.1 Step A.5 完成;H2 `augment_.jsonl` 跑两层(`validate_label_output.py` 格式 + Skill 调用 `label-master` 语义),**逐条 1:1 全量覆盖**(`augment` 多少行 `label_master_review.jsonl` H2 部分就多少行,禁止抽样 / spot-check / X/X pass 外推),verdict 写 `results/data_clean_/label_master_review.jsonl`,**不通过必须 = 0**(任何不通过必须修正后重新 review 直到全 pass 才能进 Step 5) +- [ ] **§4.5 层 2 语义复核必须调用 `Skill(skill="label-master")`,禁止正则/规则脚本替代**:层 2 的本质是"用 label-master 知识体系对每条 (query, label) 做独立语义判定"。label-master 已标记 `repeatable: true`,可在同一 run 内多次调用。**禁止**:纯正则/关键词匹配脚本、"target 都一样所以直接 pass"逻辑、批量写 pass 不看 query 内容。已踩坑:R1 agent 写了个正则脚本充当"layer 2",完全没走语义判断。 ### Step 4 → Step 5 边界(**NEVER STOP 硬连接**,反复踩坑) - [ ] 写完 `augment=complete` 那一刻,**同一轮 bash 不许结束**:紧接着跑 §4.5 label-master 复核 → 写 `sft=running` → 调 `submit_sft.sh` → 挂 watcher(评测在 SFT `_SUCCESS` 落盘后的下一轮单独用 `submit_cml_eval.sh` 起,不要在 SFT bg 里串接评测) @@ -418,9 +426,11 @@ echo '{"log":{"ts":"11:55:10","iter":"R1","text":"Step 1 dist-analysis: 写 work ## 达标条件 -- **需求集合** ≥ 95%(最高优先级) -- **大盘集(车载)** 降幅 ≤ 0.3% -- **specific test** 降幅 ≤ 1% +- **需求集合(目标集)** ≥ 95%(**最高优先级,一切以此为准**) +- **大盘集(车载)** 降幅 ≤ 0.3%(次要,目标集未达 95% 前不因大盘微降而回滚/停训) +- **specific test** 降幅 ≤ 1%(次要,同上) + +⚠️ **优先级铁律**:目标集上 95% 是主线任务。只要目标集还没到 95%,其他集合的轻微下降(大盘 ≤1%、specific ≤2%)**不构成回滚理由**,继续迭代。只有目标集已达标后,才需要关注并修复其他集合的 regression。 ## 主流程 diff --git a/skills/model-iteration/references/program.md b/skills/model-iteration/references/program.md index fbf58ec..18b08b0 100644 --- a/skills/model-iteration/references/program.md +++ b/skills/model-iteration/references/program.md @@ -178,13 +178,17 @@ for k in prev: 3. **specific test** **达标条件**: -- 需求集合 ≥ 95% -- 大盘集(车载)降幅 ≤ 0.3% -- specific test 降幅 ≤ 1% +- 需求集合(目标集) ≥ 95%(**最高优先级**) +- 大盘集(车载)降幅 ≤ 0.3%(次要) +- specific test 降幅 ≤ 1%(次要) + +⚠️ **优先级铁律**:目标集上 95% 是主线任务。只要目标集还没到 95%,其他集合的轻微下降(大盘 ≤1%、specific ≤2%)**不构成回滚理由**,应继续迭代优化目标集。只有目标集已达 95% 后,才需要关注并修复其他集合的 regression。 **决策**: - 全部达标 → 部署(`python modules/cloudml_deploy.py `),记录,循环结束 -- 不达标 → 在 Step 1 内继续做问题分析(见下文),**分析顺序:需求集合 → 车载大盘 → specific test** +- 目标集未达标 → 继续迭代(即使其他集合有轻微下降也不回滚) +- 目标集已达标但其他集合超限 → 微调修复其他集合 +- 分析顺序:**需求集合 → 车载大盘 → specific test** > **首次评测差异**:由于新旧模型相同,"降幅"和"vs 基线"无意义(均为 0)。此轮只记录各集合的**绝对准确率**作为 baseline,不做达标/不达标判定,不触发部署。直接在 Step 1 内分析基准模型的错误分布,为后续迭代建立对照基准。 @@ -655,6 +659,12 @@ for r in b_rows: > 若 Step 1 显示全部达标 → 跳过 Step 3–5,直接部署。 +> ⛔ **CRITICAL: hypothesis=complete 后禁止空手结束 turn**。写完 hypothesis 到 program-state.jsonl 之后,当前 turn 内必须做下面两件之一(不能都不做就结束): +> - 命中 HiTL → 写 `human-check` gate entry (running) → chat reply 给用户选项 +> - 不命中 HiTL → 写 `augment=running` → 开始 Step 4 +> +> **"turn 预算不够"不是合法理由**——没有自动 wake 机制,turn 结束 = session 永久停止。已踩坑:agent 在 R0 hypothesis=complete 后误判"turn 用满"而空手结束,用户看到 pipeline 卡在 hypothesis 不动、无任何后续(session `qf1ni1jh`,25/50 turns 实际只用一半)。 + ### 4. 数据生成(GPT-5.4 从 badcase 增强) **只在 §2.4 归因为 `data` 时做。** 其他归因直接跳过 Step 4,按下表路由: @@ -743,18 +753,19 @@ with open(out_csv, 'w', encoding='utf-8-sig') as fp: 候选阶段(246 条等量级)的 CSV 必须写到 `output/relabel_candidates_.csv` 让分析卡片可读;确认修改阶段(用户审核后落盘的 135 条)写入 `results/data_clean_/modified_samples.jsonl` 让数据增强卡片可读。**不要混落**——把候选 CSV 写到 `/tmp/` 或 `data_clean_/` 都会让 UI 看不到。 -> **runDic 命名语义(backend 已对齐,改 backend 时勿 revert)**:augment 类产物(`data_clean_/`、`augment_raw/augment__raw.jsonl`、`zk_intent/augment_.jsonl`、`label_master_review.jsonl`)的 `` 永远是**「源轮次的 runDic」**,也就是 `R{n-1}.runDic`——agent 在 R{n} 跑 augment 时处理的是 R{n-1} 评测落地的错例,文件命名沿用 R{n-1} 的 workflow id。Backend 的 step-detail 端点 (`server.py` 内 `_step_artifact_paths('augment', ...)` 调用方) 在 `step=='augment'` 时也是查 `R{n-1}` 的 runDic,跟落盘命名一一对应。其他 step(cml / dist-analysis / hypothesis / gold-drift)的产物按本轮自己的 runDic 命名,没有偏移。**不要把 augment 产物改名成 R{n} 的 runDic**——会同时打破历史归档和 backend 解析。 +> **runDic 命名语义(backend 已对齐)**:augment 类产物(`data_clean_/`、`augment_raw/augment__raw.jsonl`、`zk_intent/augment_.jsonl`、`label_master_review.jsonl`)的 `` = `$SFT_RUNDIC` = **当轮 eval 落盘后 `resolve_run_ids.sh` 取到的 max(已落盘 workflow)**。由于 resolve 是在当轮 eval 的 `lark_template.json` 落盘之后调用的,此时 max 就是当轮 eval 自身的 workflow id。Backend step-detail 端点对 augment 也直接用本轮(`per_round[R{n}]`)的 runDic 去读。其他 step(cml / dist-analysis / hypothesis / gold-drift)的产物同理按本轮自己的 runDic 命名。 > > ⚠️ **runDic 不许手算**:augment 落盘、SFT yaml、归档目录、cml workflow run 用的 runDic **必须**从 `scripts/resolve_run_ids.sh` 取,不准 agent 自己 `ls workflow5 | tail -1` 然后心算 +1。规则简单到没必要靠模型: > > ```bash > eval "$(./scripts/resolve_run_ids.sh)" > # 现在 shell 里有: -> # $SFT_RUNDIC = R{n-1}.runDic (augment 落盘 / yaml / 归档全用这个,不 +1) -> # $EVAL_RUNDIC = R{n-1}.runDic + 1 (仅 cml workflow run 时使用,唯一一次 +1) +> # $SFT_RUNDIC = max(已落盘 workflow) = 当轮 eval 的 workflow id +> # augment 落盘 / yaml / 归档全用这个 +> # $EVAL_RUNDIC = SFT_RUNDIC + 1(仅下一轮 cml workflow run 提交时使用) > ``` > -> 已踩坑:R2 augment 把 `data_clean_/` 写成了 `EVAL_RUNDIC`(多 +1 一次),跳过了 R1.runDic 整段命名空间,前端 augment 卡片直接读不到。**写 augment / SFT / 归档脚本时一律用 `$SFT_RUNDIC`,不要写 `${RUNDIC}` 也不要写 `${EVAL_RUNDIC}`**。 +> **写 augment / SFT / 归档脚本时一律用 `$SFT_RUNDIC`,不要写 `${RUNDIC}` 也不要写 `${EVAL_RUNDIC}`**。 > > 🚨 **调用时序(CRITICAL)**:`resolve_run_ids.sh` **只能在当轮 CML eval 的 `lark_template.json` 已落盘之后调用**。禁止在 eval 提交前或 watcher 等待期间预先调用——此时 max workflow 仍是上一轮的值,会导致 SFT_RUNDIC 偏移 -1(已踩坑:R0 workflow17823 评测中提前 resolve 得到 17822,augment 文件全部错位)。正确时序:`cml step=complete` → `resolve_run_ids.sh` → augment/SFT。每次进入 augment/SFT 步骤前**必须重新调用**,不许复用之前缓存的值。 @@ -1262,6 +1273,8 @@ python skills/label-master/scripts/validate_label_output.py \ **层 2:语义复核(Skill 调用 label-master Agent,仅 H2)** +⛔ **禁止用正则/规则脚本替代 LLM 语义判断**。层 2 不是格式校验,是"该 query 在 label-master 知识体系下是否应该标成这个 label"的语义问题。**唯一合法实现方式是调用 `Skill(skill="label-master")`**(label-master 已标记 `repeatable: true`,可在同一 run 内多次调用)。已踩坑:R1 agent 写了个检查 hist 里有没有导航触发词的正则脚本充当"layer 2",完全没走语义判断,导致错误样本全量混入训练集。 + H1 在 §4.0.1 Step A.5 已经过 label-master 推荐覆盖,这里**不再重跑**。H2 仿写是 §4.2 GPT 新增的样本,没经过 Step A.5,必须在此补一次语义判定: 🚨 **覆盖率硬规则**:`label_master_review.jsonl` 里 H2 部分的行数**必须等于** `augment_.jsonl` 的行数(1:1 全量覆盖)。**禁止抽样、禁止"前 N 条 spot-check"、禁止"5/5 pass 推 100 OK"**——§5.0 准入门会用 `wc -l` 拦。已踩坑:R17817 跑了 5 条样本就上报"100 条 pass",被 §5.0 退回。 @@ -1485,6 +1498,16 @@ python prepare_and_train_sft.py prepare --regen_from_csv --output_dir ./sft_data > 验证集文件名不能含 `valid`/`test`/`eval`(HF datasets 会推断错误的 split),脚本自动重命名为 `part-0.jsonl`。 +⚠️ **每轮 SFT 前必须在 log 中展示训练数据组成** + +`prepare_and_train_sft.py` 会 glob 目录下**所有** `augment_*.jsonl`。agent 在每轮 SFT 前必须在 program-state log 中列出最终参与训练的文件及行数: + +```jsonl +{"log":{"ts":"...","iter":"R3","text":"SFT 数据组成: all_train.jsonl(35379) + augment_17829.jsonl(210) = 35589 条 | 排除: augment_17828.jsonl(150) rename .pre_R3(R2 pattern 导致导航 bvt 劣化)"}} +``` + +必须展示:(1) 参与训练的所有文件及行数 (2) 被排除的旧轮文件及排除原因(如果有)。 + #### 5.2 启动训练(cml 任务,R29 起统一走这条路径) R23-R28 期间用本地 `nohup python3 prepare_and_train_sft.py train ...` 启动,会因登出/网络/会话退出而中断,并占用本地工作机 8 卡 GPU。**R29 起统一改为 cml custom_train submit 提交训练任务**,由 CloudML 调度到 `bj-nlp` 队列的 h20-96g 8 卡,本地零占用。 diff --git a/skills/model-iteration/scripts/resolve_run_ids.sh b/skills/model-iteration/scripts/resolve_run_ids.sh index 056711e..91b0e10 100755 --- a/skills/model-iteration/scripts/resolve_run_ids.sh +++ b/skills/model-iteration/scripts/resolve_run_ids.sh @@ -1,11 +1,12 @@ #!/bin/bash # resolve_run_ids.sh —— SFT/EVAL runDic 单一可信来源 # -# 规则(与 program.md §746 + §5.2 对齐): +# 规则(与 program.md §756 对齐): # SFT_RUNDIC = workflow5/ 下「已落盘」(含 metric_diff/lark_template.json)的最大 runDic -# 即 R{n-1}.runDic —— augment 落盘、SFT yaml、modified_samples 归档全用这个值 +# = 当轮 eval 自身的 workflow id(因为 resolve 在 eval 落盘后调用) +# augment 落盘、SFT yaml、modified_samples 归档全用这个值 # EVAL_RUNDIC = SFT_RUNDIC + 1 -# 仅 cml workflow run 提交本轮评测时使用 +# 仅下一轮 cml workflow run 提交时使用 # # 用法: # eval "$(./scripts/resolve_run_ids.sh)" diff --git a/src/agent_runtime.py b/src/agent_runtime.py index 58fb368..7db63f8 100644 --- a/src/agent_runtime.py +++ b/src/agent_runtime.py @@ -2681,7 +2681,7 @@ class LocalCodingAgent: bundled = find_bundled_skill(skill_name, cwd=self.runtime_config.cwd) if bundled is not None: skill_key = bundled.name.strip().lower() - if active_skill_names is not None and skill_key in active_skill_names: + if active_skill_names is not None and skill_key in active_skill_names and not bundled.repeatable: duplicate_count = 1 if duplicate_skill_counts is not None: duplicate_count = duplicate_skill_counts.get(skill_key, 0) + 1 diff --git a/src/bundled_skills.py b/src/bundled_skills.py index d651853..3f518ab 100644 --- a/src/bundled_skills.py +++ b/src/bundled_skills.py @@ -35,6 +35,7 @@ class BundledSkill: aliases: tuple[str, ...] = () allowed_tools: tuple[str, ...] = () user_invocable: bool = True + repeatable: bool = False source: str = 'python' path: str | None = None get_prompt: Callable[['LocalCodingAgent', str], str] = lambda _a, _args: '' @@ -190,6 +191,7 @@ def _load_directory_skill(path: Path, *, source: str = 'directory') -> BundledSk aliases=_split_csv(metadata.get('aliases')), allowed_tools=_split_csv(metadata.get('allowed_tools')), user_invocable=_parse_bool(metadata.get('user_invocable'), default=True), + repeatable=_parse_bool(metadata.get('repeatable'), default=False), source=source, path=str(path), get_prompt=_directory_skill_prompt(body.strip()),