dsh-math-modeling-agent 0.2.5 → 0.2.7

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/README.md CHANGED
@@ -108,7 +108,7 @@ D3 方向/D4 裁决),`run-state.mjs gate` 在每次状态转移前强制校
108
108
  安装:
109
109
 
110
110
  ```bash
111
- dsh plugin --profile web add github:yohanchen1/MathModelingAgent#v0.2.5
111
+ dsh plugin --profile web add github:yohanchen1/MathModelingAgent#v0.2.7
112
112
  dsh --profile web --dump-config # 检查组合层(应看到 dsh-math-modeling-agent-skills 行)
113
113
  dsh web # 重启以加载插件
114
114
  ```
@@ -118,10 +118,11 @@ dsh web # 重启以加载插件
118
118
  不要用 `npm install` 代替,那会装到错误位置。如本机镜像源同步滞后,可显式走官方源:
119
119
  `dsh plugin --profile web add dsh-math-modeling-agent --registry=https://registry.npmjs.org/`)
120
120
 
121
- 开始建模——直接描述任务即可。每个会改变模型结构的决策都会在对话中交互确认
122
- (任务路由、各子问题假设、候选方向、每轮裁决),每轮在对话直接输出 runlog 式摘要
123
- (问题重述/分析/假设/建模求解/验证/鲁棒性/评价改进/参考文献),失败尝试按类归档到独立目录,
124
- 终态前强制鲁棒性敏感度分析:
121
+ 开始建模——直接描述任务即可。每个环节都是人与 LLM 的深度信息交换:LLM 给出带依据链的
122
+ 完整分析(题面原句/数据证据/文献/显式判断标记),你纠正、补充背景或提供自己的参考文献,
123
+ LLM 更新并展示差异。建模前必须先做文献调研(AI 检索原理与方法文献,你可增删),每个候选
124
+ 方向都有文献出处;每轮在对话直接输出 runlog 式摘要(问题重述/分析/假设/建模求解/验证/鲁棒性/
125
+ 评价改进/参考文献),失败尝试按类归档,终态前强制鲁棒性敏感度分析:
125
126
 
