dsh-math-modeling-agent 0.1.2 → 0.2.1
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 +16 -7
- package/package.json +1 -1
- package/skills/math-modeling-agent/SKILL.md +16 -11
- package/skills/math-modeling-agent/examples/minimal-run/README.md +7 -1
- package/skills/math-modeling-agent/examples/resumed-run/README.md +5 -3
- package/skills/math-modeling-agent/references/claims-evidence.md +10 -2
- package/skills/math-modeling-agent/references/interaction-protocol.md +64 -0
- package/skills/math-modeling-agent/references/math-grill.md +16 -10
- package/skills/math-modeling-agent/references/report-contract.md +38 -2
- package/skills/math-modeling-agent/references/run-directory.md +62 -0
- package/skills/math-modeling-agent/references/subagent-dispatch.md +47 -0
- package/skills/math-modeling-agent/references/tool-policy.md +18 -0
- package/skills/math-modeling-agent/references/workflow.md +31 -0
- package/skills/math-modeling-agent/schemas/ledger.schema.json +25 -4
- package/skills/math-modeling-agent/scripts/python-environment.mjs +13 -5
- package/skills/math-modeling-agent/scripts/run-state.mjs +114 -10
package/README.md
CHANGED
|
@@ -81,14 +81,20 @@ NOT_CHECKED → DERIVED → EXECUTED → VERIFIED → INDEPENDENTLY_VERIFIED →
|
|
|
81
81
|
默认运行目录 `math-modeling-runs/<task-id>/`:
|
|
82
82
|
|
|
83
83
|
```text
|
|
84
|
-
run.json
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
run.json ledger.json events.jsonl # 原子状态 + 契约(interactions/decisionStack)+ 日志
|
|
85
|
+
problem-brief.md inputs.json # 冻结的问题重述 + 输入清单
|
|
86
|
+
attempts/<n>/ # 每轮:report.md + code/ + data/ + plots/ + _drafts/
|
|
87
|
+
failed/directions/<id>/ # 方向级失败:wall memo + 代码 + 放弃理由
|
|
88
|
+
failed/code-drafts/<attempt>-<name>/ # 实现级失败:bug 版/超时版/弃用版(不删除)
|
|
89
|
+
failed/issues.md # 失败账本(DATA/TOOL/IMPLEMENTATION/MODEL/VALIDATION/EVIDENCE/RESEARCH_GAP)
|
|
90
|
+
research/ walls/ # 文献检索 / 突破备忘录
|
|
89
91
|
reproducibility.json final-report.md
|
|
90
92
|
```
|
|
91
93
|
|
|
94
|
+
v0.2.0 起:每个会改变模型结构的决策在对话中交互确认并写入 ledger(D0 重述/D1 路由/
|
|
95
|
+
D2 子问题假设/D3 方向/D4 裁决),`run-state.mjs gate` 在每次状态转移前强制校验;
|
|
96
|
+
终态前强制鲁棒性敏感度分析;失败尝试按类归档到独立目录。
|
|
97
|
+
|
|
92
98
|
崩溃恢复:跨进程互斥锁 + stale 锁与 reclaim guard 回收 + Windows 共享冲突重试;已完成且输入未变的工作不重跑;任何失败都保留最佳候选与原始产物。
|
|
93
99
|
|
|
94
100
|
## MCM / ICM 终审(audit Skill)
|
|
@@ -100,14 +106,17 @@ reproducibility.json final-report.md
|
|
|
100
106
|
安装:
|
|
101
107
|
|
|
102
108
|
```bash
|
|
103
|
-
dsh plugin --profile web add github:yohanchen1/MathModelingAgent#v0.1
|
|
109
|
+
dsh plugin --profile web add github:yohanchen1/MathModelingAgent#v0.2.1
|
|
104
110
|
dsh --profile web --dump-config # 检查组合层
|
|
105
111
|
dsh web # 然后重启
|
|
106
112
|
```
|
|
107
113
|
|
|
108
114
|
(npm 通道:`dsh plugin --profile web add dsh-math-modeling-agent`)
|
|
109
115
|
|
|
110
|
-
|
|
116
|
+
开始建模——直接描述任务即可。v0.2.0 起每个会改变模型结构的决策都会在对话中交互确认
|
|
117
|
+
(任务路由、各子问题假设、候选方向、每轮裁决),每轮在对话直接输出 runlog 式摘要
|
|
118
|
+
(问题重述/分析/假设/建模求解/验证/鲁棒性/评价改进/参考文献),失败尝试按类归档到独立目录,
|
|
119
|
+
终态前强制鲁棒性敏感度分析:
|
|
111
120
|
|
|
112
121
|
```text
|
|
113
122
|
建立这个数学建模问题的模型,先分析题目和数据。
|
package/package.json
CHANGED
|
@@ -13,22 +13,26 @@ Move one mathematical problem from scoped intake to an evidence-backed conclusio
|
|
|
13
13
|
|
|
14
14
|
## Interface
|
|
15
15
|
|
|
16
|
-
Accept problem text, attachments, or an existing run directory.
|
|
16
|
+
Accept problem text, attachments, or an existing run directory.
|
|
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.
|
|
17
19
|
|
|
18
20
|
Return a concise current status, supported claims, unresolved obligations, attempt delta, artifact path, and next action or final report.
|
|
19
21
|
|
|
20
22
|
## Default workflow
|
|
21
23
|
|
|
22
|
-
1. Route the task to Fast, Standard, or High-Assurance using `references/problem-types.md
|
|
23
|
-
2. Read available inputs before asking questions; apply the
|
|
24
|
-
3. Create or validate run state with `scripts/run-state.mjs` and follow `references/state-recovery.md`.
|
|
24
|
+
1. Route the task to Fast, Standard, or High-Assurance using `references/problem-types.md`; ask D1 (task type + assurance mode).
|
|
25
|
+
2. Read available inputs before asking questions; apply the per-subproblem math grill in `references/math-grill.md`; show the D0 restatement and intake digest in the chat.
|
|
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 "..."`.
|
|
25
27
|
4. Profile inputs and build the subproblem DAG using `references/data-subproblems.md`.
|
|
26
|
-
5. Register assumptions, claims, and verification obligations from `references/claims-evidence.md
|
|
27
|
-
6. Build a baseline and materially different candidates using `references/modeling-methodology.md
|
|
28
|
-
7. Probe tools with `scripts/capability-probe.mjs`; create a run-local Python environment with `scripts/python-environment.mjs` only when computation is required.
|
|
29
|
-
8. Execute, verify, critique, and revise according to `references/workflow.md` and `references/tool-policy.md
|
|
30
|
-
9.
|
|
31
|
-
10.
|
|
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).
|
|
32
36
|
|
|
33
37
|
## Invariants
|
|
34
38
|
|
|
@@ -36,10 +40,11 @@ Return a concise current status, supported claims, unresolved obligations, attem
|
|
|
36
40
|
- Tool success, model confidence, paper count, or an analyzer adjective is not evidence of correctness.
|
|
37
41
|
- Every status transition cites evidence or issue IDs.
|
|
38
42
|
- Every material claim has explicit verification obligations.
|
|
39
|
-
- Upstream artifact changes make dependent downstream results stale.
|
|
43
|
+
- Upstream artifact changes make dependent downstream results stale; assumption revisions mark dependent claims stale.
|
|
40
44
|
- Failed tools or providers never erase the best valid candidate or raw output.
|
|
41
45
|
- Private chain-of-thought is never stored as evidence.
|
|
42
46
|
- Continue only when a round closes an obligation, adds reproducible evidence, removes a blocker, tightens uncertainty, or refutes a candidate.
|
|
47
|
+
- The run directory stays clean at every moment, and every failed path is preserved in its own folder (references/run-directory.md).
|
|
43
48
|
|
|
44
49
|
## Tool degradation
|
|
45
50
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Minimal Run Example
|
|
1
|
+
# Minimal Run Example (v2)
|
|
2
2
|
|
|
3
3
|
Problem: solve 2x + 3 = 11.
|
|
4
4
|
|
|
@@ -11,4 +11,10 @@ Evidence E-002: substitution 2(4) + 3 = 11.
|
|
|
11
11
|
|
|
12
12
|
Status: SOLVED.
|
|
13
13
|
|
|
14
|
+
v2 interaction records (ledger.scope.interactions): D0 restatement (auto), D1 routing
|
|
15
|
+
(algebra / Fast), D2 assumptions (none beyond the equation), D3 direction
|
|
16
|
+
(direct inverse), D4 verdict (user accepted). scope.robustnessExempt = true —
|
|
17
|
+
a pure algebraic equation has no perturbable parameters, so the robustness
|
|
18
|
+
gate is exempted with that reason. decisionStack holds the D3 entry.
|
|
19
|
+
|
|
14
20
|
No Python, Lean, Wolfram, research directory, or empty artifact directory is required.
|
|
@@ -7,7 +7,9 @@ Resume sequence:
|
|
|
7
7
|
1. run `node scripts/run-state.mjs validate <run-root>`;
|
|
8
8
|
2. run `node scripts/run-state.mjs recover <run-root>`;
|
|
9
9
|
3. confirm C-003 remains the best candidate;
|
|
10
|
-
4.
|
|
11
|
-
5.
|
|
10
|
+
4. run `node scripts/run-state.mjs gate <run-root> --to REVISE --reason "回退到 D3-SP1:..."` (the reason must reference the decisionStack entry);
|
|
11
|
+
5. rerun only the incomplete verifier;
|
|
12
|
+
6. append a new evidence-linked transition (the CLI enforces the gate).
|
|
12
13
|
|
|
13
|
-
Previously hash-stable Python outputs are not recomputed.
|
|
14
|
+
Previously hash-stable Python outputs are not recomputed. Interaction records,
|
|
15
|
+
the decision stack, and the failure ledger survive the crash unchanged.
|
|
@@ -4,15 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
Store claim ID, exact wording, type, scope/quantifiers, assumptions, risk, verification obligations, evidence IDs, countercheck, status, and limitations.
|
|
6
6
|
|
|
7
|
-
## Assumption record
|
|
7
|
+
## Assumption record (v2)
|
|
8
8
|
|
|
9
|
-
Store statement, scope, source category, risk, sensitivity plan, validation status, and
|
|
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
|
+
|
|
11
|
+
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
|
+
|
|
13
|
+
## Robustness obligation (v2, terminal gate)
|
|
14
|
+
|
|
15
|
+
Every subproblem that produces numerical conclusions registers a robustness obligation automatically. Execution standard: perturbation targets (problem parameters, data-fitted values, objective/weighting choice, aperture/boundary rules, discretization) × per-parameter magnitudes (±0.1%/±1%/±10% by parameter nature) × judgment (does the conclusion direction flip? what is the magnitude of change?), reported as a table (参数/扰动/结果变化/对结论影响). The run-state gate refuses terminal states without a PASSED robustness obligation unless scope.robustnessExempt is set with a reason (pure proofs only).
|
|
10
16
|
|
|
11
17
|
## Evidence record
|
|
12
18
|
|
|
13
19
|
Store evidence ID, method, tool, timestamp, covered claim IDs, input/output hashes, command/workdir/environment, exit code, output artifacts, tolerance, limitations, and level:
|
|
14
20
|
DERIVED, EXECUTED, VERIFIED, INDEPENDENTLY_VERIFIED, EXTERNALLY_VALIDATED, or NOT_CHECKED.
|
|
15
21
|
|
|
22
|
+
INDEPENDENTLY_VERIFIED (v2) means a blind verifier subagent confirmed it (claim + data + problem text only — never the solver's private reasoning); record the verifier label and brief hash (subagent-dispatch.md).
|
|
23
|
+
|
|
16
24
|
## Obligation patterns
|
|
17
25
|
|
|
18
26
|
- Numeric value: independent recomputation, residual/error bound, domain, and boundary check.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Interaction Protocol (v2)
|
|
2
|
+
|
|
3
|
+
Three interaction layers. Execution details stay autonomous; every
|
|
4
|
+
model-shaping decision is visible and user-owned.
|
|
5
|
+
|
|
6
|
+
## L1 decision points (MUST ask via the question tool)
|
|
7
|
+
|
|
8
|
+
| Point | Trigger | Question | Default |
|
|
9
|
+
|---|---|---|---|
|
|
10
|
+
| D0 问题重述 | intake complete | confirm the full problem restatement (all sub-questions, constraints, deliverables) | accept |
|
|
11
|
+
| 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 |
|
|
13
|
+
| 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 |
|
|
16
|
+
|
|
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.
|
|
19
|
+
- Every L1 record is appended to `ledger.scope.interactions` as
|
|
20
|
+
`{id, decisionPoint, question, options, userAnswer, timestamp, effectOnModel}`
|
|
21
|
+
or, for explicit auto authorization,
|
|
22
|
+
`{auto: true, decisionPoint, reason}`.
|
|
23
|
+
- 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).
|
|
28
|
+
|
|
29
|
+
## L2 display points (MUST show, never ask)
|
|
30
|
+
|
|
31
|
+
- After every attempt round: the 8-section runlog digest (see report-contract.md).
|
|
32
|
+
- Before every transition: one status line: `[状态] FROM → TO:一句话原因`.
|
|
33
|
+
- When starting any command estimated > 60 s: say what, how long, and what
|
|
34
|
+
continues in parallel (see tool-policy.md).
|
|
35
|
+
|
|
36
|
+
## L3 interruption
|
|
37
|
+
|
|
38
|
+
- The user may interrupt at ANY time ("停/换方向/改假设/结束").
|
|
39
|
+
- On interrupt: stop the current execution chain immediately, then return to
|
|
40
|
+
the nearest decision point on the stack and ask again.
|
|
41
|
+
- An interrupt never leaves the run directory inconsistent: run-state writes
|
|
42
|
+
are atomic and lock-guarded; in-flight background jobs are killed or awaited
|
|
43
|
+
before any transition is recorded.
|
|
44
|
+
|
|
45
|
+
## Decision stack and backtracking
|
|
46
|
+
|
|
47
|
+
- Every L1 decision also appends to `ledger.scope.decisionStack`:
|
|
48
|
+
`{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").
|
|
51
|
+
- A wrong direction returns to its fork point: FORK records the fork entry in
|
|
52
|
+
the stack; each failed branch leaves a wall memo before the next branch runs.
|
|
53
|
+
- Assumption revisions keep history: `revisionHistory: [{at, reason, changedTo}]`,
|
|
54
|
+
and every downstream claim/obligation that depended on the old assumption is
|
|
55
|
+
marked `stale: true` with the revising assumption id (see claims-evidence.md).
|
|
56
|
+
|
|
57
|
+
## Rolling documents
|
|
58
|
+
|
|
59
|
+
- Problem restatement: written once at intake (problem-brief.md); only
|
|
60
|
+
referenced afterwards; a restatement edit re-opens scope freeze.
|
|
61
|
+
- Problem analysis: written per subproblem and appended on every discovery;
|
|
62
|
+
ledger.subproblems[].analysisLog = `[{at, entry, trigger}]`.
|
|
63
|
+
- Assumptions: registered per subproblem, revised incrementally; never assume
|
|
64
|
+
the full problem's assumptions are known up front.
|
|
@@ -10,18 +10,23 @@ Ask one question at a time. Read the problem and attachments first. Ask only que
|
|
|
10
10
|
4. What error, confidence, or decision risk is acceptable?
|
|
11
11
|
5. Is the result for practice, competition, publication, production, or high-stakes use?
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Problem Brief fields (16, collect all applicable)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
1. 问题背景与实际问题;2. 目标;3. 子问题(显式+隐含);4. 输入数据(来源/质量/单位/时间/分组);5. 数据质量问题;
|
|
16
|
+
6. 决策/状态变量;7. 参数;8. 目标函数/评价指标;9. 约束(等式/不等式/整数/非负/逻辑/初始/边界);
|
|
17
|
+
10. 假设及依据;11. 任务类型(预测/优化/解释/证明/机制/决策);12. 误差/精度要求;
|
|
18
|
+
13. 验证标准;14. 最终交付物;15. 风险与失败模式;16. 不适用字段(标注原因)。
|
|
16
19
|
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
20
|
+
## Per-subproblem grilling (v2)
|
|
21
|
+
|
|
22
|
+
- Grill **one subproblem at a time**. Never demand assumptions for Q2 while Q1 is unsolved.
|
|
23
|
+
- For each subproblem: ask one question at a time, highest-impact first;
|
|
24
|
+
every answer is recorded as the D2 interaction for that subproblem
|
|
25
|
+
(see interaction-protocol.md).
|
|
26
|
+
- Mid-subproblem discoveries that change model structure trigger D2':
|
|
27
|
+
register the new assumption, show it in the chat, ask only if it is model-shaping.
|
|
28
|
+
- The 16 Problem Brief fields are filled once at intake for the whole problem;
|
|
29
|
+
subproblem-level analysis and assumptions roll forward per attempt.
|
|
25
30
|
|
|
26
31
|
## Rules
|
|
27
32
|
|
|
@@ -29,5 +34,6 @@ Collect only applicable fields:
|
|
|
29
34
|
- Ask the highest-impact unknown first.
|
|
30
35
|
- Mark irrelevant fields “not applicable” with a reason.
|
|
31
36
|
- Turn “自行判断” into an explicit modeling assumption with risk and sensitivity plan.
|
|
37
|
+
- Assumptions are revisable: record revisionHistory and mark dependent claims stale.
|
|
32
38
|
- Do not freeze scope while a missing answer can change the problem type or claim strength.
|
|
33
39
|
- Do not force a proof problem to invent data or an exploratory analysis to invent an objective function.
|
|
@@ -24,6 +24,42 @@ Do not copy full prior solutions or private reasoning.
|
|
|
24
24
|
|
|
25
25
|
State the frozen question, final scientific status, answer/recommendation, verified claims and evidence links, conditional/unresolved claims, assumptions and sensitivity, candidate comparison, data and parameter sources, validation, failure cases, limitations, reproducibility commands, and resume conditions when non-SOLVED.
|
|
26
26
|
|
|
27
|
-
## Chat
|
|
27
|
+
## Chat display — the runlog digest (MUST output, v2)
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
The following is shown DIRECTLY IN THE CHAT, not only on disk.
|
|
30
|
+
|
|
31
|
+
### Intake digest (once, at intake completion)
|
|
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)
|
|
36
|
+
|
|
37
|
+
### Per-round digest (after EVERY attempt round)
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
## Attempt N 摘要
|
|
41
|
+
问题重述:<1-2 sentences, current subproblem>
|
|
42
|
+
问题分析:<2-4 sentences, this round's inputs/insights/delta vs last round>
|
|
43
|
+
模型假设:<沿用 X 条 + 新增 Y 条 + 修订 Z 条;修订注明原因;重中之中,不得省略>
|
|
44
|
+
模型建立与求解:<core equations + method + key numbers>
|
|
45
|
+
验证:<obligations ✓/✗ + evidence levels + most important failure>
|
|
46
|
+
鲁棒性:<this round's sensitivity results, or "未覆盖,计划在 attempt M">
|
|
47
|
+
评价与改进:<candidate comparison / limitations / next improvement>
|
|
48
|
+
参考文献:<new citations or "无新增">
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Rules:
|
|
52
|
+
- The restatement is frozen after intake; analysis and assumptions ROLL FORWARD
|
|
53
|
+
per subproblem and per round (they change, and the digest shows the delta).
|
|
54
|
+
- Private chain-of-thought is never shown; only auditable content.
|
|
55
|
+
- The digest is a hard requirement of the round contract: a round whose digest
|
|
56
|
+
is missing does not count as a completed round.
|
|
57
|
+
|
|
58
|
+
## Cleanup checklist (before ANY terminal transition)
|
|
59
|
+
|
|
60
|
+
1. attempts/<n>/code holds only final reproducible scripts with header comments.
|
|
61
|
+
2. Exploratory/buggy code is moved to _drafts/ or failed/code-drafts/ — never deleted.
|
|
62
|
+
3. No debug prints or machine-specific hardcoded paths.
|
|
63
|
+
4. figures → plots/, tables → data/, deliverable at run root.
|
|
64
|
+
5. reproducibility.json matches the final scripts.
|
|
65
|
+
6. run-state validate passes; every obligation has a terminal status.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Run Directory Contract (v2)
|
|
2
|
+
|
|
3
|
+
The run directory is the ONLY place work artifacts live. It must stay clean at
|
|
4
|
+
every moment, not only at the end.
|
|
5
|
+
|
|
6
|
+
## Layout
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
math-modeling-runs/<task-id>/
|
|
10
|
+
├── run.json ledger.json events.jsonl # run-state files (only run-state.mjs writes them)
|
|
11
|
+
├── problem-brief.md inputs.json # frozen restatement + input manifest
|
|
12
|
+
├── attempts/<n>/
|
|
13
|
+
│ ├── report.md # 13-section contract + 8-section digest
|
|
14
|
+
│ ├── code/ # final reproducible scripts ONLY (clean)
|
|
15
|
+
│ ├── data/ # results: json/csv/npy/xlsx
|
|
16
|
+
│ ├── plots/ # figures
|
|
17
|
+
│ └── _drafts/ # this attempt's exploratory code (kept, isolated)
|
|
18
|
+
├── failed/
|
|
19
|
+
│ ├── directions/<id>/ # direction-level failures: wall memo + code + why
|
|
20
|
+
│ ├── code-drafts/<attempt>-<name>/ # implementation-level failures: buggy/timed-out/deprecated
|
|
21
|
+
│ └── issues.md # failure ledger index (see below)
|
|
22
|
+
├── research/ # literature (only when research happened)
|
|
23
|
+
├── walls/ # breakthrough memos (legacy path kept)
|
|
24
|
+
├── result.xlsx # the deliverable (when the problem asks for one)
|
|
25
|
+
├── reproducibility.json # final command list + env + hashes
|
|
26
|
+
└── final-report.md # terminal report
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Failure classification (failure ledger)
|
|
30
|
+
|
|
31
|
+
Every problem in ANY stage is recorded in `failed/issues.md` with:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
#<seq> | 环节(DATA/TOOL/IMPLEMENTATION/MODEL/VALIDATION/EVIDENCE/RESEARCH_GAP) | 发现时间/attempt
|
|
35
|
+
症状:…
|
|
36
|
+
根因:…
|
|
37
|
+
处置:修了/绕过/放弃(→wall memo id)
|
|
38
|
+
影响:哪些 claim/obligation 被污染,下游如何重验
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Direction-level failures additionally get `failed/directions/<id>/wall.md`
|
|
42
|
+
(target, paths tried, exact wall, wall type, breakthrough condition, restart
|
|
43
|
+
checklist) and their code snapshot. Implementation-level failures get
|
|
44
|
+
`failed/code-drafts/<attempt>-<name>/` with a README stating the failure.
|
|
45
|
+
|
|
46
|
+
## Cleanup gate (MUST run before ANY terminal transition)
|
|
47
|
+
|
|
48
|
+
1. attempts/<n>/code contains only the final reproducible scripts; each starts
|
|
49
|
+
with a header comment: purpose, inputs, outputs, reproduction command.
|
|
50
|
+
Exploratory/buggy versions move to `_drafts/` or `failed/code-drafts/` — never deleted.
|
|
51
|
+
2. No debug prints, unused variables, or hardcoded machine-specific paths
|
|
52
|
+
(the data-root constant is the only allowed absolute path).
|
|
53
|
+
3. Figures in `plots/`, tables in `data/`; the deliverable at the run root.
|
|
54
|
+
4. reproducibility.json is complete and matches the final scripts.
|
|
55
|
+
5. `run-state.mjs validate <dir>` passes and every obligation has a terminal status.
|
|
56
|
+
6. The cleanup itself is noted in the terminal transition reason.
|
|
57
|
+
|
|
58
|
+
## Tidiness invariants
|
|
59
|
+
|
|
60
|
+
- Code is refactored as soon as it stabilizes within an attempt (not only at the end).
|
|
61
|
+
- Every script file maps 1:1 to an attempt artifact or a final deliverable.
|
|
62
|
+
- Nothing outside the run directory is written by the modeling work.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Subagent Dispatch Protocol
|
|
2
|
+
|
|
3
|
+
The coordinator (main agent) owns the ledger and the decision stack; subagents
|
|
4
|
+
produce reports and evidence. Never let a subagent mutate run-state directly.
|
|
5
|
+
|
|
6
|
+
## Three dispatch classes
|
|
7
|
+
|
|
8
|
+
| Class | Role (lineage) | When | Count |
|
|
9
|
+
|---|---|---|---|
|
|
10
|
+
| explorer | parallel candidate exploration (legacy Modeler) | CANDIDATES_READY, >= 2 independent directions | 1 per direction |
|
|
11
|
+
| verifier | blind re-verification (legacy Analyzer/Correction) | before terminal: key numeric/feasibility claims | Standard: 1 per task; High-Assurance: 1 per claim |
|
|
12
|
+
| auditor | artifact-only audit (math-modeling-audit skill) | before SOLVED | High-Assurance: mandatory; else optional |
|
|
13
|
+
|
|
14
|
+
## Brief template (self-contained; subagents see no parent context)
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
任务目标:<one claim or one direction, exactly>
|
|
18
|
+
输入:<file paths + hashes, run dir is read-only>
|
|
19
|
+
已知事实:<data profile / geometry facts / problem restatement excerpt>
|
|
20
|
+
允许工具:<explicit list; private raw data never leaves the workspace>
|
|
21
|
+
验证要求:<which obligations this subagent must produce evidence for>
|
|
22
|
+
输出格式:
|
|
23
|
+
结论(一句话,可证伪)
|
|
24
|
+
方法
|
|
25
|
+
执行记录(命令 + 关键输出 + 退出码)
|
|
26
|
+
证据(证据 ID、等级、局限)
|
|
27
|
+
失败点与反例(若有)
|
|
28
|
+
建议(下一步或放弃理由)
|
|
29
|
+
边界:<不准做什么,例如"不得修改 run 文件、不得检索题解">
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Adjudication rules
|
|
33
|
+
|
|
34
|
+
- The coordinator reads every report against its brief; a report without
|
|
35
|
+
reproducible commands or evidence IDs is not accepted.
|
|
36
|
+
- Two conflicting reports → a second blind round with both reports as input.
|
|
37
|
+
- Blind verification means: the verifier receives the claim, the data, and the
|
|
38
|
+
problem text — never the solver's private reasoning. Its agreement upgrades
|
|
39
|
+
the claim to INDEPENDENTLY_VERIFIED; its disagreement opens a critical issue.
|
|
40
|
+
- Every dispatch is recorded in the ledger (candidates / issues) with the
|
|
41
|
+
subagent label, brief hash, and verdict.
|
|
42
|
+
|
|
43
|
+
## Failure bookkeeping
|
|
44
|
+
|
|
45
|
+
- A failed or abandoned direction writes a wall memo first
|
|
46
|
+
(`failed/directions/<id>/wall.md` per run-directory.md), then the branch is
|
|
47
|
+
closed. Nothing is deleted; every wrong path stays inspectable.
|
|
@@ -4,6 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
Run `scripts/capability-probe.mjs`; treat PATH hits as candidates, then verify version and usability through the available DSH shell. Record unavailable, candidate, available, or unknown with reason.
|
|
6
6
|
|
|
7
|
+
## Long-running commands (stall governance, v2)
|
|
8
|
+
|
|
9
|
+
- Estimate BEFORE starting: anything expected > 60 s MUST run in the background
|
|
10
|
+
(run_in_background), and the chat gets one line immediately: what runs, the
|
|
11
|
+
estimate, and what independent work continues in parallel. Never idle-wait.
|
|
12
|
+
- Tasks running > 3 minutes get a proactive progress check (job_output) with a
|
|
13
|
+
one-line progress report to the chat.
|
|
14
|
+
- Expensive computations are benchmarked on a small subset first (e.g. 20-node
|
|
15
|
+
trial) to calibrate the full-run estimate.
|
|
16
|
+
- On job completion, report the outcome within the same round.
|
|
17
|
+
|
|
18
|
+
## Tool selection visibility (v2)
|
|
19
|
+
|
|
20
|
+
At the start of every phase, say ONE line about the tool choice and why:
|
|
21
|
+
e.g. "本阶段用 Python+OSQP 做约束优化;Wolfram 对本题非必需,跳过;Lean 不适用".
|
|
22
|
+
The problem never silently depends on a single tool: every phase names its
|
|
23
|
+
backend, its fallback, and the degradation path when the backend fails.
|
|
24
|
+
|
|
7
25
|
## Python
|
|
8
26
|
|
|
9
27
|
Use `scripts/python-environment.mjs`. Prefer uv, then an existing Python runtime plus venv. Keep the environment under the run directory. Install only required normalized PyPI package names; VCS URLs, local wheels, or arbitrary indexes require explicit user approval. Record lock, seed, command, workdir, timeout, stdout/stderr, exit code, and generated files.
|
|
@@ -13,6 +13,37 @@ 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
|
+
## Interaction gates (v2)
|
|
17
|
+
|
|
18
|
+
Before EVERY transition run `run-state.mjs gate <dir> --to <status> --reason "..."`
|
|
19
|
+
and report one status line in the chat: `[状态] FROM → TO:一句话原因`.
|
|
20
|
+
The gate enforces the L1 interaction records (interaction-protocol.md) and the
|
|
21
|
+
robustness gate: terminal states require a PASSED robustness-kind obligation
|
|
22
|
+
(parameter perturbation × magnitude × conclusion-flip judgment) unless
|
|
23
|
+
scope.robustnessExempt is set with a reason.
|
|
24
|
+
|
|
25
|
+
## Backtracking
|
|
26
|
+
|
|
27
|
+
- REVISE/RESEARCH reasons MUST reference a decisionStack entry id; the round
|
|
28
|
+
returns to that decision point and tries the next option.
|
|
29
|
+
- FORK records its fork point; each failed branch writes a wall memo
|
|
30
|
+
(run-directory.md) before the next branch starts. A wrong direction always
|
|
31
|
+
returns to the fork point, never mid-branch.
|
|
32
|
+
- Every stage problem (DATA/TOOL/IMPLEMENTATION/MODEL/VALIDATION/EVIDENCE/
|
|
33
|
+
RESEARCH_GAP) is recorded in failed/issues.md.
|
|
34
|
+
|
|
35
|
+
## Dispatch (see subagent-dispatch.md)
|
|
36
|
+
|
|
37
|
+
- >= 2 independent directions → explorer subagents in parallel, coordinator adjudicates.
|
|
38
|
+
- Before terminal: blind verifier subagent for key claims (Standard: 1 per task).
|
|
39
|
+
- High-Assurance: auditor subagent (math-modeling-audit) before SOLVED.
|
|
40
|
+
|
|
41
|
+
## Cleanup gate
|
|
42
|
+
|
|
43
|
+
Before ANY terminal transition, run the cleanup checklist in run-directory.md
|
|
44
|
+
(clean code/, _drafts/, failed/ classification, reproducibility.json,
|
|
45
|
+
validate). Note the cleanup in the transition reason.
|
|
46
|
+
|
|
16
47
|
## Round contract
|
|
17
48
|
|
|
18
49
|
Each round has one objective and records:
|
|
@@ -17,16 +17,35 @@
|
|
|
17
17
|
],
|
|
18
18
|
"properties": {
|
|
19
19
|
"schemaVersion": {
|
|
20
|
-
"const":
|
|
20
|
+
"const": 2
|
|
21
21
|
},
|
|
22
22
|
"taskId": {
|
|
23
23
|
"type": "string"
|
|
24
24
|
},
|
|
25
25
|
"scope": {
|
|
26
|
-
"type": "object"
|
|
26
|
+
"type": "object",
|
|
27
|
+
"properties": {
|
|
28
|
+
"independentAuditPassed": { "type": "boolean" },
|
|
29
|
+
"interactions": {
|
|
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}",
|
|
32
|
+
"items": { "type": "object" }
|
|
33
|
+
},
|
|
34
|
+
"decisionStack": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"description": "Decision-point stack for backtracking: {id (e.g. D3-SP1), point, choice, at, reason}. REVISE/RESEARCH reasons must reference an entry id.",
|
|
37
|
+
"items": { "type": "object" }
|
|
38
|
+
},
|
|
39
|
+
"robustnessExempt": {
|
|
40
|
+
"type": "boolean",
|
|
41
|
+
"description": "True only for tasks with no perturbable parameters (pure proofs); terminal states otherwise require a PASSED robustness-kind obligation."
|
|
42
|
+
}
|
|
43
|
+
}
|
|
27
44
|
},
|
|
28
45
|
"assumptions": {
|
|
29
|
-
"type": "array"
|
|
46
|
+
"type": "array",
|
|
47
|
+
"description": "Per-subproblem incremental assumptions: {id, statement, scope: [SPx]|global, source, status: active|revised|superseded, risk, sensitivityPlan, revisionHistory: [{at, reason, changedTo}]}",
|
|
48
|
+
"items": { "type": "object" }
|
|
30
49
|
},
|
|
31
50
|
"claims": {
|
|
32
51
|
"type": "array"
|
|
@@ -35,7 +54,9 @@
|
|
|
35
54
|
"type": "array"
|
|
36
55
|
},
|
|
37
56
|
"subproblems": {
|
|
38
|
-
"type": "array"
|
|
57
|
+
"type": "array",
|
|
58
|
+
"description": "Subproblem DAG nodes: {id, task, inputs, dependencies, acceptance, status, analysisLog: [{at, entry, trigger}]} — analysisLog is the rolling analysis ledger.",
|
|
59
|
+
"items": { "type": "object" }
|
|
39
60
|
},
|
|
40
61
|
"candidates": {
|
|
41
62
|
"type": "array"
|
|
@@ -35,11 +35,12 @@ async function atomicWriteJson(path, value, { renamePath = rename, removePath =
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export function defaultSpawnOptions({ cwd, stdoutDescriptor } = {}) {
|
|
38
|
+
export function defaultSpawnOptions({ cwd, stdoutDescriptor, timeout } = {}) {
|
|
39
39
|
return {
|
|
40
40
|
cwd,
|
|
41
41
|
shell: false,
|
|
42
42
|
stdio: ['ignore', stdoutDescriptor ?? 2, 2],
|
|
43
|
+
timeout,
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -50,7 +51,10 @@ async function defaultRun(command, args, options = {}) {
|
|
|
50
51
|
const result = spawnSync(command, args, defaultSpawnOptions({
|
|
51
52
|
cwd: options.cwd,
|
|
52
53
|
stdoutDescriptor: descriptor,
|
|
54
|
+
timeout: options.timeout,
|
|
53
55
|
}))
|
|
56
|
+
if (result.error) throw result.error
|
|
57
|
+
if (result.status === null) throw new Error(`${options.label ?? command} timed out after ${options.timeout} ms — network-bound installs can be slow; check the pip mirror configuration and re-run`)
|
|
54
58
|
return result.status ?? 1
|
|
55
59
|
} finally {
|
|
56
60
|
if (descriptor !== undefined) closeSync(descriptor)
|
|
@@ -322,6 +326,8 @@ export async function createPythonEnvironment(input) {
|
|
|
322
326
|
const finalPythonPath = environmentPython(environmentDir, platform, resolvePath)
|
|
323
327
|
try {
|
|
324
328
|
let adapter
|
|
329
|
+
const installLogPath = ensureContained(runDir, resolvePath(runDir, 'python-environment-install.log'), platform)
|
|
330
|
+
const installOptions = { cwd: runDir, stdoutFile: installLogPath, timeout: 20 * 60 * 1000, label: 'package install' }
|
|
325
331
|
if (uv) {
|
|
326
332
|
adapter = 'uv'
|
|
327
333
|
const createArgs = ['venv', '--seed', environmentDir]
|
|
@@ -329,8 +335,9 @@ export async function createPythonEnvironment(input) {
|
|
|
329
335
|
assertSuccess(await runCommand(uv.executable, createArgs, { cwd: runDir }), 'uv venv')
|
|
330
336
|
if (packages.length) {
|
|
331
337
|
const installArgs = ['pip', 'install', '--python', finalPythonPath, ...packages]
|
|
332
|
-
commands.push({ command: uv.executable, args: installArgs })
|
|
333
|
-
|
|
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')
|
|
334
341
|
}
|
|
335
342
|
} else {
|
|
336
343
|
adapter = 'venv'
|
|
@@ -339,8 +346,9 @@ export async function createPythonEnvironment(input) {
|
|
|
339
346
|
assertSuccess(await runCommand(python.executable, createArgs, { cwd: runDir }), 'python venv')
|
|
340
347
|
if (packages.length) {
|
|
341
348
|
const installArgs = ['-m', 'pip', 'install', ...packages]
|
|
342
|
-
commands.push({ command: finalPythonPath, args: installArgs })
|
|
343
|
-
|
|
349
|
+
commands.push({ command: finalPythonPath, args: installArgs, stdoutFile: installLogPath })
|
|
350
|
+
process.stderr.write(`python-environment: pip installing ${packages.length} package(s); network-bound, may take minutes; log: ${installLogPath}\n`)
|
|
351
|
+
assertSuccess(await runCommand(finalPythonPath, installArgs, installOptions), 'pip install')
|
|
344
352
|
}
|
|
345
353
|
}
|
|
346
354
|
await mkdir(environmentDir, { recursive: true })
|
|
@@ -4,7 +4,9 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { isDeepStrictEqual } from 'node:util'
|
|
6
6
|
|
|
7
|
-
export const SCHEMA_VERSION =
|
|
7
|
+
export const SCHEMA_VERSION = 2
|
|
8
|
+
export const SUPPORTED_SCHEMA_VERSIONS = [1, 2]
|
|
9
|
+
export const INTERACTION_DECISIONS = Object.freeze({ D1: 'routing', D2: 'assumptions', D3: 'direction', D4: 'verdict' })
|
|
8
10
|
export const MODE_DEFAULTS = Object.freeze({
|
|
9
11
|
fast: Object.freeze({ attempts: 2, researchQueries: 0, computeSeconds: 60 }),
|
|
10
12
|
standard: Object.freeze({ attempts: 12, researchQueries: 12, computeSeconds: 1800 }),
|
|
@@ -282,7 +284,7 @@ function deeplyEqual(left, right) { return isDeepStrictEqual(left, right) }
|
|
|
282
284
|
function validateRunShape(run, diagnostics, prefix = 'run') {
|
|
283
285
|
addDiagnostic(diagnostics, exactKeys(run, RUN_KEYS), `${prefix} must contain exactly the contracted fields`)
|
|
284
286
|
if (!isObject(run)) return
|
|
285
|
-
addDiagnostic(diagnostics, run.schemaVersion
|
|
287
|
+
addDiagnostic(diagnostics, SUPPORTED_SCHEMA_VERSIONS.includes(run.schemaVersion), `${prefix}.schemaVersion must be one of ${SUPPORTED_SCHEMA_VERSIONS.join(',')}`)
|
|
286
288
|
addDiagnostic(diagnostics, typeof run.taskId === 'string' && TASK_ID.test(run.taskId), `${prefix}.taskId must be kebab-case`)
|
|
287
289
|
addDiagnostic(diagnostics, Object.hasOwn(MODE_DEFAULTS, run.mode), `${prefix}.mode is invalid`)
|
|
288
290
|
addDiagnostic(diagnostics, STATUSES.includes(run.status), `${prefix}.status is invalid`)
|
|
@@ -297,7 +299,7 @@ function validateRunShape(run, diagnostics, prefix = 'run') {
|
|
|
297
299
|
function validateLedgerShape(ledger, diagnostics) {
|
|
298
300
|
addDiagnostic(diagnostics, exactKeys(ledger, LEDGER_KEYS), 'ledger must contain exactly the contracted fields')
|
|
299
301
|
if (!isObject(ledger)) return
|
|
300
|
-
addDiagnostic(diagnostics, ledger.schemaVersion
|
|
302
|
+
addDiagnostic(diagnostics, SUPPORTED_SCHEMA_VERSIONS.includes(ledger.schemaVersion), `ledger.schemaVersion must be one of ${SUPPORTED_SCHEMA_VERSIONS.join(',')}`)
|
|
301
303
|
addDiagnostic(diagnostics, typeof ledger.taskId === 'string' && TASK_ID.test(ledger.taskId), 'ledger.taskId must be kebab-case')
|
|
302
304
|
addDiagnostic(diagnostics, isObject(ledger.scope), 'ledger.scope must be an object')
|
|
303
305
|
for (const key of ['assumptions', 'claims', 'obligations', 'subproblems', 'candidates', 'issues']) addDiagnostic(diagnostics, Array.isArray(ledger[key]), `ledger.${key} must be an array`)
|
|
@@ -312,7 +314,7 @@ function validateEventShape(event, diagnostics, index) {
|
|
|
312
314
|
? ['schemaVersion', 'sequence', 'type', 'taskId', 'timestamp', 'snapshot']
|
|
313
315
|
: ['schemaVersion', 'sequence', 'type', 'taskId', 'from', 'to', 'reason', 'evidenceIds', 'issueIds', 'timestamp', 'snapshot']
|
|
314
316
|
addDiagnostic(diagnostics, exactKeys(event, expectedKeys), `${prefix} must contain exactly the contracted fields`)
|
|
315
|
-
addDiagnostic(diagnostics, event.schemaVersion
|
|
317
|
+
addDiagnostic(diagnostics, SUPPORTED_SCHEMA_VERSIONS.includes(event.schemaVersion), `${prefix}.schemaVersion must be one of ${SUPPORTED_SCHEMA_VERSIONS.join(',')}`)
|
|
316
318
|
addDiagnostic(diagnostics, event.sequence === index, `${prefix}.sequence must equal ${index}`)
|
|
317
319
|
addDiagnostic(diagnostics, typeof event.taskId === 'string' && TASK_ID.test(event.taskId), `${prefix}.taskId must be kebab-case`)
|
|
318
320
|
addDiagnostic(diagnostics, event.type === expectedType, `${prefix}.type must be ${expectedType}`)
|
|
@@ -377,8 +379,8 @@ export async function initRun(root, { taskId, mode = 'standard', budget, budgets
|
|
|
377
379
|
bestCandidateId: null, budget: effectiveBudget, createdAt: timestamp, updatedAt: timestamp,
|
|
378
380
|
}
|
|
379
381
|
const ledger = {
|
|
380
|
-
schemaVersion: SCHEMA_VERSION, taskId, scope: { independentAuditPassed: false
|
|
381
|
-
obligations: [], subproblems: [], candidates: [], issues: [],
|
|
382
|
+
schemaVersion: SCHEMA_VERSION, taskId, scope: { independentAuditPassed: false, interactions: [], decisionStack: [], robustnessExempt: false },
|
|
383
|
+
assumptions: [], claims: [], obligations: [], subproblems: [], candidates: [], issues: [],
|
|
382
384
|
}
|
|
383
385
|
const event = { schemaVersion: SCHEMA_VERSION, sequence: 0, type: 'RUN_INITIALIZED', taskId, timestamp, snapshot: clone(run) }
|
|
384
386
|
const p = paths(root)
|
|
@@ -409,6 +411,61 @@ export async function initRun(root, { taskId, mode = 'standard', budget, budgets
|
|
|
409
411
|
}, runtime)
|
|
410
412
|
}
|
|
411
413
|
|
|
414
|
+
/** The v2 interaction contract: decision records, decision-stack backtracking, and the robustness gate. */
|
|
415
|
+
function hasDecisionRecord(scope, decisionPoint) {
|
|
416
|
+
return Array.isArray(scope?.interactions) && scope.interactions.some(entry =>
|
|
417
|
+
entry && (entry.decisionPoint === decisionPoint || entry.auto === true))
|
|
418
|
+
}
|
|
419
|
+
function decisionStackIds(scope) {
|
|
420
|
+
return Array.isArray(scope?.decisionStack)
|
|
421
|
+
? scope.decisionStack.filter(entry => entry && typeof entry.id === 'string').map(entry => entry.id)
|
|
422
|
+
: []
|
|
423
|
+
}
|
|
424
|
+
/** D2 records carry the subproblem id(s) they cover: {decisionPoint:'D2', scope:['SP1'], ...} */
|
|
425
|
+
function hasSubproblemD2(scope, subproblem) {
|
|
426
|
+
if (!subproblem || !Array.isArray(scope?.interactions)) return true
|
|
427
|
+
return scope.interactions.some(entry => entry && entry.decisionPoint === 'D2' &&
|
|
428
|
+
(Array.isArray(entry.scope) ? entry.scope.includes(subproblem) : entry.subproblem === subproblem))
|
|
429
|
+
}
|
|
430
|
+
function interactionContractViolations(run, ledger, to, reason, subproblem) {
|
|
431
|
+
if (ledger.schemaVersion !== SCHEMA_VERSION) return []
|
|
432
|
+
const scope = ledger.scope ?? {}
|
|
433
|
+
const violations = []
|
|
434
|
+
const transitional = to !== undefined
|
|
435
|
+
const earlyStop = ['BLOCKED', 'CANCELLED'].includes(run.status)
|
|
436
|
+
if ((transitional && to === 'SCOPE_FROZEN') || (!transitional && run.status !== 'TRIAGE' && !earlyStop)) {
|
|
437
|
+
if (!hasDecisionRecord(scope, 'D0')) violations.push('D0 restatement interaction record missing (ledger.scope.interactions)')
|
|
438
|
+
if (!hasDecisionRecord(scope, 'D1')) violations.push('D1 routing interaction record missing (ledger.scope.interactions)')
|
|
439
|
+
}
|
|
440
|
+
const enteringAttemptFromCandidates = transitional ? (to === 'ATTEMPT' && run.status === 'CANDIDATES_READY') : (run.currentAttempt >= 1)
|
|
441
|
+
if (enteringAttemptFromCandidates) {
|
|
442
|
+
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>)`)
|
|
444
|
+
}
|
|
445
|
+
const enteringTerminalFromVerify = transitional
|
|
446
|
+
? (FINAL_STATES.includes(to) && run.status === 'VERIFY')
|
|
447
|
+
: (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')) {
|
|
450
|
+
const ids = decisionStackIds(scope)
|
|
451
|
+
if (ids.length > 0 && !ids.some(id => typeof reason === 'string' && reason.includes(id))) {
|
|
452
|
+
violations.push(`REVISE/RESEARCH reason must reference a decisionStack entry id (available: ${ids.join(', ')})`)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const terminalAndExempt = transitional
|
|
456
|
+
? (FINAL_STATES.includes(to) && !['BLOCKED', 'CANCELLED'].includes(to))
|
|
457
|
+
: (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)')
|
|
461
|
+
}
|
|
462
|
+
return violations
|
|
463
|
+
}
|
|
464
|
+
function enforceInteractionContract(run, ledger, to, reason) {
|
|
465
|
+
const violations = interactionContractViolations(run, ledger, to, reason)
|
|
466
|
+
if (violations.length > 0) throw new Error(`interaction contract violated: ${violations.join('; ')}`)
|
|
467
|
+
}
|
|
468
|
+
|
|
412
469
|
function enforceSolvedGate(run, ledger) {
|
|
413
470
|
if (ledger.claims.length < 1) throw new Error('SOLVED requires at least one claim')
|
|
414
471
|
if (ledger.obligations.some(item => item.required !== false && item.status !== 'PASS')) throw new Error('required obligations remain open')
|
|
@@ -473,12 +530,13 @@ async function readEvents(path) {
|
|
|
473
530
|
|
|
474
531
|
async function validateRunUnlocked(root, expectedTaskId) {
|
|
475
532
|
const diagnostics = []
|
|
533
|
+
const warnings = []
|
|
476
534
|
let run, ledger, events
|
|
477
535
|
const p = paths(root)
|
|
478
536
|
try { run = await readJson(p.run) } catch (error) { diagnostics.push(`run unreadable: ${error.message}`) }
|
|
479
537
|
try { ledger = await readJson(p.ledger) } catch (error) { diagnostics.push(`ledger unreadable: ${error.message}`) }
|
|
480
538
|
try { events = await readEvents(p.events) } catch (error) { diagnostics.push(error.message) }
|
|
481
|
-
if (!run || !ledger || !events) return { valid: false, diagnostics }
|
|
539
|
+
if (!run || !ledger || !events) return { valid: false, diagnostics, warnings }
|
|
482
540
|
validateRunShape(run, diagnostics)
|
|
483
541
|
validateLedgerShape(ledger, diagnostics)
|
|
484
542
|
validateJournal(events, diagnostics)
|
|
@@ -486,13 +544,45 @@ async function validateRunUnlocked(root, expectedTaskId) {
|
|
|
486
544
|
const last = events.at(-1)
|
|
487
545
|
if (last && run.eventSequence !== last.sequence) diagnostics.push('state/journal sequence mismatch')
|
|
488
546
|
if (last && JSON.stringify(run) !== JSON.stringify(last.snapshot)) diagnostics.push('state/journal snapshot mismatch')
|
|
489
|
-
|
|
547
|
+
if (ledger.schemaVersion !== SCHEMA_VERSION) warnings.push(`legacy run (ledger schema v${ledger.schemaVersion}): interaction contract not enforced`)
|
|
548
|
+
else {
|
|
549
|
+
for (const violation of interactionContractViolations(run, ledger, undefined, undefined)) {
|
|
550
|
+
warnings.push(`interaction contract: ${violation}`)
|
|
551
|
+
}
|
|
552
|
+
if (run.currentAttempt >= 1) {
|
|
553
|
+
const reportPath = join(root, 'attempts', String(run.currentAttempt), 'report.md')
|
|
554
|
+
try {
|
|
555
|
+
const reportText = await readFile(reportPath, 'utf8')
|
|
556
|
+
for (const section of ['问题重述', '问题分析', '模型假设', '模型建立与求解', '验证', '鲁棒性', '评价与改进', '参考文献']) {
|
|
557
|
+
if (!reportText.includes(section)) warnings.push(`attempt ${run.currentAttempt} report.md missing runlog digest section: ${section}`)
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
warnings.push(`attempt ${run.currentAttempt} report.md unreadable (runlog digest required)`)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return { valid: diagnostics.length === 0, diagnostics, warnings, run: clone(run) }
|
|
490
565
|
}
|
|
491
566
|
|
|
492
567
|
export async function validateRun(root, expectedTaskId, runtime = {}) {
|
|
493
568
|
return withLock(root, MUTATION_LOCK_FILE, 'validate', () => validateRunUnlocked(root, expectedTaskId), runtime)
|
|
494
569
|
}
|
|
495
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Check the v2 interaction contract for a PROSPECTIVE transition without
|
|
573
|
+
* mutating anything. The agent workflow must call this before `transition`;
|
|
574
|
+
* the CLI `transition` command enforces it internally unless --skip-gate.
|
|
575
|
+
*/
|
|
576
|
+
export async function gateTransition(root, { to, reason, subproblem }, runtime = {}) {
|
|
577
|
+
return withLock(root, MUTATION_LOCK_FILE, 'gate', async () => {
|
|
578
|
+
const p = paths(root)
|
|
579
|
+
const run = await readJson(p.run)
|
|
580
|
+
const ledger = await readJson(p.ledger)
|
|
581
|
+
const violations = interactionContractViolations(run, ledger, to, reason, subproblem)
|
|
582
|
+
return { allowed: violations.length === 0, violations, status: run.status }
|
|
583
|
+
}, runtime)
|
|
584
|
+
}
|
|
585
|
+
|
|
496
586
|
export async function recoverRun(root, runtime = {}) {
|
|
497
587
|
return withLock(root, MUTATION_LOCK_FILE, 'recover', async () => {
|
|
498
588
|
const p = paths(root)
|
|
@@ -533,7 +623,21 @@ async function cli(argv) {
|
|
|
533
623
|
const root = resolve(rootArg)
|
|
534
624
|
const options = parseOptions(rest)
|
|
535
625
|
if (command === 'init') return initRun(root, { taskId: options['task-id'], mode: options.mode ?? 'standard' })
|
|
536
|
-
if (command === '
|
|
626
|
+
if (command === 'gate') {
|
|
627
|
+
const result = await gateTransition(root, { to: options.to, reason: options.reason, subproblem: options.subproblem })
|
|
628
|
+
if (!result.allowed) process.exitCode = 1
|
|
629
|
+
return result
|
|
630
|
+
}
|
|
631
|
+
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
|
+
}
|
|
638
|
+
}
|
|
639
|
+
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
|
+
}
|
|
537
641
|
if (command === 'validate') {
|
|
538
642
|
const result = await validateRun(root, options['task-id'])
|
|
539
643
|
if (!result.valid) process.exitCode = 1
|
|
@@ -541,7 +645,7 @@ async function cli(argv) {
|
|
|
541
645
|
}
|
|
542
646
|
if (command === 'recover') return recoverRun(root)
|
|
543
647
|
if (command === 'status') return statusRun(root)
|
|
544
|
-
throw new Error('usage: run-state.mjs <init|transition|validate|recover|status> <run-directory> [options]')
|
|
648
|
+
throw new Error('usage: run-state.mjs <init|transition|gate|validate|recover|status> <run-directory> [options]')
|
|
545
649
|
}
|
|
546
650
|
const invoked = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
547
651
|
if (invoked) cli(process.argv.slice(2)).then(result => process.stdout.write(`${JSON.stringify(result)}\n`)).catch(error => { process.stdout.write(`${JSON.stringify({ error: error.message })}\n`); process.exitCode = 1 })
|