@namewta/speculo 1.0.19 → 1.0.21

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 (194) 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 +7 -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 +156 -28
  32. package/template/canonical/canonical-specdev-grill-with-docs.md +153 -31
  33. package/template/canonical/canonical-specdev-spec.md +153 -27
  34. package/template/canonical/canonical-specdev-tickets.md +153 -27
  35. package/template/canonical/canonical-specdev-wayfinder.md +142 -27
  36. package/template/commands/retro.md +11 -9
  37. package/template/skills/archive-and-consolidate/SKILL.md +163 -7
  38. package/template/skills/archive-and-consolidate/references/cleanup-rules.md +1 -1
  39. package/template/skills/docs-sync/references/agents/claude-redirect.md +8 -8
  40. package/template/skills/docs-sync/references/agents/content-contract.md +1 -1
  41. package/template/skills/docs-sync/references/agents/manifest-discovery.md +7 -7
  42. package/template/skills/docs-sync/references/agents/role-classification.md +4 -4
  43. package/template/skills/docs-sync/references/agents-contract.md +1 -1
  44. package/template/skills/engineering-standards-builder/SKILL.md +149 -9
  45. package/template/skills/engineering-standards-builder/agents/openai.yaml +2 -0
  46. package/template/skills/engineering-standards-builder/manifest.txt +2 -1
  47. package/template/skills/engineering-standards-builder/references/rules/09-mechanical-guardrails.md +9 -0
  48. package/template/skills/engineering-standards-builder/scripts/validate-builder.mjs +4 -7
  49. package/template/skills/git-history-squash/SKILL.md +87 -11
  50. package/template/skills/git-history-squash/agents/openai.yaml +2 -0
  51. package/template/skills/optimize-codex-config/SKILL.md +71 -11
  52. package/template/skills/{speculo-retro/references/entry-procedure.md → retrospective/SKILL.md} +12 -8
  53. package/template/skills/{speculo-retro → retrospective}/references/friction-taxonomy.md +22 -13
  54. package/template/skills/{speculo-retro → retrospective}/references/issue-drafting-sop.md +10 -20
  55. package/template/skills/source-code-zip/SKILL.md +44 -8
  56. package/template/skills/source-code-zip/references/{entry-procedure.md → cli-reference.md} +0 -43
  57. package/template/skills/upstream-fork-sync/SKILL.md +71 -8
  58. package/template/skills/{writing-great-skills → writing-for-agents}/GLOSSARY.md +1 -1
  59. package/template/skills/writing-for-agents/SKILL.md +22 -0
  60. package/template/skills/writing-for-agents/agents/openai.yaml +2 -0
  61. package/template/skills/{writing-great-skills/references/entry-procedure.md → writing-for-agents/references/authoring-principles.md} +2 -9
  62. package/template/workflows/ops/D-project-deploy/D-project-deploy.md +1 -1
  63. package/template/workflows/ops/H-host-manage/H-host-manage.md +4 -0
  64. package/template/workflows/ops/H-host-manage/references/host-profiles.md +4 -0
  65. package/template/workflows/ops/I-initialize/I-initialize.md +1 -1
  66. package/template/workflows/ops/INDEX.md +2 -0
  67. package/template/workflows/ops/README.md +7 -3
  68. package/template/workflows/ops/S-server-connect/S-server-connect.md +5 -5
  69. package/template/workflows/ops/V-inventory-view/V-inventory-view.md +2 -0
  70. package/template/workflows/ops/common/CAPABILITIES.md +2 -0
  71. package/template/workflows/ops/common/USAGE.md +5 -5
  72. package/template/workflows/ops/common/WORKSPACE-USAGE.md +29 -2
  73. package/template/workflows/ops/common/examples/README.md +1 -1
  74. package/template/workflows/ops/common/examples/register.example.json +7 -3
  75. package/template/workflows/ops/common/examples/server-connection.example.json +11 -0
  76. package/template/workflows/ops/common/rules/deployment-layout.md +1 -1
  77. package/template/workflows/ops/common/rules/host-root-and-onboarding.md +47 -0
  78. package/template/workflows/ops/common/rules/persistence-and-secrets.md +1 -1
  79. package/template/workflows/ops/common/rules/research-and-decisions.md +8 -0
  80. package/template/workflows/ops/common/rules/server-onboarding.md +3 -3
  81. package/template/workflows/ops/common/rules/workspace-and-authorization.md +2 -2
  82. package/template/workflows/ops/common/schemas/fleet-view.schema.json +64 -0
  83. package/template/workflows/ops/common/schemas/host.schema.json +19 -0
  84. package/template/workflows/ops/common/schemas/plan.schema.json +57 -0
  85. package/template/workflows/ops/common/schemas/root-confirmation.schema.json +118 -0
  86. package/template/workflows/ops/common/schemas/server-connection.schema.json +67 -0
  87. package/template/workflows/ops/common/schemas/server-discovery.schema.json +171 -0
  88. package/template/workflows/ops/common/schemas/spec.schema.json +19 -0
  89. package/template/workflows/ops/common/schemas/status.schema.json +19 -0
  90. package/template/workflows/ops/common/templates/FLEET.html +1 -1
  91. package/template/workflows/ops/common/templates/HOST-README.md +3 -1
  92. package/template/workflows/ops/common/tests/test_ops.mjs +8 -11
  93. package/template/workflows/ops/common/tests/test_ops_bootstrap.mjs +101 -16
  94. package/template/workflows/ops/common/tests/test_ops_onboarding.mjs +225 -0
  95. package/template/workflows/ops/common/tests/test_ops_workspace.mjs +17 -8
  96. package/template/workflows/ops/common/tools/demo-local.mjs +4 -0
  97. package/template/workflows/ops/common/tools/opslib/agent.mjs +26 -0
  98. package/template/workflows/ops/common/tools/opslib/bootstrap.mjs +52 -26
  99. package/template/workflows/ops/common/tools/opslib/cli.mjs +1 -0
  100. package/template/workflows/ops/common/tools/opslib/core.mjs +1 -1
  101. package/template/workflows/ops/common/tools/opslib/docs.mjs +1 -0
  102. package/template/workflows/ops/common/tools/opslib/execution.mjs +4 -0
  103. package/template/workflows/ops/common/tools/opslib/fleet.mjs +26 -4
  104. package/template/workflows/ops/common/tools/opslib/host_recipes.mjs +34 -0
  105. package/template/workflows/ops/common/tools/opslib/model.mjs +6 -0
  106. package/template/workflows/ops/common/tools/opslib/onboarding.mjs +215 -0
  107. package/template/workflows/ops/common/tools/opslib/planner.mjs +3 -0
  108. package/template/workflows/ops/common/tools/opslib/transport.mjs +10 -6
  109. package/template/workflows/ops/common/tools/opslib/workspace_cli.mjs +20 -1
  110. package/template/workflows/ops/common/tools/server-discover.sh +65 -0
  111. package/template/workflows/ops/common/tools/validate-ops.mjs +1 -1
  112. package/template/workflows/ops/manifest.json +12 -5
  113. package/template/workflows/specdev/A-archive-and-consolidate/consolidation-interview.md +3 -3
  114. package/template/workflows/specdev/C-code-review/C-code-review.md +1 -1
  115. package/template/workflows/specdev/D-diagnose-bugs/D-diagnose-bugs.md +6 -0
  116. package/template/workflows/specdev/G-grill-with-docs/references/interview-procedure.md +6 -6
  117. package/template/workflows/specdev/I-implement/references/implementation-procedure.md +1 -1
  118. package/template/workflows/specdev/I-implement/tdd-rules.md +4 -0
  119. package/template/workflows/specdev/I-init-setup/config-template.json +5 -1
  120. package/template/workflows/specdev/INDEX.md +2 -2
  121. package/template/workflows/specdev/P-goal-plan/planning-modes.md +1 -1
  122. package/template/workflows/specdev/P-prototype/P-prototype.md +7 -3
  123. package/template/workflows/specdev/P-prototype/logic-protocol.md +11 -0
  124. package/template/workflows/specdev/P-prototype/logic-template.md +27 -0
  125. package/template/workflows/specdev/P-prototype/project-preview-protocol.md +9 -0
  126. package/template/workflows/specdev/R-retro/R-retro.md +26 -0
  127. package/template/workflows/specdev/R-retro/retro-template.md +27 -0
  128. package/template/workflows/specdev/R-review-architecture/references/selected-design.md +1 -1
  129. package/template/workflows/specdev/README.md +13 -2
  130. package/template/workflows/specdev/S-spec/S-spec.md +4 -0
  131. package/template/workflows/specdev/T-tickets/T-tickets.md +4 -0
  132. package/template/workflows/specdev/T-triage/T-triage.md +31 -25
  133. package/template/workflows/specdev/T-triage/capture-protocol.md +1 -1
  134. package/template/workflows/specdev/T-triage/ci-security-protocol.md +11 -0
  135. package/template/workflows/specdev/T-triage/intake-protocol.md +10 -4
  136. package/template/workflows/specdev/T-triage/pr-delivery-protocol.md +13 -0
  137. package/template/workflows/specdev/T-triage/pr-record-template.md +39 -0
  138. package/template/workflows/specdev/T-triage/publish-protocol.md +1 -1
  139. package/template/workflows/specdev/T-triage/queue-protocol.md +7 -0
  140. package/template/workflows/specdev/T-triage/reconcile-protocol.md +1 -1
  141. package/template/workflows/specdev/T-triage/references/issue-pr-policy.md +23 -0
  142. package/template/workflows/specdev/T-triage/references/release-authentication.md +9 -0
  143. package/template/workflows/specdev/T-triage/references/release-notes-injection.md +21 -0
  144. package/template/workflows/specdev/T-triage/references/workflow-yaml-reference.md +37 -0
  145. package/template/workflows/specdev/T-triage/release-preflight.md +13 -0
  146. package/template/workflows/specdev/T-triage/release-protocol.md +12 -0
  147. package/template/workflows/specdev/T-triage/release-recovery.md +18 -0
  148. package/template/workflows/specdev/T-triage/remote-operations.md +25 -0
  149. package/template/workflows/specdev/T-triage/scripts/github-transport.mjs +208 -0
  150. package/template/workflows/specdev/T-triage/source-template.md +7 -0
  151. package/template/workflows/specdev/T-triage/triage-run-template.md +34 -0
  152. package/template/workflows/specdev/T-triage/triage-template.md +4 -1
  153. package/template/workflows/specdev/common/rules/artifact-contract.md +7 -0
  154. package/template/workflows/specdev/common/rules/change-completion.md +6 -0
  155. package/template/workflows/specdev/common/rules/workflow-routing.md +6 -0
  156. package/template/workflows/specdev/common/rules/workflow-state-and-lifecycle.md +1 -1
  157. package/template/workflows/specdev/common/schemas/config.schema.json +137 -26
  158. package/template/workflows/specdev/common/schemas/logic-prototype.schema.json +62 -0
  159. package/template/workflows/specdev/common/schemas/pull-request.schema.json +90 -0
  160. package/template/workflows/specdev/common/schemas/retrospective.schema.json +58 -0
  161. package/template/workflows/specdev/common/schemas/triage-run.schema.json +149 -0
  162. package/template/workflows/specdev/common/schemas/triage.schema.json +117 -15
  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 -227
  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/upstream-fork-sync/references/entry-procedure.md +0 -78
  189. package/template/skills/writing-great-skills/SKILL.md +0 -23
  190. /package/template/skills/{writing-great-skills → writing-for-agents}/references/document-contract.md +0 -0
  191. /package/template/workflows/specdev/{G-grill-with-docs → common/rules}/adr-format.md +0 -0
  192. /package/template/workflows/specdev/{G-grill-with-docs → common/rules}/context-format.md +0 -0
  193. /package/template/workflows/specdev/{I-implement → common/rules}/design-it-twice.md +0 -0
  194. /package/template/workflows/specdev/{G-grill-with-docs/domain-modeling-rules.md → common/rules/domain-modeling.md} +0 -0
