@namewta/speculo 1.0.11 → 1.0.13
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 +1 -1
- package/package.json +1 -1
- package/template/workflows/learning/G-goal/G-goal.md +13 -8
- package/template/workflows/learning/G-goal/chain-template.md +14 -6
- package/template/workflows/learning/G-goal/goal-plan-template.md +72 -43
- package/template/workflows/learning/G-goal/orchestration-protocol.md +7 -6
- package/template/workflows/learning/G-goal/planning-modes.md +3 -3
- package/template/workflows/learning/G-goal/probe-template.md +28 -9
- package/template/workflows/learning/G-goal/progress-template.md +9 -5
- package/template/workflows/learning/G-goal/references/coverage-bar.md +7 -2
- package/template/workflows/learning/G-goal/references/external-goal-runner.md +13 -6
- package/template/workflows/learning/G-goal/references/lead-orchestration.md +52 -0
- package/template/workflows/learning/G-goal/references/mine-unit.md +49 -0
- package/template/workflows/learning/G-goal/references/split-rules.md +54 -0
- package/template/workflows/learning/G-goal/references/stop-rules.md +8 -3
- package/template/workflows/learning/INDEX.md +1 -1
- package/template/workflows/learning/README.md +5 -5
- package/template/workflows/learning/common/rules/artifact-contract.md +3 -3
- package/template/workflows/learning/common/rules/questioning-policy.md +5 -0
- package/template/workflows/learning/common/rules/teaching-policy.md +1 -1
- package/template/workflows/learning/common/skills/socratic-questioning/SKILL.md +4 -2
- package/template/workflows/learning/common/tools/validate-learning.mjs +213 -4
- package/template/workflows/ops/H-host-manage/H-host-manage.md +3 -0
- package/template/workflows/ops/INDEX.md +1 -1
- package/template/workflows/ops/README.md +1 -1
- package/template/workflows/ops/common/CAPABILITIES.md +2 -2
- package/template/workflows/ops/common/USAGE.md +23 -4
- package/template/workflows/ops/common/examples/register.example.json +1 -1
- package/template/workflows/ops/common/tests/test_ops_bootstrap.mjs +522 -0
- package/template/workflows/ops/common/toolchains/volta-linux.json +35 -0
- package/template/workflows/ops/common/tools/bootstrap-volta.sh +210 -0
- package/template/workflows/ops/common/tools/bootstrap.sh +1 -0
- package/template/workflows/ops/common/tools/opslib/bootstrap.mjs +221 -0
- package/template/workflows/ops/common/tools/opslib/cli.mjs +27 -5
- package/template/workflows/ops/common/tools/opslib/core.mjs +12 -0
- package/template/workflows/ops/common/tools/opslib/host_recipes.mjs +20 -4
- package/template/workflows/ops/common/tools/opslib/transport.mjs +101 -22
- package/template/workflows/ops/common/tools/validate-ops.mjs +1 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# 挖掘单元(mine unit)
|
|
2
|
+
|
|
3
|
+
覆盖波次(Wave 1 架构 / Wave 2 API)描述「学什么」,不单独决定「何时挖」。真正启动挖掘的单位叫 **mine unit**。
|
|
4
|
+
|
|
5
|
+
## Frontmatter
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
mine_unit: project # 按项目切分,不再写死 wave | goal
|
|
9
|
+
mine_unit_cap: 15 # 硬顶
|
|
10
|
+
learning_agent_limit: 4 # 默认并行度;硬顶 min(4, 本单元课数, 宿主能力)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`wave_cap` 不再当作挖掘门。Wave 只保留为覆盖尺上的内容分组。
|
|
14
|
+
|
|
15
|
+
## 怎么切
|
|
16
|
+
|
|
17
|
+
plan 阶段做编程库存之后,用下面规则切 mine unit(写入 `goal/chain.md` 的 Units 表,不要另造权威文件):
|
|
18
|
+
|
|
19
|
+
1. 先按覆盖波分组:Wave 1 = C4 Context + Container + 主/失败数据流;Wave 2 = 范围内公开 API 与方法性状。
|
|
20
|
+
2. 再按模块 / 容器切课。琐碎 helper、生成代码、测试夹具标 `covered-by-parent` 或 `deferred`,不为它们开课。
|
|
21
|
+
3. 把课按依赖序装进 mine unit:
|
|
22
|
+
- 同一单元内课数 ≤ 15。
|
|
23
|
+
- 尽量不把有前置依赖的课和它的前置拆到两个尚未挖掘的单元里;若必须拆,后一单元的 T 要等前一单元 D 结束(或至少等前置课 `mined` / `revised`)。
|
|
24
|
+
- 一课不得同时属于两个 unit。
|
|
25
|
+
- 前置课尚未 `written` / `mined` / `revised` 的课,不得提前塞进当前单元。
|
|
26
|
+
4. 估算课数在 plan 时写进 chain。`/goal` 不得为了凑单元而加灌水课,也不得把已规划的第 16 节塞进当前单元。
|
|
27
|
+
|
|
28
|
+
规则:
|
|
29
|
+
|
|
30
|
+
1. 若估算课数 **≤ 15**:整个 Goal 就是一个 mine unit。先写完全部课,再按课扇出 miner。
|
|
31
|
+
2. 若估算课数 **> 15**:按模块 / 容器切成连续单元,**每个单元 ≤ 15 节**。写完一个单元的全部课 → 挖这一个单元 → 再写下一个单元。
|
|
32
|
+
3. 任何 mine unit 不得超过 15 节。校验器对「同一 mine unit 规划超过 15 节」报错。
|
|
33
|
+
4. **15 是扇出上界,不是配额,不是必须凑满。** 4 节的小项目按 4 节挖,不要为了填满 15 而灌水开课。
|
|
34
|
+
5. 3 课的波不必撑到 15;单波若超过 15,只把挖掘切开,不把覆盖波扩成配额。
|
|
35
|
+
|
|
36
|
+
## 与 Wave 的关系
|
|
37
|
+
|
|
38
|
+
Wave 1 / Wave 2 仍按覆盖轴分组。挖掘扇出上界是 `mine_unit_cap=15`,不是必须凑满,也不是 Wave 配额。若某一覆盖波本身超过 15 节,必须先拆成多个 mine unit,不得整波一次扇出超过 15 个 miner。
|
|
39
|
+
|
|
40
|
+
## 示例
|
|
41
|
+
|
|
42
|
+
- 6 节的小库:1 个 unit,写完 6 节再 6 路挖掘(受 `learning_agent_limit` 限制,默认同时 4 个,其余排队)。
|
|
43
|
+
- 11 节:1 个 unit。
|
|
44
|
+
- 18 节:例如 Unit A 10 节 + Unit B 8 节,不得 18 节一次挖。
|
|
45
|
+
- 某容器公开 API 单独就超过 15 节:说明课图过粗或范围过大。plan 阶段就应拆课或把一部分标 `deferred`,而不是把 16+ 节装进一个 unit。
|
|
46
|
+
|
|
47
|
+
## `/goal` 循环
|
|
48
|
+
|
|
49
|
+
当前 mine unit:阶段 T 写完全部 L → 阶段 M 按课 mine → 阶段 D Lead 处置 → 下一 unit。禁止 `L → mine → L → mine` 交错。当前 mine unit 课数不得超过 15。超过则停止,回到 G-goal `replan` 重新切单元。
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# 拆课规则(split-lesson)
|
|
2
|
+
|
|
3
|
+
每课最多 10 问。10 问仍说不清,说明课拆得不够,应继续拆课而不是继续问。`split-lesson` 不是 `C-consolidate`。
|
|
4
|
+
|
|
5
|
+
## 计数
|
|
6
|
+
|
|
7
|
+
- 计数键是 `lesson_id`,不是 wave,不是 Change,不是 mine unit。
|
|
8
|
+
- 文件:`goal/probes/GP-<lesson>-b01.md` 承载 Q1–Q5;`GP-<lesson>-b02.md` 承载 Q6–Q10。
|
|
9
|
+
- 两批都必须走 socratic-questioning skill 的 5 槽(澄清 / 证据 / 反例 / 视角 / 元问题)。`b02` 只把审问对象挖深,不改槽位,不改 `audience=mine` 协议。
|
|
10
|
+
- 第 11 问非法。出现 `b03` 非法。满 10 后 `mine-more` 非法,只走 `split-lesson`。
|
|
11
|
+
- `re-dispatch-L` 改写同一课,**不重置**该课 `question_budget_used`。已经问过的题仍算在原课上。
|
|
12
|
+
- split 出的新课各自获得新的 10 问预算。原课收窄覆盖后,剩余预算冻结,不再追加问题。
|
|
13
|
+
- `validate-learning` 按 `lesson_id` 统计 Q 数,`>10` 报错;出现 `b03` 报错。
|
|
14
|
+
|
|
15
|
+
## 「10 问仍解释不清」的操作定义
|
|
16
|
+
|
|
17
|
+
满足任一即可由 miner 填写 `split-proposal`(Lead 裁决):
|
|
18
|
+
|
|
19
|
+
1. 该课声称覆盖的矩阵格子仍有 `uncovered`,且 `b02` 的 Q5 / Q10 仍建议 `mine-more`。
|
|
20
|
+
2. 合格读者读完 Lesson + 两批 probe,仍说不出该课声称覆盖的函数目的、主路径或失败路径、或方法副作用。
|
|
21
|
+
3. 若把缺口继续写进同一课,`time_budget` 将超过 40 分钟(违反 L 合同)。
|
|
22
|
+
|
|
23
|
+
不触发 split:
|
|
24
|
+
|
|
25
|
+
- 只是缺 L 合同章节(直觉 / 机制 / 边界 / 例子 / 文字等价物)→ `re-dispatch-L`,同一课补丁。
|
|
26
|
+
- 格子可以诚实标 `deferred(reason)` → `defer`,不再提问。
|
|
27
|
+
- 格子本就该 `covered-by-parent` → 改矩阵,不开新课。
|
|
28
|
+
|
|
29
|
+
## `re-dispatch-L` 与 split 的分界
|
|
30
|
+
|
|
31
|
+
| 证据 | Lead 处置 | 谁执笔 |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| 同一 OBJ,缺动机 / 机制 / 边界 / 例子 / 文字等价物 | `re-dispatch-L` | 原课的 L 子代理补丁 `lessons/L-*.md` |
|
|
34
|
+
| 一课声称覆盖超过 1 个容器,或 10 问后仍有解不开的函数簇 | `split-lesson` | Lead 改 chain;新 L 子代理写新课 |
|
|
35
|
+
| 源码里没有该符号,或超出 Scope | `defer(reason)` | Lead 改矩阵 |
|
|
36
|
+
| 需要改 `course.md` OBJ | 停止并 `replan` | 用户激活 G-goal + A |
|
|
37
|
+
| 当前 unit 加新课后将超过 15 节 | 新课进下一 unit,本 unit 先收束 | Lead |
|
|
38
|
+
|
|
39
|
+
Miner 只填表,不选择处置。
|
|
40
|
+
|
|
41
|
+
## `split-lesson` 不是整合
|
|
42
|
+
|
|
43
|
+
- Miner 不得改 `course.md`,不得改 `chain.md`,不得写 `lessons/`。它只在 probe 里填写 `split-proposal`:建议新课标题、从原课拿走哪些格子、为什么 10 问不够。
|
|
44
|
+
- Lead 若判断 OBJ 不变、只是课粒度粗:可在 `chain.md` 追加子节点。新课优先进入**当前或下一个 mine unit**;若当前 unit 加进去会超过 15 节,必须开新 unit,不得把第 16 节塞进当前扇出。
|
|
45
|
+
- Lead 若判断必须改 `course.md` 的 OBJ:停止,要求用户激活 G-goal `replan`(跟随 A)。`/goal` 不得偷偷改写 A 的目标。
|
|
46
|
+
- max split depth = 2。超过则 `defer(reason=needs-replan)`,写 verify 草稿,回到 G-goal `replan`。
|
|
47
|
+
- 禁止把 split 做成 `C-consolidate`,禁止搬到 `children/`,禁止写 `synthesis/` 或 `context/`,禁止调用 `relocate-learning.mjs`。
|
|
48
|
+
|
|
49
|
+
## HARD NO
|
|
50
|
+
|
|
51
|
+
- 同一 `lesson_id` 第 11 问非法
|
|
52
|
+
- 满 10 问后禁止 `mine-more`
|
|
53
|
+
- `split-lesson` 不得调用 `C-consolidate` 或 `relocate-learning.mjs`
|
|
54
|
+
- 当前 unit 加新课会超过 15 时,新课进入下一 unit
|
|
@@ -4,15 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
## 完成即停
|
|
6
6
|
|
|
7
|
-
当范围内格子全部为 `covered`、`deferred(reason)` 或 `covered-by-parent`,且每个 chain 节点有 L 合同 Lesson,且每节新课或改写课有对应 probe
|
|
7
|
+
当范围内格子全部为 `covered`、`deferred(reason)` 或 `covered-by-parent`,且每个 chain 节点有 L 合同 Lesson,且每节新课或改写课有对应 probe(每课 ≤10 问),且 `goal/verify.md` 已写,则 Goal 完成。
|
|
8
8
|
|
|
9
9
|
## 过程即停(任一即停)
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
11
|
+
- 本 mine unit 没有矩阵格子发生变化
|
|
12
|
+
- 出现第 11 问,或 `GP-*-b03.md`
|
|
13
|
+
- 交错 L/mine(阶段 T 完成前启动 miner,或 `L → mine → L`)
|
|
14
|
+
- miner 写入 `lessons/` 或 matrix
|
|
15
|
+
- 同一 mine unit 规划或扇出超过 15 节
|
|
13
16
|
- 用户暂停
|
|
14
17
|
- 缺源码、缺 baseline,或目标主要是无法核对的生成物
|
|
15
18
|
- 所有权冲突或路径越界
|
|
19
|
+
- split depth=2 后仍不清:`defer(needs-replan)`,写 verify 草稿,回到 G-goal `replan`
|
|
16
20
|
|
|
17
21
|
## 不算完成
|
|
18
22
|
|
|
@@ -20,3 +24,4 @@
|
|
|
20
24
|
- 文件巡览结束
|
|
21
25
|
- 模型表示「已经理解」
|
|
22
26
|
- 主观百分比
|
|
27
|
+
- 阶段 T 结束但尚未 mine
|
|
@@ -3,7 +3,7 @@ id: learning
|
|
|
3
3
|
type: workflow
|
|
4
4
|
workflow: learning
|
|
5
5
|
name: Learning Workflow
|
|
6
|
-
description: 以完整、通俗、多表示的课程,苏格拉底问答课、目标模式 Goal-Plan
|
|
6
|
+
description: 以完整、通俗、多表示的课程,苏格拉底问答课、目标模式 Goal-Plan 编译(先教后挖)、单文件作业评审、可选延迟复习和带溯源的主题综合,持续建立个人 Markdown 知识库。
|
|
7
7
|
keywords: [learning, 学习, 教学, 苏格拉底, 反问, goal, 目标模式, 作业, 复习, 综合, 知识, eli5]
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
- **A-archive** — 冷归档:用户明确关闭后移动整个 Change 树到日期目录;不做知识综合或掌握判断。
|
|
12
12
|
- **A-assess-and-plan** — 评估背景并设计课程:以目标和证据为起点建立课程、背景、基线、来源和可变 Lesson 地图。
|
|
13
13
|
- **C-consolidate** — 主题整合:将选定 Change 物理嵌入父 Change,生成带 claim 级 provenance 的可迭代主题综合。
|
|
14
|
-
- **G-goal** — 目标学习(目标模式):为选定编程项目编译一份可被外部 /goal 执行的完整 Goal-Plan
|
|
14
|
+
- **G-goal** — 目标学习(目标模式):为选定编程项目编译一份可被外部 /goal 执行的完整 Goal-Plan;按项目切 ≤15 节的 mine unit,先写齐再按课派 miner。计划会话可跟随 A 写出课程地图,但不写 Lesson、不向学习者提问、不自动串联 H/R/C。
|
|
15
15
|
- **H-homework** — 课程作业与评审:以单一 Markdown 文件生成题目、接收显式提交并追加逐题评审;不与 Lesson 混写。
|
|
16
16
|
- **I-init-setup** — 初始化学习系统:初始化 Learning v2 的教学偏好、空索引、位置登记和可验证状态。
|
|
17
17
|
- **L-lesson** — 完整课程讲解:一次输出 30–40 分钟、通俗但完整的 Lesson;不生成作业、不评分、不宣称掌握。
|
|
@@ -38,13 +38,13 @@ I-init-setup -> A-assess-and-plan -> (user chooses) L-lesson
|
|
|
38
38
|
|\
|
|
39
39
|
+-> Q-question (inquiry/)
|
|
40
40
|
|\
|
|
41
|
-
+-> G-goal (goal/ Goal-Plan; later /goal
|
|
41
|
+
+-> G-goal (goal/ Goal-Plan; later /goal: mine-unit T then M then D)
|
|
42
42
|
|
|
43
43
|
Any active or closed changes --(user chooses C)--> consolidation parent
|
|
44
44
|
Any closed root tree --(user chooses A)--> archive/YYYY-MM/<change>
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
`L` 完成后只报告已生成的 Lesson 和可选的下一步,不自动激活 `H`;`H` 评审后可结束本轮,不自动激活 `R`、`C` 或 `A`。`Q-question` 不自动激活 L/H/R,也不写入 `lessons/` 或 `homework/`。`G-goal` 激活只编译 Goal-Plan 并停止,不写 `lessons/` 或 `goal/probes/`;用户稍后用外部 `/goal`
|
|
47
|
+
`L` 完成后只报告已生成的 Lesson 和可选的下一步,不自动激活 `H`;`H` 评审后可结束本轮,不自动激活 `R`、`C` 或 `A`。`Q-question` 不自动激活 L/H/R,也不写入 `lessons/` 或 `homework/`。`G-goal` 激活只编译 Goal-Plan 并停止,不写 `lessons/` 或 `goal/probes/`;用户稍后用外部 `/goal` 按项目切出不超过 15 节的 mine unit,先写齐该单元全部课,再按课派 miner(socratic-questioning `audience=mine`)。不自动串联 H/R/C/A。`C` 和 `A` 都需要用户明确确认,未确认的 dry-run 不得移动或写入 context。
|
|
48
48
|
|
|
49
49
|
## 启动协议
|
|
50
50
|
|
|
@@ -77,7 +77,7 @@ changes/<change-id>/
|
|
|
77
77
|
.status.json
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
`inquiry/` 由 `Q-question` 拥有;`goal/` 由 `G-goal` 拥有。计划会话只写 Goal-Plan 骨架(`goal-plan.md` / `chain.md` / `coverage-matrix.md` / `progress.md
|
|
80
|
+
`inquiry/` 由 `Q-question` 拥有;`goal/` 由 `G-goal` 拥有。计划会话只写 Goal-Plan 骨架(`goal-plan.md` / `chain.md` / `coverage-matrix.md` / `progress.md` 与 mine unit 切分),不写 `goal/probes/` 或 `lessons/` 正文。`/goal` Lead 可更新矩阵与 progress 并写入 probes/verify;L 子代理只写被分配的 Lesson;miner 只写自己的 `GP-*-b0N`;不得写入 `inquiry/`。
|
|
81
81
|
|
|
82
82
|
综合父 Change 使用:
|
|
83
83
|
|
|
@@ -119,7 +119,7 @@ Workflow 自身只读模板;Change 内容只写当前 Change 或其 `children/
|
|
|
119
119
|
|
|
120
120
|
`expression_level=eli5|plain` 只控制词汇、句法、脚手架和类比比例;`coverage_depth=overview|standard|deep` 控制覆盖强度。Lesson 可放非评分的 pause/self-check,但不得生成 Q/A、答案、分数、verdict 或 mastered 字段。外部图片只是可选增强,必须有 alt、caption、source、访问日期和文字等价物,课程不能依赖链接可用性。
|
|
121
121
|
|
|
122
|
-
学习者苏格拉底批次只允许出现在 `Q-question` 拥有的 `inquiry/` 内。`G-goal` 的 `audience=mine` probes 写入 `goal/probes/`,不是 Lesson Q/A,且不占用 30–40 分钟 Lesson
|
|
122
|
+
学习者苏格拉底批次只允许出现在 `Q-question` 拥有的 `inquiry/` 内。`G-goal` 的 `audience=mine` probes 写入 `goal/probes/`,不是 Lesson Q/A,且不占用 30–40 分钟 Lesson 预算。每课最多 10 问(两批 5 槽);满 10 后只拆课,不续问。
|
|
123
123
|
|
|
124
124
|
## Homework 合同
|
|
125
125
|
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
| 工件 | Owner | 规则 |
|
|
4
4
|
| --- | --- | --- |
|
|
5
5
|
| `course.md`、`background/`、`baseline.md`、`sources.md`、Change `INDEX.md` | A-assess-and-plan | 目标、宏观基础、原始基线、来源和课程地图;实质修改留 revision |
|
|
6
|
-
| `lessons/` | L-lesson | 完整 Lesson 和 Lesson INDEX
|
|
6
|
+
| `lessons/` | L-lesson | 完整 Lesson 和 Lesson INDEX;不写作业答案或掌握结论。`/goal` 阶段 T 每课一个 L 子代理只写被分配的 `L-*.md`;`lessons/INDEX` 只有 Lead 可写 |
|
|
7
7
|
| `homework/` | H-homework + learner | H 写问题/评审,learner 写 A;提交后只追加 Review,旧 attempt 不可改写 |
|
|
8
8
|
| `inquiry/` | Q-question + learner | Q 写问题/Teaching/Inquiry Lesson,learner 写 A;`Response: ready` 后只追加讲解,旧 batch 不可改写;不写入 `lessons/` 或 `homework/` |
|
|
9
|
-
| `goal/` | G-goal | goal-plan.md, chain.md, coverage-matrix.md, progress.md, probes/, verify.md, revisions/; plan session writes plan skeleton only, not probes;
|
|
9
|
+
| `goal/` | G-goal | goal-plan.md, chain.md, coverage-matrix.md, progress.md, probes/, verify.md, revisions/; plan session writes plan skeleton and mine units only, not probes or deepening lessons; `/goal` Lead 独占 matrix/progress/verify/chain 追加;miner 只写自己的 `GP-*-b0N`;must not write inquiry/, lessons/, homework/, review/, synthesis/, archive/, mastered |
|
|
10
10
|
| `review/` | R-review | 延迟保持题、原始回答、证据和复习日期 |
|
|
11
11
|
| `children/<id>/` | child Work | 子 Change 原有工件;父只负责根锁、路由、位置登记 |
|
|
12
|
-
| `synthesis/`、topic context | C-consolidate | claim 级综合、冲突、空白、provenance
|
|
12
|
+
| `synthesis/`、topic context | C-consolidate | claim 级综合、冲突、空白、provenance 和版本;不得覆盖原料。`split-lesson` 不得调用 C 或 relocate |
|
|
13
13
|
| `archive/`、locations projection | A-archive | 用户关闭后的物理移动和只读登记;不判断掌握、不写 context |
|
|
14
14
|
|
|
15
15
|
状态 JSON 只是 projection;真实内容、学习者原始回答、评审和 source manifest 是证据权威。
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
|
|
19
19
|
每题标注 `objective_id`、`bloom_level`、`socratic_move`、`expected_evidence`、`difficulty`。数量默认 5,可由用户改,但必须 Q/A 成对连续。
|
|
20
20
|
|
|
21
|
+
## mine 数量上限
|
|
22
|
+
|
|
23
|
+
`audience=mine` 每课最多两批 10 问:`b01` = Q1–Q5,`b02` = Q6–Q10。第 11 问非法。第三批必须返回 `split-proposal` 而不是再出题。满 10 后禁止 `mine-more`。`re-dispatch-L` 不重置该课 `question_budget_used`。tutor 的 `inquiry/` 仍是一批约 5 题,Response 协议不变。
|
|
24
|
+
|
|
21
25
|
## 文件协议
|
|
22
26
|
|
|
23
27
|
- 出题文件含空 A 与精确行 `Response: pending`。
|
|
@@ -26,6 +30,7 @@
|
|
|
26
30
|
- 闭环后 `Response: closed`。重答新建 `IQ-…-batch-NN`,旧文件只读。
|
|
27
31
|
- 禁止 `Submission:`、禁止 H 的 `verdict: correct|partial|incorrect` 字段名。教学判定用 `aligned|partial|off|uncertain`。
|
|
28
32
|
- 禁止写入 `lessons/` 或 `homework/`。
|
|
33
|
+
- `audience=mine` 禁止上述 learner 协议字段,也禁止 miner 写 `lessons/`。
|
|
29
34
|
|
|
30
35
|
## 讲解与 keep-alive
|
|
31
36
|
|
|
@@ -14,4 +14,4 @@ ASCII、Markdown table、公式或可选外链图片可以组合使用;每个
|
|
|
14
14
|
|
|
15
15
|
Lesson 可以有非评分 pause/self-check,但不含 Q/A、答案、分数、verdict 或 mastered 字段。
|
|
16
16
|
|
|
17
|
-
学习者苏格拉底批次只允许出现在 `Q-question` 拥有的 `inquiry/` 内。`G-goal` 的 `audience=mine` probes 写入 `goal/probes/`,不是 Lesson Q/A,且不占用 30–40 分钟 Lesson
|
|
17
|
+
学习者苏格拉底批次只允许出现在 `Q-question` 拥有的 `inquiry/` 内。`G-goal` 的 `audience=mine` probes 写入 `goal/probes/`,不是 Lesson Q/A,且不占用 30–40 分钟 Lesson 预算。每课最多 10 问。`L-lesson` 仍不得写 Q/A。
|
|
@@ -12,7 +12,7 @@ description: 苏格拉底问答、反问授课或用提问教;当学习者卡
|
|
|
12
12
|
3. 先判断能否提问:真新手或高元素交互时返回降级探针或「先走 L-lesson」建议,而不是硬出机制题。
|
|
13
13
|
4. 默认 `socratic` 五槽:澄清、机制证据、假设反例、迁移视角、元问题/下一步。用户指定 `5e-recipe` / `feynman` / `productive-failure` / custom 时只改题目,不改 inquiry 文件协议。
|
|
14
14
|
5. `audience=tutor` 写入 `inquiry/IQ-*.md`:Q1–Q5、空 A、`Response: pending`。`Response: ready` 后只追加 Teaching 与 Inquiry Lesson;不改写答案,不写 `lessons/`、`homework/`、mastered、`Submission`、`verdict`。
|
|
15
|
-
6. `audience=mine` 写入 `goal/probes/GP-*.md`:只审问已写 Lesson 与源码,不写学习者问答,不写 `Response:`、`Submission:`、verdict、mastered,不写 `inquiry
|
|
15
|
+
6. `audience=mine` 写入 `goal/probes/GP-*.md`:只审问已写 Lesson 与源码,不写学习者问答,不写 `Response:`、`Submission:`、verdict、mastered,不写 `inquiry/`,不得写 `lessons/`。默认一批 5 问(`b01` = Q1–Q5);每课最多两批 10 问(`b02` = Q6–Q10)。第三批必须返回 `split-proposal` 路由而不是再出题。禁止 `b03`。满 10 后只走 `split-lesson`。
|
|
16
16
|
7. Teaching 每题含思路复原、`aligned|partial|off|uncertain`、中文详解、`Explain (English)`、纠错、未见知识、来源。Inquiry Lesson 把 5 题收成一课并给 keep-alive 钩子。需要完整 30–40 分钟讲义或正式测评时只返回路由,不自动激活 L/H/R。
|
|
17
17
|
|
|
18
18
|
## 题槽
|
|
@@ -25,7 +25,9 @@ description: 苏格拉底问答、反问授课或用提问教;当学习者卡
|
|
|
25
25
|
| Q4 | 迁移/视角 | viewpoints |
|
|
26
26
|
| Q5 | 元问题/下一步 | question-the-question |
|
|
27
27
|
|
|
28
|
+
`audience=mine` 的 `b02` 使用同一 5 槽,编号为 Q6–Q10。不改槽位,不改 `audience=mine` 协议。
|
|
29
|
+
|
|
28
30
|
## 完成标准
|
|
29
31
|
|
|
30
32
|
- `audience=tutor`:一批 Q/A 成对;讲解在作答之后;指定教法只改变题目,不改变 Response 协议;
|
|
31
|
-
- `audience=mine`:输出可被 `/goal` 直接写入 `goal/probes/GP
|
|
33
|
+
- `audience=mine`:输出可被 `/goal` 直接写入 `goal/probes/GP-*-b0N.md`,且不触发 learner 协议字段;每课 Q 数 ≤10;第三批只返回 `split-proposal`。
|
|
@@ -14,6 +14,13 @@ const OLD_WORKS = new Set(["A-archive-and-consolidate", "E-eli5", "P-practice",
|
|
|
14
14
|
const PHASE = new Set(["planning", "teaching", "homework", "review", "consolidating", "closed", "archived"]);
|
|
15
15
|
const LIFECYCLE = new Set(["active", "blocked", "closed", "archived"]);
|
|
16
16
|
const EVIDENCE = new Set(["not_attempted", "passed", "needs_review"]);
|
|
17
|
+
const MINE_UNIT_CAP = 15;
|
|
18
|
+
const MAX_LESSON_QUESTIONS = 10;
|
|
19
|
+
const PROBE_BATCH = /^GP-(L-\d{3}(?:-[a-z0-9]+)*)-b0(\d+)\.md$/;
|
|
20
|
+
const QUESTION_HEADING = /^##\s+Q(\d+)\b/gm;
|
|
21
|
+
const OLD_INTERLEAVE = /同一时间只推进一个 chain 节点|L\s*→\s*mine\s*→\s*L|先 L 写课,再 mine 探针/;
|
|
22
|
+
const LEARNER_PROBE = /(?:^|\n)\s*Response:|(?:^|\n)\s*Submission:|\binquiry\//;
|
|
23
|
+
const SOCRATIC_SKILL_PATH = "{roots.workflows}/learning/common/skills/socratic-questioning/SKILL.md";
|
|
17
24
|
|
|
18
25
|
function options(argv) {
|
|
19
26
|
const result = { workflowRoot: null, stateRoot: null, stage: null, change: null, selfCheck: false };
|
|
@@ -98,11 +105,22 @@ function validateWorkflowRoot(root, errors) {
|
|
|
98
105
|
["H-homework/homework-template.md", ["Q1", "A1", "Submission: pending"]],
|
|
99
106
|
["Q-question/Q-question.md", ["Response: pending", "Inquiry Lesson", "inquiry/"]],
|
|
100
107
|
["Q-question/inquiry-template.md", ["Response: pending", "Q1", "A1"]],
|
|
101
|
-
["G-goal/G-goal.md", ["ready_for_execution", "goal/goal-plan.md", "covered-by-parent"]],
|
|
102
|
-
["G-goal/goal-plan-template.md", ["ready_for_execution: false", "covered-by-parent", "goal/probes"]],
|
|
103
|
-
["G-goal/
|
|
108
|
+
["G-goal/G-goal.md", ["ready_for_execution", "goal/goal-plan.md", "covered-by-parent", "mine unit", "mine_unit_cap"]],
|
|
109
|
+
["G-goal/goal-plan-template.md", ["ready_for_execution: false", "covered-by-parent", "goal/probes", "mine_unit: project", "mine_unit_cap: 15", "learning_agent_limit: 4", "split-lesson", "teach-then-mine"]],
|
|
110
|
+
["G-goal/chain-template.md", ["Units", "mined", "split_proposed", "split"]],
|
|
111
|
+
["G-goal/probe-template.md", ["b01", "b02", "question_budget_used", "split-proposal", "split-lesson"]],
|
|
112
|
+
["G-goal/progress-template.md", ["mine_unit", "question_budget_used", "active_subagents"]],
|
|
113
|
+
["G-goal/orchestration-protocol.md", ["lead-orchestration.md", "mine-unit.md", "audience=mine"]],
|
|
114
|
+
["G-goal/planning-modes.md", ["mine unit", "T(写完全部课)"]],
|
|
115
|
+
["G-goal/references/external-goal-runner.md", ["audience=mine", "Q-quiz", "inquiry/", "socratic-questioning/SKILL.md", "mine unit"]],
|
|
116
|
+
["G-goal/references/lead-orchestration.md", ["learning_agent_limit", "写集", "只有 Lead 可写", SOCRATIC_SKILL_PATH]],
|
|
117
|
+
["G-goal/references/mine-unit.md", ["mine_unit_cap", "不是配额", "15"]],
|
|
118
|
+
["G-goal/references/split-rules.md", ["split-lesson", "question_budget", "C-consolidate", "relocate-learning.mjs"]],
|
|
119
|
+
["G-goal/references/stop-rules.md", ["第 11 问", "交错 L/mine", "15 节"]],
|
|
120
|
+
["G-goal/references/coverage-bar.md", ["mine_unit_cap=15", "不是必须凑满"]],
|
|
104
121
|
["common/rules/teaching-policy.md", ["coverage_depth", "不以字符数"]],
|
|
105
|
-
["common/rules/questioning-policy.md", ["audience=mine", "goal/probes"]],
|
|
122
|
+
["common/rules/questioning-policy.md", ["audience=mine", "goal/probes", "最多两批 10 问"]],
|
|
123
|
+
["common/skills/socratic-questioning/SKILL.md", ["audience=mine", "b02", "split-proposal"]],
|
|
106
124
|
];
|
|
107
125
|
for (const [file, markers] of contracts) {
|
|
108
126
|
const path = join(root, file);
|
|
@@ -110,6 +128,196 @@ function validateWorkflowRoot(root, errors) {
|
|
|
110
128
|
const text = readFileSync(path, "utf8");
|
|
111
129
|
for (const marker of markers) if (!text.includes(marker)) errors.push(`${file}: missing '${marker}'`);
|
|
112
130
|
}
|
|
131
|
+
const goalPlan = readFileSync(join(root, "G-goal/goal-plan-template.md"), "utf8");
|
|
132
|
+
if (OLD_INTERLEAVE.test(goalPlan) && !goalPlan.includes("禁止 L → mine → L → mine")) {
|
|
133
|
+
errors.push("G-goal/goal-plan-template.md: old interleave iteration policy remains");
|
|
134
|
+
}
|
|
135
|
+
const orch = readFileSync(join(root, "G-goal/orchestration-protocol.md"), "utf8");
|
|
136
|
+
if (orch.includes("默认 current、严格串行") || (orch.includes("同一时间只推进一个 chain 节点") && !orch.includes("禁止"))) {
|
|
137
|
+
errors.push("G-goal/orchestration-protocol.md: old serial one-node recipe remains");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function markdownTables(text) {
|
|
142
|
+
const tables = [];
|
|
143
|
+
const lines = String(text).split(/\r?\n/);
|
|
144
|
+
let i = 0;
|
|
145
|
+
while (i < lines.length) {
|
|
146
|
+
if (/^\|.+\|$/.test(lines[i]) && i + 1 < lines.length && /^\|\s*[-:| ]+\|$/.test(lines[i + 1])) {
|
|
147
|
+
const header = lines[i].split("|").slice(1, -1).map((cell) => cell.trim());
|
|
148
|
+
i += 2;
|
|
149
|
+
const rows = [];
|
|
150
|
+
while (i < lines.length && /^\|.+\|$/.test(lines[i]) && !/^\|\s*[-:| ]+\|$/.test(lines[i])) {
|
|
151
|
+
rows.push(lines[i].split("|").slice(1, -1).map((cell) => cell.trim()));
|
|
152
|
+
i += 1;
|
|
153
|
+
}
|
|
154
|
+
tables.push({ header, rows });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
i += 1;
|
|
158
|
+
}
|
|
159
|
+
return tables;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function tableColumn(table, names) {
|
|
163
|
+
const index = table.header.findIndex((cell) => names.some((name) => cell.toLowerCase() === name.toLowerCase()));
|
|
164
|
+
return index;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function lessonIdsIn(text) {
|
|
168
|
+
return [...String(text).matchAll(/\bL-\d{3}(?:-[a-z0-9]+)*\b/g)].map((match) => match[0]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function filledSplitProposal(text) {
|
|
172
|
+
const section = /##\s+split-proposal\b([\s\S]*?)(?:\n##\s+|$)/i.exec(text);
|
|
173
|
+
if (!section) return false;
|
|
174
|
+
const body = section[1];
|
|
175
|
+
if (/^\s*none\s*$/im.test(body.trim())) return false;
|
|
176
|
+
const tables = markdownTables(body);
|
|
177
|
+
for (const table of tables) {
|
|
178
|
+
for (const row of table.rows) {
|
|
179
|
+
if (row.some((cell) => cell && cell !== "—" && cell !== "-" && cell !== "none")) return true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return /L-\d{3}/.test(body);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseChainLessons(chainText) {
|
|
186
|
+
const lessons = new Map();
|
|
187
|
+
for (const table of markdownTables(chainText)) {
|
|
188
|
+
const idCol = tableColumn(table, ["ID", "id", "lesson_id"]);
|
|
189
|
+
const statusCol = tableColumn(table, ["状态", "status"]);
|
|
190
|
+
if (idCol < 0 || statusCol < 0) continue;
|
|
191
|
+
for (const row of table.rows) {
|
|
192
|
+
const id = lessonIdsIn(row[idCol] ?? "")[0];
|
|
193
|
+
if (id) lessons.set(id, { status: (row[statusCol] ?? "").trim(), row });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return lessons;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseChainUnits(chainText) {
|
|
200
|
+
const units = [];
|
|
201
|
+
for (const table of markdownTables(chainText)) {
|
|
202
|
+
const unitCol = tableColumn(table, ["unit_id", "unit"]);
|
|
203
|
+
const lessonsCol = tableColumn(table, ["Lessons", "lessons", "课列表"]);
|
|
204
|
+
if (unitCol < 0 || lessonsCol < 0) continue;
|
|
205
|
+
for (const row of table.rows) {
|
|
206
|
+
const unitId = (row[unitCol] ?? "").trim();
|
|
207
|
+
if (!unitId || unitId === "unit_id") continue;
|
|
208
|
+
units.push({ unitId, lessons: lessonIdsIn(row[lessonsCol] ?? "") });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return units;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function validateGoal(changeRoot, errors) {
|
|
215
|
+
const goalRoot = join(changeRoot, "goal");
|
|
216
|
+
if (!isDir(goalRoot)) return;
|
|
217
|
+
const rel = (path) => relative(changeRoot, path).split(sep).join("/");
|
|
218
|
+
const chainPath = join(goalRoot, "chain.md");
|
|
219
|
+
const planPath = join(goalRoot, "goal-plan.md");
|
|
220
|
+
const progressPath = join(goalRoot, "progress.md");
|
|
221
|
+
const matrixPath = join(goalRoot, "coverage-matrix.md");
|
|
222
|
+
const probesRoot = join(goalRoot, "probes");
|
|
223
|
+
const verifyPath = join(goalRoot, "verify.md");
|
|
224
|
+
const chainText = isFile(chainPath) ? readFileSync(chainPath, "utf8") : "";
|
|
225
|
+
const planText = isFile(planPath) ? readFileSync(planPath, "utf8") : "";
|
|
226
|
+
const progressText = isFile(progressPath) ? readFileSync(progressPath, "utf8") : "";
|
|
227
|
+
const matrixText = isFile(matrixPath) ? readFileSync(matrixPath, "utf8") : "";
|
|
228
|
+
const verifyText = isFile(verifyPath) ? readFileSync(verifyPath, "utf8") : "";
|
|
229
|
+
|
|
230
|
+
if (planText && OLD_INTERLEAVE.test(planText) && !planText.includes("禁止 L → mine → L → mine")) {
|
|
231
|
+
errors.push(`${rel(planPath)}: interleaved L/mine iteration policy is forbidden`);
|
|
232
|
+
}
|
|
233
|
+
if (progressText && OLD_INTERLEAVE.test(progressText)) {
|
|
234
|
+
errors.push(`${rel(progressPath)}: interleaved L/mine progress is forbidden`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const units = parseChainUnits(chainText);
|
|
238
|
+
const seenLesson = new Set();
|
|
239
|
+
for (const unit of units) {
|
|
240
|
+
if (unit.lessons.length > MINE_UNIT_CAP) {
|
|
241
|
+
errors.push(`${rel(chainPath)}: mine unit ${unit.unitId} has ${unit.lessons.length} lessons; mine_unit_cap=${MINE_UNIT_CAP}`);
|
|
242
|
+
}
|
|
243
|
+
for (const id of unit.lessons) {
|
|
244
|
+
if (seenLesson.has(id)) errors.push(`${rel(chainPath)}: lesson ${id} belongs to more than one mine unit`);
|
|
245
|
+
seenLesson.add(id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const lessons = parseChainLessons(chainText);
|
|
250
|
+
const probeFiles = isDir(probesRoot)
|
|
251
|
+
? files(probesRoot).filter((path) => path.endsWith(".md") && !path.endsWith("INDEX.md"))
|
|
252
|
+
: [];
|
|
253
|
+
const byLesson = new Map();
|
|
254
|
+
for (const path of probeFiles) {
|
|
255
|
+
const name = path.split(sep).pop() ?? "";
|
|
256
|
+
const label = rel(path);
|
|
257
|
+
const text = readFileSync(path, "utf8");
|
|
258
|
+
const meta = frontmatter(text);
|
|
259
|
+
const match = PROBE_BATCH.exec(name);
|
|
260
|
+
if (!match) {
|
|
261
|
+
errors.push(`${label}: probe filename must be GP-<lesson>-b0N.md`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const fileLesson = match[1];
|
|
265
|
+
const batch = Number(match[2]);
|
|
266
|
+
if (batch > 2) errors.push(`${label}: b03+ is forbidden; max two batches per lesson`);
|
|
267
|
+
if (meta.audience && meta.audience !== "mine") errors.push(`${label}: audience must be mine`);
|
|
268
|
+
if (meta.lesson_id && meta.lesson_id !== fileLesson && !String(meta.lesson_id).startsWith(fileLesson) && !fileLesson.startsWith(String(meta.lesson_id))) {
|
|
269
|
+
errors.push(`${label}: miner write-set isolation; lesson_id ${meta.lesson_id} does not match filename ${fileLesson}`);
|
|
270
|
+
}
|
|
271
|
+
if (LEARNER_PROBE.test(text)) errors.push(`${label}: mine probe must not contain Response:, Submission:, or inquiry/`);
|
|
272
|
+
const questions = [...text.matchAll(QUESTION_HEADING)].map((item) => Number(item[1]));
|
|
273
|
+
const record = byLesson.get(fileLesson) ?? { questions: [], batches: new Set(), files: [], texts: [], budgets: [], split: false, mineMore: false, splitDepth: 0 };
|
|
274
|
+
record.questions.push(...questions);
|
|
275
|
+
record.batches.add(batch);
|
|
276
|
+
record.files.push(label);
|
|
277
|
+
record.texts.push(text);
|
|
278
|
+
if (Number.isInteger(meta.question_budget_used)) record.budgets.push(meta.question_budget_used);
|
|
279
|
+
if (filledSplitProposal(text)) record.split = true;
|
|
280
|
+
if (/\bmine-more\b/.test(text)) record.mineMore = true;
|
|
281
|
+
if (Number.isInteger(meta.split_depth)) record.splitDepth = Math.max(record.splitDepth, meta.split_depth);
|
|
282
|
+
byLesson.set(fileLesson, record);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const minedLessons = new Set(byLesson.keys());
|
|
286
|
+
for (const unit of units) {
|
|
287
|
+
const statuses = unit.lessons.map((id) => lessons.get(id)?.status ?? "");
|
|
288
|
+
const unitHasProbe = unit.lessons.some((id) => minedLessons.has(id));
|
|
289
|
+
const unitStillWriting = unit.lessons.some((id) => {
|
|
290
|
+
const status = lessons.get(id)?.status ?? "";
|
|
291
|
+
return status === "planned" || status === "writing";
|
|
292
|
+
});
|
|
293
|
+
if (unitHasProbe && unitStillWriting) {
|
|
294
|
+
errors.push(`${rel(chainPath)}: mine unit ${unit.unitId} interleaved L/mine (probes exist while a lesson is still planned/writing)`);
|
|
295
|
+
}
|
|
296
|
+
void statuses;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const matrixUncovered = /\buncovered\b/.test(matrixText);
|
|
300
|
+
for (const [lessonId, record] of byLesson) {
|
|
301
|
+
const uniqueQs = new Set(record.questions);
|
|
302
|
+
if (uniqueQs.size > MAX_LESSON_QUESTIONS || record.questions.some((number) => number > MAX_LESSON_QUESTIONS)) {
|
|
303
|
+
errors.push(`${record.files[0]}: lesson ${lessonId} has more than ${MAX_LESSON_QUESTIONS} questions`);
|
|
304
|
+
}
|
|
305
|
+
const budget = record.budgets.length ? Math.max(...record.budgets) : uniqueQs.size;
|
|
306
|
+
const status = lessons.get(lessonId)?.status ?? "";
|
|
307
|
+
if (status === "revised" && uniqueQs.size > 0 && record.budgets.some((value) => value === 0)) {
|
|
308
|
+
errors.push(`${record.files[0]}: re-dispatch-L must not reset question_budget_used for ${lessonId}`);
|
|
309
|
+
}
|
|
310
|
+
if (uniqueQs.size >= MAX_LESSON_QUESTIONS && record.mineMore && !record.split) {
|
|
311
|
+
errors.push(`${record.files[0]}: 10 questions with remaining uncovered/mine-more requires split-proposal, not b03`);
|
|
312
|
+
}
|
|
313
|
+
if (uniqueQs.size >= MAX_LESSON_QUESTIONS && matrixUncovered && !record.split && record.mineMore) {
|
|
314
|
+
errors.push(`${record.files[0]}: 10 questions still uncovered requires split-proposal`);
|
|
315
|
+
}
|
|
316
|
+
if (record.splitDepth >= 2 && matrixUncovered && !/needs-replan/.test(`${progressText}\n${verifyText}\n${record.texts.join("\n")}`)) {
|
|
317
|
+
errors.push(`${record.files[0]}: split depth=2 still unclear must defer(needs-replan)`);
|
|
318
|
+
}
|
|
319
|
+
void budget;
|
|
320
|
+
}
|
|
113
321
|
}
|
|
114
322
|
|
|
115
323
|
function validateEntry(entry, label, errors, archived) {
|
|
@@ -229,6 +437,7 @@ function validateStateRoot(root, opts, errors) {
|
|
|
229
437
|
const changeRoot = join(root, ...String(entry.locator).split("/"));
|
|
230
438
|
validateLessons(changeRoot, errors);
|
|
231
439
|
validateHomework(changeRoot, errors);
|
|
440
|
+
validateGoal(changeRoot, errors);
|
|
232
441
|
if (opts.stage === "pre-archive" && (!opts.change || opts.change === entry.change_id) && value?.lifecycle !== "closed") errors.push(`${entry.change_id}: pre-archive requires lifecycle closed`);
|
|
233
442
|
if (opts.stage === "complete" && (!opts.change || opts.change === entry.change_id) && !String(entry.locator).startsWith("archive/")) errors.push(`${entry.change_id}: complete stage requires archived locator`);
|
|
234
443
|
}
|
|
@@ -19,6 +19,9 @@ keywords: [ops, host-manage, 主机, 持久化, 审批]
|
|
|
19
19
|
|
|
20
20
|
指定 local 或 SSH 的稳定 host_id;读取最近盘点与有限问题证据。初次 SSH 用用户已验证的 known_hosts,identity=discover 只允许只读探测;把返回身份明确登记后才允许计划和执行。
|
|
21
21
|
|
|
22
|
+
Linux SSH 目标若没有 Node:必须先 `ops.mjs bootstrap-node`(控制端校验已审核 tar 的 SHA256,scp 到目标,远端 POSIX 展开固定 Volta/Node),禁止把 POSIX 盘点只写在对话里。引导成功后必须 `ops.mjs enroll`(或 `register` 再 `probe --host`),`hosts/{host_id}/inventory/snapshot-*.json` 与 `status.json.hosts` 是完成标准的一部分。引导是 ops.mjs 第一阶段通道,需要明文 ack 与 SHA256,不走 plan/approve;已有 Node 的目标不重复安装、不改 `.bashrc`/`.profile`。缺 Node 只能阻塞 apply,不能阻塞登记。本轮仅 Linux SSH;Windows/macOS 仍要求目标已有 Node。
|
|
23
|
+
|
|
24
|
+
|
|
22
25
|
区分系统版本、用户默认、项目 pin、服务环境。environment-spec 支持明确版本的 uv、Volta、SDKMAN 管理配方;管理器缺失先用经过审核的安装器。保留旧默认与旧目录,不能为统一外观先删除旧环境。工作流不擅自改写用户 shell profile;激活新管理器入口是另一个明确的准备动作。
|
|
23
26
|
|
|
24
27
|
Docker 缺失使用经审核且版本固定的 Linux Engine 安装配方,完成服务、Compose、data-root、账号权限验证之后才进入 D。既有 data-root 不匹配时单独备份、停机、迁移、验证,不把 /var/lib/docker 直接 mv 当作安装步骤。镜像源按可信清单、样本哈希和目标网络测试,切换配置仍需批准。
|
|
@@ -24,7 +24,7 @@ keywords: [ops, 主机, SSH, 部署, Docker, 持久化, 公共服务, 运维]
|
|
|
24
24
|
## Work 激活
|
|
25
25
|
|
|
26
26
|
I-initialize:控制端初始化。
|
|
27
|
-
H-host-manage:本地或远程主机的环境、诊断、清理和维护。
|
|
27
|
+
H-host-manage:本地或远程主机的环境、诊断、清理和维护。Linux SSH 目标无 Node 时先走 USAGE 中的 `bootstrap-node` 再 `enroll`,盘点必须写入 `hosts/`。
|
|
28
28
|
D-project-deploy:业务 APP 与公共服务部署、复用、更新、停止及显式迁移。
|
|
29
29
|
|
|
30
30
|
三个入口都使用 plan → approve → apply → verify → dual-documents;不得自动激活额外旧 Work。
|
|
@@ -89,7 +89,7 @@ init/register/credential-put 是用户显式请求的部署机本地记录操作
|
|
|
89
89
|
|
|
90
90
|
## 阅读与操作入口
|
|
91
91
|
|
|
92
|
-
详细命令和可运行演练:`<Path>{roots.workflows}/ops/common/USAGE.md</Path>`。
|
|
92
|
+
详细命令和可运行演练:`<Path>{roots.workflows}/ops/common/USAGE.md</Path>`。Linux SSH 缺 Node 的固定 Volta 引导见 USAGE §1。
|
|
93
93
|
数据与账户:`<Path>{roots.workflows}/ops/common/rules/persistence-and-secrets.md</Path>`。
|
|
94
94
|
共享服务:`<Path>{roots.workflows}/ops/common/rules/shared-services.md</Path>`。
|
|
95
95
|
恢复:`<Path>{roots.workflows}/ops/common/rules/recovery.md</Path>`。
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
|---|---|---|
|
|
7
7
|
| 控制端 | Node 标准库 CLI(ops.mjs);状态、锁、明文、计划、执行 | 无第三方 npm 包;控制端可本地执行完整闭环 |
|
|
8
8
|
| Linux local | 原生 oneshot;systemd 配置/权限/健康门;Docker Engine Compose | 本轮隔离 local oneshot 演练;systemd/Docker 需要真实有权限主机验收 |
|
|
9
|
-
| SSH | OpenSSH 密钥或 agent;固定 known_hosts;目标身份;可 sudo -n |
|
|
9
|
+
| SSH | OpenSSH 密钥或 agent;固定 known_hosts;目标身份;可 sudo -n;Linux 目标无 Node 时可走固定 Volta 引导 | 目标无 Node 时 Linux SSH 可走固定 Volta/Node 引导(pin 见 common/toolchains/volta-linux.json);Windows 仍要求已有 Node。本轮没有远程主机/ssh 客户端实机验证,由模拟 SSH/scp 覆盖;不实现交互式 SSH 密码登录 |
|
|
10
10
|
| Windows 原生 | PowerShell 引导、路径/DACL、同账户登录态 Scheduled Task | 不是无人登录的 Windows Service;真正后台服务需专用 adapter;本轮非 Windows 实机 |
|
|
11
11
|
| WSL | 单独登记为 Linux 执行目标 | 不把 Windows 主机路径/Docker Desktop VM 当作 WSL 根 |
|
|
12
12
|
| macOS | 身份探测、POSIX 路径、oneshot | 无 launchd 适配;不是承诺全部平台常驻部署 |
|
|
13
13
|
| Compose | JSON-as-YAML、显式 context/project、raw env_file、digest image、bind 与 VOLUME检查 | 要求 Compose >=2.30;strict Engine data-root;Docker Desktop 不自动迁移 |
|
|
14
|
-
| uv/Volta/SDKMAN |
|
|
14
|
+
| uv/Volta/SDKMAN | 明确版本准备与原默认保留配方;命令/默认验证;Volta 本体 + 固定 Node 镜像可由 Linux SSH `bootstrap-node` 安装 | 管理器安装包必须可信固定;旧 profile 不自动改写;项目真实兼容性另验 |
|
|
15
15
|
| 共享服务 | MySQL/Redis/MinIO 分配、existing 验证、owner/binding/依赖排序 | 无本轮数据库实机;未知产品须真实版本适配;没有万能数据库备份/恢复 |
|
|
16
16
|
| 升级/回滚/迁移 | 新 spec、固定版本、显式主机操作、跨机排序、逐步回执 | 没有隐式停机零损失承诺;没有自动生成任意项目迁移脚本 |
|
|
17
17
|
| 清理 | 只读诊断、cache/log 隔离、精确回执受控 purge | 不普遍删除 Windows/Linux 任意系统垃圾;不 drop_caches 或 prune volume |
|
|
@@ -10,9 +10,28 @@ node /path/to/ops/common/tools/ops.mjs --state /path/to/.speculo/ops init --cont
|
|
|
10
10
|
node /path/to/ops/common/tools/ops.mjs probe --output /safe/path/local-inventory.json
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
没有 Node 时运行 common/tools/bootstrap.sh probe 或 bootstrap.ps1 -Probe,只做检测。经过批准的本地安装器还需精确 SHA256 与确认字符串。完成后重新 init。
|
|
13
|
+
没有 Node 时运行 common/tools/bootstrap.sh probe 或 bootstrap.ps1 -Probe,只做检测。经过批准的本地安装器还需精确 SHA256 与确认字符串。完成后重新 init。SSH 目标缺 Node 不要对本机 bootstrap.sh 假装能装远端,改走下面的 `bootstrap-node`。
|
|
14
|
+
|
|
15
|
+
初次远端将 host.json 中 identity 写为 discover(仅 probe 支持),connection 包含 hostname、username、known_hosts,可选 port、identity_file、sudo、shell。known_hosts 必须已经通过可信方式核对,不能自动信任 ssh-keyscan 输出。目标还没有 Node 时不要填一个 PATH 上的 `node` 然后把盘点留在对话里:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
node /path/to/ops/common/tools/ops.mjs bootstrap-node --connection-file /safe/path/host-discovery.json --probe
|
|
19
|
+
# 若 tools.node=missing,准备已审核的 volta/node tar(SHA256 见 common/toolchains/volta-linux.json),再:
|
|
20
|
+
node /path/to/ops/common/tools/ops.mjs --state /path/to/.speculo/ops bootstrap-node --apply \
|
|
21
|
+
--connection-file /safe/path/host-discovery.json --host-id node-a --account ops --host-root /srv/ops \
|
|
22
|
+
--volta-archive /safe/volta-2.0.2-linux.tar.gz --volta-sha256 PINNED \
|
|
23
|
+
--node-archive /safe/node-v24.21.0-linux-x64.tar.gz --node-sha256 PINNED \
|
|
24
|
+
--ack I-APPROVE-THIS-BOOTSTRAP
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
引导把 `VOLTA_HOME` 放在 `{host.root}/_host/toolchains/{account}/volta`,`connection.node` 指向其中的 `bin/ops-node`(导出 VOLTA_HOME 后 exec 真实 Node),不改 `.bashrc`/`.profile`,不跑官方 `install.sh`。已有 Node 则 `skipped-existing-node`,原默认不动。随后必须落盘:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
node /path/to/ops/common/tools/ops.mjs --state /path/to/.speculo/ops enroll --file /safe/path/register.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`enroll` 接受 `identity=discover` 与引导返回的绝对 `connection.node`,写入 `status.json` 与 `hosts/{host_id}/inventory/snapshot-*.json`。单独 `probe --connection-file` 仍只打印 JSON,下一步必须 enroll(或 register + `probe --host`)。
|
|
14
34
|
|
|
15
|
-
初次远端将 host.json 中 identity 写为 discover(仅 probe 支持),connection 包含 hostname、username、known_hosts、node,可选 port、identity_file、sudo、shell。known_hosts 必须已经通过可信方式核对,不能自动信任 ssh-keyscan 输出。
|
|
16
35
|
|
|
17
36
|
```sh
|
|
18
37
|
node /path/to/ops/common/tools/ops.mjs probe --connection-file /safe/path/host-discovery.json --output /safe/path/host-observed.json
|
|
@@ -63,7 +82,7 @@ node /path/to/ops/common/tools/ops.mjs --state /path/to/.speculo/ops environment
|
|
|
63
82
|
node /path/to/ops/common/tools/ops.mjs --state /path/to/.speculo/ops mirror-probe --host node-a --file /safe/path/mirror-candidates.json --allow-network
|
|
64
83
|
```
|
|
65
84
|
|
|
66
|
-
环境配方不抓 latest
|
|
85
|
+
环境配方不抓 latest;管理器缺失先明确准备安装器。Linux SSH 上 Volta 本体与固定 Node 镜像用 `bootstrap-node`,不要指望 environment-spec 自己装 Volta。来源测速不修改配置,证书/哈希失败候选不使用,真实配置更换通过 H 的精确 write-file/write-control/命令计划。不能将 Python/npm/Docker/Maven 镜像混为一套规则。
|
|
67
86
|
|
|
68
87
|
H.host_actions 支持 mkdir、write-file(host.root 内相对路径;不得写生成器负责的 README/DEPLOYMENTS/knowledge 账本)、write-control(精确系统控制文件:内置 docker daemon.json 与 ops-*.service,以及经理由/回滚/验证声明的 nginx、wireguard、非 ops- 前缀单元)、install-toolchain(明确写集/原默认/验证)、command(明确写集/验证)、defaults、quarantine、purge-quarantine。系统软件包安装使用明确批准的可信安装脚本,不自动猜当前发行版安装命令;Docker 示例见 service-profiles/docker-engine.md。主机级入口用 resource_updates.hosts[].host_services 与 spec.public_ingress 入账,不要为 Nginx/WireGuard 伪造 APP 部署。
|
|
69
88
|
|
|
@@ -105,7 +124,7 @@ node /path/to/ops/common/tools/ops.mjs --state /path/to/new-ops-state import-leg
|
|
|
105
124
|
|
|
106
125
|
```sh
|
|
107
126
|
node /path/to/ops/common/tools/demo-local.mjs --output /absolute/empty/demo-root
|
|
108
|
-
node --test /path/to/ops/common/tests/test_ops.mjs
|
|
127
|
+
node --test /path/to/ops/common/tests/test_ops.mjs /path/to/ops/common/tests/test_ops_bootstrap.mjs
|
|
109
128
|
node /path/to/ops/common/tools/validate-ops.mjs --self-check
|
|
110
129
|
```
|
|
111
130
|
|