create-yss-spec 3.3.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/package.json +3 -2
  2. package/src/command-runner.mjs +107 -0
  3. package/src/self-update.js +7 -4
  4. package/src/template/instance-runtime.js +1 -1
  5. package/template/.agents/skills/.strategic-design-skills-manifest.json +2 -2
  6. package/template/.agents/skills/competitive-intelligence/SKILL.md +4 -4
  7. package/template/.agents/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
  8. package/template/.agents/skills/grill-with-docs/SKILL.md +1 -1
  9. package/template/.agents/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
  10. package/template/.agents/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
  11. package/template/.agents/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  12. package/template/.agents/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
  13. package/template/.agents/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
  14. package/template/.agents/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
  15. package/template/.agents/skills/yss-product-lifecycle/SKILL.md +7 -4
  16. package/template/.agents/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
  17. package/template/.agents/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
  18. package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
  19. package/template/.agents/skills/yss-product-lifecycle/references/orchestration.md +11 -1
  20. package/template/.agents/skills/yss-research/SKILL.md +1 -1
  21. package/template/.agents/skills/yss-stage-decision/SKILL.md +5 -3
  22. package/template/.agents/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
  23. package/template/.claude/skills/competitive-intelligence/SKILL.md +4 -4
  24. package/template/.claude/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
  25. package/template/.claude/skills/grill-with-docs/SKILL.md +1 -1
  26. package/template/.claude/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
  27. package/template/.claude/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
  28. package/template/.claude/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  29. package/template/.claude/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
  30. package/template/.claude/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
  31. package/template/.claude/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
  32. package/template/.claude/skills/yss-product-lifecycle/SKILL.md +7 -4
  33. package/template/.claude/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
  34. package/template/.claude/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
  35. package/template/.claude/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
  36. package/template/.claude/skills/yss-product-lifecycle/references/orchestration.md +11 -1
  37. package/template/.claude/skills/yss-research/SKILL.md +1 -1
  38. package/template/.claude/skills/yss-stage-decision/SKILL.md +5 -3
  39. package/template/.claude/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
  40. package/template/.codex/skills/competitive-intelligence/SKILL.md +4 -4
  41. package/template/.codex/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
  42. package/template/.codex/skills/grill-with-docs/SKILL.md +1 -1
  43. package/template/.codex/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
  44. package/template/.codex/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
  45. package/template/.codex/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  46. package/template/.codex/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
  47. package/template/.codex/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
  48. package/template/.codex/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
  49. package/template/.codex/skills/yss-product-lifecycle/SKILL.md +7 -4
  50. package/template/.codex/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
  51. package/template/.codex/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
  52. package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
  53. package/template/.codex/skills/yss-product-lifecycle/references/orchestration.md +11 -1
  54. package/template/.codex/skills/yss-research/SKILL.md +1 -1
  55. package/template/.codex/skills/yss-stage-decision/SKILL.md +5 -3
  56. package/template/.codex/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
  57. package/template/.cursor/skills/competitive-intelligence/SKILL.md +4 -4
  58. package/template/.cursor/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
  59. package/template/.cursor/skills/grill-with-docs/SKILL.md +1 -1
  60. package/template/.cursor/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
  61. package/template/.cursor/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
  62. package/template/.cursor/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  63. package/template/.cursor/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
  64. package/template/.cursor/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
  65. package/template/.cursor/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
  66. package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +7 -4
  67. package/template/.cursor/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
  68. package/template/.cursor/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
  69. package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
  70. package/template/.cursor/skills/yss-product-lifecycle/references/orchestration.md +11 -1
  71. package/template/.cursor/skills/yss-research/SKILL.md +1 -1
  72. package/template/.cursor/skills/yss-stage-decision/SKILL.md +5 -3
  73. package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
  74. package/template/.pi/skills/competitive-intelligence/SKILL.md +4 -4
  75. package/template/.pi/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
  76. package/template/.pi/skills/grill-with-docs/SKILL.md +1 -1
  77. package/template/.pi/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
  78. package/template/.pi/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
  79. package/template/.pi/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  80. package/template/.pi/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
  81. package/template/.pi/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
  82. package/template/.pi/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
  83. package/template/.pi/skills/yss-product-lifecycle/SKILL.md +7 -4
  84. package/template/.pi/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
  85. package/template/.pi/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
  86. package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
  87. package/template/.pi/skills/yss-product-lifecycle/references/orchestration.md +11 -1
  88. package/template/.pi/skills/yss-research/SKILL.md +1 -1
  89. package/template/.pi/skills/yss-stage-decision/SKILL.md +5 -3
  90. package/template/.pi/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
  91. package/template/.qoder/skills/competitive-intelligence/SKILL.md +4 -4
  92. package/template/.qoder/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
  93. package/template/.qoder/skills/grill-with-docs/SKILL.md +1 -1
  94. package/template/.qoder/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
  95. package/template/.qoder/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
  96. package/template/.qoder/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  97. package/template/.qoder/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
  98. package/template/.qoder/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
  99. package/template/.qoder/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
  100. package/template/.qoder/skills/yss-product-lifecycle/SKILL.md +7 -4
  101. package/template/.qoder/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
  102. package/template/.qoder/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
  103. package/template/.qoder/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
  104. package/template/.qoder/skills/yss-product-lifecycle/references/orchestration.md +11 -1
  105. package/template/.qoder/skills/yss-research/SKILL.md +1 -1
  106. package/template/.qoder/skills/yss-stage-decision/SKILL.md +5 -3
  107. package/template/.qoder/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
  108. package/template/.trae/skills/competitive-intelligence/SKILL.md +4 -4
  109. package/template/.trae/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
  110. package/template/.trae/skills/grill-with-docs/SKILL.md +1 -1
  111. package/template/.trae/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
  112. package/template/.trae/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
  113. package/template/.trae/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  114. package/template/.trae/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
  115. package/template/.trae/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
  116. package/template/.trae/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
  117. package/template/.trae/skills/yss-product-lifecycle/SKILL.md +7 -4
  118. package/template/.trae/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
  119. package/template/.trae/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
  120. package/template/.trae/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
  121. package/template/.trae/skills/yss-product-lifecycle/references/orchestration.md +11 -1
  122. package/template/.trae/skills/yss-research/SKILL.md +1 -1
  123. package/template/.trae/skills/yss-stage-decision/SKILL.md +5 -3
  124. package/template/.trae/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
  125. package/template/AGENTS.md +2 -2
  126. package/template/CONTEXT.md +3 -2
  127. package/template/README.md +3 -3
  128. package/template/docs/agents/digital-human-roles.md +1 -1
  129. package/template/docs/agents/digital-human-roles.yaml +11 -11
  130. package/template/docs/agents/issue-tracker.md +1 -1
  131. package/template/docs/agents/strategic-design-skills-integration.md +2 -0
  132. package/template/docs/architecture/README.md +1 -1
  133. package/template/docs/architecture/templates/business-architecture-template.md +1 -1
  134. package/template/docs/architecture/templates/functional-architecture-template.md +1 -1
  135. package/template/docs/design/README.md +1 -1
  136. package/template/docs/design/templates/interaction-spec-template.md +1 -1
  137. package/template/docs/design/templates/product-overview-design-template.md +1 -1
  138. package/template/docs/{discovery → plan}/IDEATION.md +5 -5
  139. package/template/docs/plan/README.md +9 -0
  140. package/template/docs/plan/entry-review.md +23 -0
  141. package/template/docs/plan/templates/plan-template.md +58 -0
  142. package/template/docs/process/harness-process-tailoring.md +1 -1
  143. package/template/docs/process/lifecycle-artifact-map.md +6 -6
  144. package/template/docs/process/lifecycle-registry-baseline.json +5 -1
  145. package/template/docs/process/lifecycle-registry.yaml +25 -20
  146. package/template/docs/process/plan-migration.md +13 -0
  147. package/template/docs/process/schemas/digital-human-task-package.schema.json +2 -0
  148. package/template/docs/process/schemas/lifecycle-checkpoint.schema.json +3 -0
  149. package/template/docs/process/schemas/lifecycle-registry.schema.json +1 -1
  150. package/template/docs/process/template-engineering-overview.md +2 -2
  151. package/template/docs/process/template-verification-profiles.yaml +2 -0
  152. package/template/docs/process/templates/context-reconciliation-template.yaml +2 -2
  153. package/template/docs/process/templates/lifecycle-checkpoint-template.yaml +1 -1
  154. package/template/docs/requirements/README.md +2 -2
  155. package/template/docs/templates/local-parent-ticket-template.md +1 -1
  156. package/template/docs/templates/subagent-task-package-template.md +1 -1
  157. package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214.md +1 -1
  158. package/template/scripts/cache-template-commit +2 -2
  159. package/template/scripts/fixtures/strategic-handoff/source-roles.json +7 -7
  160. package/template/scripts/fixtures/user-decision/plan-fixture.mjs +30 -0
  161. package/template/scripts/lib/backend-delivery.mjs +5 -2
  162. package/template/scripts/lib/command-runner.mjs +107 -0
  163. package/template/scripts/lib/context-reconciliation.mjs +34 -0
  164. package/template/scripts/lib/json-schema.mjs +42 -13
  165. package/template/scripts/lib/lifecycle-context-query.mjs +7 -3
  166. package/template/scripts/lib/lifecycle-transition.mjs +9 -3
  167. package/template/scripts/lib/plan-spec-entry.mjs +87 -0
  168. package/template/scripts/lib/scenario-checks.mjs +15 -12
  169. package/template/scripts/lib/skill-supply-chain.mjs +18 -2
  170. package/template/scripts/lib/strategic-handoff-io.mjs +5 -4
  171. package/template/scripts/lib/strategic-handoff.mjs +10 -9
  172. package/template/scripts/lib/task-package.mjs +2 -0
  173. package/template/scripts/lib/template-commit-cache.mjs +7 -5
  174. package/template/scripts/run-template-verification +9 -3
  175. package/template/scripts/sync-strategic-handoff-tools +9 -0
  176. package/template/scripts/verify-context-reconciliation +2 -48
  177. package/template/scripts/verify-context-reconciliation-scenarios +1 -1
  178. package/template/scripts/verify-lifecycle-checkpoint +2 -0
  179. package/template/scripts/verify-lifecycle-context-query-scenarios +28 -8
  180. package/template/scripts/verify-plan-spec-entry +11 -0
  181. package/template/scripts/verify-plan-spec-entry-scenarios +79 -0
  182. package/template/scripts/verify-script-performance-scenarios +56 -0
  183. package/template/scripts/verify-subagent-task-package-scenarios +9 -8
  184. package/template/scripts/verify-template-cache-scenarios +11 -1
  185. package/template/scripts/verify-user-decision-scenarios +4 -4
  186. package/template/skills-lock.json +17 -17
  187. package/template.manifest.json +2 -1
  188. package/template.snapshot.json +5 -5
  189. package/template/docs/discovery/README.md +0 -87
  190. package/template/docs/discovery/reports/.gitkeep +0 -0
  191. package/template/docs/discovery/reports/agent-governance-options.md +0 -71
  192. package/template/docs/discovery/reports/competitor-deep-dive/.gitkeep +0 -0
  193. package/template/docs/discovery/templates/discovery-template.md +0 -94
  194. /package/template/docs/{discovery → plan}/templates/competitive-analysis-template.md +0 -0
  195. /package/template/docs/{discovery → plan}/templates/competitive-matrix-template.md +0 -0
  196. /package/template/docs/{discovery → plan}/templates/market-analysis-template.md +0 -0
  197. /package/template/docs/{discovery → plan}/templates/user-pain-points-template.md +0 -0