126
127
  ```text
127
128
  建立这个数学建模问题的模型,先分析题目和数据。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-math-modeling-agent",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Evidence-driven mathematical modeling and verification skills for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "files": [
@@ -15,7 +15,7 @@ Move one mathematical problem from scoped intake to an evidence-backed conclusio
15
15
 
16
16
  Accept problem text, attachments, or an existing run directory.
17
17
 
18
- Interaction model (references/interaction-protocol.md): execution details stay autonomous (file names, tool adapters, state fields), but every model-shaping decision is interactive — D0 restatement / D1 routing once per task, D2 assumptions and D3 direction per subproblem, D2' on mid-subproblem discoveries, D4 verdict per round. Show the runlog digest in the chat after every round (references/report-contract.md); announce one status line before every transition; the user may interrupt at any time.
18
+ Interaction model (references/interaction-protocol.md): execution details stay autonomous (file names, tool adapters, state fields), but every model-shaping decision is a DEEP three-beat exchange LLM presents a complete analysis with its evidence chain, the user responds with substance (corrections, background, their own references), the LLM updates the artifact and shows the delta. Decision points: D0 six-section restatement / D1 routing once per task, D-R literature research before any candidate is built, D2 assumptions and D3 direction per subproblem, D2' on mid-subproblem discoveries, D4 verdict per round. Show the runlog digest in the chat after every round (references/report-contract.md); announce one status line before every transition; the user may interrupt at any time.
19
19
 
20
20
  Return a concise current status, supported claims, unresolved obligations, attempt delta, artifact path, and next action or final report.
21
21
 
@@ -26,13 +26,14 @@ Return a concise current status, supported claims, unresolved obligations, attem
26
26
  3. Create or validate run state with `scripts/run-state.mjs` and follow `references/state-recovery.md`. Gate EVERY transition: `run-state.mjs gate <dir> --to <status> --reason "..."`.
27
27
  4. Profile inputs and build the subproblem DAG using `references/data-subproblems.md`.
28
28
  5. Register assumptions (per subproblem), claims, and verification obligations from `references/claims-evidence.md`; ask D2 for the subproblem being started.
29
- 6. Build a baseline and materially different candidates using `references/modeling-methodology.md`; ask D3 (candidate matrix).
30
- 7. Probe tools with `scripts/capability-probe.mjs`; create a run-local Python environment with `scripts/python-environment.mjs` only when computation is required. Announce the tool choice and fallback for the phase (references/tool-policy.md).
31
- 8. Execute, verify, critique, and revise according to `references/workflow.md` and `references/tool-policy.md`; ask D4 after each VERIFY.
32
- 9. Dispatch subagents for parallel exploration, blind verification, and audits per `references/subagent-dispatch.md`.
33
- 10. Escalate evidence gaps or stalled directions through `references/research-breakthrough.md`; write wall memos and the failure ledger per `references/run-directory.md`.
34
- 11. Write only incremental attempt and terminal reports defined by `references/report-contract.md`; output the per-round runlog digest in the chat.
35
- 12. Before any terminal transition, run the cleanup gate (references/run-directory.md) and the robustness gate (a PASSED robustness-kind obligation, or scope.robustnessExempt with reason).
29
+ 6. **Literature research (mandatory, before candidates)**: transition CLAIMS_REGISTERED → RESEARCH, search principles and methods by direction (never contest solutions), write `research/sources.jsonl`, attach a literature column to the candidate matrix, then ask D-R (user endorses/rejects/adds references) and move to CANDIDATES_READY.
30
+ 7. Build a baseline and materially different candidates using `references/modeling-methodology.md`; ask D3 (candidate matrix).
31
+ 8. Probe tools with `scripts/capability-probe.mjs`; create a run-local Python environment with `scripts/python-environment.mjs` only when computation is required. Announce the tool choice and fallback for the phase (references/tool-policy.md).
32
+ 9. Execute, verify, critique, and revise according to `references/workflow.md` and `references/tool-policy.md`; ask D4 after each VERIFY.
33
+ 10. Dispatch subagents for parallel exploration, blind verification, and audits per `references/subagent-dispatch.md`.
34
+ 11. Escalate evidence gaps or stalled directions through `references/research-breakthrough.md`; write wall memos and the failure ledger per `references/run-directory.md`.
35
+ 12. Write only incremental attempt and terminal reports defined by `references/report-contract.md`; output the per-round runlog digest in the chat.
36
+ 13. Before any terminal transition, run the cleanup gate (references/run-directory.md) and the robustness gate (a PASSED robustness-kind obligation, or scope.robustnessExempt with reason).
36
37
 
37
38
  ## Invariants
38
39
 
@@ -8,6 +8,8 @@ Store claim ID, exact wording, type, scope/quantifiers, assumptions, risk, verif
8
8
 
9
9
  Store id, statement, scope (subproblem ids or global), source category, risk, sensitivity plan, validation status, affected claims, status (active/revised/superseded), and revisionHistory [{at, reason, changedTo}]. Allowed source categories are problem, data, theory, verified literature, domain instruction, and modeling simplification.
10
10
 
11
+ Every assumption additionally carries a `basis` field — the evidence chain behind it: `problem-quote` (exact original wording), `data` (evidence id + statistic), `literature` (source id + excerpt location), or `llm-judgment` (explicitly flagged as the LLM's own inference, challengeable in D2).
12
+
11
13
  Revisions are hard operations: when an assumption is revised, every claim/obligation that depended on it is marked `stale: true` with the revising assumption id, and the affected verification reruns (or the conclusion is explicitly downgraded) before any terminal state.
12
14
 
13
15
  ## Robustness obligation (v2, terminal gate)
@@ -3,28 +3,101 @@
3
3
  Three interaction layers. Execution details stay autonomous; every
4
4
  model-shaping decision is visible and user-owned.
5
5
 
6
+ ## Deep-exchange protocol (v2.6)
7
+
8
+ Every L1 decision point follows a three-beat exchange, never a one-way
9
+ "here is my conclusion, confirm it":
10
+
11
+ 1. **LLM presents a complete analysis with its evidence chain.** Every claim
12
+ inside the analysis carries its basis: an exact quote from the problem
13
+ text, a data statistic, a literature source id, or an explicit
14
+ "LLM judgment" marker. A naked conclusion is not a presentation.
15
+ 2. **The user responds with substance**: confirmation, correction, background
16
+ knowledge, their own references, or their own judgment. A pure "ok" is
17
+ accepted, but the protocol must have OFFERED the space for substance —
18
+ the questions must be open, not yes/no gates.
19
+ 3. **The LLM updates the artifact and shows the delta.** The restatement,
20
+ analysis log, assumption list, or candidate matrix is edited and the chat
21
+ shows exactly which points changed because of the user's input.
22
+
23
+ High-impact points (D0, D-R, D2) may loop beats 2-3 as many times as needed
24
+ until the user says "可以了". Mathematical modeling is a deep activity;
25
+ "skim and start solving" is a protocol violation.
26
+
6
27
  ## L1 decision points (MUST ask via the question tool)
7
28
 
8
29
  | Point | Trigger | Question | Default |
9
30
  |---|---|---|---|
10
- | D0 问题重述 | intake complete | confirm the full problem restatement (all sub-questions, constraints, deliverables) | accept |
31
+ | D0 问题重述 | intake complete | the six-section deep restatement below + open clarification questions | accept + no additions |
11
32
  | D1 路由 | TRIAGE | task type (prediction/optimization/explanation/proof/…) + assurance mode (Fast/Standard/High-Assurance) | auto-routing result |
12
- | D2 假设确认 | **each subproblem starts** | the assumptions registered for THIS subproblem (numbered list: statement + source + risk); user confirms or edits | accept all |
33
+ | D-R 文献研究 | CLAIMS_REGISTERED (before CANDIDATES_READY) | the literature list by direction (AI-found); user endorses / rejects / adds their own | accept list |
34
+ | D2 假设确认 | **each subproblem starts** | the assumptions for THIS subproblem, each with statement + basis chain (problem quote / data evidence / literature / LLM-judgment marker) + risk + challengeable point | accept all |
13
35
  | D2' 新假设插问 | mid-subproblem discovery that changes model structure | the new assumption + its effect | record-and-continue |
14
- | D3 方向选择 | CANDIDATES_READY | candidate matrix (mechanism/assumptions/cost/verification/expected gain); user picks or authorizes auto | baseline |
15
- | D4 结果裁决 | VERIFY done | verdict (PASS/FAIL/INCONCLUSIVE) + evidence digest; user chooses accept/REVISE/switch direction/end | verdict-implied |
36
+ | D3 方向选择 | CANDIDATES_READY | candidate matrix with a literature column per direction; user picks or authorizes auto | baseline |
37
+ | D4 结果裁决 | VERIFY done | verdict + evidence digest + key intermediate results (residual/constraint-violation distributions, figure paths) + failure details | verdict-implied |
38
+
39
+ ## D0 deep restatement — six sections (MANDATORY structure)
40
+
41
+ 1. **题目要点摘录**:the problem's own numbers and constraints, quoted.
42
+ 2. **背景解读**:the engineering/physical mechanism behind the problem.
43
+ 3. **数据语义**:every attachment, column by column — meaning, units,
44
+ coordinate system, magnitudes; unreadable artifacts are flagged with the
45
+ minimum representation needed.
46
+ 4. **隐含信息与歧义清单**:numbered; each entry = the LLM's reading + its
47
+ basis + the risk if the reading is wrong. This is the heart of the
48
+ exchange: the user corrects entries here.
49
+ 5. **三问数学本质判定**:each sub-question classified (fitting/optimization/
50
+ geometric optics/simulation/…), with why.
51
+ 6. **待澄清问题**:open questions inviting the user to add background,
52
+ correct a reading, or supply their own references — never a yes/no list.
53
+
54
+ After the user answers, problem-brief.md is updated and the delta is shown.
55
+
56
+ ## D-R literature research (pre-modeling, mandatory)
57
+
58
+ 1. The LLM searches by direction × method for principles and methods only —
59
+ engineering background, methodology papers, textbooks, official docs.
60
+ **Never** search for solutions/answers to the specific contest problem.
61
+ 2. Findings are written to research/sources.jsonl (author/title/year/venue/
62
+ URL/retrieval date/excerpt with section or theorem/table location/mapped
63
+ direction). Every candidate in the method matrix gets a literature column
64
+ referencing these source ids; a direction with no literature must be
65
+ explicitly marked "原创方向" with its derivation basis.
66
+ 3. The user is asked: endorse / reject / add their own references. User-added
67
+ references join sources.jsonl with the same fields.
68
+ 4. The D-R interaction record goes into ledger.scope.interactions; the gate
69
+ refuses CANDIDATES_READY without it.
70
+
71
+ ## Honest boundary of the gate
72
+
73
+ The gate is a MECHANISM guarantee, not an anti-fraud guarantee: it proves a
74
+ record exists, never that the record is true — every interaction record is
75
+ written by the agent itself. Falsified records are therefore out of the
76
+ gate's reach; the defense against them is the blind verifier subagent
77
+ (subagent-dispatch.md), which re-derives key claims from data alone, and the
78
+ artifact checks (research/sources.jsonl, attempts/N/report.md) that tie each
79
+ record to real files on disk.
80
+
81
+ ## Rules
16
82
 
17
- Rules:
18
- - D0/D1: once per task. D2/D3: once per subproblem. D2': only when a discovery changes model structure. D4: once per attempt round.
83
+ - D0/D1/D-R: once per task. D2/D3: once per subproblem. D2': only when a
84
+ discovery changes model structure. D4: once per attempt round.
19
85
  - Every L1 record is appended to `ledger.scope.interactions` as
20
86
  `{id, decisionPoint, question, options, userAnswer, timestamp, effectOnModel}`
21
87
  or, for explicit auto authorization,
22
88
  `{auto: true, decisionPoint, reason}`.
23
89
  - Run the gate before every transition: `run-state.mjs gate <dir> --to <status> --reason "<...>"`.
24
- The gate refuses SCOPE_FROZEN without D1, ATTEMPT-from-CANDIDATES_READY without D3,
25
- terminals-from-VERIFY without D4, and REVISE/RESEARCH whose reason fails to
26
- reference a `decisionStack` entry id. The CLI `transition` command enforces
27
- the same gate (escape hatch `--skip-gate true` is for mechanism testing only).
90
+ The gate refuses SCOPE_FROZEN without D0+D1, CANDIDATES_READY without D-R,
91
+ ATTEMPT-from-CANDIDATES_READY without D3, terminals-from-VERIFY without D4,
92
+ and REVISE/RESEARCH (from VERIFY) whose reason fails to reference a
93
+ `decisionStack` entry id. The CLI `transition` command enforces the same
94
+ gate and has NO escape hatch. Every D4 record carries the attempt number it
95
+ adjudicates (`attempt: N`); the terminal gate requires the D4 for the
96
+ CURRENT attempt. ATTEMPT from CANDIDATES_READY requires `--subproblem <id>`.
97
+ Terminal gates additionally require `scope.cleanupPassed: true` and the
98
+ current attempt's report.md on disk. Mechanism-level tests and unattended
99
+ batch runs may declare `scope.contractExempt: true` — production modeling
100
+ must never set it.
28
101
 
29
102
  ## L2 display points (MUST show, never ask)
30
103
 
@@ -46,8 +119,8 @@ Rules:
46
119
 
47
120
  - Every L1 decision also appends to `ledger.scope.decisionStack`:
48
121
  `{id (e.g. D3-SP1), point, choice, at, reason}`.
49
- - REVISE/RESEARCH reasons MUST reference the stack entry being revisited
50
- (e.g. "回退到 D3-SP1:M2-area 违反 0.07% 边长约束,改 M1-cheb").
122
+ - REVISE/RESEARCH (from VERIFY) reasons MUST reference the stack entry being
123
+ revisited (e.g. "回退到 D3-SP1:M2-area 违反 0.07% 边长约束,改 M1-cheb").
51
124
  - A wrong direction returns to its fork point: FORK records the fork entry in
52
125
  the stack; each failed branch leaves a wall memo before the next branch runs.
53
126
  - Assumption revisions keep history: `revisionHistory: [{at, reason, changedTo}]`,
@@ -28,12 +28,21 @@ Ask one question at a time. Read the problem and attachments first. Ask only que
28
28
  - The 16 Problem Brief fields are filled once at intake for the whole problem;
29
29
  subproblem-level analysis and assumptions roll forward per attempt.
30
30
 
31
- ## Rules
31
+ ## Exchange depth per field
32
32
 
33
+ - **用户必须输入**:目标(1,2)、误差/精度要求(12)、验证标准(13)、最终交付物(14)——这些只能来自用户,LLM 不得替用户定。
34
+ - **LLM 可推断但必须展示依据**:子问题(3)、决策变量(6)、参数(7)、目标函数(8)、约束(9)、假设(10)、任务类型(11)——每条附依据链(题面原句/数据证据/文献/显式 LLM 判断标记)。
35
+ - **LLM 可纯推断但必须展示证据**:数据质量(5)——附画像统计;背景(1)、风险(15)、不适用字段(16)——附理由。
36
+
37
+ ## Deep-interaction rules
38
+
39
+ - Every answer the LLM infers is PRESENTED with its basis before freezing; the user may challenge any item.
40
+ - Open questions, not yes/no gates: after the LLM's analysis, ask "你有哪些补充、纠正或自己的资料?" — not merely "确认吗?".
41
+ - High-impact exchanges loop until the user says "可以了".
33
42
  - Infer before asking.
34
43
  - Ask the highest-impact unknown first.
35
44
  - Mark irrelevant fields “not applicable” with a reason.
36
- - Turn “自行判断” into an explicit modeling assumption with risk and sensitivity plan.
45
+ - Turn “自行判断” into an explicit modeling assumption with risk and sensitivity plan, listed in D2 for later challenge.
37
46
  - Assumptions are revisable: record revisionHistory and mark dependent claims stale.
38
47
  - Do not freeze scope while a missing answer can change the problem type or claim strength.
39
48
  - Do not force a proof problem to invent data or an exploratory analysis to invent an objective function.
@@ -28,11 +28,16 @@ State the frozen question, final scientific status, answer/recommendation, verif
28
28
 
29
29
  The following is shown DIRECTLY IN THE CHAT, not only on disk.
30
30
 
31
- ### Intake digest (once, at intake completion)
31
+ ### Intake digest (once, at intake completion) — the six-section deep restatement
32
32
 
33
- 【问题重述】full restatement: all sub-questions, constraints, deliverables
34
- 【问题分析】input profile, geometric/physical structure, subproblem DAG, key difficulties
35
- 【模型假设】the assumptions known at intake (numbered: statement + source + risk + sensitivity plan)
33
+ 【题目要点摘录】problem's own numbers and constraints, quoted
34
+ 【背景解读】engineering/physical mechanism
35
+ 【数据语义】every attachment column by column (meaning/units/coordinate system/magnitudes)
36
+ 【隐含信息与歧义清单】numbered; each = LLM reading + basis + risk (the user corrects entries here)
37
+ 【三问数学本质判定】fitting/optimization/geometric optics/… + why
38
+ 【待澄清问题】open questions inviting user background/corrections/references
39
+
40
+ After the D0 exchange, show the delta applied to problem-brief.md.
36
41
 
37
42
  ### Per-round digest (after EVERY attempt round)
38
43
 
@@ -8,7 +8,7 @@ Record author, title, year, venue, DOI/stable URL, version, retrieval date, exac
8
8
 
9
9
  ## Method matrix
10
10
 
11
- For each candidate method record applicability, assumptions, data needs, implementation cost, verification route, reason to try, and reason to reject.
11
+ For each candidate method record applicability, assumptions, data needs, implementation cost, verification route, reason to try, and reason to reject — plus a mandatory `literature: [sourceId]` column. Every direction must be traceable to at least one source (engineering background, methodology paper, textbook, official documentation); a literature-free direction is marked "原创方向" with its derivation basis written out. This matrix is built during the pre-modeling RESEARCH hop (D-R exchange, interaction-protocol.md), not after solving.
12
12
 
13
13
  ## Isolated campaign
14
14
 
@@ -13,6 +13,17 @@ TRIAGE → SCOPE_FROZEN → INPUT_PROFILED → CLAIMS_REGISTERED → CANDIDATES_
13
13
  VERIFY may move to REVISE, RESEARCH, FORK, an independent audit, or one terminal status:
14
14
  SOLVED, PARTIAL, CONDITIONAL, INCONCLUSIVE, REFUTED, INFEASIBLE, UNIDENTIFIABLE, BLOCKED, CANCELLED.
15
15
 
16
+ ## Pre-modeling literature research (v2.6, mandatory)
17
+
18
+ CLAIMS_REGISTERED → RESEARCH → CANDIDATES_READY. Before any candidate is
19
+ built, run the D-R exchange (interaction-protocol.md): search principles and
20
+ methods by direction, write research/sources.jsonl, attach a literature column
21
+ to the candidate matrix. Every direction must be traceable to at least one
22
+ source — engineering background, methodology paper, textbook, or official
23
+ documentation; a literature-free direction must be explicitly marked
24
+ "原创方向" with its derivation basis. Never search for solutions to the
25
+ specific contest problem.
26
+
16
27
  ## Interaction gates (v2)
17
28
 
18
29
  Before EVERY transition run `run-state.mjs gate <dir> --to <status> --reason "..."`
@@ -28,7 +28,7 @@
28
28
  "independentAuditPassed": { "type": "boolean" },
29
29
  "interactions": {
30
30
  "type": "array",
31
- "description": "L1 decision-point records: {id, decisionPoint: D1|D2|D3|D4, question, options, userAnswer, timestamp, effectOnModel} or {auto: true, decisionPoint, reason}",
31
+ "description": "L1 decision-point records: {id, decisionPoint: D0|D1|D-R|D2|D3|D4, question, options, userAnswer, timestamp, effectOnModel} or {auto: true, decisionPoint, reason}; D-R is the pre-modeling literature-research exchange",
32
32
  "items": { "type": "object" }
33
33
  },
34
34
  "decisionStack": {
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "subproblems": {
57
57
  "type": "array",
58
- "description": "Subproblem DAG nodes: {id, task, inputs, dependencies, acceptance, status, analysisLog: [{at, entry, trigger}]} — analysisLog is the rolling analysis ledger.",
58
+ "description": "Subproblem DAG nodes: {id, task, inputs, dependencies, acceptance, status, analysisLog: [{at, entry, trigger}]} — analysisLog is the rolling analysis ledger, updated after every deep-exchange beat.",
59
59
  "items": { "type": "object" }
60
60
  },
61
61
  "candidates": {
@@ -326,24 +326,34 @@ export async function createPythonEnvironment(input) {
326
326
  const finalPythonPath = environmentPython(environmentDir, platform, resolvePath)
327
327
  try {
328
328
  let adapter
329
+ let fallbackReason = null
329
330
  const installLogPath = ensureContained(runDir, resolvePath(runDir, 'python-environment-install.log'), platform)
330
331
  const installOptions = { cwd: runDir, stdoutFile: installLogPath, timeout: 20 * 60 * 1000, label: 'package install' }
331
332
  if (uv) {
332
- adapter = 'uv'
333
- const createArgs = ['venv', '--seed', environmentDir]
334
- commands.push({ command: uv.executable, args: createArgs })
335
- assertSuccess(await runCommand(uv.executable, createArgs, { cwd: runDir }), 'uv venv')
336
- if (packages.length) {
337
- const installArgs = ['pip', 'install', '--python', finalPythonPath, ...packages]
338
- commands.push({ command: uv.executable, args: installArgs, stdoutFile: installLogPath })
339
- process.stderr.write(`python-environment: uv installing ${packages.length} package(s); network-bound, may take minutes; log: ${installLogPath}\n`)
340
- assertSuccess(await runCommand(uv.executable, installArgs, installOptions), 'uv pip install')
333
+ try {
334
+ adapter = 'uv'
335
+ const createArgs = ['venv', '--seed', environmentDir]
336
+ commands.push({ command: uv.executable, args: createArgs })
337
+ assertSuccess(await runCommand(uv.executable, createArgs, { cwd: runDir }), 'uv venv')
338
+ if (packages.length) {
339
+ const installArgs = ['pip', 'install', '--python', finalPythonPath, ...packages]
340
+ commands.push({ command: uv.executable, args: installArgs, stdoutFile: installLogPath })
341
+ process.stderr.write(`python-environment: uv installing ${packages.length} package(s); network-bound, may take minutes; log: ${installLogPath}\n`)
342
+ assertSuccess(await runCommand(uv.executable, installArgs, installOptions), 'uv pip install')
343
+ }
344
+ } catch (error) {
345
+ fallbackReason = error.message
346
+ adapter = null
347
+ process.stderr.write(`python-environment: uv failed (${fallbackReason}); falling back to the user's pip (which honors the system pip index configuration)\n`)
341
348
  }