@@ -0,0 +1,215 @@
1
+ /** S-owned account discovery and explicit root confirmation. No target writes. */
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join, posix, win32 } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { digest, identifier, newId, now, noSymlinks, OpsError, rootPath, withLock, writeJson } from "./core.mjs";
6
+ import { load, validate } from "./model.mjs";
7
+ import { call, posixCall, validateSshEndpoint } from "./transport.mjs";
8
+ import { readBoundedJson, statePath } from "./workspace.mjs";
9
+
10
+ const SCRIPT = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../server-discover.sh"), "utf8").replace(/\r\n?/g, "\n");
11
+ export const onboardingHooks = { observe: observeServer };
12
+ const pathApi = (platform) => platform === "windows" ? win32 : posix;
13
+ const clean = (s) => typeof s === "string" && s.trim().length > 0 && s.length <= 4096 && !/[\x00-\x1f\x7f]/.test(s);
14
+
15
+ export function endpointDigest(host) {
16
+ const c = { ...(host.connection || {}) };
17
+ // Runtime selection may change after a pinned Node bootstrap, not SSH identity/trust.
18
+ delete c.node;
19
+ if (host.transport === "ssh") {
20
+ validateSshEndpoint(c);
21
+ noSymlinks(c.known_hosts, { allowMissing: false });
22
+ }
23
+ return digest({ platform: host.platform, transport: host.transport, connection: c,
24
+ known_hosts_digest: host.transport === "ssh" ? digest(readFileSync(c.known_hosts)) : null });
25
+ }
26
+
27
+ export function parseLinuxDiscovery(stdout, withRoot = false) {
28
+ const values = {};
29
+ const keys = ["machine", "system", "username", "uid", "home", "node", "node_version",
30
+ ...(withRoot ? ["root_exists", "root_writable", "root_nonempty", "root_owner"] : [])];
31
+ for (const line of String(stdout).trimEnd().split(/\r?\n/)) {
32
+ const match = /^([a-z_]+)=((?:[a-f0-9]{2})*)$/.exec(line);
33
+ if (!match || !keys.includes(match[1]) || Object.hasOwn(values, match[1])) throw new OpsError("invalid Linux discovery response");
34
+ values[match[1]] = Buffer.from(match[2], "hex").toString("utf8");
35
+ }
36
+ if (Object.keys(values).length !== keys.length || values.system !== "Linux" || !clean(values.machine)) throw new OpsError("incomplete Linux identity discovery");
37
+ const facts = { identity: digest({ machine: values.machine.trim(), platform: "Linux" }), platform: "linux",
38
+ username: values.username, uid: values.uid, home: values.home, node: values.node || null, node_version: values.node_version || null };
39
+ if (withRoot) {
40
+ for (const key of ["root_exists", "root_writable", "root_nonempty"]) if (!["true", "false"].includes(values[key])) throw new OpsError("invalid root observation");
41
+ let owner = null;
42
+ if (values.root_owner) { try { owner = JSON.parse(values.root_owner); } catch { throw new OpsError("invalid root owner marker"); } }
43
+ facts.root_check = { exists: values.root_exists === "true", writable: values.root_writable === "true", nonempty: values.root_nonempty === "true", owner };
44
+ }
45
+ return facts;
46
+ }
47
+
48
+ export function observeServer(host, root = null) {
49
+ if (host.transport === "ssh" && host.platform === "linux") {
50
+ const result = posixCall(host.connection, SCRIPT, { sudo: false, args: root ? [root] : [], timeout: 30 });
51
+ return parseLinuxDiscovery(result.stdout, Boolean(root));
52
+ }
53
+ if (host.transport === "ssh" && !host.connection?.node) throw new OpsError("non-Linux SSH discovery requires an existing Node executable");
54
+ return call({ ...host, connection: host.transport === "ssh" ? { ...host.connection, sudo: false } : {} },
55
+ { action: "onboarding", identity: null, root }, { timeout: 30 });
56
+ }
57
+
58
+ function validateFacts(host, facts) {
59
+ if (!facts || facts.platform !== host.platform || !/^[a-f0-9]{64}$/.test(facts.identity || "")
60
+ || !clean(facts.username) || !clean(facts.home) || !pathApi(host.platform).isAbsolute(facts.home)) throw new OpsError("reliable login account/home/identity unavailable");
61
+ if (host.platform !== "windows" && !/^\d+$/.test(facts.uid)) throw new OpsError("reliable login uid unavailable");
62
+ if (host.transport === "ssh") {
63
+ const accountMatches = host.platform === "windows" ? facts.username.toLowerCase() === host.connection.username.toLowerCase() : facts.username === host.connection.username;
64
+ if (!accountMatches) throw new OpsError("observed login account differs from SSH username");
65
+ }
66
+ return facts;
67
+ }
68
+
69
+ export function assertUniqueMachine(registry, host) {
70
+ const peers = Object.values(registry.hosts).filter((h) => h.identity === host.identity);
71
+ if (peers.length > 1) throw new OpsError("ambiguous legacy machine roots; preserve evidence and resolve the affected Host explicitly");
72
+ if (peers.length && peers[0].host_id !== host.host_id) throw new OpsError("machine already registered; reuse Host " + peers[0].host_id + " and its root " + peers[0].root);
73
+ return peers[0] || null;
74
+ }
75
+
76
+ function folder(state, id) { identifier(id, "server"); return join(state, "records", "servers", id, "onboarding"); }
77
+ function artifactPath(state, id, rid) { identifier(rid, "receipt"); return join(folder(state, id), rid + ".json"); }
78
+ function persist(state, id, rid, value, schema) {
79
+ validate(value, schema);
80
+ const path = artifactPath(state, id, rid);
81
+ writeJson(path, value, { exclusive: true });
82
+ if (digest(readBoundedJson(path)) !== digest(value)) throw new OpsError("onboarding receipt readback mismatch");
83
+ return { receipt_id: rid, digest: digest(value) };
84
+ }
85
+
86
+ function assertSelectedPath(host, facts, root) {
87
+ const normalized = rootPath(root, host.platform);
88
+ if (!clean(root) || normalized !== root || (host.platform !== "windows" && root.includes("\\"))) throw new OpsError("selected root must be a normalized absolute path without control characters");
89
+ const p = pathApi(host.platform);
90
+ const home = p.normalize(facts.home);
91
+ if (host.platform === "windows" ? home.toLowerCase() === root.toLowerCase() : home === root) throw new OpsError("select a dedicated child, not the login home itself");
92
+ const system = host.platform === "windows" ? ["C:\\Windows", "C:\\Program Files", "C:\\Program Files (x86)"] : ["/etc", "/usr", "/bin", "/sbin", "/dev", "/proc", "/sys", "/boot"];
93
+ for (const base of system) {
94
+ const rel = p.relative(base, root);
95
+ if (rel === "" || (!rel.startsWith("..") && !p.isAbsolute(rel))) throw new OpsError("system directory cannot contain a managed root");
96
+ }
97
+ }
98
+
99
+ function assertRootObservation(host, facts, { legacy = false } = {}) {
100
+ const c = facts.root_check;
101
+ if (!c || typeof c.exists !== "boolean" || typeof c.nonempty !== "boolean" || c.writable !== true) throw new OpsError("selected root or nearest parent is not writable/searchable by the login account; provision permissions explicitly");
102
+ if (c.owner && (c.owner.host_id !== host.host_id || c.owner.identity !== facts.identity)) throw new OpsError("root ownership conflict");
103
+ if (!legacy && c.nonempty && !c.owner) throw new OpsError("nonempty unowned root; choose a dedicated empty directory or use a reviewed adoption plan");
104
+ }
105
+
106
+ export function discoverServer(state, serverId, endpoint) {
107
+ statePath(state); identifier(serverId);
108
+ const host = { host_id: serverId, platform: endpoint.platform, transport: endpoint.transport, connection: endpoint.connection || {} };
109
+ validate({ platform: host.platform, transport: host.transport, connection: host.connection }, "server-connection");
110
+ const registry = load(state);
111
+ if (!registry.controller) throw new OpsError("initialize the controller before connecting a Host");
112
+ if (!["linux", "windows", "darwin"].includes(host.platform) || !["local", "ssh"].includes(host.transport)) throw new OpsError("explicit platform and transport required");
113
+ if (host.transport === "local" && Object.keys(host.connection).length) throw new OpsError("local connection must be empty");
114
+ const before = endpointDigest(host);
115
+ const facts = validateFacts(host, onboardingHooks.observe(host));
116
+ const previous = assertUniqueMachine(registry, { ...host, identity: facts.identity });
117
+ if (registry.hosts[serverId] && !previous) throw new OpsError("registered machine identity drift");
118
+ if (before !== endpointDigest(host)) throw new OpsError("SSH trust changed during discovery");
119
+ const id = newId("discovery");
120
+ const record = { schema_version: 1, artifact: "ops-server-discovery", discovery_id: id, host_id: serverId,
121
+ discovered_at: now(), endpoint: host, endpoint_digest: before, facts,
122
+ suggested_root: previous?.root ?? pathApi(host.platform).join(facts.home, "ops"), registered: Boolean(previous) };
123
+ const ref = persist(state, serverId, id, record, "server-discovery");
124
+ return { status: previous ? "discovered-existing-host" : "discovered-not-confirmed", discovery: ref, ...record,
125
+ next: previous ? "Reuse the registered root; connection changes require S verification and an exact approved plan." : "Ask the user to specify or accept the suggested root, then server-root-confirm." };
126
+ }
127
+
128
+ export function confirmServerRoot(state, serverId, discoveryId, root, by, statement) {
129
+ statePath(state);
130
+ if (!clean(by) || !clean(statement)) throw new OpsError("actual user confirmation by/statement required");
131
+ const discovery = readBoundedJson(artifactPath(state, serverId, discoveryId));
132
+ validate(discovery, "server-discovery");
133
+ if (discovery.host_id !== serverId || discovery.discovery_id !== discoveryId) throw new OpsError("discovery identity mismatch");
134
+ const host = discovery.endpoint;
135
+ assertSelectedPath(host, discovery.facts, root);
136
+ if (endpointDigest(host) !== discovery.endpoint_digest) throw new OpsError("SSH endpoint/trust changed; rediscover");
137
+ const facts = validateFacts(host, onboardingHooks.observe(host, root));
138
+ for (const k of ["identity", "username", "uid", "home"]) if (facts[k] !== discovery.facts[k]) throw new OpsError("login account/home/identity drift; rediscover");
139
+ return withLock(join(state, ".locks", "catalog"), { operation: "server-root-confirm" }, () => {
140
+ const registry = load(state);
141
+ const previous = assertUniqueMachine(registry, { ...host, identity: facts.identity });
142
+ if (registry.hosts[serverId] && !previous) throw new OpsError("registered machine identity drift");
143
+ if (previous && previous.root !== root) throw new OpsError("registered root is immutable; explicit migration required");
144
+ assertRootObservation(host, facts, { legacy: Boolean(previous && !previous.root_confirmation) });
145
+ if (endpointDigest(host) !== discovery.endpoint_digest) throw new OpsError("SSH trust changed during root confirmation");
146
+ const id = newId("root");
147
+ const record = { schema_version: 1, artifact: "ops-root-confirmation", receipt_id: id, host_id: serverId,
148
+ platform: host.platform, transport: host.transport, identity: facts.identity, root,
149
+ account: { username: facts.username, uid: facts.uid, home: facts.home }, endpoint_digest: discovery.endpoint_digest,
150
+ discovery: { receipt_id: discoveryId, digest: digest(discovery) }, confirmed_at: now(), by, statement };
151
+ const ref = persist(state, serverId, id, record, "root-confirmation");
152
+ return { status: "root-confirmed", host_id: serverId, root, identity: facts.identity, root_confirmation: ref,
153
+ note: "Local confirmation evidence only; no directory created or software installed." };
154
+ });
155
+ }
156
+
157
+ export function validateRootConfirmation(state, host, { fresh = false, freshLegacy = false, require = false, checkEndpoint = false } = {}) {
158
+ const registry = load(state);
159
+ const previous = registry.hosts[host.host_id];
160
+ const ref = host.root_confirmation;
161
+ if (!ref) {
162
+ if (require || !previous || previous.identity !== host.identity || previous.root !== host.root || previous.root_confirmation) throw new OpsError("new Host requires server-root-confirm evidence before registration or target writes");
163
+ if (freshLegacy) {
164
+ const facts = validateFacts(host, onboardingHooks.observe(host, host.root));
165
+ if (facts.identity !== host.identity) throw new OpsError("legacy target machine identity drift");
166
+ assertRootObservation(host, facts, { legacy: true });
167
+ }
168
+ return null; // Existing v3 root remains authoritative; never fabricate retrospective consent.
169
+ }
170
+ assertUniqueMachine(registry, host);
171
+ const record = readBoundedJson(artifactPath(state, host.host_id, ref.receipt_id));
172
+ validate(record, "root-confirmation");
173
+ if (digest(record) !== ref.digest || record.receipt_id !== ref.receipt_id
174
+ || ["host_id", "identity", "platform", "transport", "root"].some((k) => host[k] !== record[k])) throw new OpsError("root confirmation digest/target mismatch");
175
+ const discovery = readBoundedJson(artifactPath(state, host.host_id, record.discovery.receipt_id));
176
+ validate(discovery, "server-discovery");
177
+ if (digest(discovery) !== record.discovery.digest) throw new OpsError("discovery evidence digest mismatch");
178
+ if (checkEndpoint && record.endpoint_digest !== endpointDigest(host)) throw new OpsError("confirmed SSH endpoint/trust drift; rediscover");
179
+ if (fresh) {
180
+ const facts = validateFacts(host, onboardingHooks.observe(host, host.root));
181
+ if (facts.identity !== host.identity) throw new OpsError("target machine identity drift");
182
+ // A reviewed connection-account update keeps the original root and confirmation.
183
+ if (endpointDigest(host) === record.endpoint_digest && ["username", "uid", "home"].some((k) => facts[k] !== record.account[k])) throw new OpsError("login account/home drift; rediscover and review the connection");
184
+ assertSelectedPath(host, facts, host.root);
185
+ assertRootObservation(host, facts);
186
+ }
187
+ return record;
188
+ }
189
+
190
+ export function confirmedBootstrapHost(state, raw, serverId, root) {
191
+ if (!state || !serverId) throw new OpsError("bootstrap apply requires --state and --host-id with root confirmation evidence");
192
+ statePath(state);
193
+ const previous = load(state).hosts[serverId];
194
+ const ref = raw.root_confirmation ?? previous?.root_confirmation;
195
+ if (!ref) {
196
+ if (!previous || previous.root !== root || previous.platform !== "linux" || previous.transport !== "ssh") throw new OpsError("bootstrap requires server-root-confirm evidence");
197
+ const host = { ...previous, connection: raw.connection || raw };
198
+ if (endpointDigest(previous) !== endpointDigest(host)) throw new OpsError("legacy bootstrap connection changes require S verification first");
199
+ assertUniqueMachine(load(state), host);
200
+ validateRootConfirmation(state, host, { freshLegacy: true });
201
+ return host;
202
+ }
203
+ const receipt = readBoundedJson(artifactPath(state, serverId, ref.receipt_id));
204
+ const host = { host_id: serverId, platform: "linux", transport: "ssh", root,
205
+ identity: receipt.identity, connection: raw.connection || raw, root_confirmation: ref };
206
+ if (raw.identity && raw.identity !== "discover" && raw.identity !== receipt.identity) throw new OpsError("bootstrap machine identity mismatch");
207
+ validateRootConfirmation(state, host, { require: true, fresh: true, checkEndpoint: true });
208
+ return host;
209
+ }
210
+
211
+ export function rootSummary(state, host) {
212
+ if (!host.root_confirmation) return { status: "legacy-registered", login_home: null, confirmed_at: null };
213
+ const r = validateRootConfirmation(state, host);
214
+ return { status: "confirmed", login_home: r.account.home, confirmed_at: r.confirmed_at };
215
+ }
@@ -12,6 +12,7 @@ import { allocationOperation } from "./services.mjs";
12
12
  import { credentialRefs, planReport, remotePaths } from "./docs.mjs";