@@ -2,21 +2,50 @@ import { existsSync } from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import path from "node:path";
4
4
 
5
- export function validateJsonSchema(value, schemaPath, { cwd = process.cwd(), label = "JSON Schema" } = {}) {
6
- const effectiveSchema = path.resolve(schemaPath);
7
- if (!existsSync(effectiveSchema)) throw new TypeError(`缺少 ${label}: ${effectiveSchema}`);
8
- const validator = String.raw`
5
+ const validator = String.raw`
9
6
  import json
10
7
  import sys
8
+ import traceback
11
9
  from jsonschema import Draft202012Validator, FormatChecker
12
- schema = json.load(open(sys.argv[1], encoding="utf-8"))
13
- value = json.load(sys.stdin)
14
- errors = sorted(Draft202012Validator(schema, format_checker=FormatChecker()).iter_errors(value), key=lambda error: list(error.absolute_path))
15
- for error in errors:
16
- location = ".".join(str(part) for part in error.absolute_path) or "<root>"
17
- print(f"{location}: {error.message}", file=sys.stderr)
18
- sys.exit(1 if errors else 0)
10
+ validators = {}
11
+ results = []
12
+ for item in json.load(sys.stdin):
13
+ try:
14
+ key = (item['schemaPath'], item['formatChecker'])
15
+ if key not in validators:
16
+ with open(item['schemaPath'], encoding='utf-8') as source:
17
+ schema = json.load(source)
18
+ validators[key] = Draft202012Validator(schema, format_checker=FormatChecker() if key[1] else None)
19
+ errors = list(validators[key].iter_errors(item['value']))
20
+ if item['errorStyle'] == 'verbose':
21
+ message = '\n'.join(str(error) for error in errors)
22
+ else:
23
+ errors.sort(key=lambda error: list(error.absolute_path))
24
+ message = '\n'.join(f"{'.'.join(str(part) for part in error.absolute_path) or '<root>'}: {error.message}" for error in errors)
25
+ results.append({'valid': not errors, 'error': message})
26
+ except Exception:
27
+ results.append({'valid': False, 'error': traceback.format_exc().strip()})
28
+ json.dump(results, sys.stdout)
19
29
  `;
20
- const result = spawnSync("python3", ["-c", validator, effectiveSchema], { cwd, encoding: "utf8", input: JSON.stringify(value) });
21
- if (result.status !== 0) throw new TypeError(`${result.stdout}${result.stderr}`.trim());
30
+
31
+ /** One immutable validation phase, never a cache of successful work units. */
32
+ export function validateJsonSchemas(items, { cwd = process.cwd() } = {}) {
33
+ if (!Array.isArray(items)) throw new TypeError("schema batch 必须是数组");
34
+ if (!items.length) return [];
35
+ const jobs = items.map(({ value, schemaPath, formatChecker = true, errorStyle = "compact", label = "JSON Schema" }) => {
36
+ const effectiveSchema = path.resolve(schemaPath);
37
+ if (!existsSync(effectiveSchema)) throw new TypeError(`缺少 ${label}: ${effectiveSchema}`);
38
+ if (!["compact", "verbose"].includes(errorStyle)) throw new TypeError(`未知 schema errorStyle: ${errorStyle}`);
39
+ return { value, schemaPath: effectiveSchema, formatChecker, errorStyle };
40
+ });
41
+ const result = spawnSync("python3", ["-c", validator], { cwd, encoding: "utf8", input: JSON.stringify(jobs), maxBuffer: 16 * 1024 * 1024 });
42
+ if (result.error || result.status !== 0) throw new TypeError(result.error?.message || `${result.stdout}${result.stderr}`.trim());
43
+ const results = JSON.parse(result.stdout);
44
+ if (results.length !== jobs.length) throw new TypeError("schema batch 返回数量不匹配");
45
+ return results;
46
+ }
47
+
48
+ export function validateJsonSchema(value, schemaPath, options = {}) {
49
+ const [result] = validateJsonSchemas([{ value, schemaPath, ...options }], options);
50
+ if (!result.valid) throw new TypeError(result.error);
22
51
  }
@@ -70,6 +70,10 @@ export function queryLifecycleContext({ mode, stageId, workUnitId, include = []
70
70
  const lifecycle = validateRegistry(loadRegistry());
71
71
  const orchestration = loadYaml(ORCHESTRATION_CONTRACT_REF, "生命周期编排合同");
72
72
  const skillRegistry = loadSkillRegistry();
73
+ const planUnit = ['work-unit.plan-opportunity', 'work-unit.plan-requirements', 'work-unit.domain-strategy-design', 'work-unit.stage-decision', 'work-unit.spec-synthesis'].includes(workUnitId);
74
+ const loadPlan = ['stage.plan', 'stage.spec-architecture'].includes(stageId) || planUnit;
75
+ stageId ??= planUnit ? 'stage.plan' : undefined;
76
+ if (loadPlan) include = [...include, 'planning', 'grill_exit'];
73
77
  const stage = selectById(lifecycle.stages, stageId, "阶段");
74
78
  const workUnit = selectById(lifecycle.work_units, workUnitId, "工作单元");
75
79
 
@@ -80,8 +84,8 @@ export function queryLifecycleContext({ mode, stageId, workUnitId, include = []
80
84
  }
81
85
 
82
86
  const route = workUnitId ? orchestration.work_unit_routes?.[workUnitId] ?? null : null;
83
- const gates = stageId ? lifecycle.gates.filter((gate) => gate.stage === stageId) : [];
84
- const artifacts = stageId ? lifecycle.artifacts.filter((artifact) => artifact.stage === stageId) : [];
87
+ const gates = lifecycle.gates.filter((gate) => gate.stage === stageId || (loadPlan && gate.stage === 'stage.plan'));
88
+ const artifacts = lifecycle.artifacts.filter((artifact) => artifact.stage === stageId || (loadPlan && artifact.stage === 'stage.plan'));
85
89
  const evidenceIds = new Set(gates.flatMap((gate) => gate.evidence ?? []));
86
90
  const selected = Object.fromEntries(normalizedIncludes.map((key) => [key, orchestration[key]]));
87
91
  const transition = workUnitId ? {
@@ -124,6 +128,7 @@ export function queryLifecycleContext({ mode, stageId, workUnitId, include = []
124
128
  execution: {
125
129
  mode: mode ? orchestration.modes[mode] : null,
126
130
  selected,
131
+ ...(loadPlan ? { plan_checks: lifecycle.stages.find(stage => stage.id === 'stage.plan').spec_entry.required_checks.map(id => ({ id, status: 'pending', evidence_refs: [] })) } : {}),
127
132
  transition,
128
133
  work_unit_route: route,
129
134
  },
@@ -134,4 +139,3 @@ export function queryLifecycleContext({ mode, stageId, workUnitId, include = []
134
139
  .sort();
135
140
  return canonicalize(result);
136
141
  }
137
-
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { parseDocument } from "../vendor/yaml.mjs";
3
3
  import { assertWorkUnitUserDecision, assertImplementationDecision } from "./user-decision.mjs";
4
4
  import { enforceFrontendDelivery } from "./frontend-delivery-boundary.mjs";
5
+ import { validatePlanSpecEntry } from "./plan-spec-entry.mjs";
5
6
 
6
7
  const IMPLEMENTATION_WORK_UNIT = "work-unit.slice-implementation";
7
8
  const TICKET_DECOMPOSITION_WORK_UNIT = "work-unit.ticket-decomposition";
@@ -15,7 +16,7 @@ function deepFreeze(value) {
15
16
  }
16
17
 
17
18
  const NEXT_ROUTES = deepFreeze({
18
- "work-unit.entry-triage": ["work-unit.discovery-opportunity", "work-unit.discovery-requirements"],
19
+ "work-unit.entry-triage": ["work-unit.plan-opportunity", "work-unit.plan-requirements"],
19
20
  "work-unit.ssot-update": ["work-unit.skill-projection-sync", "work-unit.intensity-aware-verification"],
20
21
  "work-unit.skill-projection-sync": ["work-unit.template-snapshot-build", "work-unit.intensity-aware-verification"],
21
22
  "work-unit.template-snapshot-build": ["work-unit.attach-sync-integration", "work-unit.intensity-aware-verification"],
@@ -23,8 +24,8 @@ const NEXT_ROUTES = deepFreeze({
23
24
  "work-unit.intensity-aware-verification": ["work-unit.intensity-aware-review"],
24
25
  "work-unit.intensity-aware-review": ["work-unit.release-and-rollback"],
25
26
  "work-unit.release-and-rollback": [],
26
- "work-unit.discovery-opportunity": ["work-unit.discovery-requirements", "work-unit.domain-strategy-design", "work-unit.stage-decision", "work-unit.spec-synthesis"],
27
- "work-unit.discovery-requirements": ["work-unit.domain-strategy-design", "work-unit.stage-decision", "work-unit.spec-synthesis"],
27
+ "work-unit.plan-opportunity": ["work-unit.plan-requirements", "work-unit.domain-strategy-design", "work-unit.stage-decision", "work-unit.spec-synthesis"],
28
+ "work-unit.plan-requirements": ["work-unit.domain-strategy-design", "work-unit.stage-decision", "work-unit.spec-synthesis"],
28
29
  "work-unit.domain-strategy-design": ["work-unit.stage-decision", "work-unit.spec-synthesis"],
29
30
  "work-unit.stage-decision": ["work-unit.spec-synthesis"],
30
31
  "work-unit.spec-synthesis": ["work-unit.prototype-design", "work-unit.technical-analysis", REPOSITORY_PREPARATION_WORK_UNIT],
@@ -124,6 +125,7 @@ function validateTicketReference(ref, trackerKind) {
124
125
  * by `validateWorkflowExecutionResult` before this function is called.
125
126
  */
126
127
  export function validateNextRoute(currentWorkUnit, nextRoute, decisionState, options = {}) {
128
+
127
129
  if (["work-unit.technical-analysis", TICKET_DECOMPOSITION_WORK_UNIT, IMPLEMENTATION_WORK_UNIT, "work-unit.frontend-implementation-verification"].includes(nextRoute)) {
128
130
  try { enforceFrontendDelivery(decisionState, { root: options.root, phase: nextRoute === IMPLEMENTATION_WORK_UNIT ? "implementation" : "inputs" }); }
129
131
  catch (error) { return blockedResult(["frontend-delivery-blocked"], [error.message]); }
@@ -141,6 +143,10 @@ export function validateNextRoute(currentWorkUnit, nextRoute, decisionState, opt
141
143
  }
142
144
  return blockedResult(signals, ["allowed_next_route"]);
143
145
  }
146
+ if (nextRoute === 'work-unit.spec-synthesis' || currentWorkUnit === 'work-unit.spec-synthesis') {
147
+ const entry = validatePlanSpecEntry(decisionState, options);
148
+ if (entry.result === 'blocked' || nextRoute === 'work-unit.spec-synthesis') return entry;
149
+ }
144
150
  if (nextRoute === TICKET_DECOMPOSITION_WORK_UNIT) {
145
151
  const readiness = validateImplementationRepositoriesReady(decisionState, options);
146
152
  if (readiness.result === "blocked") return readiness;
@@ -0,0 +1,87 @@
1
+ import path from 'node:path';
2
+ import { verifyContextReconciliation } from './context-reconciliation.mjs';
3
+ import { decisionIO, decisionDigest, assertUserDecisionRequirement } from './user-decision.mjs';
4
+ import { validateApprovalRecord } from './approval-record.mjs';
5
+
6
+ const fail = message => { throw new TypeError(`plan-spec-entry-blocked: ${message}`); };
7
+ const text = value => typeof value === 'string' && value.trim().length > 0;
8
+
9
+ export function planEntryPolicy(options = {}) {
10
+ const registry = decisionIO(options).document('docs/process/lifecycle-registry.yaml');
11
+ const policy = registry.stages.find(stage => stage.id === 'stage.plan')?.spec_entry;
12
+ if (!policy?.required_checks?.length || !policy?.gate_impacts) fail('缺少 Plan 入口策略');
13
+ const gateIds = registry.gates.filter(gate => gate.stage === 'stage.plan').map(gate => gate.id).sort();
14
+ if (JSON.stringify(gateIds) !== JSON.stringify(Object.keys(policy.gate_impacts).sort())) fail('Plan 门禁触发策略覆盖不完整');
15
+ return policy;
16
+ }
17
+
18
+ // 自动加载只产生 pending;持久化审阅包、证据新鲜度和真实回复共同决定放行。
19
+ export function assertPlanSpecEntry(state, options = {}) {
20
+ if (!text(state?.plan_review_ref) || !text(state?.feature_id)) fail('缺少 plan_review_ref / feature_id');
21
+ const io = decisionIO(options);
22
+ const policy = planEntryPolicy(options);
23
+ const review = io.document(state.plan_review_ref);
24
+ if (review.schema_version !== 1 || review.kind !== 'plan-entry-review' || review.feature_id !== state.feature_id) fail('审阅包身份或范围不匹配');
25
+ if (!Array.isArray(review.basis) || !review.basis.length) fail('缺少带摘要依据');
26
+ const basis = new Map();
27
+ for (const asset of review.basis) {
28
+ if (!text(asset.ref) || basis.has(asset.ref) || decisionDigest(io.bytes(asset.ref)) !== asset.digest) fail(`依据缺失、重复或已变更: ${asset.ref}`);
29
+ basis.set(asset.ref, asset);
30
+ }
31
+ const evidence = refs => {
32
+ if (!Array.isArray(refs) || !refs.length || refs.some(ref => !basis.has(ref))) fail('检查项缺少当前依据');
33
+ };
34
+ for (const ref of [review.plan_ref, 'CONTEXT.md', 'docs/process/lifecycle-registry.yaml', review.context_reconciliation_ref]) {
35
+ if (!basis.has(ref)) fail(`必需依据未绑定: ${ref}`);
36
+ }
37
+ if (Object.keys(review.checks || {}).length !== policy.required_checks.length) fail('检查项缺失或存在未知项');
38
+ for (const id of policy.required_checks) {
39
+ const check = review.checks[id];
40
+ if (check?.status !== 'passed') fail(`检查未通过: ${id}`);
41
+ evidence(check.evidence_refs);
42
+ }
43
+ if (!Array.isArray(review.open_items)) fail('必须显式列出未决项');
44
+ for (const item of review.open_items) {
45
+ if (!text(item.id) || typeof item.critical !== 'boolean' || typeof item.runnable_blocker !== 'boolean') fail('未决项分类不完整');
46
+ if (item.status === 'resolved') { evidence(item.evidence_refs); continue; }
47
+ if (item.status !== 'deferred' || item.critical || item.runnable_blocker) fail(`关键问题或可执行阻塞未解决: ${item.id}`);
48
+ for (const field of ['noncritical_reason', 'owner', 'resolution_point', 'downstream_recipient']) if (!text(item[field])) fail(`延期项缺少 ${field}`);
49
+ evidence(item.evidence_refs);
50
+ }
51
+ for (const [gateId, impact] of Object.entries(policy.gate_impacts)) {
52
+ if (typeof review.impacts?.[impact] !== 'boolean') fail(`未评估影响面: ${impact}`);
53
+ const gate = review.gates?.[gateId];
54
+ evidence(gate?.evidence_refs);
55
+ if (review.impacts[impact]) {
56
+ if (gate.status !== 'approved' || !basis.has(gate.approval_ref) || !basis.has(gate.subject_ref)) fail(`命中门禁未批准或未绑定依据: ${gateId}`);
57
+ const record = io.document(gate.approval_ref);
58
+ if (record.gate_id !== gateId || record.subject_ref !== gate.subject_ref || !gate.approval_scope?.includes(state.feature_id) || JSON.stringify([...(gate.approval_scope || [])].sort()) !== JSON.stringify([...(record.approval_scope || [])].sort())) fail(`会签资产或范围不匹配: ${gateId}`);
59
+ validateApprovalRecord(record, { ...options, requireApproved: true });
60
+ assertUserDecisionRequirement({ boundary: gateId, subject_ref: gate.subject_ref, scope: gate.approval_scope, user_decision_ref: record.user_decision_ref }, options);
61
+ } else if (gate.status !== 'not-applicable' || !text(gate.reason)) fail(`未命中门禁须有原因和依据: ${gateId}`);
62
+ }
63
+ // 门禁依赖不能通过将上游标成 N/A 来跳过。
64
+ const registry = io.document('docs/process/lifecycle-registry.yaml');
65
+ for (const gate of registry.gates.filter(gate => gate.stage === 'stage.plan')) {
66
+ if (review.gates[gate.id]?.status === 'approved') {
67
+ for (const dependency of gate.requires_gates || []) if (review.gates[dependency]?.status !== 'approved') fail(`门禁依赖未批准: ${dependency}`);
68
+ }
69
+ }
70
+ const reconciliation = io.document(review.context_reconciliation_ref);
71
+ if (reconciliation.status !== 'reconciled' || reconciliation.repository_mode !== 'project-instance') fail('Context 未调和');
72
+ try { verifyContextReconciliation(path.resolve(io.root, review.context_reconciliation_ref), { root: io.root }); }
73
+ catch (error) { fail(`Context reconciliation 验证失败: ${error.message}\n`); }
74
+ assertUserDecisionRequirement({ boundary: 'plan-conclusion', subject_ref: state.plan_review_ref, scope: [state.feature_id], user_decision_ref: state.plan_user_decision_ref }, options);
75
+ return { result: 'allowed', blocking_signals: [], missing_requirements: [], evidence_refs: [state.plan_review_ref, ...basis.keys()], next_work_unit: null };
76
+ }
77
+
78
+ export function validatePlanSpecEntry(state, options = {}) {
79
+ try { return assertPlanSpecEntry(state, options); }
80
+ catch (error) { return { result: 'blocked', blocking_signals: ['plan-spec-entry-blocked'], missing_requirements: [error.message], evidence_refs: [], next_work_unit: null }; }
81
+ }
82
+
83
+ export function assertPlanCheckpoint(checkpoint, options = {}) {
84
+ if (checkpoint.repository_mode !== 'project-instance') return;
85
+ const entering = checkpoint.next_work_unit === 'work-unit.spec-synthesis' || checkpoint.stage === 'stage.spec-architecture' || checkpoint.stage_trace?.completed_work_unit === 'work-unit.spec-synthesis';
86
+ if (entering) assertPlanSpecEntry(checkpoint, options);
87
+ }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, mkdtempSync, rmSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { buildPlanFixture } from "../fixtures/user-decision/plan-fixture.mjs";
3
4
  import os from "node:os";
4
5
  import { buildDecisionFixture, buildImplementationFixture } from "../fixtures/user-decision/build-fixture.mjs";
5
6
  import { fileURLToPath } from "node:url";
@@ -58,8 +59,8 @@ function validateInvocationBoundary(data) {
58
59
  ensure(native?.default_entry === "yss-product-lifecycle" && native?.formal_artifact_owner === "yss-product-lifecycle", "生命周期原生入口未持有默认正式资产所有权");
59
60
  ensure(native?.user_confirmation_policy_ref === "docs/agents/digital-human-roles.yaml#user_decision_policy", "生命周期人工门禁集合已漂移");
60
61
  const routes = data.work_unit_routes;
61
- ensure(routes?.["work-unit.discovery-requirements"]?.skills?.includes("grilling") && routes?.["work-unit.discovery-requirements"]?.skills?.includes("domain-modeling"), "需求分析工作单元缺少 grilling/domain-modeling");
62
- ensure(routes?.["work-unit.discovery-opportunity"]?.route_by?.market_or_competitor_fact === "competitive-intelligence" && routes["work-unit.discovery-opportunity"].route_by.technical_or_standard_fact === "yss-research:technical-evidence" && routes["work-unit.discovery-opportunity"].route_by.strategy_fact === "yss-research:strategy-evidence", "机会调研事实路由不准确");
62
+ ensure(routes?.["work-unit.plan-requirements"]?.skills?.includes("grilling") && routes?.["work-unit.plan-requirements"]?.skills?.includes("domain-modeling"), "需求分析工作单元缺少 grilling/domain-modeling");
63
+ ensure(routes?.["work-unit.plan-opportunity"]?.route_by?.market_or_competitor_fact === "competitive-intelligence" && routes["work-unit.plan-opportunity"].route_by.technical_or_standard_fact === "yss-research:technical-evidence" && routes["work-unit.plan-opportunity"].route_by.strategy_fact === "yss-research:strategy-evidence", "机会调研事实路由不准确");
63
64
  const strategyResearch = routes?.["work-unit.domain-strategy-design"]?.research_contract;
64
65
  ensure(strategyResearch?.profile === "strategy-evidence" && strategyResearch.mode_before_gate === "evidence-audited" && strategyResearch.artifact_owner === "yss-research" && strategyResearch.downstream_owner === "yss-stage-decision", "领域战略研究合同缺少 profile、门禁前审计或资产所有权边界");
65
66
  const stageDecisionResearch = routes?.["work-unit.stage-decision"]?.research_contract;
@@ -94,7 +95,7 @@ function validateInvocationBoundary(data) {
94
95
  }
95
96
  }
96
97
 
97
- function validateWorkflowExecutionResult(payload, contract, workUnitRoutes) {
98
+ function validateWorkflowExecutionResult(payload, contract, workUnitRoutes, options = {}) {
98
99
  for (const field of contract.required) ensure(Object.hasOwn(payload, field), `Workflow Execution Result 缺少 ${field}`);
99
100
  ensure(contract.result_values.includes(payload.result), "Workflow Execution Result result 无效");
100
101
  if (Object.hasOwn(payload, "unavailable_skill")) {
@@ -107,7 +108,7 @@ function validateWorkflowExecutionResult(payload, contract, workUnitRoutes) {
107
108
  const workUnit = workUnitRoutes?.[payload.work_unit];
108
109
  ensure(workUnit, `未知 Workflow Execution Result work_unit: ${payload.work_unit}`);
109
110
  if (payload.result === "completed") {
110
- const routeResult = validateNextRoute(payload.work_unit, payload.next_route, payload);
111
+ const routeResult = validateNextRoute(payload.work_unit, payload.next_route, payload, options);
111
112
  ensure(routeResult.result === "allowed", `Workflow Execution Result next_route 非法: ${routeResult.blocking_signals.join(", ")}`);
112
113
  ensure(payload.context_reconciliation?.status === "reconciled", "project-instance 完成态必须具有 reconciled context_reconciliation");
113
114
  ensure(hasText(payload.context_reconciliation?.ref) && payload.evidence_refs.includes(payload.context_reconciliation.ref) && exists(payload.context_reconciliation.ref), "context_reconciliation.ref 必须可读并包含在 evidence_refs 中");
@@ -268,7 +269,9 @@ export function runScenario(name) {
268
269
  process.on("exit", () => rmSync(decisionTemp, { recursive: true, force: true }));
269
270
  const specDecision = buildDecisionFixture(path.join(decisionTemp, "spec"));
270
271
  const implementationDecision = buildImplementationFixture(path.join(decisionTemp, "implementation"), "docs/.scratch/demo/issues/01-valid-slice.md");
272
+ const planFixture = buildPlanFixture(path.join(decisionTemp, 'plan'));
271
273
  const validResult = {
274
+ ...planFixture.state,
272
275
  user_decisions: [specDecision.requirement],
273
276
  result_schema: "workflow-execution-result-v1",
274
277
  work_unit: "work-unit.spec-synthesis",
@@ -282,7 +285,7 @@ export function runScenario(name) {
282
285
  next_route: "work-unit.implementation-repository-preparation",
283
286
  blocking_signals: []
284
287
  };
285
- validateWorkflowExecutionResult(validResult, data.workflow_execution_result, data.work_unit_routes);
288
+ validateWorkflowExecutionResult(validResult, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root });
286
289
  const validTicketResult = {
287
290
  ...validResult,
288
291
  work_unit: "work-unit.ticket-decomposition",
@@ -290,7 +293,7 @@ export function runScenario(name) {
290
293
  ticket_decomposition_result_ref: "docs/.scratch/demo/evidence/ticket-decomposition-result.yaml",
291
294
  vertical_slice_ticket_ref: "docs/.scratch/demo/issues/01-valid-slice.md",
292
295
  };
293
- validateWorkflowExecutionResult(validTicketResult, data.workflow_execution_result, data.work_unit_routes);
296
+ validateWorkflowExecutionResult(validTicketResult, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root });
294
297
  const validImplementationResult = {
295
298
  ...validResult,
296
299
  ...implementationDecision.state,
@@ -317,7 +320,7 @@ export function runScenario(name) {
317
320
  ],
318
321
  },
319
322
  };
320
- validateWorkflowExecutionResult(validImplementationResult, data.workflow_execution_result, data.work_unit_routes);
323
+ validateWorkflowExecutionResult(validImplementationResult, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root });
321
324
  for (const mutate of [
322
325
  (item) => { item.vertical_slice_ticket_role = "ready-for-human"; },
323
326
  (item) => { item.vertical_slice_ticket_kind = "parent-ticket"; },
@@ -329,22 +332,22 @@ export function runScenario(name) {
329
332
  ]) {
330
333
  const invalid = structuredClone(validImplementationResult); mutate(invalid);
331
334
  let rejected = false;
332
- try { validateWorkflowExecutionResult(invalid, data.workflow_execution_result, data.work_unit_routes); } catch { rejected = true; }
335
+ try { validateWorkflowExecutionResult(invalid, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root }); } catch { rejected = true; }
333
336
  ensure(rejected, "实现 Workflow Execution Result 的 Ticket 语义变异未被拒绝");
334
337
  }
335
338
  const unavailableResult = structuredClone(validResult);
336
339
  unavailableResult.result = "blocked";
337
340
  unavailableResult.unavailable_skill = { skill: "yss-ui", provider: "codex", fallback: "manual-review", resolution: "needs-human" };
338
341
  unavailableResult.blocking_signals = ["missing_evidence"];
339
- validateWorkflowExecutionResult(unavailableResult, data.workflow_execution_result, data.work_unit_routes);
342
+ validateWorkflowExecutionResult(unavailableResult, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root });
340
343
  const malformedUnavailable = structuredClone(unavailableResult);
341
344
  delete malformedUnavailable.unavailable_skill.fallback;
342
345
  let unavailableRejected = false;
343
- try { validateWorkflowExecutionResult(malformedUnavailable, data.workflow_execution_result, data.work_unit_routes); } catch { unavailableRejected = true; }
346
+ try { validateWorkflowExecutionResult(malformedUnavailable, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root }); } catch { unavailableRejected = true; }
344
347
  ensure(unavailableRejected, "技能不可用结果缺少 fallback 时未被拒绝");
345
348
  const compatibleResult = structuredClone(validResult);
346
349
  compatibleResult.workflow_reference = { source: "mattpocock/skills", skill: "to-spec", invocation_mode: "reference" };
347
- validateWorkflowExecutionResult(compatibleResult, data.workflow_execution_result, data.work_unit_routes);
350
+ validateWorkflowExecutionResult(compatibleResult, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root });
348
351
  for (const mutate of [
349
352
  (item) => { delete item.workflow_reference; },
350
353
  (item) => { delete item.context_reconciliation; },
@@ -360,7 +363,7 @@ export function runScenario(name) {
360
363
  ]) {
361
364
  const invalid = structuredClone(validResult); mutate(invalid);
362
365
  let rejected = false;
363
- try { validateWorkflowExecutionResult(invalid, data.workflow_execution_result, data.work_unit_routes); } catch { rejected = true; }
366
+ try { validateWorkflowExecutionResult(invalid, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root }); } catch { rejected = true; }
364
367
  ensure(rejected, "Workflow Execution Result 完成态变异未被拒绝");
365
368
  }
366
369
  let metadataRejected = false;
@@ -117,7 +117,16 @@ function loadStrategicDesignSkillManifest() {
117
117
  return validateStrategicDesignSkillManifest(JSON.parse(readFileSync(STRATEGIC_DESIGN_MANIFEST_PATH, "utf8")));
118
118
  }
119
119
  function git(args) { return spawnSync("git", args, { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); }
120
- function tracked(relativePath) { return git(["ls-files", "-z", "--", relativePath]).stdout.length > 0; }
120
+ function trackedPaths() {
121
+ const result = git(["ls-files", "-z"]);
122
+ if (result.status !== 0) throw new TypeError("无法读取 Git 受跟踪路径");
123
+ const paths = new Set();
124
+ for (const file of result.stdout.split("\0").filter(Boolean)) {
125
+ let ref = file;
126
+ while (ref !== ".") { paths.add(ref); ref = path.posix.dirname(ref); }
127
+ }
128
+ return paths;
129
+ }
121
130
  function parseLock() { return existsSync(LOCK_PATH) ? JSON.parse(readFileSync(LOCK_PATH, "utf8")) : null; }
122
131
  function sharedFromLock(lock) {
123
132
  const shared = lock?.version === 3 && lock.skills?.shared;
@@ -137,6 +146,13 @@ export function unlockedCanonicalEntries(names, allowedNames, hasSkillMd = () =>
137
146
  return names.filter((name) => !allowed.has(name) && !OBSOLETE.has(name) && hasSkillMd(name)).sort();
138
147
  }
139
148
  export function syncSkills({ check = false } = {}) {
149
+ let trackedSet;
150
+ const tracked = ref => (trackedSet ??= trackedPaths()).has(ref);
151
+ const sourceHashes = new Map();
152
+ const sourceHash = source => {
153
+ if (!sourceHashes.has(source)) sourceHashes.set(source, treeHash(source));
154
+ return sourceHashes.get(source);
155
+ };
140
156
  const lock = parseLock();
141
157
  const shared = sharedFromLock(lock);
142
158
  const absent = shared.filter((name) => !lstatSafe(path.join(SOURCE_ROOT, name))?.isDirectory());
@@ -162,7 +178,7 @@ export function syncSkills({ check = false } = {}) {
162
178
  if (info?.isSymbolicLink()) {
163
179
  if (!existsSync(target) || realpathSync(target) !== realpathSync(source)) drift.push(`projection target mismatch: ${relative(target)}`);
164
180
  } else if (!info?.isDirectory()) drift.push(`missing projection: ${relative(target)}`);
165
- else if (treeHash(source) !== treeHash(target)) drift.push(`projection drift: ${relative(target)}`);
181
+ else if (sourceHash(source) !== treeHash(target)) drift.push(`projection drift: ${relative(target)}`);
166
182
  } else if (info?.isSymbolicLink() && existsSync(target) && realpathSync(target) === realpathSync(source)) {
167
183
  continue;
168
184
  } else {
@@ -3,6 +3,7 @@ import { existsSync, lstatSync, readdirSync, readFileSync, mkdirSync, writeFileS
3
3
  import path from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import { validateJsonSchemas } from './json-schema.mjs';
6
7
  import { parseDocument } from '../vendor/yaml.mjs';
7
8
  export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
8
9
  export const ensure = (condition, message) => { if (!condition) throw new TypeError(message); };
@@ -50,11 +51,11 @@ export function project(root) {
50
51
  ensure(value.schema_version === 1 && value.repository_mode === 'project-instance', '只允许 project-instance 导出或导入业务包');
51
52
  return realpathSync(root);
52
53
  }
53
- export function schema(value, ref) {
54
- const program = 'import json,sys; from jsonschema import Draft202012Validator; s=json.load(open(sys.argv[1])); v=json.load(sys.stdin); errors=list(Draft202012Validator(s).iter_errors(v)); print("\\n".join(str(e) for e in errors)); sys.exit(bool(errors))';
55
- const out = spawnSync('python3', ['-c', program, safe(ROOT, ref)], { input: JSON.stringify(value), encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 });
56
- ensure(out.status === 0, `schema 校验失败: ${out.error?.message || out.stdout || out.stderr}`);
54
+ export function schemaBatch(items) {
55
+ const results = validateJsonSchemas(items.map(([value, ref]) => ({ value, schemaPath: safe(ROOT, ref), formatChecker: false, errorStyle: 'verbose' })));
56
+ for (const result of results) ensure(result.valid, `schema 校验失败: ${result.error}\n`);
57
57
  }
58
+ export function schema(value, ref) { schemaBatch([[value, ref]]); }
58
59
  export function archive(command, source, destination) {
59
60
  const out = spawnSync('python3', [path.join(ROOT, 'scripts/lib/strategic-handoff-zip.py'), command, source, destination], { encoding: 'utf8' });
60
61
  ensure(out.status === 0, `ZIP 操作失败: ${out.error?.message || out.stderr}`);
@@ -1,7 +1,7 @@
1
1
  import { existsSync, lstatSync, readFileSync, mkdirSync, mkdtempSync, renameSync, rmSync, cpSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { tmpdir } from 'node:os';
4
- import { ensure, canonical, digest, hash, json, parse, read, safe, relative, files, write, project, schema, archive, ROOT, sourceApprovalPolicy } from './strategic-handoff-io.mjs';
4
+ import { ensure, canonical, digest, hash, json, parse, read, safe, relative, files, write, project, schema, schemaBatch, archive, ROOT, sourceApprovalPolicy } from './strategic-handoff-io.mjs';
5
5
  import { parseContextSource, parseContextContract, resolveContextTermRefs } from './context-contract.mjs';
6
6
  import { countersignRuleForGate } from './digital-human-roles.mjs';
7
7
  import { validateApprovalRecord } from './approval-record.mjs';
@@ -91,8 +91,7 @@ export async function inspectSource(root, handoffRef) {
91
91
  snapshot(handoff.source_context_snapshot,source); checkDelta(handoff,source);
92
92
  const strategy=read(safe(root,handoff.source.domain_strategy_ref.persisted_ref));
93
93
  const stage=read(safe(root,handoff.source.stage_decision_package_ref.persisted_ref));
94
- schema(strategy,'docs/process/schemas/strategic-handoff-domain-strategy.schema.json');
95
- schema(stage,'docs/process/schemas/strategic-handoff-stage-decision-package.schema.json');
94
+ schemaBatch([[strategy,'docs/process/schemas/strategic-handoff-domain-strategy.schema.json'],[stage,'docs/process/schemas/strategic-handoff-stage-decision-package.schema.json']]);
96
95
  snapshot(strategy.context_snapshot,source); snapshot(stage.context_snapshot,source);
97
96
  ensure(strategy.status==='approved' && stage.status==='approved', '战略合同/方案决策包尚未批准');
98
97
  ensure(strategy.domain_strategy_id===handoff.source.domain_strategy_ref.id && strategy.domain_version===handoff.source.domain_strategy_ref.version, '战略身份/版本不一致');
@@ -141,21 +140,22 @@ function collect(root, handoffRef, handoff, config) {
141
140
  if(Array.isArray(value)) { if(key==='evidence_refs') value.forEach(enqueue); else value.forEach(v=>refs(v,key)); }
142
141
  else if(value && typeof value==='object') for(const [k,v]of Object.entries(value)) { if(['approval_ref','persisted_ref','user_decision_ref'].includes(k)&&typeof v==='string')enqueue(v); else if(k==='ref'&&typeof v==='string'&&!/^[a-z]+:\/\//i.test(v))enqueue(v); else refs(v,k); }
143
142
  }
143
+ let totalBytes = 0;
144
144
  while(queue.length) {
145
145
  const ref=symbolic[queue[0]]||queue[0]; queue.shift(); if(/^https?:\/\//i.test(ref))continue; relative(ref);
146
146
  if(collected.has(ref))continue;
147
147
  const full=safe(root,ref),stat=lstatSync(full);
148
148
  if(stat.isDirectory()) { queue.push(...files(root,ref)); continue; }
149
149
  ensure(stat.isFile() && stat.size<=MAX_BYTES,`源文件类型/大小无效: ${ref}`);
150
- const bytes=readFileSync(full); collected.set(ref,bytes);
151
- ensure(collected.size<=20000 && [...collected.values()].reduce((n,b)=>n+b.length,0)<=MAX_BYTES,'交接包大小超限');
150
+ const bytes=readFileSync(full); collected.set(ref,bytes); totalBytes += bytes.length;
151
+ ensure(collected.size<=20000 && totalBytes<=MAX_BYTES,'交接包大小超限');
152
152
  if(/\.(yaml|yml|json)$/.test(ref))refs(parse(bytes));
153
153
  queue.push(...documentLinks(ref,bytes));
154
154
  }
155
155
  return collected;
156
156
  }
157
- function materialize(bundleRoot, manifest, destination) {
158
- for(const file of manifest.files.filter(x=>x.original_ref))write(destination,file.original_ref,readFileSync(safe(bundleRoot,file.path)));
157
+ function materialize(captured, manifest, destination) {
158
+ for(const file of manifest.files.filter(x=>x.original_ref))write(destination,file.original_ref,captured.get(file.path));
159
159
  }
160
160
  export async function openBundle(input, action) {
161
161
  const temp=mkdtempSync(path.join(tmpdir(),'yss-handoff-'));
@@ -170,10 +170,11 @@ export async function openBundle(input, action) {
170
170
  ensure(new Set(paths.map(x=>x.toLowerCase())).size===paths.length,'包文件路径重复');
171
171
  ensure(own([...paths,'manifest.json'].sort(),files(root).sort()),'包文件缺失或存在未登记文件');
172
172
  ensure(manifest.files.length<=20000 && manifest.files.reduce((n,f)=>n+f.size_bytes,0)<=MAX_BYTES,'包大小超限');
173
- for(const file of manifest.files){const bytes=readFileSync(safe(root,file.path));ensure(bytes.length===file.size_bytes && hash(bytes)===file.sha256,`文件摘要不一致: ${file.path}`);if(file.original_ref)relative(file.original_ref);}
173
+ const captured = new Map();
174
+ for(const file of manifest.files){const bytes=readFileSync(safe(root,file.path));ensure(bytes.length===file.size_bytes && hash(bytes)===file.sha256,`文件摘要不一致: ${file.path}`);if(file.original_ref)relative(file.original_ref);captured.set(file.path,bytes);}
174
175
  const originals=manifest.files.filter(x=>x.original_ref).map(x=>x.original_ref);
175
176
  ensure(new Set(originals.map(x=>x.toLowerCase())).size===originals.length,'源路径重复');
176
- const source=path.join(temp,'source');mkdirSync(source);materialize(root,manifest,source);
177
+ const source=path.join(temp,'source');mkdirSync(source);materialize(captured,manifest,source);
177
178
  const inspected=await inspectSource(source,manifest.handoff_ref);
178
179
  ensure(manifest.bundle_id===inspected.handoff.handoff_id && manifest.version===inspected.handoff.handoff_version,'包身份与源交接不一致');
179
180
  ensure(own(read(safe(root,'indexes/rules.json')),inspected.indexes.rules) && own(read(safe(root,'indexes/scenarios.json')),inspected.indexes.scenarios),'规则/场景索引与源资产不一致');
@@ -6,6 +6,7 @@ import { loadRegistry, ROOT } from "./lifecycle-registry.mjs";
6
6
  import { loadMaintenanceCheckpoint, validateMaintenanceCheckpoint } from "./maintenance-intensity.mjs";
7
7
  import { assertImplementationDecision } from "./user-decision.mjs";
8
8
  import { validateNextRoute } from "./lifecycle-transition.mjs";
9
+ import { assertPlanSpecEntry } from "./plan-spec-entry.mjs";
9
10
  import { enforceHarnessTaskScope } from "./harness-execution-scope.mjs";
10
11
  import { enforceFrontendDelivery } from "./frontend-delivery-boundary.mjs";
11
12
  import { LEGACY_TASK_PACKAGE_SCHEMA, TASK_PACKAGE_SCHEMA, validateTaskPackageSchema } from "./task-package-schema.mjs";
@@ -59,6 +60,7 @@ function validateSkillSource(value, registry) {
59
60
  }
60
61
 
61
62
  function validateCommon(value, registry, lifecycle) {
63
+ if (value.work_unit_id === 'work-unit.spec-synthesis') assertPlanSpecEntry(value);
62
64
  const workUnit = lifecycle.work_units.find((item) => item.id === value.work_unit_id);
63
65
  if (!workUnit && value.contract.kind !== "slice-implementation") fail(`未知 work_unit_id: ${value.work_unit_id}`);
64
66
  const roleDefaults = taskPackageDefaults(value.role_id, registry);
@@ -1,3 +1,4 @@
1
+ import { runCommandSync } from "./command-runner.mjs";
1
2
  import { spawnSync } from "node:child_process";
2
3
  import { createHash } from "node:crypto";
3
4
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
@@ -7,13 +8,14 @@ import { ROOT } from "./template-verification.mjs";
7
8
 
8
9
  function fail(message) { throw new TypeError(message); }
9
10
  function ensure(condition, message) { if (!condition) fail(message); }
10
- function git(args, cwd) {
11
- const result = spawnSync("git", args, { cwd, encoding: "utf8" });
12
- if (result.status !== 0) fail(result.stderr.trim() || `git ${args.join(" ")} 执行失败`);
11
+ function git(args, cwd, timeoutMs = 0, cleanupPathsOnFailure = []) {
12
+ const result = args.includes("fetch") ? runCommandSync("git", args, { cwd, timeoutMs, progress: true, cleanupPathsOnFailure }) : spawnSync("git", args, { cwd, encoding: "utf8" });
13
+ if (result.status !== 0) fail(result.termination || result.error?.message || result.stderr.trim() || `git ${args.join(" ")} 执行失败`);
13
14
  return result.stdout.trim();
14
15
  }
15
16
 
16
- export function acquireTemplateCommit({ repository, commit, cacheRoot = path.join(ROOT, ".template-source/cache/remote-templates"), allowLocal = false }) {
17
+ export function acquireTemplateCommit({ repository, commit, cacheRoot = path.join(ROOT, ".template-source/cache/remote-templates"), allowLocal = false, timeoutMs = 0 }) {
18
+ ensure(Number.isFinite(timeoutMs) && timeoutMs >= 0, "timeoutMs 必须是非负毫秒数");
17
19
  ensure(typeof repository === "string" && repository.trim(), "repository 不能为空");
18
20
  ensure(/^[a-f0-9]{40}$/.test(commit), "模板 ref 必须是 40 位 commit");
19
21
  if (!allowLocal) ensure(/^(?:https?:\/\/|ssh:\/\/|git@)/.test(repository), "正式模板缓存只接受远程 repository URL");
@@ -35,7 +37,7 @@ export function acquireTemplateCommit({ repository, commit, cacheRoot = path.joi
35
37
  try {
36
38
  const temporaryRepository = path.join(temporary, "repository.git");
37
39
  git(["init", "--bare", "-q", temporaryRepository], ROOT);
38
- git(["--git-dir", temporaryRepository, "fetch", "--depth=1", "--no-tags", repository, commit], ROOT);
40
+ git(["--git-dir", temporaryRepository, "fetch", "--depth=1", "--no-tags", repository, commit], ROOT, timeoutMs, [temporary]);
39
41
  const actual = git(["--git-dir", temporaryRepository, "rev-parse", "FETCH_HEAD^{commit}"], ROOT);
40
42
  ensure(actual === commit, `远程返回 commit 与请求不一致: ${actual}`);
41
43
  writeFileSync(path.join(temporary, "metadata.json"), `${JSON.stringify({ schema_version: 1, key, repository, commit })}\n`);
@@ -23,12 +23,18 @@ function collectChangedFiles(base) {
23
23
  function runCommand(command) {
24
24
  return new Promise((resolve) => {
25
25
  const started = Date.now();
26
+ process.stderr.write(`[开始] ${command}\n`);
26
27
  const child = spawn(command, { cwd: ROOT, shell: true, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
27
28
  let stdout = "";
28
29
  let stderr = "";
29
30
  child.stdout.on("data", (chunk) => { stdout += chunk; });
30
31
  child.stderr.on("data", (chunk) => { stderr += chunk; });
31
- child.on("close", (code) => resolve({ command, code: code ?? 1, duration_ms: Date.now() - started, stdout, stderr }));
32
+ child.on("error", error => { stderr += `${error.message}\n`; });
33
+ child.on("close", (code) => {
34
+ const duration_ms = Date.now() - started;
35
+ process.stderr.write(`[${code === 0 ? "完成" : "失败"}] ${command} (${duration_ms}ms, exit=${code ?? 1})\n`);
36
+ resolve({ command, code: code ?? 1, duration_ms, stdout, stderr });
37
+ });
32
38
  });
33
39
  }
34
40
 
@@ -106,8 +112,8 @@ try {
106
112
  const concurrency = Number(values.concurrency || plan.max_concurrency);
107
113
  if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 4) throw new TypeError("concurrency 必须为 1..4");
108
114
 
109
- process.stdout.write(`模板核验 profile: ${plan.requested_profile} -> ${plan.effective_profile}\n`);
110
- if (plan.escalation_reason) process.stdout.write(`自动升级原因: ${plan.escalation_reason}\n`);
115
+ process.stderr.write(`模板核验 profile: ${plan.requested_profile} -> ${plan.effective_profile}\n`);
116
+ if (plan.escalation_reason) process.stderr.write(`自动升级原因: ${plan.escalation_reason}\n`);
111
117
  const groups = await runGroups(plan, modeResult.stdout.trim(), concurrency);
112
118
  printResults(groups);
113
119
  const failed = groups.flatMap((group) => group.results).find((result) => result.code !== 0);
@@ -7,10 +7,19 @@ const root=path.resolve(import.meta.dirname,'..');
7
7
  const check=process.argv.includes('--check');
8
8
  const targets=['submodules/yss-harness-design-agent','submodules/yss-harness-dev-agent','submodules/yss-harness-backend-agent','submodules/yss-harness-frontend-agent'];
9
9
  const shared=['scripts/strategic-handoff','scripts/verify-strategic-handoff-consumption','scripts/verify-strategic-handoff-package-scenarios','scripts/verify-strategic-context-import','scripts/verify-strategic-context-import-scenarios','scripts/lib/context-contract.mjs','docs/process/strategic-handoff-package.md','docs/process/schemas/strategic-design-handoff.schema.json',...readdirSync(path.join(root,'scripts/lib')).filter(x=>x.startsWith('strategic-handoff-')||x==='strategic-handoff.mjs').map(x=>`scripts/lib/${x}`),...readdirSync(path.join(root,'docs/process/schemas')).filter(x=>x.startsWith('strategic-handoff-')).map(x=>`docs/process/schemas/${x}`),...readdirSync(path.join(root,'scripts/fixtures/strategic-handoff')).map(x=>`scripts/fixtures/strategic-handoff/${x}`)];
10
+ shared.push('scripts/lib/command-runner.mjs','scripts/lib/json-schema.mjs','scripts/lib/context-reconciliation.mjs','scripts/verify-context-reconciliation');
10
11
  shared.push('scripts/instantiate-harness','scripts/backend-delivery','scripts/verify-frontend-delivery','scripts/lib/backend-delivery.mjs','scripts/lib/frontend-delivery.mjs','scripts/lib/frontend-delivery-boundary.mjs','docs/process/frontend-backend-delivery.md','docs/process/schemas/backend-delivery.schema.json','docs/process/schemas/backend-delivery-verification.schema.json','docs/process/schemas/frontend-delivery-acceptance.schema.json');
11
12
  let differences=[];
12
13
  // 专职执行范围由各模板持有;后端只消费 wire 校验器,不恢复原型构建技能。
13
14
  function sync(source,dest){let bytes=readFileSync(source);if(dest.includes('/yss-harness-backend-agent/')){let content=bytes.toString();if(dest.endsWith('/scripts/lib/strategic-handoff.mjs'))content=content.replace('../../.agents/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs','./visual-baseline-contract.mjs');if(dest.endsWith('/scripts/fixtures/strategic-handoff/fixture.mjs'))content=content.replace('../../../.agents/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs','../../lib/visual-baseline-contract.mjs');bytes=Buffer.from(content);}if(!existsSync(dest)||!bytes.equals(readFileSync(dest))){differences.push(path.relative(root,dest));if(!check){mkdirSync(path.dirname(dest),{recursive:true});writeFileSync(dest,bytes);}}if(!check&&path.basename(path.dirname(dest))==='scripts'&&!path.extname(dest))chmodSync(dest,0o755);}
15
+ // CLI core owns the portable external-command runner; all runtime copies are generated here.
16
+ sync(path.join(root,'.template-source/cli-core/command-runner.mjs'),path.join(root,'scripts/lib/command-runner.mjs'));
17
+ for(const name of ['create-yss-spec','create-yss-strategic-design','create-yss-harness-dev']) {
18
+ const target=`submodules/${name}`;
19
+ if(!existsSync(path.join(root,target,'.git')))continue;
20
+ if(!check){const state=inspectWorkingTreeScope(root,{project_root:target,repository_scope:'git-submodule',checkout_state:'attached-branch'});if(!state.writable)throw new Error(`${target}: ${state.violation}`);}
21
+ sync(path.join(root,'.template-source/cli-core/command-runner.mjs'),path.join(root,target,'src/command-runner.mjs'));
22
+ }
14
23
  for(const kind of ['domain-strategy','stage-decision-package'])sync(path.join(root,`.agents/skills/yss-stage-decision/references/${kind}.schema.json`),path.join(root,`docs/process/schemas/strategic-handoff-${kind}.schema.json`));
15
24
  for(const target of targets){
16
25
  if(!existsSync(path.join(root,target,'.git')))continue;