@namewta/speculo 1.0.18 → 1.0.20

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.
Files changed (195) hide show
  1. package/README.md +8 -7
  2. package/THIRD-PARTY-NOTICES.md +33 -0
  3. package/dist/src/agent-files.js +2 -2
  4. package/dist/src/agent-files.js.map +1 -1
  5. package/dist/src/agent-skills.d.ts +81 -0
  6. package/dist/src/agent-skills.js +530 -0
  7. package/dist/src/agent-skills.js.map +1 -0
  8. package/dist/src/cli.js +11 -3
  9. package/dist/src/cli.js.map +1 -1
  10. package/dist/src/config.js +5 -3
  11. package/dist/src/config.js.map +1 -1
  12. package/dist/src/doctor.js +7 -2
  13. package/dist/src/doctor.js.map +1 -1
  14. package/dist/src/index.d.ts +8 -0
  15. package/dist/src/index.js +23 -3
  16. package/dist/src/index.js.map +1 -1
  17. package/dist/src/manifest.d.ts +9 -0
  18. package/dist/src/manifest.js +45 -7
  19. package/dist/src/manifest.js.map +1 -1
  20. package/dist/src/refresh.js +40 -0
  21. package/dist/src/refresh.js.map +1 -1
  22. package/dist/src/structured.js +15 -4
  23. package/dist/src/structured.js.map +1 -1
  24. package/dist/src/transaction.d.ts +24 -3
  25. package/dist/src/transaction.js +191 -12
  26. package/dist/src/transaction.js.map +1 -1
  27. package/package.json +10 -4
  28. package/scripts/validate-skills.mjs +56 -0
  29. package/template/AGENTS.md +1 -1
  30. package/template/CLAUDE.md +1 -1
  31. package/template/canonical/canonical-specdev-goal-plan.md +158 -30
  32. package/template/canonical/canonical-specdev-grill-with-docs.md +155 -33
  33. package/template/canonical/canonical-specdev-spec.md +162 -161
  34. package/template/canonical/canonical-specdev-tickets.md +155 -29
  35. package/template/canonical/canonical-specdev-wayfinder.md +144 -29
  36. package/template/commands/handoff.md +16 -0
  37. package/template/commands/retro.md +12 -10
  38. package/template/skills/archive-and-consolidate/SKILL.md +163 -7
  39. package/template/skills/archive-and-consolidate/references/cleanup-rules.md +1 -1
  40. package/template/skills/docs-sync/references/agents/claude-redirect.md +8 -8
  41. package/template/skills/docs-sync/references/agents/content-contract.md +1 -1
  42. package/template/skills/docs-sync/references/agents/manifest-discovery.md +7 -7
  43. package/template/skills/docs-sync/references/agents/role-classification.md +4 -4
  44. package/template/skills/docs-sync/references/agents-contract.md +1 -1
  45. package/template/skills/engineering-standards-builder/SKILL.md +149 -9
  46. package/template/skills/engineering-standards-builder/agents/openai.yaml +2 -0
  47. package/template/skills/engineering-standards-builder/manifest.txt +2 -1
  48. package/template/skills/engineering-standards-builder/references/rules/09-mechanical-guardrails.md +9 -0
  49. package/template/skills/engineering-standards-builder/scripts/validate-builder.mjs +4 -7
  50. package/template/skills/git-history-squash/SKILL.md +87 -11
  51. package/template/skills/git-history-squash/agents/openai.yaml +2 -0
  52. package/template/skills/optimize-codex-config/SKILL.md +71 -11
  53. package/template/skills/retrospective/SKILL.md +43 -0
  54. package/template/skills/{speculo-retro → retrospective}/references/friction-taxonomy.md +22 -13
  55. package/template/skills/{speculo-retro → retrospective}/references/issue-drafting-sop.md +13 -21
  56. package/template/skills/source-code-zip/SKILL.md +44 -8
  57. package/template/skills/source-code-zip/references/{entry-procedure.md → cli-reference.md} +0 -43
  58. package/template/skills/upstream-fork-sync/SKILL.md +71 -8
  59. package/template/skills/{writing-great-skills → writing-for-agents}/GLOSSARY.md +1 -1
  60. package/template/skills/writing-for-agents/SKILL.md +22 -0
  61. package/template/skills/writing-for-agents/agents/openai.yaml +2 -0
  62. package/template/skills/{writing-great-skills/references/entry-procedure.md → writing-for-agents/references/authoring-principles.md} +2 -9
  63. package/template/workflows/learning/A-archive/A-archive.md +2 -5
  64. package/template/workflows/learning/A-assess-and-plan/A-assess-and-plan.md +2 -5
  65. package/template/workflows/learning/C-consolidate/C-consolidate.md +2 -5
  66. package/template/workflows/learning/G-goal/G-goal.md +5 -23
  67. package/template/workflows/learning/H-homework/H-homework.md +2 -5
  68. package/template/workflows/learning/I-init-setup/I-init-setup.md +4 -9
  69. package/template/workflows/learning/L-lesson/L-lesson.md +2 -5
  70. package/template/workflows/learning/Q-question/Q-question.md +16 -8
  71. package/template/workflows/learning/Q-question/inquiry-template.md +2 -0
  72. package/template/workflows/learning/Q-question/tools/validate-inquiry.mjs +112 -0
  73. package/template/workflows/learning/R-review/R-review.md +3 -6
  74. package/template/workflows/learning/README.md +2 -2
  75. package/template/workflows/learning/common/rules/knowledge-organization.md +1 -1
  76. package/template/workflows/learning/common/rules/mastery-policy.md +1 -1
  77. package/template/workflows/learning/common/skills/socratic-questioning/SKILL.md +1 -1
  78. package/template/workflows/ops/D-project-deploy/D-project-deploy.md +6 -2
  79. package/template/workflows/ops/H-host-manage/H-host-manage.md +10 -18
  80. package/template/workflows/ops/H-host-manage/references/host-profiles.md +21 -0
  81. package/template/workflows/ops/I-initialize/I-initialize.md +4 -4
  82. package/template/workflows/ops/README.md +22 -84
  83. package/template/workflows/ops/S-server-connect/S-server-connect.md +3 -3
  84. package/template/workflows/ops/V-inventory-view/V-inventory-view.md +2 -2
  85. package/template/workflows/ops/common/rules/deployment-layout.md +51 -0
  86. package/template/workflows/ops/common/rules/recovery.md +6 -0
  87. package/template/workflows/ops/common/rules/server-onboarding.md +6 -0
  88. package/template/workflows/ops/common/tests/test_ops.mjs +5 -8
  89. package/template/workflows/ops/common/tests/test_ops_bootstrap.mjs +71 -14
  90. package/template/workflows/ops/common/tests/test_ops_workspace.mjs +17 -8
  91. package/template/workflows/ops/common/tools/opslib/bootstrap.mjs +4 -4
  92. package/template/workflows/ops/common/tools/opslib/transport.mjs +3 -3
  93. package/template/workflows/person/M-mao-zedong-cognitive-os/M-mao-zedong-cognitive-os.md +13 -68
  94. package/template/workflows/person/S-steelman-deliberation/S-steelman-deliberation.md +11 -123
  95. package/template/workflows/person/S-steelman-deliberation/references/state-and-publication.md +86 -0
  96. package/template/workflows/specdev/A-archive-and-consolidate/A-archive-and-consolidate.md +2 -17
  97. package/template/workflows/specdev/A-archive-and-consolidate/consolidation-interview.md +13 -4
  98. package/template/workflows/specdev/C-code-review/C-code-review.md +4 -15
  99. package/template/workflows/specdev/C-code-review/code-review-template.md +4 -0
  100. package/template/workflows/specdev/D-diagnose-bugs/D-diagnose-bugs.md +4 -20
  101. package/template/workflows/specdev/G-grill-with-docs/G-grill-with-docs.md +2 -2
  102. package/template/workflows/specdev/G-grill-with-docs/references/interview-procedure.md +6 -6
  103. package/template/workflows/specdev/I-implement/I-implement.md +2 -2
  104. package/template/workflows/specdev/I-implement/evidence-template.md +8 -0
  105. package/template/workflows/specdev/I-implement/references/implementation-procedure.md +9 -43
  106. package/template/workflows/specdev/I-implement/tdd-rules.md +4 -0
  107. package/template/workflows/specdev/I-init-setup/I-init-setup.md +16 -117
  108. package/template/workflows/specdev/I-init-setup/config-template.json +5 -1
  109. package/template/workflows/specdev/I-init-setup/references/first-setup.md +46 -0
  110. package/template/workflows/specdev/INDEX.md +2 -2
  111. package/template/workflows/specdev/L-learn-change/L-learn-change.md +11 -89
  112. package/template/workflows/specdev/L-learn-change/references/lesson-format.md +47 -0
  113. package/template/workflows/specdev/P-goal-plan/P-goal-plan.md +2 -2
  114. package/template/workflows/specdev/P-goal-plan/planning-modes.md +1 -1
  115. package/template/workflows/specdev/P-prototype/P-prototype.md +7 -17
  116. package/template/workflows/specdev/P-prototype/logic-protocol.md +11 -0
  117. package/template/workflows/specdev/P-prototype/logic-template.md +27 -0
  118. package/template/workflows/specdev/P-prototype/project-preview-protocol.md +9 -0
  119. package/template/workflows/specdev/R-retro/R-retro.md +26 -0
  120. package/template/workflows/specdev/R-retro/retro-template.md +27 -0
  121. package/template/workflows/specdev/R-review-architecture/R-review-architecture.md +19 -57
  122. package/template/workflows/specdev/R-review-architecture/architecture-review-template.md +2 -1
  123. package/template/workflows/specdev/R-review-architecture/references/selected-design.md +23 -0
  124. package/template/workflows/specdev/R-review-architecture/review-rubric.md +2 -2
  125. package/template/workflows/specdev/README.md +17 -6
  126. package/template/workflows/specdev/S-spec/S-spec.md +13 -136
  127. package/template/workflows/specdev/T-tickets/T-tickets.md +6 -2
  128. package/template/workflows/specdev/T-triage/T-triage.md +32 -141
  129. package/template/workflows/specdev/T-triage/capture-protocol.md +1 -1
  130. package/template/workflows/specdev/T-triage/ci-security-protocol.md +11 -0
  131. package/template/workflows/specdev/T-triage/intake-protocol.md +29 -4
  132. package/template/workflows/specdev/T-triage/pr-delivery-protocol.md +13 -0
  133. package/template/workflows/specdev/T-triage/pr-record-template.md +39 -0
  134. package/template/workflows/specdev/T-triage/publish-protocol.md +1 -1
  135. package/template/workflows/specdev/T-triage/queue-protocol.md +7 -0
  136. package/template/workflows/specdev/T-triage/reconcile-protocol.md +1 -1
  137. package/template/workflows/specdev/T-triage/references/issue-pr-policy.md +23 -0
  138. package/template/workflows/specdev/T-triage/references/release-authentication.md +9 -0
  139. package/template/workflows/specdev/T-triage/references/release-notes-injection.md +21 -0
  140. package/template/workflows/specdev/T-triage/references/workflow-yaml-reference.md +37 -0
  141. package/template/workflows/specdev/T-triage/release-preflight.md +13 -0
  142. package/template/workflows/specdev/T-triage/release-protocol.md +12 -0
  143. package/template/workflows/specdev/T-triage/release-recovery.md +18 -0
  144. package/template/workflows/specdev/T-triage/remote-operations.md +25 -0
  145. package/template/workflows/specdev/T-triage/scripts/github-transport.mjs +208 -0
  146. package/template/workflows/specdev/T-triage/source-template.md +7 -0
  147. package/template/workflows/specdev/T-triage/triage-run-template.md +34 -0
  148. package/template/workflows/specdev/T-triage/triage-template.md +4 -1
  149. package/template/workflows/specdev/W-wayfinder/W-wayfinder.md +2 -2
  150. package/template/workflows/specdev/common/rules/artifact-contract.md +7 -0
  151. package/template/workflows/specdev/common/rules/change-completion.md +6 -0
  152. package/template/workflows/specdev/common/rules/deployment-handoff.md +23 -0
  153. package/template/workflows/specdev/common/rules/workflow-routing.md +6 -0
  154. package/template/workflows/specdev/common/rules/workflow-state-and-lifecycle.md +1 -1
  155. package/template/workflows/specdev/common/schemas/config.schema.json +137 -26
  156. package/template/workflows/specdev/common/schemas/logic-prototype.schema.json +62 -0
  157. package/template/workflows/specdev/common/schemas/pull-request.schema.json +90 -0
  158. package/template/workflows/specdev/common/schemas/retrospective.schema.json +58 -0
  159. package/template/workflows/specdev/common/schemas/triage-run.schema.json +149 -0
  160. package/template/workflows/specdev/common/schemas/triage.schema.json +117 -15
  161. package/template/workflows/specdev/common/skills/code-review/SKILL.md +5 -4
  162. package/template/workflows/specdev/common/skills/code-review/references/result-reuse.md +15 -0
  163. package/template/workflows/specdev/common/tools/delivery-artifacts.mjs +95 -0
  164. package/template/workflows/specdev/common/tools/validate-specdev.mjs +60 -19
  165. package/template/workflows/specdev/runtime-contract.json +4 -2
  166. package/template/skills/archive-and-consolidate/references/entry-procedure.md +0 -178
  167. package/template/skills/docs-sync/references/entry-procedure.md +0 -3
  168. package/template/skills/engineering-standards-builder/references/entry-procedure.md +0 -154
  169. package/template/skills/git-history-squash/references/entry-procedure.md +0 -94
  170. package/template/skills/github-npm-ops/SKILL.md +0 -19
  171. package/template/skills/github-npm-ops/references/ci-and-security-ops.md +0 -178
  172. package/template/skills/github-npm-ops/references/entry-procedure.md +0 -25
  173. package/template/skills/github-npm-ops/references/failure-recovery.md +0 -123
  174. package/template/skills/github-npm-ops/references/issue-pr-triage.md +0 -219
  175. package/template/skills/github-npm-ops/references/issue-transport.md +0 -26
  176. package/template/skills/github-npm-ops/references/package-json-checklist.md +0 -171
  177. package/template/skills/github-npm-ops/references/preflight-checklist.md +0 -39
  178. package/template/skills/github-npm-ops/references/publish-detection.md +0 -68
  179. package/template/skills/github-npm-ops/references/release-notes-injection.md +0 -238
  180. package/template/skills/github-npm-ops/references/release-pipeline.md +0 -108
  181. package/template/skills/github-npm-ops/references/setup-npm-token.md +0 -63
  182. package/template/skills/github-npm-ops/references/troubleshooting-playbook.md +0 -293
  183. package/template/skills/github-npm-ops/references/version-bump-flow.md +0 -217
  184. package/template/skills/github-npm-ops/references/workflow-yaml-reference.md +0 -268
  185. package/template/skills/github-npm-ops/scripts/issue-transport.mjs +0 -227
  186. package/template/skills/optimize-codex-config/references/entry-procedure.md +0 -78
  187. package/template/skills/speculo-retro/SKILL.md +0 -19
  188. package/template/skills/speculo-retro/references/entry-procedure.md +0 -23
  189. package/template/skills/upstream-fork-sync/references/entry-procedure.md +0 -78
  190. package/template/skills/writing-great-skills/SKILL.md +0 -23
  191. /package/template/skills/{writing-great-skills → writing-for-agents}/references/document-contract.md +0 -0
  192. /package/template/workflows/specdev/{G-grill-with-docs → common/rules}/adr-format.md +0 -0
  193. /package/template/workflows/specdev/{G-grill-with-docs → common/rules}/context-format.md +0 -0
  194. /package/template/workflows/specdev/{I-implement → common/rules}/design-it-twice.md +0 -0
  195. /package/template/workflows/specdev/{G-grill-with-docs/domain-modeling-rules.md → common/rules/domain-modeling.md} +0 -0
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
- import { dirname, join } from "node:path";
6
+ import { dirname, join, posix } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import test from "node:test";
9
9
  import { createHash } from "node:crypto";