342
- } else {
349
+ }
350
+ if (!adapter && python) {
343
351
  adapter = 'venv'
344
- const createArgs = pythonVenvArgs(python, environmentDir, platform)
345
- commands.push({ command: python.executable, args: createArgs })
346
- assertSuccess(await runCommand(python.executable, createArgs, { cwd: runDir }), 'python venv')
352
+ if (!await exists(finalPythonPath, fileStat)) {
353
+ const createArgs = pythonVenvArgs(python, environmentDir, platform)
354
+ commands.push({ command: python.executable, args: createArgs })
355
+ assertSuccess(await runCommand(python.executable, createArgs, { cwd: runDir }), 'python venv')
356
+ }
347
357
  if (packages.length) {
348
358
  const installArgs = ['-m', 'pip', 'install', ...packages]
349
359
  commands.push({ command: finalPythonPath, args: installArgs, stdoutFile: installLogPath })
@@ -351,6 +361,7 @@ export async function createPythonEnvironment(input) {
351
361
  assertSuccess(await runCommand(finalPythonPath, installArgs, installOptions), 'pip install')
352
362
  }
353
363
  }
364
+ if (!adapter) throw new Error(`package installation failed${fallbackReason ? `: ${fallbackReason}` : ''}`)
354
365
  await mkdir(environmentDir, { recursive: true })
355
366
  await freezeRequirements(runCommand, finalPythonPath, finalPythonPath, runDir, stagedLockPath, lockPath, commands)
356
367
  const manifest = {
@@ -21,7 +21,7 @@ const TRANSITIONS = Object.freeze({
21
21
  TRIAGE: ['SCOPE_FROZEN', ...STOP_TRANSITIONS],
22
22
  SCOPE_FROZEN: ['INPUT_PROFILED', ...STOP_TRANSITIONS],
23
23
  INPUT_PROFILED: ['CLAIMS_REGISTERED', ...STOP_TRANSITIONS],
24
- CLAIMS_REGISTERED: ['CANDIDATES_READY', ...STOP_TRANSITIONS],
24
+ CLAIMS_REGISTERED: ['RESEARCH', 'CANDIDATES_READY', ...STOP_TRANSITIONS],
25
25
  CANDIDATES_READY: ['ATTEMPT', ...STOP_TRANSITIONS],
26
26
  ATTEMPT: ['EXECUTE', ...STOP_TRANSITIONS],
27
27
  EXECUTE: ['VERIFY', ...STOP_TRANSITIONS],
@@ -366,7 +366,7 @@ function validateJournal(events, diagnostics) {
366
366
  })
367
367
  }
368
368
 
369
- export async function initRun(root, { taskId, mode = 'standard', budget, budgets }, runtime = {}) {
369
+ export async function initRun(root, { taskId, mode = 'standard', budget, budgets, contract }, runtime = {}) {
370
370
  assertTaskId(taskId); assertMode(mode)
371
371
  const effectiveBudget = { ...MODE_DEFAULTS[mode], ...(budget ?? budgets ?? {}) }
372
372
  const budgetDiagnostics = []
@@ -379,7 +379,8 @@ export async function initRun(root, { taskId, mode = 'standard', budget, budgets
379
379
  bestCandidateId: null, budget: effectiveBudget, createdAt: timestamp, updatedAt: timestamp,
380
380
  }
381
381
  const ledger = {
382
- schemaVersion: SCHEMA_VERSION, taskId, scope: { independentAuditPassed: false, interactions: [], decisionStack: [], robustnessExempt: false },
382
+ schemaVersion: SCHEMA_VERSION, taskId,
383
+ scope: { independentAuditPassed: false, interactions: [], decisionStack: [], robustnessExempt: false, ...(contract === false ? { contractExempt: true } : {}) },
383
384
  assumptions: [], claims: [], obligations: [], subproblems: [], candidates: [], issues: [],
384
385
  }
385
386
  const event = { schemaVersion: SCHEMA_VERSION, sequence: 0, type: 'RUN_INITIALIZED', taskId, timestamp, snapshot: clone(run) }
@@ -412,9 +413,14 @@ export async function initRun(root, { taskId, mode = 'standard', budget, budgets
412
413
  }
413
414
 
414
415
  /** The v2 interaction contract: decision records, decision-stack backtracking, and the robustness gate. */
415
- function hasDecisionRecord(scope, decisionPoint) {
416
+ function hasDecisionRecord(scope, decisionPoint, { allowAuto = true } = {}) {
416
417
  return Array.isArray(scope?.interactions) && scope.interactions.some(entry =>
417
- entry && (entry.decisionPoint === decisionPoint || entry.auto === true))
418
+ entry && (entry.decisionPoint === decisionPoint || (allowAuto && entry.auto === true)))
419
+ }
420
+ /** D4 verdict records carry the attempt number they adjudicate: {decisionPoint:'D4', attempt: 2, ...} */
421
+ function hasD4ForAttempt(scope, attempt) {
422
+ return Array.isArray(scope?.interactions) && scope.interactions.some(entry =>
423
+ entry && (entry.auto === true || (entry.decisionPoint === 'D4' && entry.attempt === attempt)))
418
424
  }
419
425
  function decisionStackIds(scope) {
420
426
  return Array.isArray(scope?.decisionStack)
@@ -430,6 +436,7 @@ function hasSubproblemD2(scope, subproblem) {
430
436
  function interactionContractViolations(run, ledger, to, reason, subproblem) {
431
437
  if (ledger.schemaVersion !== SCHEMA_VERSION) return []
432
438
  const scope = ledger.scope ?? {}
439
+ if (scope.contractExempt === true) return []
433
440
  const violations = []
434
441
  const transitional = to !== undefined
435
442
  const earlyStop = ['BLOCKED', 'CANCELLED'].includes(run.status)
@@ -437,16 +444,20 @@ function interactionContractViolations(run, ledger, to, reason, subproblem) {
437
444
  if (!hasDecisionRecord(scope, 'D0')) violations.push('D0 restatement interaction record missing (ledger.scope.interactions)')
438
445
  if (!hasDecisionRecord(scope, 'D1')) violations.push('D1 routing interaction record missing (ledger.scope.interactions)')
439
446
  }
447
+ const afterCandidates = ['CANDIDATES_READY', 'ATTEMPT', 'EXECUTE', 'VERIFY', 'REVISE', 'FORK', ...FINAL_STATES]
448
+ const candidatesGate = transitional ? (to === 'CANDIDATES_READY') : (afterCandidates.includes(run.status) && !earlyStop)
449
+ if (candidatesGate && !hasDecisionRecord(scope, 'D-R', { allowAuto: false })) violations.push('D-R literature-research interaction record missing (pre-modeling literature survey is mandatory; auto-authorization is not accepted)')
440
450
  const enteringAttemptFromCandidates = transitional ? (to === 'ATTEMPT' && run.status === 'CANDIDATES_READY') : (run.currentAttempt >= 1)
441
451
  if (enteringAttemptFromCandidates) {
452
+ if (transitional && !subproblem) violations.push('ATTEMPT from CANDIDATES_READY requires --subproblem <id>')
442
453
  if (!hasDecisionRecord(scope, 'D3')) violations.push('D3 direction-selection interaction record missing')
443
- if (transitional && !hasSubproblemD2(scope, subproblem)) violations.push(`D2 assumption interaction record missing for subproblem ${subproblem} (run the gate with --subproblem <id>)`)
454
+ if (transitional && subproblem && !hasSubproblemD2(scope, subproblem)) violations.push(`D2 assumption interaction record missing for subproblem ${subproblem} (run the gate with --subproblem <id>)`)
444
455
  }
445
456
  const enteringTerminalFromVerify = transitional
446
457
  ? (FINAL_STATES.includes(to) && run.status === 'VERIFY')
447
458
  : (FINAL_STATES.includes(run.status) && !['BLOCKED', 'CANCELLED'].includes(run.status))
448
- if (enteringTerminalFromVerify && !hasDecisionRecord(scope, 'D4')) violations.push('D4 verdict interaction record missing')
449
- if (transitional && (to === 'REVISE' || to === 'RESEARCH')) {
459
+ if (enteringTerminalFromVerify && !hasD4ForAttempt(scope, run.currentAttempt)) violations.push(`D4 verdict interaction record for attempt ${run.currentAttempt} missing`)
460
+ if (transitional && (to === 'REVISE' || (to === 'RESEARCH' && run.status === 'VERIFY'))) {
450
461
  const ids = decisionStackIds(scope)
451
462
  if (ids.length > 0 && !ids.some(id => typeof reason === 'string' && reason.includes(id))) {
452
463
  violations.push(`REVISE/RESEARCH reason must reference a decisionStack entry id (available: ${ids.join(', ')})`)
@@ -455,9 +466,12 @@ function interactionContractViolations(run, ledger, to, reason, subproblem) {
455
466
  const terminalAndExempt = transitional
456
467
  ? (FINAL_STATES.includes(to) && !['BLOCKED', 'CANCELLED'].includes(to))
457
468
  : (FINAL_STATES.includes(run.status) && !['BLOCKED', 'CANCELLED'].includes(run.status))
458
- if (terminalAndExempt && scope.robustnessExempt !== true) {
459
- const passed = Array.isArray(ledger.obligations) && ledger.obligations.some(o => o && o.kind === 'robustness' && o.status === 'PASS')
460
- if (!passed) violations.push('terminal state requires a PASSED robustness-kind obligation (or scope.robustnessExempt: true)')
469
+ if (terminalAndExempt) {
470
+ if (scope.robustnessExempt !== true) {
471
+ const passed = Array.isArray(ledger.obligations) && ledger.obligations.some(o => o && o.kind === 'robustness' && o.status === 'PASS')
472
+ if (!passed) violations.push('terminal state requires a PASSED robustness-kind obligation (or scope.robustnessExempt: true)')
473
+ }
474
+ if (scope.cleanupPassed !== true) violations.push('terminal state requires scope.cleanupPassed: true (run the cleanup checklist in run-directory.md)')
461
475
  }
462
476
  return violations
463
477
  }
@@ -547,7 +561,7 @@ async function validateRunUnlocked(root, expectedTaskId) {
547
561
  if (ledger.schemaVersion !== SCHEMA_VERSION) warnings.push(`legacy run (ledger schema v${ledger.schemaVersion}): interaction contract not enforced`)
548
562
  else {
549
563
  for (const violation of interactionContractViolations(run, ledger, undefined, undefined)) {
550
- warnings.push(`interaction contract: ${violation}`)
564
+ diagnostics.push(`interaction contract: ${violation}`)
551
565
  }
552
566
  if (run.currentAttempt >= 1) {
553
567
  const reportPath = join(root, 'attempts', String(run.currentAttempt), 'report.md')
@@ -571,7 +585,7 @@ export async function validateRun(root, expectedTaskId, runtime = {}) {
571
585
  /**
572
586
  * Check the v2 interaction contract for a PROSPECTIVE transition without
573
587
  * mutating anything. The agent workflow must call this before `transition`;
574
- * the CLI `transition` command enforces it internally unless --skip-gate.
588
+ * the CLI `transition` command enforces it internally and has NO escape hatch.
575
589
  */
576
590
  export async function gateTransition(root, { to, reason, subproblem }, runtime = {}) {
577
591
  return withLock(root, MUTATION_LOCK_FILE, 'gate', async () => {
@@ -579,6 +593,23 @@ export async function gateTransition(root, { to, reason, subproblem }, runtime =
579
593
  const run = await readJson(p.run)
580
594
  const ledger = await readJson(p.ledger)
581
595
  const violations = interactionContractViolations(run, ledger, to, reason, subproblem)
596
+ const exempt = ledger.scope?.contractExempt === true
597
+ if (to === 'CANDIDATES_READY' && !exempt) {
598
+ const sourcesPath = join(root, 'research', 'sources.jsonl')
599
+ try {
600
+ if (!(await readFile(sourcesPath, 'utf8')).trim()) violations.push('research/sources.jsonl is empty (pre-modeling literature survey artifacts are mandatory)')
601
+ } catch {
602
+ violations.push('research/sources.jsonl missing (pre-modeling literature survey artifacts are mandatory)')
603
+ }
604
+ }
605
+ if (FINAL_STATES.includes(to) && !['BLOCKED', 'CANCELLED'].includes(to) && !exempt) {
606
+ const reportPath = join(root, 'attempts', String(run.currentAttempt), 'report.md')
607
+ try {
608
+ await readFile(reportPath, 'utf8')
609
+ } catch {
610
+ violations.push(`attempts/${run.currentAttempt}/report.md missing (write the runlog report before a terminal transition)`)
611
+ }
612
+ }
582
613
  return { allowed: violations.length === 0, violations, status: run.status }
583
614
  }, runtime)
584
615
  }
@@ -629,12 +660,10 @@ async function cli(argv) {
629
660
  return result
630
661
  }
631
662
  if (command === 'transition') {
632
- if (options['skip-gate'] !== 'true') {
633
- const gate = await gateTransition(root, { to: options.to, reason: options.reason, subproblem: options.subproblem })
634
- if (!gate.allowed) {
635
- process.exitCode = 1
636
- return { error: `interaction contract violated: ${gate.violations.join('; ')}`, gate }
637
- }
663
+ const gate = await gateTransition(root, { to: options.to, reason: options.reason, subproblem: options.subproblem })
664
+ if (!gate.allowed) {
665
+ process.exitCode = 1
666
+ return { error: `interaction contract violated: ${gate.violations.join('; ')}`, gate }
638
667
  }
639
668
  return transitionRun(root, { to: options.to, reason: options.reason, evidenceIds: options.evidence ? options.evidence.split(',').filter(Boolean) : [], issueIds: options.issues ? options.issues.split(',').filter(Boolean) : [], patch: options.candidate ? { bestCandidateId: options.candidate } : undefined })
640
669
  }