13
13
  import { taskFiles } from "./native_windows.mjs";
14
14
  import { engineDigest } from "./execution.mjs";
15
+ import { validateRootConfirmation } from "./onboarding.mjs";
15
16
  import { assertSafeControlPath, defaultFileMode, reservedHostDocumentPaths } from "./control_files.mjs";
16
17
 
17
18
  export const plannerHooks = { call: transportCall };
@@ -246,6 +247,7 @@ export function compilePlan(state, specPath) {
246
247
  if (previous == null || fixed.some((k) => previous[k] !== item[k])) {
247
248
  throw new OpsError("resource update cannot change identity/root/kind; create explicit migration resources");
248
249
  }
250
+ if (group === "hosts" && digest(previous.root_confirmation ?? null) !== digest(item.root_confirmation ?? null)) throw new OpsError("root confirmation cannot be removed or replaced through resource updates");
249
251
  after[group][item[idkey]] = structuredClone(item);
250
252
  }
251
253
  }
@@ -668,6 +670,7 @@ export function compilePlan(state, specPath) {
668
670
  const snapshots = {};
669
671
  const docPreconditions = {};
670
672
  for (const [hid, host] of Object.entries(selected)) {
673
+ validateRootConfirmation(state, host, { fresh: true });
671
674
  const paths = new Set(ops.filter((o) => o.host_id === hid && ["write", "quarantine", "purge-quarantine"].includes(o.kind)).map((o) => o.path));
672
675
  for (const did of uniqueDocs) {
673
676
  if (after.deployments[did].host_id === hid) for (const p of remotePaths(after, after.deployments[did])) paths.add(p);
@@ -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";
@@ -75,12 +75,12 @@ function buffers(p) {
75
75
  return { stdout, stderr };
76
76
  }
77
77
 
78
- export function posixCall(endpoint, script, { timeout = 180, args = [], sudo = false } = {}) {
78
+ export function posixCall(endpoint, script, { timeout = 180, args = [], sudo } = {}) {
79
79
  validateSshEndpoint(endpoint);
80
80
  if ((endpoint.shell ?? "posix") === "powershell") {
81
81
  throw new OpsError("POSIX bootstrap is Linux-only; PowerShell targets still require an existing Node");
82
82
  }
83
- const useSudo = sudo || endpoint.sudo;
83
+ const useSudo = sudo ?? endpoint.sudo;
84
84
  let remote = ["/bin/sh", "-s", "--", ...args.map(String)];
85
85
  if (useSudo) remote = ["sudo", "-n", "--", ...remote];
86
86
  const argv = sshArgv(endpoint);
@@ -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}`);
@@ -155,7 +155,11 @@ export function call(host, request, { timeout = 1800 } = {}) {
155
155
  const c = host.connection;
156
156
  const argv = sshArgv(c);
157
157
  let remote = [c.node, "--input-type=module"];
158
- if (c.sudo) remote = ["sudo", "-n", "--", ...remote];
158
+ // New confirmed roots and their owner/lock files belong to the login account.
159
+ // Running first lock as root would make later unprivileged discovery unreadable.
160
+ // Legacy Hosts retain their existing privilege behavior.
161
+ const useSudo = c.sudo && !(host.root_confirmation && ["lock", "unlock"].includes(request.action));
162
+ if (useSudo) remote = ["sudo", "-n", "--", ...remote];
159
163
  let remoteCommand;
160
164
  if ((c.shell ?? "posix") === "powershell") {
161
165
  if (c.sudo) throw new OpsError("sudo not valid for PowerShell transport");
@@ -5,9 +5,16 @@ import { readBoundedJson, readRegistry, route, statePath } from "./workspace.mjs
5
5
  import { checkServer } from "./server_checks.mjs";
6
6
  import { generateFleet } from "./fleet.mjs";
7
7
  import { authorizeTask, prepareTask, runTask } from "./tasks.mjs";
8
+ import { discoverServer, confirmServerRoot } from "./onboarding.mjs";
9
+ import { initializeHostSpec, connectionSpec } from "./host_recipes.mjs";
10
+ import { writeJson } from "./core.mjs";
8
11
 
9
- export const COMMANDS = Object.freeze(["workspace-help", "route", "server-check", "fleet", "task-plan", "task-authorize", "task-run"]);
12
+ export const COMMANDS = Object.freeze(["workspace-help", "route", "server-discover", "server-root-confirm", "server-initialize-spec", "server-connection-spec", "server-check", "fleet", "task-plan", "task-authorize", "task-run"]);
10
13
  const options = {
14
+ "server-discover": ["server", "connection-file"],
15
+ "server-root-confirm": ["server", "discovery", "root", "by", "statement"],
16
+ "server-initialize-spec": ["server", "output"],
17
+ "server-connection-spec": ["server", "connection-file", "output"],
11
18
  route: ["scope", "server", "project"],
12
19
  "server-check": ["server", "profile", "min-free-mib", "min-memory-mib"],
13
20
  fleet: ["stale-hours"], "task-plan": ["file"],
@@ -59,6 +66,18 @@ export async function main(argv = process.argv.slice(2)) {
59
66
  load(state);
60
67
  let result;
61
68
  switch (args.command) {
69
+ case "server-discover": result = discoverServer(state, required(args, "server"), readBoundedJson(required(args, "connection-file"))); break;
70
+ case "server-root-confirm": result = confirmServerRoot(state, required(args, "server"), required(args, "discovery"), required(args, "root"), required(args, "by"), required(args, "statement")); break;
71
+ case "server-initialize-spec":
72
+ case "server-connection-spec": {
73
+ const output = required(args, "output");
74
+ if (!isAbsolute(output)) throw new OpsError("--output must be absolute");
75
+ const spec = args.command === "server-initialize-spec" ? initializeHostSpec(state, required(args, "server"))
76
+ : connectionSpec(state, required(args, "server"), readBoundedJson(required(args, "connection-file")));
77
+ writeJson(output, spec, { exclusive: true });
78
+ result = { status: "generated-not-executed", spec_path: output, next: "plan -> exact approve -> apply; root selection is not installation authorization" };
79
+ break;
80
+ }
62
81
  case "route": result = route(readRegistry(state), { scope: required(args, "scope"), server_ids: list(args.server), project_ids: list(args.project) }); break;
63
82
  case "server-check": result = await checkServer(state, required(args, "server"), { profile: args.profile ?? "base",
64
83
  minFreeMiB: integer(args["min-free-mib"], 1024), minMemoryMiB: integer(args["min-memory-mib"], 256) }); break;
@@ -0,0 +1,65 @@
1
+ #!/bin/sh
2
+ # Read-only Linux account and root inspection. Never sudo, mkdir or install here.
3
+ set -eu
4
+ [ "$(uname -s)" = Linux ] || { echo 'Linux discovery adapter required' >&2; exit 2; }
5
+ emit() { printf '%s=' "$1"; printf '%s' "$2" | od -An -v -tx1 | tr -d ' \n'; printf '\n'; }
6
+ account=$(id -un)
7
+ uid=$(id -u)
8
+ if command -v getent >/dev/null 2>&1; then
9
+ record=$(getent passwd "$uid")
10
+ else
11
+ record=$(awk -F: -v uid="$uid" '$3 == uid { print }' /etc/passwd)
12
+ fi
13
+ [ "$(printf '%s\n' "$record" | wc -l | tr -d ' ')" = 1 ] || { echo 'Ambiguous account database entry' >&2; exit 2; }
14
+ [ "$(printf '%s' "$record" | cut -d: -f1)" = "$account" ] || { echo 'Account database mismatch' >&2; exit 2; }
15
+ home=$(printf '%s' "$record" | cut -d: -f6)
16
+ case "$home" in /*) ;; *) echo 'Account home is not absolute' >&2; exit 2;; esac
17
+ machine=''
18
+ for p in /etc/machine-id /var/lib/dbus/machine-id; do
19
+ if [ -r "$p" ]; then machine=$(cat "$p"); [ -z "$machine" ] || break; fi
20
+ done
21
+ [ -n "$machine" ] || { echo 'Stable machine identity unavailable' >&2; exit 2; }
22
+ node=$(command -v node 2>/dev/null || true)
23
+ version=''
24
+ if [ -n "$node" ]; then version=$("$node" --version 2>/dev/null || true); fi
25
+ emit machine "$machine"
26
+ emit system Linux
27
+ emit username "$account"
28
+ emit uid "$uid"
29
+ emit home "$home"
30
+ emit node "$node"
31
+ emit node_version "$version"
32
+ root=${1-}
33
+ if [ -n "$root" ]; then
34
+ cursor=$root
35
+ while :; do
36
+ [ ! -L "$cursor" ] || { echo 'Symlink root traversal rejected' >&2; exit 2; }
37
+ if [ -e "$cursor" ] && [ ! -d "$cursor" ]; then echo 'Root ancestor is not a directory' >&2; exit 2; fi
38
+ [ "$cursor" != / ] || break
39
+ cursor=$(dirname -- "$cursor")
40
+ done
41
+ cursor=$root
42
+ while [ ! -e "$cursor" ]; do cursor=$(dirname -- "$cursor"); done
43
+ writable=false
44
+ if [ -w "$cursor" ] && [ -x "$cursor" ]; then writable=true; fi
45
+ present=false
46
+ nonempty=false
47
+ owner=''
48
+ if [ -d "$root" ]; then
49
+ present=true
50
+ [ -r "$root" ] || { echo 'Selected root cannot be inspected by the login account' >&2; exit 2; }
51
+ entries=$(find "$root" -mindepth 1 -maxdepth 1 -print -quit) || { echo 'Selected root inspection failed' >&2; exit 2; }
52
+ if [ -n "$entries" ]; then nonempty=true; fi
53
+ for rel in .ops-host.json _host _runtime docs knowledge README.md DEPLOYMENTS.md; do
54
+ [ ! -L "$root/$rel" ] || { echo 'Managed path is a symlink' >&2; exit 2; }
55
+ done
56
+ if [ -e "$root/.ops-host.json" ]; then
57
+ [ -f "$root/.ops-host.json" ] && [ "$(wc -c < "$root/.ops-host.json")" -le 16384 ] || { echo 'Invalid root owner marker' >&2; exit 2; }
58
+ owner=$(cat "$root/.ops-host.json")
59
+ fi
60
+ fi
61
+ emit root_exists "$present"
62
+ emit root_writable "$writable"
63
+ emit root_nonempty "$nonempty"
64
+ emit root_owner "$owner"
65
+ fi
@@ -26,7 +26,7 @@ try {
26
26
  const text = readFileSync(join(root, id, id + ".md"), "utf8");
27
27
  if (!text.includes("type: workflow-entry") || !text.includes("读取范围") || !text.includes("id: ops/" + id.slice(2))) throw new Error("invalid work " + id);
28
28
  }
29
- for (const file of ["INDEX.md", "README.md", "manifest.json", "runtime-contract.json", "common/USAGE.md", "common/WORKSPACE-USAGE.md", "common/CAPABILITIES.md", "common/templates/FLEET.html", "common/schemas/fleet-view.schema.json", "common/schemas/task-request.schema.json", "common/schemas/server-check.schema.json", "common/tools/opslib/execution.mjs", "common/tools/opslib/tasks.mjs", "common/tools/opslib/fleet.mjs"]) {
29
+ for (const file of ["INDEX.md", "README.md", "manifest.json", "runtime-contract.json", "common/USAGE.md", "common/WORKSPACE-USAGE.md", "common/CAPABILITIES.md", "common/templates/FLEET.html", "common/schemas/fleet-view.schema.json", "common/schemas/task-request.schema.json", "common/schemas/server-check.schema.json", "common/tools/opslib/execution.mjs", "common/tools/opslib/tasks.mjs", "common/tools/opslib/fleet.mjs", "common/tools/opslib/onboarding.mjs", "common/tools/server-discover.sh", "common/schemas/server-discovery.schema.json", "common/schemas/root-confirmation.schema.json", "common/rules/host-root-and-onboarding.md"]) {
30
30
  if (!existsSync(join(root, file))) throw new Error("missing " + file);
31
31
  }
32
32
  for (const name of readdirSync(join(root, "common/schemas")).filter((x) => x.endsWith(".json"))) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "id": "ops",
4
- "version": "2.2.0",
4
+ "version": "2.3.0",
5
5
  "persistent_knowledge": [
6
6
  "<Path>{roots.state}/ops/knowledge/</Path>",
7
7
  "<Path>{roots.state}/ops/hosts/{host_id}/knowledge/</Path>",
@@ -30,19 +30,24 @@
30
30
  "inputs": [
31
31
  "server",
32
32
  "verified-host-key",
33
- "key-or-agent"
33
+ "key-or-agent",
34
+ "user-selected-persistence-root"
34
35
  ],
35
36
  "outputs": [
36
37
  "server-registration",
37
38
  "inventory",
38
- "server-checks"
39
+ "server-checks",
40
+ "login-account-discovery",
41
+ "root-confirmation"
39
42
  ],
40
43
  "risk": "production-critical",
41
44
  "context_budget": 20000
42
45
  },
43
46
  {
44
47
  "id": "host-manage",
45
- "after": [],
48
+ "after": [
49
+ "server-connect"
50
+ ],
46
51
  "inputs": [
47
52
  "server",
48
53
  "spec",
@@ -59,7 +64,9 @@
59
64
  },
60
65
  {
61
66
  "id": "project-deploy",
62
- "after": [],
67
+ "after": [
68
+ "host-manage"
69
+ ],
63
70
  "inputs": [
64
71
  "project",
65
72
  "explicit-servers",
@@ -38,9 +38,9 @@
38
38
  change 内三份文档复用 grill 的既有格式,避免重复发明:
39
39
 
40
40
  - 设计日志:`<Path>{roots.workflows}/specdev/G-grill-with-docs/log-format.md</Path>`;
41
- - 领域上下文:`<Path>{roots.workflows}/specdev/G-grill-with-docs/context-format.md</Path>`;
42
- - 架构决策:`<Path>{roots.workflows}/specdev/G-grill-with-docs/adr-format.md</Path>`;
43
- - 领域建模规则:`<Path>{roots.workflows}/specdev/G-grill-with-docs/domain-modeling-rules.md</Path>`。
41
+ - 领域上下文:`<Path>{roots.workflows}/specdev/common/rules/context-format.md</Path>`;
42
+ - 架构决策:`<Path>{roots.workflows}/specdev/common/rules/adr-format.md</Path>`;
43
+ - 领域建模规则:`<Path>{roots.workflows}/specdev/common/rules/domain-modeling.md</Path>`。
44
44
 
45
45
  同步顺序固定:先 LOG,再 CONTEXT,最后 ADR。CONTEXT 只描述当前真相,历史轨迹留在 LOG;未确认的解读不得写成已接受 ADR;与现有永久 ADR 冲突时建立 supersedes 链,不重写历史。高影响条目带来源标识(`USER-DECISION`、`CODE:`、`RESEARCH:`、`ADR-###`)。
46
46
 
@@ -23,7 +23,7 @@ C 是独立 review 入口,不实施修复。它拥有 `<Path>{roots.state}/spe
23
23
 
24
24
  ## 流程
25
25
 
26
- 1. **解析固定点**:使用 `git rev-parse` 把固定点和 HEAD 固定为 SHA;PR 先通过 `<Path>{roots.skills}/github-npm-ops/SKILL.md</Path>` 的 `pr-read` 获得 base/head SHA,并确保对应对象可在本地解析。
26
+ 1. **解析固定点**:使用 `git rev-parse` 把固定点和 HEAD 固定为 SHA;PR 先通过 `<Path>{roots.workflows}/specdev/T-triage/remote-operations.md</Path>` 的 `pr-read` 获得 base/head SHA,并确保对应对象可在本地解析。
27
27
  2. **冻结输入**:记录 `git diff <fixed>...<head>` 和 `git log <fixed>..<head> --oneline`。引用无效或 diff 为空时失败,不创建报告。
28
28
  3. **发现来源**:调用 `<Path>{roots.workflows}/specdev/common/skills/code-review/SKILL.md</Path>`;规范来源缺失时经确认跳过规范轴,标准轴继续。
29
29
  4. **隔离审查**:仅按公共 Skill 的完整输入复用门接受既有结果;需要执行的轴在平台支持时并行 reviewer,否则用两个独立完整输入包顺序执行。两轴不得读取对方 finding。
@@ -69,3 +69,9 @@ node <Path>{roots.workflows}/specdev/common/tools/validate-specdev.mjs</Path> \
69
69
  ```
70
70
 
71
71
  根因确认后将本 Work 加入 `works_run` 并清空 `current_work`,返回 diagnosis 和下一 Work:局部修复进入 Tickets/I,公共行为或高风险进入 S/Tickets,缺 seam 进入 R,仍无根因则保持 blocked 或进入 W。
72
+
73
+ ## 脱敏与强制变异证据
74
+
75
+ 在日志、报告和重现材料中移除 token、Cookie、认证头、秘密配置、个人数据及机器绝对路径;保留项目相对定位和必要错误形态。原始敏感数据不进入长期工件。
76
+
77
+ 如果使用强制变异检验测试敏感性,先记录未变异文件的 hash/diff,实际修改目标后读取 git diff 与运行路径,证明变异已落到执行文件。只有测试因预期行为断言失败才算有效 red;编译错误、未选中测试、变异未落地均为无效证据。恢复原内容,复跑基线并核对 diff。D 输出诊断及修复不变量,正式修复仍交 I。
@@ -40,9 +40,9 @@
40
40
 
41
41
  分别使用:
42
42
 
43
- - `<Path>{roots.workflows}/specdev/G-grill-with-docs/adr-format.md</Path>`
43
+ - `<Path>{roots.workflows}/specdev/common/rules/adr-format.md</Path>`
44
44
  - `<Path>{roots.workflows}/specdev/G-grill-with-docs/log-format.md</Path>`
45
- - `<Path>{roots.workflows}/specdev/G-grill-with-docs/context-format.md</Path>`
45
+ - `<Path>{roots.workflows}/specdev/common/rules/context-format.md</Path>`
46
46
  - `<Path>{roots.workflows}/specdev/common/schemas/design-tree.schema.json</Path>`
47
47
 
48
48
  恢复时先读取四份工件,按 design tree 的节点状态恢复,避免重复询问已关闭问题。
@@ -90,7 +90,7 @@
90
90
 
91
91
  ### 5. 同步 change-local 领域模型
92
92
 
93
- 加载 `<Path>{roots.workflows}/specdev/G-grill-with-docs/domain-modeling-rules.md</Path>`。每轮先写 LOG,再把已确认且本 change 下游必须使用的项目规范术语同步到 change CONTEXT,最后把同时满足三个准入条件、已成为本 change 合同的架构决定写入 change ADR。
93
+ 加载 `<Path>{roots.workflows}/specdev/common/rules/domain-modeling.md</Path>`。每轮先写 LOG,再把已确认且本 change 下游必须使用的项目规范术语同步到 change CONTEXT,最后把同时满足三个准入条件、已成为本 change 合同的架构决定写入 change ADR。
94
94
 
95
95
  历史轨迹只留在 LOG;未确认选项不写成已接受 ADR;已有 change ADR 被替代时建立 supersedes 链。同步只更新本 change 工件,不创建、合并或改写永久 `context/`、`adr/`;它记录共识生长过程,不授权产品实现。
96
96
 
@@ -127,8 +127,8 @@ frontier 为空时,向用户确认设计树的每个分支均已走过且已
127
127
 
128
128
  - 质询协议:`<Path>{roots.workflows}/specdev/G-grill-with-docs/grilling-protocol.md</Path>`
129
129
  - 设计树模板:`<Path>{roots.workflows}/specdev/G-grill-with-docs/design-tree-template.json</Path>`
130
- - 领域建模:`<Path>{roots.workflows}/specdev/G-grill-with-docs/domain-modeling-rules.md</Path>`
131
- - ADR 格式:`<Path>{roots.workflows}/specdev/G-grill-with-docs/adr-format.md</Path>`
132
- - CONTEXT 格式:`<Path>{roots.workflows}/specdev/G-grill-with-docs/context-format.md</Path>`
130
+ - 领域建模:`<Path>{roots.workflows}/specdev/common/rules/domain-modeling.md</Path>`
131
+ - ADR 格式:`<Path>{roots.workflows}/specdev/common/rules/adr-format.md</Path>`
132
+ - CONTEXT 格式:`<Path>{roots.workflows}/specdev/common/rules/context-format.md</Path>`
133
133
  - LOG 格式:`<Path>{roots.workflows}/specdev/G-grill-with-docs/log-format.md</Path>`
134
134
  - Stakeholder 问卷:`<Path>{roots.workflows}/specdev/G-grill-with-docs/stakeholder-questionnaire.md</Path>`
@@ -74,7 +74,7 @@ Ticket 模式下,Lead 根据 Ticket 独立性、路径冲突、上下文、风
74
74
 
75
75
  加载 `<Path>{roots.workflows}/specdev/common/rules/codebase-design.md</Path>`,检查模块、接口、类型、不变量、顺序/错误/性能语义、接缝、适配器、依赖分类、测试观察点和既有公共合同。
76
76
 
77
- 存在多个不改变上层契约的局部设计时,可运行 `<Path>{roots.workflows}/specdev/I-implement/design-it-twice.md</Path>`。超出 Ticket 或改变产品/公共合同/数据/兼容/安全时,返回架构审查、Grill、Spec 或 Ticket owner。陌生外部依赖使用 research Skill。
77
+ 存在多个不改变上层契约的局部设计时,可运行 `<Path>{roots.workflows}/specdev/common/rules/design-it-twice.md</Path>`。超出 Ticket 或改变产品/公共合同/数据/兼容/安全时,返回架构审查、Grill、Spec 或 Ticket owner。陌生外部依赖使用 research Skill。
78
78
 
79
79
  **完成标准**:局部设计与上层契约一致,稳定接缝和依赖策略明确。
80
80
 
@@ -15,3 +15,7 @@
15
15
  - 一个循环只有一个 seam、一个测试和一个最小实现;
16
16
  - 测试没有通过删除、跳过、吞错或放宽断言制造绿色;
17
17
  - review 前没有以“顺手重构”扩大切片。
18
+
19
+ ## 强制变异验证
20
+
21
+ 使用强制变异时,先保存基线 hash/diff,修改后核对实际执行文件的 diff 与测试选择。只有预期行为断言 red 才有效;编译错误、错测或变异未落地均不计。恢复原内容、复跑基线并核对 diff,把全过程记入 Evidence。诊断协议见 <Path>{roots.workflows}/specdev/D-diagnose-bugs/D-diagnose-bugs.md</Path>。
@@ -1,5 +1,5 @@
1
1
  {
2
- "schema_version": 5,
2
+ "schema_version": 6,
3
3
  "interaction_language": "zh-CN",
4
4
  "artifact_language": "zh-CN",
5
5
  "git": {
@@ -23,5 +23,9 @@
23
23
  "require_evidence": true,
24
24
  "ui_design_default_candidates": 3,
25
25
  "ui_design_max_candidates": 4
26
+ },
27
+ "github": {
28
+ "include_external_prs": false,
29
+ "labels": {}
26
30
  }
27
31
  }
@@ -3,8 +3,8 @@ id: specdev
3
3
  type: workflow
4
4
  workflow: specdev
5
5
  name: SpecDev Workflow
6
- description: 以本地工件为唯一开发权威,从来源冻结、诊断、设计、原型、规格、Ticket、编排和审查推进到证据驱动实现、远程 reconcile 或票级发布投影、记事项捕获与知识归档。
7
- keywords: [specdev, local-first, 规格驱动开发, decision-complete, prototype, code-review, TDD, 证据]
6
+ description: 以本地工件为开发权威,覆盖来源分诊、诊断、设计、逻辑/UI 原型、规格、Ticket、编排、实现、审查、复盘、GitHub/PR/包发布交付与知识归档。
7
+ keywords: [specdev, local-first, 规格驱动开发, decision-complete, prototype, code-review, TDD, retro, PR, release, 证据]
8
8
  ---
9
9
 
10
10
  # SpecDev Index
@@ -13,7 +13,7 @@
13
13
  - 等待真实上游产物的未来 Ticket 保持 `status: draft`、`ready: false`,在未决问题中记录缺口、生产者 Ticket、所需证据和 DoR 重审条件;不编造版本、路径或验证入口;
14
14
  - 每个验收合同被 Ticket 覆盖;
15
15
  - writable/shared path 有唯一 owner,Wave 候选无写冲突;
16
- - config schema v5,`max_implementation_agents` 与 `max_integration_attempts` 为正整数;UI 设计候选范围读取 planning 配置;
16
+ - config schema v6,`max_implementation_agents` 与 `max_integration_attempts` 为正整数;UI 设计候选范围读取 planning 配置;
17
17
  - 父分支可定位,implementation commit 与本地 integration 的授权状态已记录;`plan` 可以列出待批准项并保持 `ready_for_execution: false`;
18
18
  - 待执行 Deep Ticket 的迁移、兼容、监控、恢复和不可逆批准点完整。
19
19
  - Ticket 与 `<Path>{roots.state}/specdev/changes/{change}/spec.md</Path>`、`<Path>{roots.state}/specdev/changes/{change}/ADR.md</Path>`、`<Path>{roots.state}/specdev/adr/</Path>`、`<Path>{roots.state}/specdev/context/</Path>` 和当前代码事实不存在未处理冲突;
@@ -2,12 +2,12 @@
2
2
  id: specdev/prototype
3
3
  type: workflow-entry
4
4
  workflow: specdev
5
- name: UI 设计原型
6
- description: 检测现有项目的 UI 事实,按产品任务推荐并逐步选择设计风格,生成持久化设计系统文档、多风格 HTML 对照和可运行 HTML/CSS/JS 原型。
5
+ name: 逻辑与 UI 原型
6
+ description: 按需验证离线逻辑模型,或检测现有项目的 UI 事实,按产品任务推荐并逐步选择设计风格,生成持久化设计系统文档、多风格 HTML 对照和可运行 HTML/CSS/JS 原型。
7
7
  keywords: [prototype, UI 原型, 风格检测, 设计系统, HTML, CSS, 交互, design tokens]
8
8
  ---
9
9
 
10
- # UI 设计原型
10
+ # 逻辑与 UI 原型
11
11
 
12
12
  > 激活本 Work 后,先读取 `<Path>{roots.workflows}/specdev/README.md</Path>`,再执行本入口。
13
13
 
@@ -15,6 +15,10 @@ keywords: [prototype, UI 原型, 风格检测, 设计系统, HTML, CSS, 交互,
15
15
 
16
16
  激活时读取 `<Path>{roots.workflows}/specdev/common/rules/activation-and-memory.md</Path>`,按当前步骤定位输入和证据。
17
17
 
18
+ ## 分支选择
19
+
20
+ 逻辑/状态/规则验证读取 <Path>{roots.workflows}/specdev/P-prototype/logic-protocol.md</Path>,跳过下方 UI 专属步骤。UI 仍默认离线持久化,默认 3 个、最多 4 个方案。只有用户明确要求项目内预览时读取 <Path>{roots.workflows}/specdev/P-prototype/project-preview-protocol.md</Path>;其临时写集与清理由该分支管理,下方 UI 设计权威与离线交付保持。
21
+
18
22
  ## 输入与所有权
19
23
 
20
24
  - 必需输入:目标项目或新产品设想、核心用户任务,以及当前 SpecDev change。