@@ -18,6 +18,7 @@ import * as model from "../tools/opslib/model.mjs";
18
18
  const toolsDir = join(dirname(fileURLToPath(import.meta.url)), "../tools");
19
19
  const scriptPath = join(toolsDir, "bootstrap-volta.sh");
20
20
  const ACK = "I-APPROVE-THIS-BOOTSTRAP";
21
+ const linuxExecution = { skip: process.platform !== "linux" && "Requires Linux /bin/sh and a Linux Node executable; SSH protocol contracts run on every platform" };
21
22
 
22
23
  function sha256(p) {
23
24
  return createHash("sha256").update(readFileSync(p)).digest("hex");
@@ -58,13 +59,14 @@ function pinFor(archives, arch = "linux-x64") {
58
59
  return { ...base, archives: { ...base.archives, [arch]: slot } };
59
60
  }
60
61
 
61
- function mockTransport({ probeJson = null } = {}) {
62
+ function mockTransport({ probeJson = null, agentJson = null, applyJson = null } = {}) {
62
63
  const prev = transport.transportHooks.spawnSync;
63
64
  const log = [];
64
65
  transport.transportHooks.spawnSync = (cmd, args, kw) => {
65
66
  log.push({ cmd, args: [...args], encoding: kw.encoding, inputIsBuffer: Buffer.isBuffer(kw.input) });
66
67
  const last = args[args.length - 1];
67
68
  if (cmd === "scp") {
69
+ if (applyJson) return { status: 0, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0) };
68
70
  const local = args[args.length - 2];
69
71
  const spec = args[args.length - 1];
70
72
  const remotePath = spec.slice(spec.indexOf(":") + 1);
@@ -80,11 +82,13 @@ function mockTransport({ probeJson = null } = {}) {
80
82
  return { status: 0, stdout: Buffer.from(JSON.stringify(probeJson) + "\n"), stderr: Buffer.alloc(0) };
81
83
  }
82
84
  if (String(last).includes("--input-type=module")) {
85
+ if (agentJson) return { status: 0, stdout: Buffer.from(JSON.stringify({ ok: true, result: agentJson }) + "\n"), stderr: Buffer.alloc(0) };
83
86
  const parts = splitRemote(last);
84
87
  return spawnSync(parts[0], parts.slice(1), {
85
88
  input: kw.input, encoding: "buffer", timeout: kw.timeout, maxBuffer: kw.maxBuffer,
86
89
  });
87
90
  }
91
+ if (applyJson) return { status: 0, stdout: Buffer.from(String(last).includes("--apply") ? JSON.stringify(applyJson) + "\n" : ""), stderr: Buffer.alloc(0) };
88
92
  const parts = splitRemote(last);
89
93
  return spawnSync(parts[0], parts.slice(1), {
90
94
  input: kw.input, encoding: "buffer", timeout: kw.timeout, maxBuffer: kw.maxBuffer,
@@ -182,7 +186,20 @@ test("scp uses the same host-key contract", () => {
182
186
  } finally { transport.transportHooks.spawnSync = prev; rmSync(d, { recursive: true, force: true }); }
183
187
  });
184
188
 
185
- test("bootstrap-volta apply installs wrapper and does not touch profile", () => {
189
+ test("scp rejects controller paths and unsafe Linux paths before spawning", (t) => {
190
+ const d = mkdtempSync(join(tmpdir(), "ops-scp-path-"));
191
+ t.after(() => rmSync(d, { recursive: true, force: true }));
192
+ const kh = join(d, "known_hosts"), local = join(d, "archive.tar.gz");
193
+ writeFileSync(kh, "fixture pinned key\n"); writeFileSync(local, "reviewed archive");
194
+ const prev = transport.transportHooks.spawnSync;
195
+ t.after(() => { transport.transportHooks.spawnSync = prev; });
196
+ transport.transportHooks.spawnSync = () => { assert.fail("invalid paths must not reach scp"); };
197
+ for (const path of ["C:\\server\\archive.tar.gz", "C:/server/archive.tar.gz", "\\\\server\\share\\archive.tar.gz", "/srv/ops\\archive.tar.gz", "relative.tar.gz", "/srv/../archive.tar.gz", "/srv/space name.tar.gz", "/srv/nul\0.tar.gz"]) {
198
+ assert.throws(() => transport.posixSend({ hostname: "host.invalid", username: "ops", known_hosts: kh }, local, path), /remote scp path/);
199
+ }
200
+ });
201
+
202
+ test("bootstrap-volta apply installs wrapper and does not touch profile", linuxExecution, () => {
186
203
  const d = mkdtempSync(join(tmpdir(), "ops-vol-"));
187
204
  const home = join(d, "home");
188
205
  mkdirSync(home);
@@ -205,28 +222,33 @@ test("bootstrap-volta apply installs wrapper and does not touch profile", () =>
205
222
  } finally { rmSync(d, { recursive: true, force: true }); }
206
223
  });
207
224
 
208
- test("bootstrap-volta apply without ack blocked", () => {
225
+ test("bootstrap-volta apply without ack blocked", linuxExecution, () => {
209
226
  const d = mkdtempSync(join(tmpdir(), "ops-ack-"));
210
227
  const archives = makeArchives(d);
211
228
  const p = spawnSync("/bin/sh", [scriptPath, "--apply", archives.voltaTar, archives.voltaSha, archives.nodeTar, archives.nodeSha, join(d, "volta"), "24.21.0", "nope"], { encoding: "utf8" });
212
- assert.notEqual(p.status, 0);
229
+ assert.equal(p.error, undefined);
230
+ assert.equal(p.status, 2);
231
+ assert.match(p.stderr, /acknowledgement/);
213
232
  rmSync(d, { recursive: true, force: true });
214
233
  });
215
234
 
216
- test("bootstrap-volta sha mismatch blocked", () => {
235
+ test("bootstrap-volta sha mismatch blocked", linuxExecution, () => {
217
236
  const d = mkdtempSync(join(tmpdir(), "ops-sha-"));
218
237
  const archives = makeArchives(d);
219
238
  const p = spawnSync("/bin/sh", [scriptPath, "--apply", archives.voltaTar, "0".repeat(64), archives.nodeTar, archives.nodeSha, join(d, "volta"), "24.21.0", ACK], { encoding: "utf8" });
220
- assert.notEqual(p.status, 0);
239
+ assert.equal(p.error, undefined);
240
+ assert.equal(p.status, 2);
241
+ assert.match(p.stderr, /installer changed after review/);
221
242
  assert.ok(!existsSync(join(d, "volta", "bin", "ops-node")));
222
243
  rmSync(d, { recursive: true, force: true });
223
244
  });
224
245
 
225
- function cliFixture() {
246
+ function cliFixture({ executeLinux = false } = {}) {
226
247
  const tmp = mkdtempSync(join(tmpdir(), "ops-bootcli-"));
227
248
  const state = join(tmp, "controller");
228
- const target = join(tmp, "server");
229
- mkdirSync(target, { recursive: true });
249
+ // A simulated Linux SSH target is not a path on the controller filesystem.
250
+ const target = executeLinux ? join(tmp, "server") : "/srv/speculo-bootstrap-test";
251
+ if (executeLinux) mkdirSync(target, { recursive: true });
230
252
  const kh = join(tmp, "known_hosts");
231
253
  writeFileSync(kh, "fixture pinned key\n");
232
254
  const prevProbe = cliHooks.probeLocal;
@@ -251,6 +273,39 @@ const missingProbe = {
251
273
  node_path: null, node_version: null, node_usable: false, profile_has_volta: false,
252
274
  };
253
275
 
276
+ test("Linux SSH bootstrap and enrollment preserve remote paths on every controller platform", () => {
277
+ const fx = cliFixture();
278
+ const connectionNode = posix.join(fx.target, "_host/toolchains/ops/volta/bin/ops-node");
279
+ const mock = mockTransport({ probeJson: missingProbe, applyJson: { status: "installed", connection_node: connectionNode, profile_unchanged: true } });
280
+ try {
281
+ const host = sshHost({ root: fx.target, kh: fx.kh, node: "node" });
282
+ const conn = writeConn(fx.tmp, host);
283
+ const r = captureMain(["bootstrap-node", "--state", fx.state, "--host-id", host.host_id, "--connection-file", conn, "--apply", "--account", "ops", "--host-root", fx.target, "--ack", ACK,
284
+ "--volta-archive", fx.archives.voltaTar, "--volta-sha256", fx.archives.voltaSha, "--node-archive", fx.archives.nodeTar, "--node-sha256", fx.archives.nodeSha]);
285
+ assert.equal(r.code, 0, r.stderr);
286
+ const boot = JSON.parse(r.stdout), pins = pinFor(fx.archives).archives["linux-x64"];
287
+ const paths = [pins.volta.filename, pins.node.filename].map(name => posix.join(fx.target, "_host/installers", name));
288
+ assert.deepEqual(mock.log.filter(e => e.cmd === "scp").map(e => e.args.at(-1)), paths.map(p => "ops@host.invalid:" + p));
289
+ const applyArgs = splitRemote(mock.log.find(e => e.cmd === "ssh" && e.args.at(-1).includes("--apply")).args.at(-1));
290
+ assert.deepEqual(applyArgs.slice(applyArgs.indexOf("--apply") + 1), [paths[0], fx.archives.voltaSha, paths[1], fx.archives.nodeSha, posix.dirname(posix.dirname(connectionNode)), fx.archives.nodeVersion, ACK]);
291
+ assert.equal(JSON.parse(readFileSync(boot.receipt, "utf8")).connection_node, connectionNode);
292
+ assert.deepEqual(Object.keys(model.load(fx.state).hosts), []);
293
+ mock.restore();
294
+ host.connection.node = connectionNode;
295
+ const enrollFile = join(fx.tmp, "register.json"); writeFileSync(enrollFile, JSON.stringify({ hosts: [host], projects: [] }));
296
+ const inventory = { identity: "a".repeat(64), platform: "linux", tools: { node: { path: connectionNode, version: "v24.21.0" } } };
297
+ const enrolledMock = mockTransport({ probeJson: { ...missingProbe, tools: { ...missingProbe.tools, node: connectionNode }, node_path: connectionNode, node_usable: true }, agentJson: inventory });
298
+ try {
299
+ const enrolled = captureMain(["--state", fx.state, "enroll", "--file", enrollFile]);
300
+ assert.equal(enrolled.code, 0, enrolled.stderr);
301
+ assert.equal(model.load(fx.state).hosts[host.host_id].connection.node, connectionNode);
302
+ const snapshot = JSON.parse(enrolled.stdout).hosts[0].snapshot;
303
+ assert.deepEqual(JSON.parse(readFileSync(snapshot, "utf8")), inventory);
304
+ assert.equal(enrolledMock.log.filter(e => e.args.at(-1).includes("--input-type=module")).length, 2);
305
+ } finally { enrolledMock.restore(); }
306
+ } finally { mock.restore(); fx.cleanup(); }
307
+ });
308
+
254
309
  test("discover probe without node returns node-missing", () => {
255
310
  const fx = cliFixture();
256
311
  const mock = mockTransport({ probeJson: missingProbe });
@@ -313,6 +368,7 @@ test("bootstrap-node sha mismatch blocked", () => {
313
368
  "--node-archive", fx.archives.nodeTar, "--node-sha256", fx.archives.nodeSha,
314
369
  ]);
315
370
  assert.equal(r.code, 2);
371
+ assert.match(r.stderr, /sha256 does not match the reviewed pin/i);
316
372
  assert.ok(!mock.log.some((e) => String(e.args.at(-1) || "").includes("--apply")));
317
373
  } finally { mock.restore(); fx.cleanup(); }
318
374
  });
@@ -362,8 +418,8 @@ test("bootstrap-node skips when node exists", () => {
362
418
  } finally { mock.restore(); fx.cleanup(); }
363
419
  });
364
420
 
365
- test("bootstrap-node apply then enroll writes inventory and status", () => {
366
- const fx = cliFixture();
421
+ test("bootstrap-node apply then enroll writes inventory and status", linuxExecution, () => {
422
+ const fx = cliFixture({ executeLinux: true });
367
423
  const home = join(fx.tmp, "home");
368
424
  mkdirSync(home);
369
425
  writeFileSync(join(home, ".bashrc"), "keep-me\n");
@@ -435,10 +491,11 @@ test("enroll without node blocked", () => {
435
491
  test("probe --connection-file still does not imply register", () => {
436
492
  const fx = cliFixture();
437
493
  const mock = mockTransport({
438
- probeJson: { ...missingProbe, tools: { ...missingProbe.tools, node: process.execPath }, node_path: process.execPath, node_usable: true },
494
+ probeJson: { ...missingProbe, tools: { ...missingProbe.tools, node: "/usr/bin/node" }, node_path: "/usr/bin/node", node_usable: true },
495
+ agentJson: { identity: "a".repeat(64), platform: "linux" },
439
496
  });
440
497
  try {
441
- const host = sshHost({ root: fx.target, kh: fx.kh, node: process.execPath, identity: "discover" });
498
+ const host = sshHost({ root: fx.target, kh: fx.kh, identity: "discover" });
442
499
  const conn = writeConn(fx.tmp, host);
443
500
  const r = captureMain(["probe", "--connection-file", conn]);
444
501
  assert.equal(r.code, 0, r.stderr);
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
3
3
  import { createHash } from "node:crypto";
4
4
  import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, existsSync, statSync, symlinkSync, rmSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
- import { dirname, join, resolve } from "node:path";
6
+ import { dirname, join, resolve, posix } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { spawnSync } from "node:child_process";
9
9
  import { digest, emptyStatus, now, writeJson } from "../tools/opslib/core.mjs";
@@ -18,7 +18,7 @@ function fixture(t) {
18
18
  t.after(() => rmSync(root, { recursive: true, force: true }));
19
19
  const state = join(root, "state"); mkdirSync(state);
20
20
  const s = emptyStatus(); s.controller = { controller_id: "controller-a", state_root: state, created_at: now() };
21
- for (const id of ["node-a", "node-b"]) s.hosts[id] = { host_id: id, display_name: id, platform: "linux", transport: "local", root: join(root, id), identity: "a".repeat(64), connection: {} };
21
+ for (const id of ["node-a", "node-b"]) s.hosts[id] = { host_id: id, display_name: id, platform: process.platform === "win32" ? "windows" : "linux", transport: "local", root: join(root, id), identity: "a".repeat(64), connection: {} };
22
22
  for (const id of ["app-a", "app-b"]) s.projects[id] = { project_id: id, display_name: id, kind: "app", service_type: null, source: { type: "local", location: join(root, "source"), revision: "v1" } };
23
23
  writeJson(join(state, "status.json"), s);
24
24
  const save = () => writeJson(join(state, "status.json"), s);
@@ -32,7 +32,13 @@ function request(id = "task-one") {
32
32
  function inventory(h) {
33
33
  return { identity: h.identity, platform: "linux", tools: { node: { version: "v22.22.3" } },
34
34
  snapshots: { [h.root]: { kind: "directory" } }, diagnostics: { disks: { [h.root]: { free: 4 * 1024 ** 3 } }, memory: { MemAvailable: 1024 ** 3 } },
35
- docker_control: { status: "observed", endpoint: "unix:///var/run/docker.sock", data_root: join(h.root, "_runtime/docker"), compose_version: "2.30.0" } };
35
+ docker_control: { status: "observed", endpoint: "unix:///var/run/docker.sock", data_root: posix.join(h.root, "_runtime/docker"), compose_version: "2.30.0" } };
36
+ }
37
+ function linuxServer(f) {
38
+ return Object.assign(f.s.hosts["node-a"], { platform: "linux", root: "/srv/speculo-test/node-a" });
39
+ }
40
+ function directoryLink(target, path) {
41
+ symlinkSync(target, path, process.platform === "win32" ? "junction" : "dir");
36
42
  }
37
43
  function adapterFor(f, options = {}) {
38
44
  const calls = { compile: 0, approve: 0, apply: 0 };
@@ -76,7 +82,7 @@ test("explicit layer routing distinguishes controller, access, server, project a
76
82
  test("workspace rejects guessed roots, traversal, symlinks, duplicate JSON keys and orphan deployments", (t) => {
77
83
  const f = fixture(t);
78
84
  for (const path of [".", "/", f.state + "/../state"]) assert.throws(() => statePath(path));
79
- const link = join(f.root, "linked"); symlinkSync(f.state, link, "dir"); assert.throws(() => statePath(link), /symlink/);
85
+ const link = join(f.root, "linked"); directoryLink(f.state, link); assert.throws(() => statePath(link), /symlink/);
80
86
  writeFileSync(join(f.state, "status.json"), '{"schema_version":3,"schema_version":3}'); assert.throws(() => readRegistry(f.state), /duplicate JSON/);
81
87
  f.s.deployments.dangling = { deployment_id: "dangling", host_id: "missing", project_id: "app-a" }; f.save(); assert.throws(() => readRegistry(f.state), /orphan/);
82
88
  });
@@ -90,7 +96,7 @@ test("CLI help works without network, server dependencies or state", () => {
90
96
  const r = spawnSync(process.execPath, [path, "workspace-help"], { encoding: "utf8" }); assert.equal(r.status, 0, r.stderr); assert.match(r.stdout, /server-check/);
91
97
  });
92
98
  test("Linux checklist distinguishes missing, failed, profile-ready and stale evidence", (t) => {
93
- const f = fixture(t), h = f.s.hosts["node-a"], inv = inventory(h), at = new Date("2026-01-01T00:00:00Z");
99
+ const f = fixture(t), h = linuxServer(f), inv = inventory(h), at = new Date("2026-01-01T00:00:00Z");
94
100
  assert.equal(readiness(assessServer(h), at.toISOString(), { at }), "unknown");
95
101
  assert.equal(readiness(assessServer(h, { profile: "compose", inventory: inv }), at.toISOString(), { at }), "ready");
96
102
  assert.equal(readiness(assessServer(h, { profile: "compose", inventory: inv }), at.toISOString(), { at: new Date("2026-01-03T00:00:00Z") }), "stale");
@@ -100,7 +106,7 @@ test("Linux checklist distinguishes missing, failed, profile-ready and stale evi
100
106
  }
101
107
  });
102
108
  test("server check uses pinned public-key-only SSH and records restricted evidence", async (t) => {
103
- const f = fixture(t), h = f.s.hosts["node-a"], kh = join(f.root, "known_hosts"); writeFileSync(kh, "fixture verified key");
109
+ const f = fixture(t), h = linuxServer(f), kh = join(f.root, "known_hosts"); writeFileSync(kh, "fixture verified key");
104
110
  Object.assign(h, { transport: "ssh", connection: { hostname: "example.invalid", username: "deploy", known_hosts: kh } }); f.save();
105
111
  let spawned = 0, called = 0;
106
112
  const a = { sshArgv: () => ["ssh", "-o", "StrictHostKeyChecking=yes"], spawn: (_cmd, args) => { spawned++; assert.ok(args.includes("PasswordAuthentication=no")); assert.ok(args.includes("KbdInteractiveAuthentication=no")); return { status: 0 }; }, call: () => { called++; return inventory(h); } };
@@ -111,7 +117,7 @@ test("server check uses pinned public-key-only SSH and records restricted eviden
111
117
  writeFileSync(kh, "different key"); assert.notEqual(check.host_digest, targetDigest(h));
112
118
  });
113
119
  test("failed SSH login never triggers Node probe and is not reported ready", async (t) => {
114
- const f = fixture(t), h = f.s.hosts["node-a"], kh = join(f.root, "known_hosts"); writeFileSync(kh, "fixture"); Object.assign(h, { transport: "ssh", connection: { hostname: "invalid", username: "deploy", known_hosts: kh } }); f.save();
120
+ const f = fixture(t), h = linuxServer(f), kh = join(f.root, "known_hosts"); writeFileSync(kh, "fixture"); Object.assign(h, { transport: "ssh", connection: { hostname: "invalid", username: "deploy", known_hosts: kh } }); f.save();
115
121
  const r = await checkServer(f.state, "node-a", {}, { sshArgv: () => ["ssh"], spawn: () => ({ status: 255 }), call: () => { throw new Error("must not probe"); } });
116
122
  assert.equal(r.status, "blocked"); assert.equal(r.checks.find(c => c.id === "machine-identity").status, "unknown");
117
123
  });
@@ -210,7 +216,10 @@ test("fleet writes immutable generations, verifies readback and retains one cons
210
216
  const before = readFileSync(first.markdown, "utf8"); writeFileSync(first.html, "tampered"); assert.throws(() => generateFleet(f.state, { at }), /readback mismatch/); assert.equal(readFileSync(first.markdown, "utf8"), before);
211
217
  });
212
218
  test("fleet refuses symlink outputs and catalog locks without overwriting old entry", (t) => {
213
- const f = fixture(t), outside = join(f.root, "outside.txt"); writeFileSync(outside, "untouched"); symlinkSync(outside, join(f.state, "FLEET.md"));
219
+ const f = fixture(t), outsideDir = join(f.root, "outside"), outside = join(outsideDir, "sentinel.txt");
220
+ mkdirSync(outsideDir); writeFileSync(outside, "untouched");
221
+ if (process.platform === "win32") directoryLink(outsideDir, join(f.state, "FLEET.md"));
222
+ else symlinkSync(outside, join(f.state, "FLEET.md"));
214
223
  assert.throws(() => generateFleet(f.state), /symlink/); assert.equal(readFileSync(outside, "utf8"), "untouched");
215
224
  rmSync(join(f.state, "FLEET.md")); mkdirSync(join(f.state, ".locks/catalog"), { recursive: true }); assert.throws(() => generateFleet(f.state), /catalog lock/);
216
225
  });
@@ -1,7 +1,7 @@
1
1
  /** Node-less Linux SSH bootstrap: pinned Volta/Node via POSIX + scp, then enroll. */
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
- import { dirname, basename, isAbsolute, join } from "node:path";
4
+ import { dirname, basename, join, posix as posixPath } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { digest, exact, identifier, newId, noSymlinks, NodeMissing, now, OpsError, privateDir, rootPath, targetJoin, writeJson } from "./core.mjs";
7
7
  import { register, validateHost } from "./model.mjs";
@@ -142,8 +142,8 @@ export function bootstrapNode(args) {
142
142
  const installers = installerDir(hostRoot);
143
143
  const voltaHome = managedVoltaHome(hostRoot, args.account);
144
144
  posixCall(endpoint, "#!/bin/sh\nset -eu\nmkdir -p -- \"$1\" \"$2\"\n", { args: [installers, voltaHome] });
145
- const remoteVolta = join(installers, volta.filename);
146
- const remoteNode = join(installers, node.filename);
145
+ const remoteVolta = posixPath.join(installers, volta.filename);
146
+ const remoteNode = posixPath.join(installers, node.filename);
147
147
  posixSend(endpoint, volta.file, remoteVolta);
148
148
  posixSend(endpoint, node.file, remoteNode);
149
149
  const applied = posixCall(endpoint, SCRIPT, {
@@ -181,7 +181,7 @@ export function enroll(state, request) {
181
181
  if (posix.tools?.node === "missing" || posix.node_usable === false) throw new NodeMissing(posix);
182
182
  const discovery = host.identity === "discover" || host.identity == null;
183
183
  if (discovery) host.identity = "0".repeat(64);
184
- if (!isAbsolute(host.connection.node)) {
184
+ if (!posixPath.isAbsolute(host.connection.node) || host.connection.node.includes("\\")) {
185
185
  throw new OpsError("connection.node must be the absolute path returned by bootstrap-node");
186
186
  }
187
187
  validateHost(host);
@@ -1,7 +1,7 @@
1
1
  /** Strict host-key SSH and subprocess local transport. No passwords in argv. */
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { readFileSync } from "node:fs";
4
- import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { dirname, posix, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { canonical, digest, noSymlinks, OpsError, redact, UnknownResult } from "./core.mjs";
7
7
  import { fingerprint } from "./agent.mjs";
@@ -107,8 +107,8 @@ export function posixSend(endpoint, localFile, remotePath, { timeout = 180 } = {
107
107
  throw new OpsError("POSIX bootstrap is Linux-only; PowerShell targets still require an existing Node");
108
108
  }
109
109
  noSymlinks(localFile, { allowMissing: false });
110
- if (typeof remotePath !== "string" || !isAbsolute(remotePath) || remotePath.split("/").includes("..") || /[\s\n\r]/.test(remotePath)) {
111
- throw new OpsError("remote scp path must be absolute without spaces or ..");
110
+ if (typeof remotePath !== "string" || !posix.isAbsolute(remotePath) || remotePath.split("/").includes("..") || /[\s\\\0]/.test(remotePath)) {
111
+ throw new OpsError("remote scp path must be POSIX absolute without whitespace, backslashes, NUL or ..");
112
112
  }
113
113
  const argv = sshArgv(endpoint, { scp: true });
114
114
  argv.push("--", localFile, `${endpoint.username}@${endpoint.hostname}:${remotePath}`);
@@ -9,67 +9,25 @@ keywords: [毛泽东, 毛选, 矛盾分析, 战略, 组织, 咨询]
9
9
 
10
10
  # 毛泽东 · 认知操作系统
11
11
 
12
- 本入口把"分析问题—制定战略—组织行动"组织为渐进披露的咨询流程。产物写入当前 `person/changes/<change>/`。
12
+ 以特定方法论开展“分析问题—制定战略—组织行动”咨询,产物只写当前 person change。激活时读取 `<Path>{roots.workflows}/person/INDEX.md</Path>` 与 `<Path>{roots.workflows}/person/common/rules/activation-and-memory.md</Path>`;Person 没有 README,不加载其他 phase 或整套书籍。
13
13
 
14
- ## 读取范围
14
+ ## 按阶段推进
15
15
 
16
- 1. 先读取 `<Path>{roots.workflows}/person/INDEX.md</Path>` 与当前 Work 的状态入口;Person 使用整文件 INDEX,不加载不存在的 README。
17
- 2. 再读取 `<Path>{roots.workflows}/person/common/rules/activation-and-memory.md</Path>`,按当前分支、状态和关键词定位最小相关工件。
18
- 3. 只在本 Work 明确要求恢复、冲突、执行安全或归档证据时扩展为全量读取;缺少匹配证据或 owner/gateway 时停止受影响分支。
16
+ 下表有序且硬依赖:上一阶段产物完成、验证后才进入下一阶段。每个阶段只读取自己的入口,角色、声音、模型和引用纪律由该入口及其参考单点维护。
19
17
 
20
- 引用检索固定为:先读取当前 phase 的 framework index,再读取 `references/research/15-quote-bank.md` 定位篇目编号,最后只回读匹配的 `books/src/NNN-*.md`;不扫描整套书籍,无匹配证据时停止引用或结论。
18
+ | 阶段 | 进入时读取 | 产物与可判定完成门 |
19
+ |---|---|---|
20
+ | 1 激活与问诊 | `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/activate.md</Path>` | `<Path>{roots.state}/person/changes/{change}/problem-statement.md</Path>`:首次激活声明;开口三问中至少追问两题且用户回应;问题类型和主框架确定;六段填实 |
21
+ | 2 诊断分析 | `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/diagnose.md</Path>` | `<Path>{roots.state}/person/changes/{change}/analysis.md</Path>`:至少两个 Module A 模型;必过 A1,二手信息过 A3,多方利益过 A6;每个结论含条件/局限,主要矛盾有论证 |
22
+ | 3 战略制定 | `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/strategize.md</Path>` | `<Path>{roots.state}/person/changes/{change}/strategy.md</Path>`:沿定性→定向→站位→时间→投放→运用展开主框架;必要辅助框架;至少两阶段及明确关键战役 |
23
+ | 4 组织行动 | `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/mobilize.md</Path>` | `<Path>{roots.state}/person/changes/{change}/action-plan.md</Path>`:行动、责任人、期限、完成条件、失败处置与反馈机制明确 |
24
+ | 5 综合交付 | `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/deliver.md</Path>` | `<Path>{roots.state}/person/changes/{change}/consultation-output.md</Path>`:按交付模板整合四阶段,教员第一人称、五拍论证、收尾四动作、反模式检查与内在张力标注;全部原文引用有篇目出处 |
21
25
 
26
+ 每项产物回读后必须无残留 `[TODO:]`;缺少用户回应、阶段证据或原文出处时保留当前阶段,不用后续产物掩盖缺口。
22
27
 
23
- ## 流程
28
+ ## 引用检索
24
29
 
25
- ### 1. 激活与问诊
26
-
27
- 委托给 `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/activate.md</Path>`。输出首次激活声明,通过开口三问摸清用户处境,判定问题类型并匹配主框架。
28
-
29
- 产物:`<Path>{roots.state}/person/changes/{change}/problem-statement.md</Path>`
30
-
31
- **完成标准**:首次激活声明已输出,至少已追问 2 个开口问题且用户已回应,问题类型与主框架已匹配,problem-statement.md 六段均已填写无残留 `[TODO:]`。
32
-
33
- ### 2. 诊断分析
34
-
35
- 依赖步骤 1 的 problem-statement.md。委托给 `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/diagnose.md</Path>`。从 Module A 八模型中选用至少 2 个进行诊断——所有问题必过 A1(矛盾分析);信息来自二手必过 A3(调查研究);涉及多方利益必过 A6(结构分析)。
36
-
37
- 产物:`<Path>{roots.state}/person/changes/{change}/analysis.md</Path>`
38
-
39
- **完成标准**:已应用至少 2 个 Module A 模型,每个模型结论带条件与局限,主要矛盾已明确写出且带论证,analysis.md 无残留 `[TODO:]`。
40
-
41
- ### 3. 战略制定
42
-
43
- 依赖步骤 2 的 analysis.md。委托给 `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/strategize.md</Path>`。沿元框架流水线(定性→定向→站位→时间→投放→运用)展开主框架,必要时补充辅助框架。
44
-
45
- 产物:`<Path>{roots.state}/person/changes/{change}/strategy.md</Path>`
46
-
47
- **完成标准**:主框架已选定并充分展开,阶段划分清晰(至少分两步),有明确的关键战役判断,strategy.md 无残留 `[TODO:]`。
48
-
49
- ### 4. 组织行动
50
-
51
- 依赖步骤 3 的 strategy.md。委托给 `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/mobilize.md</Path>`。将战略转化为具体行动方案——谁来干、干什么、什么时候干完、干到什么程度算好、干砸了怎么办。
52
-
53
- 产物:`<Path>{roots.state}/person/changes/{change}/action-plan.md</Path>`
54
-
55
- **完成标准**:行动、责任与反馈机制已明确,action-plan.md 无残留 `[TODO:]`。
56
-
57
- ### 5. 综合交付
58
-
59
- 依赖前四步全部产物。委托给 `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/deliver.md</Path>`。按模板 `../_templates/mao-consultation-output-template.md` 整合为连贯的教员第一人称咨询输出——五拍论证节奏、收尾四动作、引用纪律、反模式检查和内在张力标注。
60
-
61
- 产物:`<Path>{roots.state}/person/changes/{change}/consultation-output.md</Path>`
62
-
63
- **完成标准**:已整合四阶段产物为连贯的教员第一人称咨询输出,五拍论证节奏可辨识,收尾四动作齐全,所有毛泽东原文引用均带篇目出处,反模式检查通过,consultation-output.md 无残留 `[TODO:]`。
64
-
65
- ## 依赖关系
66
-
67
- - 步骤 2「诊断分析」依赖步骤 1「激活与问诊」
68
- - 步骤 3「战略制定」依赖步骤 2「诊断分析」
69
- - 步骤 4「组织行动」依赖步骤 3「战略制定」
70
- - 步骤 5「综合交付」依赖步骤 4「组织行动」
71
-
72
- 所有步骤为硬依赖——前一产物必须完成并验证后才能进入下一步。
30
+ 需要引用时,先读当前 phase 的 framework index,再在 `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/references/research/15-quote-bank.md</Path>` 定位篇目编号,最后仅回读匹配的原文。目录映射查询 `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/books/README.md</Path>`;无匹配证据则停止该引用或依赖结论,不扫描整套书籍。
73
31
 
74
32
  ## 状态追踪
75
33
 
@@ -82,16 +40,3 @@ keywords: [毛泽东, 毛选, 矛盾分析, 战略, 组织, 咨询]
82
40
  - **`methods_applied`**(数组)—— 组织阶段已应用的 Module C 方法列表
83
41
  - **`quotes_cited`**(数组)—— 已引用的毛泽东原话及出处
84
42
  - **`consultation_status`**(字符串)—— 当前阶段:`activating` → `diagnosing` → `strategizing` → `mobilizing` → `delivering` → `completed`
85
-
86
- ## 渐进披露
87
-
88
- 角色、声音、模型和引用规则继续由各 phase 文件及其相对引用拥有;未进入 phase 时不加载对应材料。
89
-
90
- | 文件 | 触发条件 |
91
- |------|----------|
92
- | `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/activate.md</Path>` | 步骤 1「激活与问诊」进入时 |
93
- | `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/diagnose.md</Path>` | 步骤 2「诊断分析」进入时 |
94
- | `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/strategize.md</Path>` | 步骤 3「战略制定」进入时 |
95
- | `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/mobilize.md</Path>` | 步骤 4「组织行动」进入时 |
96
- | `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/deliver.md</Path>` | 步骤 5「综合交付」进入时 |
97
- | `<Path>{roots.workflows}/person/M-mao-zedong-cognitive-os/books/README.md</Path>` | 需要查原文时——引语库 `references/research/15-quote-bank.md` 映射篇目编号到 books 目录 |
@@ -9,39 +9,13 @@ keywords: [双向钢人, steelman, 决策, 反谄媚, 关键变量, 明确判断
9
9
 
10
10
  # 双向钢人论证
11
11
 
12
- 本 work 的主导词是**钢人化**。它不负责替用户把既有立场说得更顺耳,也不以“正反都说一点”制造表面平衡;它负责在结论形成前,把用户真正要解决的问题、支持当前倾向的最强可信论证、反对当前倾向的最强可信论证和决定结论的变量同时推进到可检验状态,再只关闭一个最高价值的不确定性,最后承担判断责任。
12
+ 重述用户真正的问题,分别形成支持与反对当前倾向的最强可信论证,找出分歧和关键变量,只关闭一个最高价值的不确定性,再给出明确判断与可验证行动。不以虚假平衡、迎合立场或通用建议替代判断。
13
13
 
14
- 核心动作保持为:
14
+ 激活时读取 `<Path>{roots.workflows}/person/INDEX.md</Path>` 与 `<Path>{roots.workflows}/person/common/rules/activation-and-memory.md</Path>`;Person 不使用不存在的 README。
15
15
 
16
- 1. 用最完整、最有力的方式,重述用户真正想解决的问题;
17
- 2. 使用钢人论证法,分别给出支持当前想法与反对它的最强论证;
18
- 3. 找出双方真正的分歧,以及最可能改变结论的关键变量;
19
- 4. 只问用户一个最关键的问题;等用户回答后,再给出明确判断、理由和下一步行动。
16
+ ## 输入与权威
20
17
 
21
- ## 读取范围
22
-
23
- 1. 先读取 `<Path>{roots.workflows}/person/INDEX.md</Path>` 与当前 Work 的状态入口;Person 使用整文件 INDEX,不加载不存在的 README。
24
- 2. 再读取 `<Path>{roots.workflows}/person/common/rules/activation-and-memory.md</Path>`,按当前分支、状态和关键词定位最小相关工件。
25
- 3. 只在本 Work 明确要求恢复、冲突、执行安全或归档证据时扩展为全量读取;缺少匹配证据或 owner/gateway 时停止受影响分支。
26
-
27
-
28
- ## 输入、权威与边界
29
-
30
- ### 必需输入
31
-
32
- - 一个需要选择、判断、取舍、排序或制定策略的具体问题。
33
-
34
- 缺失可识别问题,或输入只是无需权衡的事实查询时,返回 `blocked-input`,说明缺少什么,不制造虚假争论。
35
-
36
- ### 可选输入
37
-
38
- - 用户当前倾向、候选方案与反对意见;
39
- - 时间窗口、预算、资源、风险和不可逆约束;
40
- - 已知事实、项目材料、证据或外部来源;
41
- - 用户已经明确的目标、价值排序和成功标准;
42
- - 调用方显式提供的 `{change}`。
43
-
44
- 不存在的可选输入静默跳过;已经明确的信息不得重复询问。
18
+ 必需一个需要选择、判断、排序或策略的具体问题;缺失或只是无需权衡的事实查询时返回 `blocked-input`,不制造争论。按已知情况采用用户倾向、候选/反对意见、时间/预算/资源、不可逆约束、事实来源、价值目标和调用方 change;缺失的可选内容静默跳过,已明确信息不重复问。
45
19
 
46
20
  ### 权威顺序
47
21
 
@@ -64,7 +38,9 @@ keywords: [双向钢人, steelman, 决策, 反谄媚, 关键变量, 明确判断
64
38
 
65
39
  它不修改其他 person work 的产物或 active 条目,不把运行时状态写回 `{roots.workflows}/person/_state/`,也不提交、推送、发布、部署或执行不可逆操作。
66
40
 
67
- ## 流程
41
+ ## 按持久化阶段执行
42
+
43
+ 新建、恢复或状态写入前读取 `<Path>{roots.workflows}/person/S-steelman-deliberation/references/state-and-publication.md</Path>`,遵守 v1 字段、候选校验、原子替换顺序和未知字段保留;不得覆盖其他 Work。
68
44
 
69
45
  ### 1. 解析 roots,选择或恢复 change
70
46
 
@@ -94,23 +70,9 @@ keywords: [双向钢人, steelman, 决策, 反谄媚, 关键变量, 明确判断
94
70
 
95
71
  ### 3. 回答前的双向钢人化
96
72
 
97
- 仅在没有有效 dossier 时读取 `<Path>{roots.workflows}/person/S-steelman-deliberation/deliberate.md</Path>`,按其顺序完成真实问题重述、证据分层、双向钢人、分歧与关键变量识别,并形成唯一关键问题。
98
-
99
- 若问题依赖会变化的公开事实、专业事实、冲突材料、代码/测试/配置,或属于医疗、法律、财务、安全等高风险领域,再读取 `<Path>{roots.workflows}/person/S-steelman-deliberation/evidence-gate.md</Path>`;可发现的事实先探索,不拿来问用户。
100
-
101
- 使用 `<Path>{roots.workflows}/person/S-steelman-deliberation/_templates/steelman-dossier-template.md</Path>` 在 change 目录下的唯一临时子目录中写入候选 dossier 与候选 `.status.json`,重读后运行:
73
+ 仅在无有效 dossier 时读取 `<Path>{roots.workflows}/person/S-steelman-deliberation/deliberate.md</Path>`,完整重述问题、分层证据、正反钢人、分歧与关键变量,形成唯一关键问题。变化事实、专业事实、冲突材料、代码或高风险领域还须读 `<Path>{roots.workflows}/person/S-steelman-deliberation/evidence-gate.md</Path>`;可发现的事实先探索,不拿来问用户。
102
74
 
103
- ```bash
104
- node <Path>{roots.workflows}/person/S-steelman-deliberation/tools/validate-steelman-change.mjs</Path> \
105
- --phase awaiting-answer \
106
- --change-dir <Path>{roots.state}/person/changes/{change}</Path> \
107
- --dossier-file <Path>{roots.state}/person/changes/{change}/.steelman-stage/steelman-dossier.md</Path> \
108
- --status-file <Path>{roots.state}/person/changes/{change}/.steelman-stage/.status.json</Path>
109
- ```
110
-
111
- 临时目录名在并发时追加唯一后缀。校验通过后,先原子替换正式 dossier,再原子替换 change `.status.json`;workflow active 只在步骤 4 确认需要等待用户时更新。校验失败时删除临时目录并保持正式工件和旧状态不变。
112
-
113
- **完成标准**:dossier 是回答前的冻结快照;正反最强可信论证、真正分歧、关键变量和唯一问题均存在;没有最终判断或行动建议;校验退出码为 0。
75
+ 按状态与发布合同的“回答前”分支创建、校验并发布 dossier;其中没有最终判断或行动建议,退出码必须为 0。该 dossier 随后冻结。
114
76
 
115
77
  ### 4. 只关闭一个最高价值的不确定性
116
78
 
@@ -132,73 +94,9 @@ node <Path>{roots.workflows}/person/S-steelman-deliberation/tools/validate-steel
132
94
 
133
95
  ### 5. 明确裁决并形成行动
134
96
 
135
- 关键问题得到有效回答后,将回答原文写入 change `.status.json`,设置 `phase: judging`,再读取 `<Path>{roots.workflows}/person/S-steelman-deliberation/judge.md</Path>`。必须基于冻结 dossier 和用户回答裁决,不重做一份迎合回答的新钢人分析。
136
-
137
- 使用 `<Path>{roots.workflows}/person/S-steelman-deliberation/_templates/decision-template.md</Path>` 在唯一临时目录中写入候选 decision 与候选完成态 `.status.json`,随后运行:
97
+ 得到有效回答后原样保存,设置 `phase: judging`,再读取 `<Path>{roots.workflows}/person/S-steelman-deliberation/judge.md</Path>`。以冻结 dossier 和真实回答裁决,不重做迎合回答的新分析。
138
98
 
139
- ```bash
140
- node <Path>{roots.workflows}/person/S-steelman-deliberation/tools/validate-steelman-change.mjs</Path> \
141
- --phase completed \
142
- --change-dir <Path>{roots.state}/person/changes/{change}</Path> \
143
- --decision-file <Path>{roots.state}/person/changes/{change}/.steelman-stage/decision.md</Path> \
144
- --status-file <Path>{roots.state}/person/changes/{change}/.steelman-stage/.status.json</Path>
145
- ```
146
-
147
- 校验通过后才原子替换正式 decision 和 change 状态,并只从 workflow `status.json.active` 删除匹配当前 `change` 且属于本 work 的条目。校验失败时删除临时目录、返回 `validation-failed`,保留可恢复的 `judging` 状态。
148
-
149
- **完成标准**:decision 给出明确判断、决定性理由、对最强反方的回应、具体行动、反转条件及置信度;状态与工件一致;校验退出码为 0。
150
-
151
- ## 状态合同
152
-
153
- ### Workflow 状态
154
-
155
- `<Path>{roots.state}/person/status.json</Path>` 保持 `schema_version: 1`,本 work 只管理 `active` 数组中的如下条目:
156
-
157
- ```json
158
- {
159
- "change": "2026-08-18-steelman-company-anniversary",
160
- "work_id": "person/steelman-deliberation",
161
- "phase": "awaiting-answer",
162
- "updated_at": "2026-08-18T00:00:00.000Z"
163
- }
164
- ```
165
-
166
- 读取—合并—写入时保留未知顶层字段、未知 entry 字段和其他 work 的全部条目。写临时文件、重读 JSON、再原子替换。
167
-
168
- ### Change 状态
169
-
170
- `<Path>{roots.state}/person/changes/{change}/.status.json</Path>` 至少包含:
171
-
172
- ```json
173
- {
174
- "schema_version": 1,
175
- "artifact": "person-work-status",
176
- "workflow": "person",
177
- "change": "2026-08-18-steelman-company-anniversary",
178
- "work_id": "person/steelman-deliberation",
179
- "status": "active",
180
- "phase": "awaiting-answer",
181
- "key_question": "[唯一关键问题]",
182
- "key_question_asked": true,
183
- "question_disposition": "asked",
184
- "answer_received": false,
185
- "user_answer": null,
186
- "created_at": "2026-08-18T00:00:00.000Z",
187
- "updated_at": "2026-08-18T00:00:00.000Z",
188
- "completed_at": null,
189
- "blockers": []
190
- }
191
- ```
192
-
193
- 允许转换:
194
-
195
- ```text
196
- intake → deliberating → awaiting-answer → judging → completed
197
- 任一活动阶段 → blocked
198
- awaiting-answer → reframe-required
199
- ```
200
-
201
- `question_disposition` 只能是 `asked | already-answered`;前者要求 `key_question_asked: true`,后者要求 `key_question_asked: false`。工件先验证,change 状态后更新,workflow active 最后更新。状态只是工件的索引,不替代工件本身。
99
+ 按状态与发布合同的“回答后”分支校验候选 decision 后才发布并关闭本 Work 的 active 条目;失败保留可恢复的 judging,不报完成。decision 必须包含明确判断、决定性理由、最强反方回应、具体行动、反转条件与置信度,且能追溯到关键变量和回答。
202
100
 
203
101
  ## 返回合同
204
102
 
@@ -213,13 +111,3 @@ awaiting-answer → reframe-required
213
111
  - 下一路由:等待回答时返回 `<Path>{roots.workflows}/person/S-steelman-deliberation/S-steelman-deliberation.md</Path>` 并携带同一 change;问题已重构时创建新 change;否则明确完成。
214
112
 
215
113
  只有 `completed` 才能声明已经形成最终判断。`awaiting-user-answer` 的下一路由始终是带着同一 change 回到本入口。
216
-
217
- ## 全局完成标准
218
-
219
- - 用户真正要解决的问题与表层问法已区分;
220
- - 当前倾向和最强反方都经过可信、可识别的钢人化;
221
- - 没有编造证据、虚假平衡或因用户立场降低证据标准;
222
- - 只关闭了一个最高价值的不确定性,且没有重复追问已知信息;
223
- - 回答前 dossier 保持冻结,回答后 decision 可追溯到关键变量和用户回答;
224
- - 最终答案不是任何人都能套用的通用建议,而是明确判断与可验证行动;
225
- - owned 工件、change 状态与 workflow active 一致,验证证据已返回。