@dawipong/opcflow 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -735,6 +735,7 @@ __export(kind_exports, {
735
735
  expandPattern: () => expandPattern,
736
736
  getKindRegistry: () => getKindRegistry,
737
737
  inferKind: () => inferKind,
738
+ kindPathTemplate: () => kindPathTemplate,
738
739
  kindSpec: () => kindSpec,
739
740
  normalizeModule: () => normalizeModule
740
741
  });
@@ -766,6 +767,14 @@ function contractKinds(config) {
766
767
  function expandPattern(pattern, config) {
767
768
  return pattern.replace("{prd}", config.docs.prd).replace("{architecture}", config.docs.architecture).replace("{design}", config.docs.design).replace("{acceptance}", config.docs.acceptance);
768
769
  }
770
+ function kindPathTemplate(config, kind, lang) {
771
+ const spec = getKindRegistry(config)[kind];
772
+ if (!spec?.coords || !spec.pathPatterns?.[0]) return null;
773
+ const prefix = expandPattern(spec.pathPatterns[0], config);
774
+ const ext = spec.ext ?? "md";
775
+ const segs = spec.coords.split("/").map((s) => lang === "zh" ? COORD_LABELS_ZH[s] ?? s : s);
776
+ return `${prefix}${segs.join("/")}.${ext}`;
777
+ }
769
778
  function inferKind(relPath, config) {
770
779
  const p = relPath.replace(/\\/g, "/");
771
780
  const registry = getKindRegistry(config);
@@ -783,7 +792,7 @@ function normalizeModule(module, config) {
783
792
  if (!module) return null;
784
793
  return config.moduleMapping[module] ?? module;
785
794
  }
786
- var DEFAULT_KIND_REGISTRY, META_KINDS, PM_KINDS, KIND_TIERS;
795
+ var DEFAULT_KIND_REGISTRY, META_KINDS, PM_KINDS, KIND_TIERS, COORD_LABELS_ZH;
787
796
  var init_kind = __esm({
788
797
  "core/kind.ts"() {
789
798
  "use strict";
@@ -805,7 +814,7 @@ var init_kind = __esm({
805
814
  "api-doc": { level: "module", approval: "human", parents: ["module-prd", "db-doc"], drivesStale: true, hashMode: "text-normalize", retrieval: "full", pathPatterns: ["{architecture}/api/"], coords: "{module}", defaultEndpoint: "service" },
806
815
  "design-system": { level: "endpoint", approval: "human", parents: ["baseline"], drivesStale: true, hashMode: "text-normalize", retrieval: "full", pathPatterns: ["{design}/systems/"], coords: "{endpoint}" },
807
816
  "design-prompt": { level: "page", approval: "none", parents: ["page-prd", "api-doc", "design-system"], drivesStale: false, hashMode: "text-normalize", retrieval: "summary", pathPatterns: ["{design}/prompts/"], coords: "{endpoint}/{module}/{page}" },
808
- prototype: { level: "page", approval: "thumbs", parents: ["design-prompt", "design-system"], drivesStale: true, hashMode: "text-normalize", retrieval: "summary", pathPatterns: ["{design}/prototypes/"], coords: "{endpoint}/{module}/{page}" },
817
+ prototype: { level: "page", approval: "thumbs", parents: ["design-prompt", "design-system"], drivesStale: true, hashMode: "text-normalize", retrieval: "summary", pathPatterns: ["{design}/prototypes/"], coords: "{endpoint}/{module}/{page}", ext: "html" },
809
818
  acceptance: { level: "page", approval: "human", parents: ["page-prd", "prototype"], drivesStale: true, hashMode: "text-normalize", retrieval: "full", pathPatterns: ["{acceptance}/"], coords: "{endpoint}/{module}/{page}" },
810
819
  code: { level: "module", approval: "machine", parents: ["baseline", "api-doc", "prototype"], drivesStale: false, hashMode: "directory", retrieval: "semantic" },
811
820
  doc: { level: "module", approval: "none", parents: [], drivesStale: false, hashMode: "text-normalize", retrieval: "summary" }
@@ -824,6 +833,72 @@ var init_kind = __esm({
824
833
  ["prototype", "acceptance"],
825
834
  ["code"]
826
835
  ];
836
+ COORD_LABELS_ZH = { "{module}": "{\u6A21\u5757}", "{endpoint}": "{\u7AEF}", "{page}": "{\u9875\u9762}" };
837
+ }
838
+ });
839
+
840
+ // core/roles.ts
841
+ function getRoleRegistry(config) {
842
+ const merged = {};
843
+ for (const [role, spec] of Object.entries(DEFAULT_ROLE_REGISTRY)) merged[role] = { ...spec };
844
+ for (const [role, kinds] of Object.entries(config.roleProduces ?? {})) {
845
+ merged[role] = { ...merged[role] ?? { produces: [] }, produces: kinds };
846
+ }
847
+ for (const [role, override] of Object.entries(config.roles ?? {})) {
848
+ merged[role] = { ...merged[role] ?? { produces: [] }, ...override };
849
+ }
850
+ return merged;
851
+ }
852
+ function ownerRoleOf(config, kind) {
853
+ for (const [role, spec] of Object.entries(getRoleRegistry(config))) {
854
+ if (spec.produces.includes(kind)) return role;
855
+ }
856
+ if (kind === "code") return "developer";
857
+ return null;
858
+ }
859
+ var DEFAULT_ROLE_REGISTRY;
860
+ var init_roles = __esm({
861
+ "core/roles.ts"() {
862
+ "use strict";
863
+ init_kind();
864
+ DEFAULT_ROLE_REGISTRY = {
865
+ "product-manager": {
866
+ produces: ["project", "roles", "glossary", "flow", "module-prd", "page-prd"]
867
+ },
868
+ architect: {
869
+ produces: ["db-doc", "api-doc"],
870
+ requires: [{ desc: "PM \u4EA7\u51FA(\u6A21\u5757/\u9875\u9762 PRD)", kinds: PM_KINDS }],
871
+ dispatch: [
872
+ { at: "module", endpoint: "common", content: "\u8BBE\u8BA1 {module} \u6A21\u5757\u6570\u636E\u5E93" },
873
+ { at: "module", endpoint: "service", content: "\u8BBE\u8BA1 {module} \u6A21\u5757 API \u6587\u6863" }
874
+ ]
875
+ },
876
+ designer: {
877
+ produces: ["design-system", "design-prompt", "prototype"],
878
+ requires: [{ desc: "PM \u4EA7\u51FA(\u6A21\u5757/\u9875\u9762 PRD)", kinds: PM_KINDS }],
879
+ dispatch: [
880
+ { at: "endpoint", ifMissingKind: "design-system", produces: ["design-system"], content: "\u5EFA\u7ACB {endpoint} \u7AEF\u8BBE\u8BA1\u7CFB\u7EDF(\u8BE5\u7AEF\u9996\u4E2A\u9875\u9762\u8BBE\u8BA1\u524D\u7F6E)" },
881
+ { at: "page", produces: ["design-prompt", "prototype"], content: "\u8BBE\u8BA1 {endpoint}/{page} \u9875\u9762(\u63D0\u793A\u8BCD+\u539F\u578B)" }
882
+ ]
883
+ },
884
+ developer: {
885
+ produces: ["code"],
886
+ requires: [
887
+ { desc: "\u6570\u636E\u5E93\u6587\u6863", kinds: ["db-doc"] },
888
+ { desc: "API \u6587\u6863", kinds: ["api-doc"], when: { endpointNot: "service" } },
889
+ { desc: "designer {endpoint} \u8BBE\u8BA1\u7A3F", kinds: ["design-prompt", "prototype"], when: { endpointNot: "service" } }
890
+ ],
891
+ dispatch: [
892
+ { at: "module", endpoint: "service", content: "\u5B9E\u73B0 {module} \u6A21\u5757 service \u7AEF" },
893
+ { at: "page", content: "\u5B9E\u73B0 {endpoint}/{page} \u9875\u9762" }
894
+ ],
895
+ onQaFail: "rework"
896
+ },
897
+ qa: {
898
+ produces: ["acceptance"],
899
+ dispatch: [{ at: "page", type: "qa", content: "\u9A8C\u6536 {endpoint}/{page} \u9875\u9762" }]
900
+ }
901
+ };
827
902
  }
828
903
  });
829
904
 
@@ -964,31 +1039,22 @@ function levelFilter(level, task) {
964
1039
  }
965
1040
  }
966
1041
  function producedKinds(ctx, task) {
967
- const declared = ctx.config.roleProduces[task.role] ?? [];
968
- if (task.role === "designer") {
969
- return task.page ? declared.filter((k) => k !== "design-system") : ["design-system"];
970
- }
971
- return declared;
972
- }
973
- function existRequirements(task) {
974
- const m = task.module;
975
- switch (task.role) {
976
- case "architect":
977
- case "designer":
978
- return [{ desc: "PM \u4EA7\u51FA(\u6A21\u5757/\u9875\u9762 PRD)", level: "exist", kinds: PM_KINDS, filter: { module: m } }];
979
- case "developer": {
980
- if (task.endpoint === "service") {
981
- return [{ desc: "\u6570\u636E\u5E93\u6587\u6863", level: "exist", kinds: ["db-doc"], filter: { module: m } }];
982
- }
983
- return [
984
- { desc: "\u6570\u636E\u5E93\u6587\u6863", level: "exist", kinds: ["db-doc"], filter: { module: m } },
985
- { desc: "API \u6587\u6863", level: "exist", kinds: ["api-doc"], filter: { module: m } },
986
- { desc: `designer ${task.endpoint} \u8BBE\u8BA1\u7A3F`, level: "exist", kinds: ["design-prompt", "prototype"], filter: { module: m, endpoint: task.endpoint } }
987
- ];
988
- }
989
- default:
990
- return [];
1042
+ const spec = getRoleRegistry(ctx.config)[task.role];
1043
+ if (!spec) return [];
1044
+ const wantPage = !!task.page;
1045
+ const rule = (spec.dispatch ?? []).find((d) => (wantPage ? d.at === "page" : d.at !== "page") && d.produces);
1046
+ return rule?.produces ?? spec.produces;
1047
+ }
1048
+ function existRequirements(ctx, task) {
1049
+ const spec = getRoleRegistry(ctx.config)[task.role];
1050
+ const reqs = [];
1051
+ for (const r of spec?.requires ?? []) {
1052
+ if (r.when?.endpoint && task.endpoint !== r.when.endpoint) continue;
1053
+ if (r.when?.endpointNot && task.endpoint === r.when.endpointNot) continue;
1054
+ const filter = r.kinds.includes("design-prompt") || r.kinds.includes("prototype") ? { module: task.module, endpoint: task.endpoint } : { module: task.module };
1055
+ reqs.push({ desc: r.desc.replaceAll("{endpoint}", task.endpoint ?? ""), level: "exist", kinds: r.kinds, filter });
991
1056
  }
1057
+ return reqs;
992
1058
  }
993
1059
  function approvedRequirements(ctx, task) {
994
1060
  const registry = getKindRegistry(ctx.config);
@@ -1017,8 +1083,7 @@ function approvedRequirements(ctx, task) {
1017
1083
  }
1018
1084
  function claimRequirements(ctx, task) {
1019
1085
  if (task.type !== "build" && task.type !== "qa") return [];
1020
- if (task.role === "product-manager") return [];
1021
- return [...existRequirements(task), ...approvedRequirements(ctx, task)];
1086
+ return [...existRequirements(ctx, task), ...approvedRequirements(ctx, task)];
1022
1087
  }
1023
1088
  function requirementSatisfied(ctx, req, matched) {
1024
1089
  if (req.level === "exist") return matched.length > 0;
@@ -1144,6 +1209,7 @@ var init_gates = __esm({
1144
1209
  init_derive();
1145
1210
  init_kind();
1146
1211
  init_lints();
1212
+ init_roles();
1147
1213
  }
1148
1214
  });
1149
1215
 
@@ -1790,11 +1856,7 @@ var init_artifact_commands = __esm({
1790
1856
  import { existsSync as existsSync6 } from "node:fs";
1791
1857
  import { join as join7 } from "node:path";
1792
1858
  function ownerRole(ctx, kind) {
1793
- for (const [role, kinds] of Object.entries(ctx.config.roleProduces)) {
1794
- if (kinds.includes(kind)) return role;
1795
- }
1796
- if (kind === "code") return "developer";
1797
- return null;
1859
+ return ownerRoleOf(ctx.config, kind);
1798
1860
  }
1799
1861
  function openReviewTargetKeys(ctx, artifactId) {
1800
1862
  const rows = ctx.db.prepare(
@@ -1938,15 +2000,20 @@ var init_sync_command = __esm({
1938
2000
  init_events();
1939
2001
  init_git();
1940
2002
  init_kind();
2003
+ init_roles();
1941
2004
  init_artifact_commands();
1942
2005
  }
1943
2006
  });
1944
2007
 
1945
2008
  // core/commands/task.commands.ts
2009
+ function getRoleRegistryOwner(ctx, kind) {
2010
+ return ownerRoleOf(ctx.config, kind) ?? "qa";
2011
+ }
1946
2012
  function createTask(ctx, p) {
1947
2013
  const type = p.type ?? "build";
1948
2014
  if (!VALID_TYPES.includes(type)) throw new Error(`\u65E0\u6548\u7684\u4EFB\u52A1\u7C7B\u578B: ${type},\u53EF\u9009\u503C: ${VALID_TYPES.join(", ")}`);
1949
- if (!VALID_ROLES.includes(p.role)) throw new Error(`\u65E0\u6548\u7684\u89D2\u8272: ${p.role},\u53EF\u9009\u503C: ${VALID_ROLES.join(", ")}`);
2015
+ const validRoles = Object.keys(getRoleRegistry(ctx.config));
2016
+ if (!validRoles.includes(p.role)) throw new Error(`\u65E0\u6548\u7684\u89D2\u8272: ${p.role},\u53EF\u9009\u503C: ${validRoles.join(", ")}`);
1950
2017
  const module = normalizeModule(p.module, ctx.config);
1951
2018
  const tx = ctx.db.transaction(() => {
1952
2019
  const result = ctx.db.prepare(
@@ -2041,11 +2108,12 @@ function updateTask(ctx, { id, status, operator, force = false }) {
2041
2108
  return { id, warnings };
2042
2109
  }
2043
2110
  function spawnFollowupQa(ctx, reworkTask) {
2111
+ const qaRole = getRoleRegistryOwner(ctx, "acceptance");
2044
2112
  const tx = ctx.db.transaction(() => {
2045
2113
  const result = ctx.db.prepare(
2046
2114
  `INSERT INTO tasks (module, role, endpoint, page, type, status, assignee, creator, content)
2047
- VALUES (?, 'qa', ?, ?, 'qa', 'pending', 'qa', 'system', ?)`
2048
- ).run(reworkTask.module, reworkTask.endpoint, reworkTask.page, `[\u590D\u9A8C] rework #${reworkTask.id} \u5DF2\u5B8C\u6210,\u8BF7\u91CD\u65B0\u9A8C\u6536`);
2115
+ VALUES (?, ?, ?, ?, 'qa', 'pending', ?, 'system', ?)`
2116
+ ).run(reworkTask.module, qaRole, reworkTask.endpoint, reworkTask.page, qaRole, `[\u590D\u9A8C] rework #${reworkTask.id} \u5DF2\u5B8C\u6210,\u8BF7\u91CD\u65B0\u9A8C\u6536`);
2049
2117
  const qaId = result.lastInsertRowid;
2050
2118
  logEvent(ctx.db, {
2051
2119
  entityType: "task",
@@ -2205,7 +2273,7 @@ function getTaskDetail(ctx, id) {
2205
2273
  }
2206
2274
  return { task, events, outputs, staleness: taskStaleness(ctx.db, id) };
2207
2275
  }
2208
- var VALID_STATUS, VALID_TYPES, VALID_ROLES;
2276
+ var VALID_STATUS, VALID_TYPES;
2209
2277
  var init_task_commands = __esm({
2210
2278
  "core/commands/task.commands.ts"() {
2211
2279
  "use strict";
@@ -2215,11 +2283,11 @@ var init_task_commands = __esm({
2215
2283
  init_git();
2216
2284
  init_gh();
2217
2285
  init_kind();
2286
+ init_roles();
2218
2287
  init_artifact_commands();
2219
2288
  init_sync_command();
2220
2289
  VALID_STATUS = ["pending", "in_progress", "completed", "cancelled"];
2221
2290
  VALID_TYPES = ["build", "review", "qa", "hotfix", "baseline", "legacy", "rework"];
2222
- VALID_ROLES = ["product-manager", "architect", "designer", "developer", "qa"];
2223
2291
  }
2224
2292
  });
2225
2293
 
@@ -2742,7 +2810,8 @@ var init_scan_command = __esm({
2742
2810
  // core/commands/qa.command.ts
2743
2811
  function recordQaResult(ctx, p) {
2744
2812
  const task = getTaskRow(ctx, p.id);
2745
- if (task.role !== "qa") throw new Error(`\u4EFB\u52A1 #${p.id} \u4E0D\u662F qa \u4EFB\u52A1`);
2813
+ const acceptanceRole = ownerRoleOf(ctx.config, "acceptance") ?? "qa";
2814
+ if (task.role !== acceptanceRole) throw new Error(`\u4EFB\u52A1 #${p.id} \u4E0D\u662F ${acceptanceRole} \u4EFB\u52A1`);
2746
2815
  if (task.assignee !== p.operator) throw new Error(`\u53EA\u6709\u6267\u884C\u4EBA\u624D\u80FD\u8BB0\u5F55\u9A8C\u6536\u7ED3\u679C`);
2747
2816
  if (p.result === "fail" && !p.reason?.trim()) throw new Error(`\u9A8C\u6536\u4E0D\u901A\u8FC7\u5FC5\u987B\u9644\u539F\u56E0,\u5B83\u5C06\u6210\u4E3A rework \u4EFB\u52A1\u7684\u5185\u5BB9`);
2748
2817
  const eventName = p.result === "pass" ? "qa_passed" : "qa_failed";
@@ -2770,11 +2839,12 @@ function recordQaResult(ctx, p) {
2770
2839
  }
2771
2840
  return { reworkTaskId: null };
2772
2841
  }
2842
+ const reworkRole = ownerRoleOf(ctx.config, "code") ?? "developer";
2773
2843
  const tx2 = ctx.db.transaction(() => {
2774
2844
  const result = ctx.db.prepare(
2775
2845
  `INSERT INTO tasks (module, role, endpoint, page, type, status, assignee, creator, content)
2776
- VALUES (?, 'developer', ?, ?, 'rework', 'pending', 'developer', 'system', ?)`
2777
- ).run(task.module, task.endpoint, task.page, `[\u8FD4\u5DE5] QA #${task.id} \u9A8C\u6536\u4E0D\u901A\u8FC7:${p.reason}`);
2846
+ VALUES (?, ?, ?, ?, 'rework', 'pending', ?, 'system', ?)`
2847
+ ).run(task.module, reworkRole, task.endpoint, task.page, reworkRole, `[\u8FD4\u5DE5] QA #${task.id} \u9A8C\u6536\u4E0D\u901A\u8FC7:${p.reason}`);
2778
2848
  const reworkId = result.lastInsertRowid;
2779
2849
  logEvent(ctx.db, {
2780
2850
  entityType: "task",
@@ -2794,6 +2864,7 @@ var init_qa_command = __esm({
2794
2864
  "core/commands/qa.command.ts"() {
2795
2865
  "use strict";
2796
2866
  init_events();
2867
+ init_roles();
2797
2868
  init_artifact_commands();
2798
2869
  init_task_commands();
2799
2870
  }
@@ -3009,35 +3080,25 @@ function planModule(ctx, moduleRaw, creator = "product-manager") {
3009
3080
  }
3010
3081
  const pagePrds = byKind("page-prd").filter((a) => a.endpoint && a.page && existsSync10(join12(ctx.root, a.path)));
3011
3082
  const endpoints = [...new Set(pagePrds.map((a) => a.endpoint))];
3012
- const desired = [
3013
- { role: "architect", endpoint: "common", page: null, type: "build", assignee: "architect", content: `\u8BBE\u8BA1 ${module} \u6A21\u5757\u6570\u636E\u5E93` },
3014
- { role: "architect", endpoint: "service", page: null, type: "build", assignee: "architect", content: `\u8BBE\u8BA1 ${module} \u6A21\u5757 API \u6587\u6863` },
3015
- { role: "developer", endpoint: "service", page: null, type: "build", assignee: "developer", content: `\u5B9E\u73B0 ${module} \u6A21\u5757 service \u7AEF` }
3016
- ];
3017
- for (const endpoint of endpoints) {
3018
- const hasDesignSystem = artifacts.some((a) => a.kind === "design-system" && a.endpoint === endpoint);
3019
- if (!hasDesignSystem) {
3020
- desired.push({
3021
- role: "designer",
3022
- endpoint,
3023
- page: null,
3024
- type: "build",
3025
- assignee: "designer",
3026
- content: `\u5EFA\u7ACB ${endpoint} \u7AEF\u8BBE\u8BA1\u7CFB\u7EDF(\u8BE5\u7AEF\u9996\u4E2A\u9875\u9762\u8BBE\u8BA1\u524D\u7F6E)`
3027
- });
3083
+ const registry = getRoleRegistry(ctx.config);
3084
+ const interp = (tpl, v) => tpl.replaceAll("{module}", v.module).replaceAll("{endpoint}", v.endpoint ?? "").replaceAll("{page}", v.page ?? "");
3085
+ const desired = [];
3086
+ for (const role of ctx.config.pipeline) {
3087
+ for (const d of registry[role]?.dispatch ?? []) {
3088
+ if (d.at === "module") {
3089
+ desired.push({ role, endpoint: d.endpoint ?? null, page: null, type: d.type ?? "build", assignee: role, content: interp(d.content, { module }) });
3090
+ } else if (d.at === "endpoint") {
3091
+ for (const endpoint of endpoints) {
3092
+ if (d.ifMissingKind && artifacts.some((a) => a.kind === d.ifMissingKind && a.endpoint === endpoint)) continue;
3093
+ desired.push({ role, endpoint, page: null, type: d.type ?? "build", assignee: role, content: interp(d.content, { module, endpoint }) });
3094
+ }
3095
+ } else {
3096
+ for (const prd of pagePrds) {
3097
+ desired.push({ role, endpoint: prd.endpoint, page: prd.page, type: d.type ?? "build", assignee: role, content: interp(d.content, { module, endpoint: prd.endpoint, page: prd.page }) });
3098
+ }
3099
+ }
3028
3100
  }
3029
3101
  }
3030
- for (const prd of pagePrds) {
3031
- desired.push(
3032
- { role: "designer", endpoint: prd.endpoint, page: prd.page, type: "build", assignee: "designer", content: `\u8BBE\u8BA1 ${prd.endpoint}/${prd.page} \u9875\u9762(\u63D0\u793A\u8BCD+\u539F\u578B)` },
3033
- { role: "developer", endpoint: prd.endpoint, page: prd.page, type: "build", assignee: "developer", content: `\u5B9E\u73B0 ${prd.endpoint}/${prd.page} \u9875\u9762` },
3034
- { role: "qa", endpoint: prd.endpoint, page: prd.page, type: "qa", assignee: "qa", content: `\u9A8C\u6536 ${prd.endpoint}/${prd.page} \u9875\u9762` }
3035
- );
3036
- }
3037
- const activeRoles = new Set(ctx.config.pipeline);
3038
- const filtered = desired.filter((d) => activeRoles.has(d.role));
3039
- desired.length = 0;
3040
- desired.push(...filtered);
3041
3102
  const exists = ctx.db.prepare(
3042
3103
  `SELECT COUNT(*) AS c FROM tasks
3043
3104
  WHERE module IS ? AND role = ? AND endpoint IS ? AND page IS ? AND type = ? AND status != 'cancelled'`
@@ -3082,6 +3143,7 @@ var init_plan_command = __esm({
3082
3143
  init_derive();
3083
3144
  init_events();
3084
3145
  init_kind();
3146
+ init_roles();
3085
3147
  init_task_commands();
3086
3148
  }
3087
3149
  });
@@ -3132,7 +3194,7 @@ var init_install_hooks_command = __esm({
3132
3194
  });
3133
3195
 
3134
3196
  // core/commands/gen-agents.command.ts
3135
- import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readdirSync as readdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
3197
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
3136
3198
  import { dirname as dirname3, join as join14 } from "node:path";
3137
3199
  function expandPath(ctx, kind) {
3138
3200
  const spec = getKindRegistry(ctx.config)[kind];
@@ -3178,6 +3240,16 @@ function genAgents(ctx, templatesDir) {
3178
3240
  PATH_DESIGN_PROMPTS: expandPath(ctx, "design-prompt"),
3179
3241
  PATH_PROTOTYPES: expandPath(ctx, "prototype"),
3180
3242
  PATH_ACCEPTANCE: expandPath(ctx, "acceptance"),
3243
+ // 路径投影:完整路径模板由 kind 注册表(前缀+coords 文法+ext)推导——
3244
+ // 项目覆盖 coords 后重跑 gen-agents,agent 指示与 scan 解析自动同步(单一真相源)
3245
+ TPL_FLOW: kindPathTemplate(ctx.config, "flow", lang) ?? "",
3246
+ TPL_MODULE_PRD: kindPathTemplate(ctx.config, "module-prd", lang) ?? "",
3247
+ TPL_PAGE_PRD: kindPathTemplate(ctx.config, "page-prd", lang) ?? "",
3248
+ TPL_DB_DOC: kindPathTemplate(ctx.config, "db-doc", lang) ?? "",
3249
+ TPL_DESIGN_SYSTEM: kindPathTemplate(ctx.config, "design-system", lang) ?? "",
3250
+ TPL_DESIGN_PROMPT: kindPathTemplate(ctx.config, "design-prompt", lang) ?? "",
3251
+ TPL_PROTOTYPE: kindPathTemplate(ctx.config, "prototype", lang) ?? "",
3252
+ TPL_ACCEPTANCE: kindPathTemplate(ctx.config, "acceptance", lang) ?? "",
3181
3253
  ENDPOINTS: ctx.config.endpoints.join(" / "),
3182
3254
  PIPELINE: ctx.config.pipeline.join(" \u2192 "),
3183
3255
  CODE_ROOTS: ctx.config.endpoints.map((e) => `| ${e} | ${(ctx.config.codeRoots[e] ?? []).join(blocks.sep) || blocks.todo} |`).join("\n")
@@ -3191,13 +3263,19 @@ function genAgents(ctx, templatesDir) {
3191
3263
  return content;
3192
3264
  };
3193
3265
  const adapters = resolvePlatforms(ctx.config.platforms);
3194
- const pipeline = new Set(ctx.config.pipeline);
3266
+ const registry = getRoleRegistry(ctx.config);
3195
3267
  const written = [];
3196
- for (const file of readdirSync5(dir)) {
3197
- if (!file.endsWith(".md")) continue;
3198
- const role = file.replace(/\.md$/, "");
3199
- if (!pipeline.has(role)) continue;
3200
- const tpl = parseTemplate(readFileSync6(join14(dir, file), "utf-8"));
3268
+ const projectTplDir = join14(ctx.root, "docs/workbench/templates/agents", lang);
3269
+ for (const role of [...new Set(ctx.config.pipeline)]) {
3270
+ if (!registry[role]) {
3271
+ throw new Error(`pipeline \u89D2\u8272 ${role} \u672A\u5728\u89D2\u8272\u6CE8\u518C\u8868\u4E2D\u5B9A\u4E49(\u5185\u7F6E\u6216 config.roles)`);
3272
+ }
3273
+ const candidates = [join14(projectTplDir, `${role}.md`), join14(dir, `${role}.md`)];
3274
+ const file = candidates.find((f) => existsSync12(f));
3275
+ if (!file) {
3276
+ throw new Error(`\u89D2\u8272 ${role} \u7F3A\u5C11 agent \u6A21\u677F:\u81EA\u5B9A\u4E49\u89D2\u8272\u8BF7\u63D0\u4F9B docs/workbench/templates/agents/${lang}/${role}.md`);
3277
+ }
3278
+ const tpl = parseTemplate(readFileSync6(file, "utf-8"));
3201
3279
  for (const adapter of adapters) {
3202
3280
  const memory = adapter.memoryBlock(role, lang);
3203
3281
  const spec = {
@@ -3222,6 +3300,7 @@ var init_gen_agents_command = __esm({
3222
3300
  "use strict";
3223
3301
  init_config();
3224
3302
  init_kind();
3303
+ init_roles();
3225
3304
  init_platforms();
3226
3305
  TRUST_PROTOCOL_ZH = `## \u4FE1\u4EFB\u534F\u8BAE(\u6700\u9AD8\u4F18\u5148\u7EA7)
3227
3306
 
@@ -3345,11 +3424,13 @@ function intakeIssues(ctx) {
3345
3424
  continue;
3346
3425
  }
3347
3426
  const isBug = issue.labels.some((l) => l.name.toLowerCase() === "bug");
3427
+ const bugRole = ctx.config.intake?.bugRole ?? "developer";
3428
+ const defaultRole = ctx.config.intake?.defaultRole ?? "product-manager";
3348
3429
  const id = createTask(ctx, {
3349
- role: isBug ? "developer" : "product-manager",
3350
- endpoint: isBug ? null : null,
3430
+ role: isBug ? bugRole : defaultRole,
3431
+ endpoint: null,
3351
3432
  type: isBug ? "hotfix" : "build",
3352
- assignee: isBug ? "developer" : "product-manager",
3433
+ assignee: isBug ? bugRole : defaultRole,
3353
3434
  creator: "intake",
3354
3435
  content: `[gh#${issue.number}] ${issue.title}
3355
3436
  ${issue.url}`,
@@ -3533,6 +3614,7 @@ __export(core_exports, {
3533
3614
  CONFIG_FILENAME: () => CONFIG_FILENAME,
3534
3615
  CURRENT_SCHEMA_VERSION: () => CURRENT_SCHEMA_VERSION,
3535
3616
  DEFAULT_KIND_REGISTRY: () => DEFAULT_KIND_REGISTRY,
3617
+ DEFAULT_ROLE_REGISTRY: () => DEFAULT_ROLE_REGISTRY,
3536
3618
  KIND_TIERS: () => KIND_TIERS,
3537
3619
  META_KINDS: () => META_KINDS,
3538
3620
  MIGRATIONS: () => MIGRATIONS,
@@ -3566,6 +3648,7 @@ __export(core_exports, {
3566
3648
  genAgents: () => genAgents,
3567
3649
  getAdapter: () => getAdapter,
3568
3650
  getKindRegistry: () => getKindRegistry,
3651
+ getRoleRegistry: () => getRoleRegistry,
3569
3652
  getTaskDetail: () => getTaskDetail,
3570
3653
  getTaskRow: () => getTaskRow,
3571
3654
  gitHead: () => gitHead,
@@ -3578,6 +3661,7 @@ __export(core_exports, {
3578
3661
  initProject: () => initProject,
3579
3662
  installGitHooks: () => installGitHooks,
3580
3663
  intakeIssues: () => intakeIssues,
3664
+ kindPathTemplate: () => kindPathTemplate,
3581
3665
  kindSpec: () => kindSpec,
3582
3666
  listArtifacts: () => listArtifacts,
3583
3667
  listEvents: () => listEvents,
@@ -3593,6 +3677,7 @@ __export(core_exports, {
3593
3677
  openWorkbench: () => openWorkbench,
3594
3678
  openWorkbenchAt: () => openWorkbenchAt,
3595
3679
  ownerRole: () => ownerRole,
3680
+ ownerRoleOf: () => ownerRoleOf,
3596
3681
  planModule: () => planModule,
3597
3682
  prototypeEndorsed: () => prototypeEndorsed,
3598
3683
  recordNote: () => recordNote,
@@ -3630,6 +3715,7 @@ var init_core = __esm({
3630
3715
  init_hash();
3631
3716
  init_events();
3632
3717
  init_kind();
3718
+ init_roles();
3633
3719
  init_platforms();
3634
3720
  init_derive();
3635
3721
  init_lints();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dawipong/opcflow",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Spec-anchored, drift-enforced execution layer for AI coding agents — artifact DAG, trust gates, multi-role pipeline; generates agents/MCP/hooks for Claude Code, Codex, OpenCode & Cursor.",
5
5
  "license": "MIT",
6
6
  "author": "Dawi",
@@ -29,7 +29,7 @@ tech selection (language / framework / ORM / build), directory structure per end
29
29
  | Artifact | Path |
30
30
  | --- | --- |
31
31
  | Database model definition | Per approved TECH.md conventions (path/tech set by the baseline) |
32
- | Database docs | {{PATH_DB_DOCS}}{module}.md |
32
+ | Database docs | {{TPL_DB_DOC}} |
33
33
  | API contract docs | {{PATH_API_DOCS}}{endpoint}/{module}.md (cross-endpoint shared goes in common/) |
34
34
  | Technical baseline (changes go through review) | ARCHITECTURE.md / TECH.md |
35
35
 
@@ -21,9 +21,9 @@ You are @designer. Each of your three artifacts travels a different trust channe
21
21
 
22
22
  | Artifact | Path | Trust channel |
23
23
  | --- | --- | --- |
24
- | Design system (one per endpoint) | {{PATH_DESIGN_SYSTEMS}}{endpoint}.md | **Human approval** (endpoint-level contract; one change makes every prototype for that endpoint stale) |
25
- | Page design prompt | {{PATH_DESIGN_PROMPTS}}{endpoint}/{module}/{page}.md | Register only (working draft, not submitted) |
26
- | HTML prototype | {{PATH_PROTOTYPES}}{endpoint}/{module}/{page}.html | **👍 = feedback + approval in one** (user releases after previewing in opcflow) |
24
+ | Design system (one per endpoint) | {{TPL_DESIGN_SYSTEM}} | **Human approval** (endpoint-level contract; one change makes every prototype for that endpoint stale) |
25
+ | Page design prompt | {{TPL_DESIGN_PROMPT}} | Register only (working draft, not submitted) |
26
+ | HTML prototype | {{TPL_PROTOTYPE}} | **👍 = feedback + approval in one** (user releases after previewing in opcflow) |
27
27
 
28
28
  ## Workflow (page task)
29
29
 
@@ -37,7 +37,7 @@ You are @designer. Each of your three artifacts travels a different trust channe
37
37
 
38
38
  ## Endpoint Design-System Task (once per endpoint)
39
39
 
40
- Write to {{PATH_DESIGN_SYSTEMS}}{endpoint}.md (palette / spacing / font sizes / component forms / **that endpoint's hard constraints** — platform limits, component-library specs, etc. are all legislated here) → register → **submit for review**.
40
+ Write to {{TPL_DESIGN_SYSTEM}} (palette / spacing / font sizes / component forms / **that endpoint's hard constraints** — platform limits, component-library specs, etc. are all legislated here) → register → **submit for review**.
41
41
  For endpoints that already have prototypes or production pages, **reverse-engineer** from the established facts (legislate, don't design from scratch); for a brand-new endpoint, propose an initial version from the approved baseline (the UI stack in TECH.md) and the project's positioning.
42
42
 
43
43
  ## Red Flags
@@ -22,10 +22,10 @@ You are @developer. **Approved contract = implement directly, zero divergence**
22
22
  | Input | Path |
23
23
  | --- | --- |
24
24
  | Technical baseline (selection/directories/protocol conventions) | ARCHITECTURE.md / TECH.md |
25
- | Page PRD (incl. acceptance points) | {{PATH_PAGES}}{endpoint}/{module}/{page}.md |
25
+ | Page PRD (incl. acceptance points) | {{TPL_PAGE_PRD}} |
26
26
  | API contract | {{PATH_API_DOCS}}{endpoint}/{module}.md |
27
- | DB docs | {{PATH_DB_DOCS}}{module}.md |
28
- | 👍-approved prototype (UI truth) | {{PATH_PROTOTYPES}}{endpoint}/{module}/{page}.html |
27
+ | DB docs | {{TPL_DB_DOC}} |
28
+ | 👍-approved prototype (UI truth) | {{TPL_PROTOTYPE}} |
29
29
 
30
30
  ## Code Directory Conventions (config-injected; follow when building code)
31
31
 
@@ -25,9 +25,9 @@ You are @product-manager. Responsibility: translate requirements into **layer-by
25
25
  | Project overview | {{PATH_PROJECT}} | Project-level contract |
26
26
  | Role permission matrix | {{PATH_ROLES}} | Project-level contract |
27
27
  | Domain glossary | {{PATH_GLOSSARY}} | Project-level contract |
28
- | Business flow + entity state machine | {{PATH_FLOWS}}{module}.md | Module-level contract |
29
- | Module PRD | {{PATH_MODULES}}{module}.md | Module-level contract |
30
- | Page PRD | {{PATH_PAGES}}{endpoint}/{module}/{page}.md | Page-level contract |
28
+ | Business flow + entity state machine | {{TPL_FLOW}} | Module-level contract |
29
+ | Module PRD | {{TPL_MODULE_PRD}} | Module-level contract |
30
+ | Page PRD | {{TPL_PAGE_PRD}} | Page-level contract |
31
31
 
32
32
  ## Core Discipline: Layer-by-Layer Confirmation
33
33
 
@@ -20,7 +20,7 @@ You are @qa. **Judgment authority belongs to PM (acceptance points), execution a
20
20
  ## Two-Phase Acceptance
21
21
 
22
22
  **Phase one (before or after developer starts): translate acceptance criteria**
23
- Read the "acceptance points" section of the approved page PRD → translate into executable cases, write to {{PATH_ACCEPTANCE}}{endpoint}/{module}/{page}.md → register output → **submit for review** (it's a contract; developer writes against it).
23
+ Read the "acceptance points" section of the approved page PRD → translate into executable cases, write to {{TPL_ACCEPTANCE}} → register output → **submit for review** (it's a contract; developer writes against it).
24
24
  When a point is ambiguous: **dispute or send back to PM**, do not fill in the wording yourself.
25
25
 
26
26
  **Phase two (after developer finishes): execute acceptance**
@@ -29,7 +29,7 @@ tools: Read, Write, Edit, Glob, Grep, Bash
29
29
  | 产物 | 路径 |
30
30
  | --- | --- |
31
31
  | 数据库模型定义 | 按 approved TECH.md 的约定(路径/技术随基线定) |
32
- | 数据库文档 | {{PATH_DB_DOCS}}{模块}.md |
32
+ | 数据库文档 | {{TPL_DB_DOC}} |
33
33
  | API 契约文档 | {{PATH_API_DOCS}}{端}/{模块}.md(跨端共用放 common/) |
34
34
  | 技术基线(变更走审批) | ARCHITECTURE.md / TECH.md |
35
35
 
@@ -21,9 +21,9 @@ tools: Read, Write, Edit, Glob, Grep, Bash
21
21
 
22
22
  | 产物 | 路径 | 信任通道 |
23
23
  | --- | --- | --- |
24
- | 设计系统(每端一份) | {{PATH_DESIGN_SYSTEMS}}{端}.md | **人工审批**(端级契约,改一次全端原型 stale) |
25
- | 页面设计提示词 | {{PATH_DESIGN_PROMPTS}}{端}/{模块}/{页面}.md | 仅登记(工作底稿,不送审) |
26
- | HTML 原型 | {{PATH_PROTOTYPES}}{端}/{模块}/{页面}.html | **👍 = 反馈+审批合一**(用户在 opcflow 预览后放行) |
24
+ | 设计系统(每端一份) | {{TPL_DESIGN_SYSTEM}} | **人工审批**(端级契约,改一次全端原型 stale) |
25
+ | 页面设计提示词 | {{TPL_DESIGN_PROMPT}} | 仅登记(工作底稿,不送审) |
26
+ | HTML 原型 | {{TPL_PROTOTYPE}} | **👍 = 反馈+审批合一**(用户在 opcflow 预览后放行) |
27
27
 
28
28
  ## 工作流程(页面任务)
29
29
 
@@ -37,7 +37,7 @@ tools: Read, Write, Edit, Glob, Grep, Bash
37
37
 
38
38
  ## 端设计系统任务(每端一次)
39
39
 
40
- 写入 {{PATH_DESIGN_SYSTEMS}}{端}.md(色板/间距/字号/组件形态/**该端硬约束**——平台限制、组件库规范等都在此立法)→ 登记 → **submit 送审**。
40
+ 写入 {{TPL_DESIGN_SYSTEM}}(色板/间距/字号/组件形态/**该端硬约束**——平台限制、组件库规范等都在此立法)→ 登记 → **submit 送审**。
41
41
  已有原型或生产页面的端,从既成事实**反向提炼**(立法,不凭空设计);全新的端,依据 approved 基线(TECH.md 的 UI 栈)与项目定位提出初版。
42
42
 
43
43
  ## Red Flags
@@ -22,10 +22,10 @@ tools: Read, Write, Edit, Glob, Grep, Bash
22
22
  | 输入 | 路径 |
23
23
  | --- | --- |
24
24
  | 技术基线(选型/目录/协议约定) | ARCHITECTURE.md / TECH.md |
25
- | 页面 PRD(含验收要点) | {{PATH_PAGES}}{端}/{模块}/{页面}.md |
25
+ | 页面 PRD(含验收要点) | {{TPL_PAGE_PRD}} |
26
26
  | API 契约 | {{PATH_API_DOCS}}{端}/{模块}.md |
27
- | DB 文档 | {{PATH_DB_DOCS}}{模块}.md |
28
- | 已 👍 原型(UI 真相) | {{PATH_PROTOTYPES}}{端}/{模块}/{页面}.html |
27
+ | DB 文档 | {{TPL_DB_DOC}} |
28
+ | 已 👍 原型(UI 真相) | {{TPL_PROTOTYPE}} |
29
29
 
30
30
  ## 代码目录约定(config 注入,建代码时遵守)
31
31
 
@@ -25,9 +25,9 @@ tools: Read, Write, Edit, Glob, Grep, Bash
25
25
  | 项目全景 | {{PATH_PROJECT}} | 项目级契约 |
26
26
  | 角色权限矩阵 | {{PATH_ROLES}} | 项目级契约 |
27
27
  | 领域术语表 | {{PATH_GLOSSARY}} | 项目级契约 |
28
- | 业务流程+实体状态机 | {{PATH_FLOWS}}{模块}.md | 模块级契约 |
29
- | 模块 PRD | {{PATH_MODULES}}{模块}.md | 模块级契约 |
30
- | 页面 PRD | {{PATH_PAGES}}{端}/{模块}/{页面}.md | 页面级契约 |
28
+ | 业务流程+实体状态机 | {{TPL_FLOW}} | 模块级契约 |
29
+ | 模块 PRD | {{TPL_MODULE_PRD}} | 模块级契约 |
30
+ | 页面 PRD | {{TPL_PAGE_PRD}} | 页面级契约 |
31
31
 
32
32
  ## 核心纪律:逐层确认制
33
33
 
@@ -20,7 +20,7 @@ tools: Read, Write, Edit, Glob, Grep, Bash
20
20
  ## 两段式验收
21
21
 
22
22
  **第一段(developer 开工前后皆可):翻译验收标准**
23
- 读 approved 页面 PRD 的"验收要点"章节 → 翻译成可执行用例,写入 {{PATH_ACCEPTANCE}}{端}/{模块}/{页面}.md → output 登记 → **submit 送审**(它是契约,developer 对着它写)。
23
+ 读 approved 页面 PRD 的"验收要点"章节 → 翻译成可执行用例,写入 {{TPL_ACCEPTANCE}} → output 登记 → **submit 送审**(它是契约,developer 对着它写)。
24
24
  遇到要点含混:**dispute 或退回 PM**,禁止自行脑补口径。
25
25
 
26
26
  **第二段(developer 完成后):执行验收**