From 6c3c2cf5d3610f56b7c021f06660e8d16c71bfc0 Mon Sep 17 00:00:00 2001 From: vitaliytv Date: Sun, 9 Aug 2026 15:30:23 +0300 Subject: [PATCH] =?UTF-8?q?feat(mt-core)!:=20a.md/h.md=20=E2=80=94=20YAML-?= =?UTF-8?q?=D1=84=D1=80=D0=BE=D0=BD=D1=82=D0=BC=D0=B0=D1=82=D0=B5=D1=80=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BC=D1=96=D1=81=D1=82=D1=8C=20markdown-=D1=81?= =?UTF-8?q?=D0=B5=D0=BA=D1=86=D1=96=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Відкрите питання #2 з docs/conformance.md закрито: ні голий YAML (як показувала спека), ні markdown-секції (як робив код), а фронтматер у markdown — файл має розширення .md, тож це узгоджено з рештою артефактів вузла, дає один код-шлях парсингу, рендериться при рев'ю і лишає місце для прози в тілі. Спеку уточнено: nitra/mt#65. Жорсткий перехід — старий формат НЕ читається як порожній (це підмінило б model_tier/agent_cli дефолтами, тобто вузол тихо виконався б не тим виконавцем), а валить preflight до claim із вказівкою на graph.md. Парсер: додано дві конструкції, потрібні для форми зі спеки й раніше відсутні — inline flow-масиви (skills: [bash, write-files]) і списки мап (retry_ladder: - {} / - strategy: diagnose-first, з рядками продовження). Кома в лапках не розрізає елемент; «- http://x» лишається скаляром. Серіалізатор не чіпано — JS-байт-парність збережена. retry_ladder тепер приймає і рядок, і мапу зі strategy/model_tier_delta; раніше секційний формат зводив щабель до стрічки й багатопольові щаблі були невиразні в принципі. Міграція: 19 файлів dogfood-графа, created_at узято з дати додавання файлу в git, per-node значення (MIN/MAX, agent_cli: codex) збережені. Тести: 7 на парсер, 2 на відхилення старого формату, 2 на форму прапорів. 158 passed у mt-core, cargo test --workspace зелений (19/19 наборів), clippy чистий. Co-Authored-By: Claude Fable 5 --- crates/mt-core/src/frontmatter.rs | 165 +++++++++++++++++++++- crates/mt-core/src/lib.rs | 64 +++++++-- crates/mt-core/src/runner.rs | 134 ++++++++++++++---- docs/conformance.md | 4 +- mt/lint-fix-doc-files-check-f2f4fc1e/a.md | 16 ++- mt/lint-fix-js-eslint-04a66bfd/a.md | 16 ++- mt/m0-dogfood-smoke/a.md | 21 ++- mt/m0-kill-invalidate-scanner-sync/a.md | 16 ++- mt/m0-ladder-cross-cli/a.md | 16 ++- mt/m0-nnn-origin-truth/a.md | 16 ++- mt/m1-acp-adapters/a.md | 16 ++- mt/m1-agent-protocol/a.md | 16 ++- mt/m1-agent-server/a.md | 16 ++- mt/m1-attach-graph/a.md | 16 ++- mt/m1-interactive-done-check/a.md | 16 ++- mt/m1-session-graph-wiring/a.md | 16 ++- mt/m2-acp-approval-e2e/a.md | 16 ++- mt/m2-approvals-flow/a.md | 16 ++- mt/m2-attach-ws-wiring/a.md | 16 ++- mt/m2-handoff-core/a.md | 16 ++- mt/m2-interactive-run-file/a.md | 16 ++- mt/m2-relay-bridge/a.md | 16 ++- mt/m2-relay-core/a.md | 16 ++- 23 files changed, 488 insertions(+), 188 deletions(-) diff --git a/crates/mt-core/src/frontmatter.rs b/crates/mt-core/src/frontmatter.rs index d5a3dde..cf75232 100644 --- a/crates/mt-core/src/frontmatter.rs +++ b/crates/mt-core/src/frontmatter.rs @@ -117,9 +117,12 @@ fn parse_yaml_block(block: &str) -> Map { continue; } - if next_line.trim_start().starts_with("- ") { - // Список. + if next_line.trim_start().starts_with("- ") || next_line.trim_start() == "-" { + // Список. Елемент — скаляр (`- bash`), inline-мапа (`- {}`) або + // блокова мапа (`- strategy: x` + рядки продовження глибше). + let item_indent = get_indent(next_line); let mut arr = vec![]; + let mut item: Option> = None; while i < lines.len() { let l = lines[i]; if l.trim().is_empty() { @@ -130,11 +133,20 @@ fn parse_yaml_block(block: &str) -> Map { break; } let t = l.trim_start(); - if let Some(item) = t.strip_prefix("- ") { - arr.push(parse_scalar(item.trim())); + if get_indent(l) <= item_indent && (t == "-" || t.starts_with("- ")) { + if let Some(prev) = item.take() { + arr.push(finish_list_item(prev)); + } + item = Some(vec![t.strip_prefix('-').unwrap_or(t).trim().to_string()]); + } else if let Some(cur) = item.as_mut() { + // Рядок продовження блокової мапи цього елемента. + cur.push(t.to_string()); } i += 1; } + if let Some(prev) = item.take() { + arr.push(finish_list_item(prev)); + } result.insert(key, Value::Array(arr)); } else { // Вкладений об'єкт: нормалізуємо відступ (видаляємо перший рівень). @@ -196,6 +208,63 @@ fn js_number(s: &str) -> Option { } /// Парсить скалярне значення: булеве, null, число, лапки, або рядок. +/// Елемент списку: рядки `["strategy: x", "skills_add: [y]"]` → мапа; +/// один рядок без `key:` → скаляр. Порожній елемент (`-` або `- {}`) → `{}`. +fn finish_list_item(lines: Vec) -> Value { + let first = lines.first().map(String::as_str).unwrap_or(""); + if lines.len() == 1 { + if first.is_empty() { + return Value::Object(Map::new()); + } + if !looks_like_mapping(first) { + return parse_scalar(first); + } + } + Value::Object(parse_yaml_block(&lines.join("\n"))) +} + +/// Чи рядок є `key: value`/`key:` — ключ до першої двокрапки без пробілів +/// і лапок (щоб `http://x` чи `note: a: b` не ламали визначення). +fn looks_like_mapping(s: &str) -> bool { + let Some(idx) = s.find(':') else { + return false; + }; + let key = &s[..idx]; + let rest = &s[idx + 1..]; + !key.is_empty() + && !key.contains(' ') + && !key.contains('"') + && !key.contains('\'') + && (rest.is_empty() || rest.starts_with(' ')) +} + +/// Inline flow-масив `[a, b]` — split по комах верхнього рівня (вкладені +/// дужки й лапки не розрізаються). +fn parse_flow_seq(inner: &str) -> Vec { + let mut items: Vec = vec![]; + let (mut depth, mut quote, mut start) = (0usize, None::, 0usize); + let bytes: Vec<(usize, char)> = inner.char_indices().collect(); + for (pos, ch) in bytes { + match (quote, ch) { + (Some(q), c) if c == q => quote = None, + (Some(_), _) => {} + (None, '"' | '\'') => quote = Some(ch), + (None, '[' | '{') => depth += 1, + (None, ']' | '}') => depth = depth.saturating_sub(1), + (None, ',') if depth == 0 => { + items.push(parse_scalar(inner[start..pos].trim())); + start = pos + 1; + } + _ => {} + } + } + let tail = inner[start..].trim(); + if !tail.is_empty() { + items.push(parse_scalar(tail)); + } + items +} + fn parse_scalar(s: &str) -> Value { match s { "true" => return Value::Bool(true), @@ -203,6 +272,18 @@ fn parse_scalar(s: &str) -> Value { "null" | "~" => return Value::Null, _ => {} } + // Inline flow: `[a, b]` і `{}` / `{k: v}`. + if let Some(inner) = s.strip_prefix('[').and_then(|r| r.strip_suffix(']')) { + return Value::Array(parse_flow_seq(inner)); + } + if let Some(inner) = s.strip_prefix('{').and_then(|r| r.strip_suffix('}')) { + let block: Vec = parse_flow_seq(inner) + .iter() + .filter_map(|v| v.as_str().map(String::from)) + .filter(|p| looks_like_mapping(p)) + .collect(); + return Value::Object(parse_yaml_block(&block.join("\n"))); + } if let Some(n) = js_number(s) { return n; } @@ -371,4 +452,80 @@ mod tests { let fm = parse_front_matter(&format!("---\n{src}\n---\n")); assert_eq!(serialize_yaml(&fm, 0), src); } + + // ── inline flow і списки мап (форма a.md зі спеки graph.md) ── + + #[test] + fn inline_flow_sequence() { + let v = parse_yaml("skills: [bash, write-files]\nsecrets: [STRIPE_KEY]"); + assert_eq!(v["skills"], json!(["bash", "write-files"])); + assert_eq!(v["secrets"], json!(["STRIPE_KEY"])); + } + + #[test] + fn inline_flow_sequence_quoted_and_empty() { + let v = parse_yaml("a: []\nb: ['x, y', z]"); + assert_eq!(v["a"], json!([])); + // Кома всередині лапок не розрізає елемент. + assert_eq!(v["b"], json!(["x, y", "z"])); + } + + #[test] + fn block_sequence_of_scalars_unchanged() { + let v = parse_yaml("skills:\n - bash\n - write-files"); + assert_eq!(v["skills"], json!(["bash", "write-files"])); + } + + #[test] + fn sequence_of_mappings() { + let v = parse_yaml("retry_ladder:\n - {}\n - strategy: diagnose-first\n"); + assert_eq!(v["retry_ladder"], json!([{}, {"strategy": "diagnose-first"}])); + } + + #[test] + fn sequence_of_multiline_mappings() { + let src = "retry_ladder:\n - strategy: alternative-approach\n model_tier_delta: 1\n - strategy: diagnose-first\n"; + let v = parse_yaml(src); + assert_eq!( + v["retry_ladder"], + json!([ + {"strategy": "alternative-approach", "model_tier_delta": 1}, + {"strategy": "diagnose-first"} + ]) + ); + } + + #[test] + fn colon_in_value_is_not_a_mapping_key() { + // «- note: a: b» — ключ note, решта значення; «- http://x» — скаляр. + let v = parse_yaml("items:\n - http://x\n - note: a: b\n"); + assert_eq!(v["items"][0], json!("http://x")); + assert_eq!(v["items"][1], json!({"note": "a: b"})); + } + + #[test] + fn a_md_frontmatter_full_shape() { + let src = concat!( + "---\n", + "schema_version: 1\n", + "created_at: 2026-08-09T10:00:00Z\n", + "model_tier: AVG\n", + "agent_cli: codex\n", + "skills: [bash, write-files]\n", + "secrets: [STRIPE_KEY]\n", + "retry_ladder:\n", + " - {}\n", + " - strategy: diagnose-first\n", + "interactive: false\n", + "---\n\nПрозовий коментар до вибору виконавця.\n" + ); + let fm = parse_front_matter(src); + assert_eq!(fm["schema_version"], json!(1)); + assert_eq!(fm["model_tier"], json!("AVG")); + assert_eq!(fm["agent_cli"], json!("codex")); + assert_eq!(fm["skills"], json!(["bash", "write-files"])); + assert_eq!(fm["interactive"], json!(false)); + assert_eq!(fm["retry_ladder"][1]["strategy"], json!("diagnose-first")); + assert_eq!(get_body(src), "Прозовий коментар до вибору виконавця.\n"); + } } diff --git a/crates/mt-core/src/lib.rs b/crates/mt-core/src/lib.rs index 4e5cd53..ed4da90 100644 --- a/crates/mt-core/src/lib.rs +++ b/crates/mt-core/src/lib.rs @@ -64,6 +64,9 @@ pub struct WorkspaceInfo { pub path: String, } +/// `schema_version` прапорів виконавця (graph.md §a.md/§h.md). +pub const FLAG_SCHEMA_VERSION: u64 = 1; + /// Виконавець вузла. Істина — прапор-файл `a.md`/`h.md`, не поле frontmatter. /// Пише прапор виконавця (`a.md` або `h.md`) і видаляє протилежний — /// інваріант «рівно один прапор» (§4.3). Повертає ім'я записаного файлу. @@ -74,25 +77,33 @@ pub fn write_executor_flag( skills: &[String], qualification: Option<&str>, ) -> Result<&'static str, String> { + let created_at = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); match mode { Mode::Agent => { - let skill_lines = skills - .iter() - .map(|s| format!("- {s}")) - .collect::>() - .join("\n"); - let content = format!("## Model tier\n\n{model_tier}\n\n## Skills\n\n{skill_lines}\n"); - write_atomic(&task_dir.join("a.md"), &content)?; + let fm = serde_json::json!({ + "schema_version": FLAG_SCHEMA_VERSION, + "created_at": created_at, + "model_tier": model_tier, + "skills": skills, + "interactive": false, + }); + write_atomic(&task_dir.join("a.md"), &frontmatter::build_markdown(&fm, ""))?; let _ = fs::remove_file(task_dir.join("h.md")); Ok("a.md") } Mode::Human => { - let content = match qualification { - Some(q) => format!("## Qualification\n\n{q}\n"), - None => "## Qualification\n\n\n" - .to_string(), + let fm = serde_json::json!({ + "schema_version": FLAG_SCHEMA_VERSION, + "created_at": created_at, + "notify": true, + "qualification": qualification.unwrap_or(""), + }); + let body = if qualification.is_some() { + "" + } else { + "\n" }; - write_atomic(&task_dir.join("h.md"), &content)?; + write_atomic(&task_dir.join("h.md"), &frontmatter::build_markdown(&fm, body))?; let _ = fs::remove_file(task_dir.join("a.md")); Ok("h.md") } @@ -1701,12 +1712,35 @@ mod tests { let outcome = create_task(mt, "agentic".to_string(), opts).unwrap(); assert!(matches!(&outcome, CreateOutcome::Created { flag, .. } if flag == "a.md")); let a = fs::read_to_string(root.path().join("mt/agentic/a.md")).unwrap(); - assert!(a.contains("## Model tier\n\nMAX\n"), "got: {a}"); - assert!(a.contains("## Skills")); - assert!(a.contains("- bash")); + assert!(a.starts_with("---\n"), "a.md має бути frontmatter: {a}"); + let fm = frontmatter::parse_front_matter(&a); + assert_eq!(fm["schema_version"], serde_json::json!(1)); + assert_eq!(fm["model_tier"], serde_json::json!("MAX")); + assert_eq!(fm["skills"], serde_json::json!(["bash", "write-files"])); + assert_eq!(fm["interactive"], serde_json::json!(false)); + assert!(fm["created_at"].is_string()); assert!(!root.path().join("mt/agentic/h.md").exists()); } + #[test] + fn create_human_writes_h_md_frontmatter() { + let (root, mt) = create_repo(None); + let opts = CreateOpts { + mode: Some(Mode::Human), + qualification: Some("senior backend engineer".to_string()), + ..Default::default() + }; + create_task(mt, "manual".to_string(), opts).unwrap(); + let h = fs::read_to_string(root.path().join("mt/manual/h.md")).unwrap(); + let fm = frontmatter::parse_front_matter(&h); + assert_eq!(fm["schema_version"], serde_json::json!(1)); + assert_eq!( + fm["qualification"], + serde_json::json!("senior backend engineer") + ); + assert_eq!(fm["notify"], serde_json::json!(true)); + } + #[test] fn create_is_idempotent() { let (root, mt) = create_repo(None); diff --git a/crates/mt-core/src/runner.rs b/crates/mt-core/src/runner.rs index 3241796..50fd949 100644 --- a/crates/mt-core/src/runner.rs +++ b/crates/mt-core/src/runner.rs @@ -110,32 +110,71 @@ fn fm_u64(v: &serde_json::Value, key: &str) -> Option { v.get(key).and_then(serde_json::Value::as_u64) } -/// Непорожні рядки секції `## ` прапора `a.md` — спільний -/// markdown-конвент прапорів виконавця («## Model tier», «## Retry ladder», -/// «## Agent cli»). Немає a.md/секції/рядків → None. -fn read_flag_section(dir: &Path, title_lower: &str) -> Option<Vec<String>> { - let content = fs::read_to_string(dir.join("a.md")).ok()?; - let mut lines = content.lines(); - lines.find(|l| l.trim().to_lowercase() == title_lower)?; - let values: Vec<String> = lines - .take_while(|l| !l.trim_start().starts_with("##")) +/// Frontmatter прапора виконавця `a.md` (graph.md §a.md). Немає файлу → None. +/// +/// Старий markdown-секційний формат («## Model tier») відхиляється **явно**: +/// мовчазне читання як порожнього frontmatter підмінило б `model_tier` і +/// `agent_cli` дефолтами — вузол тихо виконався б не тим виконавцем. +pub(crate) fn read_executor_flag(dir: &Path) -> Result<Option<serde_json::Value>, String> { + let path = dir.join("a.md"); + let Ok(content) = fs::read_to_string(&path) else { + return Ok(None); + }; + if !content.trim_start().starts_with("---") { + let hint = if content.contains("## ") { + "markdown-секційний формат («## Model tier») більше не підтримується" + } else { + "файл без YAML-frontmatter" + }; + return Err(format!( + "{}: {hint} — очікується `---`-frontmatter із ключами \ + model_tier/skills/agent_cli (graph.md §a.md)", + path.display() + )); + } + Ok(Some(crate::frontmatter::parse_front_matter(&content))) +} + +fn flag_str(flag: Option<&serde_json::Value>, key: &str) -> Option<String> { + flag? + .get(key) + .and_then(serde_json::Value::as_str) .map(str::trim) - .filter(|t| !t.is_empty()) + .filter(|s| !s.is_empty()) .map(String::from) - .collect(); - (!values.is_empty()).then_some(values) } -/// Парсить рядки секції «## Retry ladder» у драбину (буліт/рядок на щабель). -/// Щабель "alternative-approach" завжди несе `model_tier_delta: 1` (graph.md). -fn parse_retry_ladder(lines: &[String]) -> Option<Vec<LadderStep>> { - let steps: Vec<LadderStep> = lines +/// `retry_ladder` зі спеки — список щаблів: рядок (`- diagnose-first`) або +/// мапа (`- {strategy: …, model_tier_delta: …}`). Щабель +/// "alternative-approach" несе `model_tier_delta: 1` за замовчуванням (graph.md). +fn parse_retry_ladder(value: &serde_json::Value) -> Option<Vec<LadderStep>> { + let steps: Vec<LadderStep> = value + .as_array()? .iter() - .map(|l| l.trim_start_matches(['-', '*']).trim().to_lowercase()) - .filter(|s| !s.is_empty()) - .map(|strategy| LadderStep { - model_tier_delta: usize::from(strategy == "alternative-approach"), - strategy, + .filter_map(|item| { + let strategy = match item { + serde_json::Value::String(s) => s.trim().to_lowercase(), + serde_json::Value::Object(_) => item + .get("strategy") + .and_then(serde_json::Value::as_str) + .unwrap_or("base") + .trim() + .to_lowercase(), + _ => return None, + }; + if strategy.is_empty() { + return None; + } + let delta = item + .get("model_tier_delta") + .and_then(serde_json::Value::as_u64) + .map_or(usize::from(strategy == "alternative-approach"), |d| { + d as usize + }); + Some(LadderStep { + model_tier_delta: delta, + strategy, + }) }) .collect(); (!steps.is_empty()).then_some(steps) @@ -367,7 +406,8 @@ pub fn preflight_env( // Істина model_tier — прапор a.md; fallback: executor.model_tier у // frontmatter (старі вузли) → default_model_tier із .mt.json → AVG. - let tier_flag = read_flag_section(&dir, "## model tier").map(|v| v[0].clone()); + let flag = read_executor_flag(&dir)?; + let tier_flag = flag_str(flag.as_ref(), "model_tier"); let executor_tier = task_fm .get("executor") .and_then(|e| e.get("model_tier")) @@ -384,15 +424,16 @@ pub fn preflight_env( .unwrap_or_else(|| "AVG".to_string()), ); - let ladder = read_flag_section(&dir, "## retry ladder") - .and_then(|lines| parse_retry_ladder(&lines)) + let ladder = flag + .as_ref() + .and_then(|f| f.get("retry_ladder")) + .and_then(parse_retry_ladder) .unwrap_or_else(default_retry_ladder); let step = resolve_retry_step(attempt, &ladder); let model_tier = bump_model_tier(&base_tier, step.model_tier_delta); let retry_strategy = step.strategy.clone(); - let agent_cli = read_flag_section(&dir, "## agent cli") - .map(|v| v[0].clone()) + let agent_cli = flag_str(flag.as_ref(), "agent_cli") .unwrap_or_else(|| cli_env.agent_cli.clone()) .to_lowercase(); // Fail-fast до claim/worktree: невідомий CLI — помилка конфігурації. @@ -840,13 +881,15 @@ mod tests { const TASK: &str = "---\nschema_version: 1\ncreated_at: 2026-06-06T10:00:00Z\nbudget_sec: 5\nbudget_hard_sec: 2\nprogress_timeout_sec: 60\n---\n\n## Task\n\nx\n"; + const FLAG: &str = "---\nschema_version: 1\nmodel_tier: AVG\n---\n"; + /// Пише task.md/a.md на диск, без git — для тестів `preflight()` /// (суто файлова логіка, git-репо не потрібне). fn node_files_only(tmp: &Path, path: &str) { let dir = tmp.join(path); fs::create_dir_all(&dir).unwrap(); fs::write(dir.join("task.md"), TASK).unwrap(); - fs::write(dir.join("a.md"), "schema_version: 1\n").unwrap(); + fs::write(dir.join("a.md"), FLAG).unwrap(); } /// Як [`node_files_only`], але комітить і пушить у `origin/main` — @@ -891,7 +934,7 @@ mod tests { node_files_only(&root, "solo"); fs::write( root.join("solo/a.md"), - "## Model tier\n\nAVG\n\n## Agent cli\n\ncursor\n", + "---\nschema_version: 1\nmodel_tier: AVG\nagent_cli: cursor\nskills: [bash]\n---\n", ) .unwrap(); let r = root.to_string_lossy().into_owned(); @@ -918,7 +961,7 @@ mod tests { node_files_only(&root, "solo"); fs::write( root.join("solo/a.md"), - "## Model tier\n\nAVG\n\n## Retry ladder\n\n- base\n- diagnose-first\n", + "---\nschema_version: 1\nmodel_tier: AVG\nretry_ladder:\n - {}\n - strategy: diagnose-first\n---\n", ) .unwrap(); fs::write(root.join("solo/run_001.md"), "---\nresult: failed\n---\n").unwrap(); @@ -932,6 +975,37 @@ mod tests { assert_eq!(plan.model_tier, "AVG"); } + #[test] + fn preflight_rejects_legacy_section_flag() { + // Жорсткий перехід: старий a.md не читається як порожній (що підмінило + // б model_tier/agent_cli дефолтами), а валить preflight до claim. + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join("mt"); + node_files_only(&root, "solo"); + fs::write( + root.join("solo/a.md"), + "## Model tier\n\nMAX\n\n## Agent cli\n\ncursor\n", + ) + .unwrap(); + let r = root.to_string_lossy().into_owned(); + + let err = preflight_env(&r, "solo", &env_default()).unwrap_err(); + assert!(err.contains("markdown-секційний формат"), "got: {err}"); + assert!(err.contains("graph.md"), "помилка має вказувати на спеку"); + } + + #[test] + fn preflight_rejects_flag_without_frontmatter() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join("mt"); + node_files_only(&root, "solo"); + fs::write(root.join("solo/a.md"), "model_tier: MAX\n").unwrap(); + let r = root.to_string_lossy().into_owned(); + + let err = preflight_env(&r, "solo", &env_default()).unwrap_err(); + assert!(err.contains("без YAML-frontmatter"), "got: {err}"); + } + #[test] fn preflight_rejects_unknown_agent_cli_fail_fast() { let tmp = tempfile::tempdir().unwrap(); @@ -1123,7 +1197,7 @@ printf -- '---\nschema_version: 1\n---\n\n## Summary\n\nok\n' > "fact_${MT_RUN_N "---\nschema_version: 1\nbudget_sec: 5\nbudget_hard_sec: 2\n---\n\n## Task\n\nx\n\n## Check\n\nfalse\n", ) .unwrap(); - fs::write(dir.join("a.md"), "schema_version: 1\n").unwrap(); + fs::write(dir.join("a.md"), FLAG).unwrap(); crate::test_support::commit_all(repo.work.path(), "add gated"); crate::test_support::push_head(repo.work.path(), "refs/heads/main"); diff --git a/docs/conformance.md b/docs/conformance.md index 0c109e4..239c6fa 100644 --- a/docs/conformance.md +++ b/docs/conformance.md @@ -31,7 +31,7 @@ | Сканування графа, `deps/`, denylist | РЕАЛІЗОВАНО | `lib.rs` `scan_tasks`/`scan_dir` | — | | Derived-стани вузла | ЧАСТКОВО | `lib.rs` `detect_state` | `stalled` не виводиться (немає інтеграції з remote claim refs); `blocked-invalid-dep` як warning | | `failed_streak`: категорія `result` + межа «прийнятий fact» | РЕАЛІЗОВАНО | `lib.rs` `failed_streak`/`is_execution_failure`/`accepted_fact_nnn` | — | -| Файловий контракт `a.md`/`h.md` | ЧАСТКОВО | `lib.rs` `write_executor_flag` | Пишуться markdown-секціями, спека вимагає YAML-фронтматер; немає `secrets`, `interactive`, `assignee`, `notify`, `parent` — **конфлікт спека↔код, потребує рішення** | +| Файловий контракт `a.md`/`h.md` | ЧАСТКОВО | `lib.rs` `write_executor_flag`, `runner.rs` `read_executor_flag` | Формат — YAML-фронтматер (закрито); читаються `model_tier`, `agent_cli`, `retry_ladder`. Ще не спожиті: `secrets` (брокер), `interactive`, `assignee`, `parent` | | Артефакти version chain (читання) | РЕАЛІЗОВАНО | `artifacts.rs` | — | | `schema_version` fail-closed | ВІДСУТНЄ | — | Поле пишеться, але жоден читач не валідує версію | | Гейт immutability (`task.md`/`a.md`/`h.md` проти `origin/main`) | ВІДСУТНЄ | — | Немає ні в `signal.rs`, ні в `runner.rs` | @@ -116,9 +116,9 @@ ## Закриті питання +- **Формат `a.md`/`h.md`** (2026-08-09): рішення — **YAML-фронтматер у markdown**, жорсткий перехід. Ні голий YAML (як пропонувала спека), ні markdown-секції (як робив код): файл має розширення `.md`, тож фронтматер узгоджений з рештою `.md`-артефактів, перевикористовує один код-шлях `parse_front_matter`, рендериться при рев'ю і лишає місце для прози в тілі. Голий YAML лишається контрактом `.yml`-файлів (`.mt-claim.yml`). Спеку уточнено (nitra/mt), 19 файлів dogfood-графа мігровано, старий формат відхиляється явною помилкою в preflight — тести `preflight_rejects_legacy_section_flag`, `preflight_rejects_flag_without_frontmatter`. - **Межа `failed_streak`** (2026-08-09): рішення — спека виграє, код приведено до неї. Межа — останній *прийнятий* fact (`accepted_fact_nnn`); відхилений аудитом fact межу не рухає. Причина: інакше цикл «провал → провал → сирий fact → аудит відхилив» обнуляє лічильник вічно, і драбина ретраїв ніколи не доходить до EngineerAgent чи `unresolvable` — livelock. Тест-сторож: `rejected_fact_livelock_terminates`. ## Відкриті питання -- **Формат `a.md`/`h.md`.** Спека вимагає YAML-фронтматер, код пише markdown-секції. Один із двох має поступитись — це контракт, який читають і люди, і агенти. - **`mt-napi`.** За рішенням Г специфікації `2026-07-23-mt-cli-rust.md` крейт мав бути видалений, але лишається у workspace members. diff --git a/mt/lint-fix-doc-files-check-f2f4fc1e/a.md b/mt/lint-fix-doc-files-check-f2f4fc1e/a.md index ab5ea4f..2e598b7 100644 --- a/mt/lint-fix-doc-files-check-f2f4fc1e/a.md +++ b/mt/lint-fix-doc-files-check-f2f4fc1e/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-27T15:41:40+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/lint-fix-js-eslint-04a66bfd/a.md b/mt/lint-fix-js-eslint-04a66bfd/a.md index ab5ea4f..637d220 100644 --- a/mt/lint-fix-js-eslint-04a66bfd/a.md +++ b/mt/lint-fix-js-eslint-04a66bfd/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m0-dogfood-smoke/a.md b/mt/m0-dogfood-smoke/a.md index 2e63254..a4e89e2 100644 --- a/mt/m0-dogfood-smoke/a.md +++ b/mt/m0-dogfood-smoke/a.md @@ -1,12 +1,11 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: MIN +agent_cli: codex +skills: + - bash + - write-files +interactive: false +--- -MIN - -## Skills - -- bash -- write-files - -## Agent cli - -codex diff --git a/mt/m0-kill-invalidate-scanner-sync/a.md b/mt/m0-kill-invalidate-scanner-sync/a.md index ab5ea4f..637d220 100644 --- a/mt/m0-kill-invalidate-scanner-sync/a.md +++ b/mt/m0-kill-invalidate-scanner-sync/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m0-ladder-cross-cli/a.md b/mt/m0-ladder-cross-cli/a.md index ab5ea4f..637d220 100644 --- a/mt/m0-ladder-cross-cli/a.md +++ b/mt/m0-ladder-cross-cli/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m0-nnn-origin-truth/a.md b/mt/m0-nnn-origin-truth/a.md index ab5ea4f..637d220 100644 --- a/mt/m0-nnn-origin-truth/a.md +++ b/mt/m0-nnn-origin-truth/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m1-acp-adapters/a.md b/mt/m1-acp-adapters/a.md index d80815e..644cae5 100644 --- a/mt/m1-acp-adapters/a.md +++ b/mt/m1-acp-adapters/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: MAX +skills: + - bash + - write-files +interactive: false +--- -MAX - -## Skills - -- bash -- write-files diff --git a/mt/m1-agent-protocol/a.md b/mt/m1-agent-protocol/a.md index ab5ea4f..637d220 100644 --- a/mt/m1-agent-protocol/a.md +++ b/mt/m1-agent-protocol/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m1-agent-server/a.md b/mt/m1-agent-server/a.md index ab5ea4f..637d220 100644 --- a/mt/m1-agent-server/a.md +++ b/mt/m1-agent-server/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m1-attach-graph/a.md b/mt/m1-attach-graph/a.md index ab5ea4f..637d220 100644 --- a/mt/m1-attach-graph/a.md +++ b/mt/m1-attach-graph/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m1-interactive-done-check/a.md b/mt/m1-interactive-done-check/a.md index ab5ea4f..637d220 100644 --- a/mt/m1-interactive-done-check/a.md +++ b/mt/m1-interactive-done-check/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m1-session-graph-wiring/a.md b/mt/m1-session-graph-wiring/a.md index ab5ea4f..637d220 100644 --- a/mt/m1-session-graph-wiring/a.md +++ b/mt/m1-session-graph-wiring/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m2-acp-approval-e2e/a.md b/mt/m2-acp-approval-e2e/a.md index d80815e..644cae5 100644 --- a/mt/m2-acp-approval-e2e/a.md +++ b/mt/m2-acp-approval-e2e/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: MAX +skills: + - bash + - write-files +interactive: false +--- -MAX - -## Skills - -- bash -- write-files diff --git a/mt/m2-approvals-flow/a.md b/mt/m2-approvals-flow/a.md index ab5ea4f..637d220 100644 --- a/mt/m2-approvals-flow/a.md +++ b/mt/m2-approvals-flow/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m2-attach-ws-wiring/a.md b/mt/m2-attach-ws-wiring/a.md index ab5ea4f..637d220 100644 --- a/mt/m2-attach-ws-wiring/a.md +++ b/mt/m2-attach-ws-wiring/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m2-handoff-core/a.md b/mt/m2-handoff-core/a.md index ab5ea4f..637d220 100644 --- a/mt/m2-handoff-core/a.md +++ b/mt/m2-handoff-core/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m2-interactive-run-file/a.md b/mt/m2-interactive-run-file/a.md index ab5ea4f..637d220 100644 --- a/mt/m2-interactive-run-file/a.md +++ b/mt/m2-interactive-run-file/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m2-relay-bridge/a.md b/mt/m2-relay-bridge/a.md index ab5ea4f..637d220 100644 --- a/mt/m2-relay-bridge/a.md +++ b/mt/m2-relay-bridge/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files diff --git a/mt/m2-relay-core/a.md b/mt/m2-relay-core/a.md index ab5ea4f..637d220 100644 --- a/mt/m2-relay-core/a.md +++ b/mt/m2-relay-core/a.md @@ -1,8 +1,10 @@ -## Model tier +--- +schema_version: 1 +created_at: 2026-07-22T13:14:55+03:00 +model_tier: AVG +skills: + - bash + - write-files +interactive: false +--- -AVG - -## Skills - -- bash -- write-files