@dawipong/opcflow 0.5.0 → 0.7.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
@@ -66,6 +66,7 @@ var init_config = __esm({
66
66
  moduleMapping: {},
67
67
  feedbackHalfLifeDays: 15,
68
68
  gates: { approvalMode: "warn", writeGate: "observe" },
69
+ taskPreconditions: [{ role: "qa", type: "qa", requiresSiblingRoleCompleted: "developer" }],
69
70
  git: { taskTrailer: "off", trailerKey: "Task" },
70
71
  legacyDb: "tasks/task.db",
71
72
  dataDir: ".workbench",
@@ -409,6 +410,13 @@ var init_events = __esm({
409
410
  });
410
411
 
411
412
  // core/platforms.ts
413
+ var platforms_exports = {};
414
+ __export(platforms_exports, {
415
+ PLATFORM_IDS: () => PLATFORM_IDS,
416
+ getAdapter: () => getAdapter,
417
+ resolveModel: () => resolveModel,
418
+ resolvePlatforms: () => resolvePlatforms
419
+ });
412
420
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "node:fs";
413
421
  import { dirname as dirname2, join as join4 } from "node:path";
414
422
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
@@ -500,6 +508,12 @@ var init_platforms = __esm({
500
508
  hooksScanDir: ".claude/hooks",
501
509
  defaultModel: "opus",
502
510
  memoryBlock: (role, lang) => claudeMemory(role, lang),
511
+ projectDirEnvVar: "CLAUDE_PROJECT_DIR",
512
+ parseHookInput: (raw) => ({
513
+ filePath: raw?.tool_input?.file_path ?? raw?.tool_input?.filePath
514
+ }),
515
+ respondBlocked: (msg) => ({ exitCode: 2, stderr: msg }),
516
+ formatModel: (_providerId, modelId) => modelId,
503
517
  notes: [],
504
518
  agentFile: (role) => `${role}.md`,
505
519
  renderAgent: (spec) => yamlFrontmatter(
@@ -543,6 +557,12 @@ var init_platforms = __esm({
543
557
  hooksScanDir: null,
544
558
  defaultModel: "gpt-5.1-codex",
545
559
  memoryBlock: (_role, lang) => agentsMdMemory("Codex", lang),
560
+ projectDirEnvVar: "CODEX_PROJECT_DIR",
561
+ parseHookInput: (raw) => ({
562
+ filePath: raw?.arguments?.file_path ?? raw?.arguments?.filePath
563
+ }),
564
+ respondBlocked: (msg) => ({ exitCode: 2, stderr: msg }),
565
+ formatModel: (_providerId, modelId) => modelId,
546
566
  notes: [
547
567
  'Codex:\u9879\u76EE\u7EA7 .codex/* \u4EC5\u5F53\u9879\u76EE\u88AB\u6807\u8BB0 trusted \u624D\u52A0\u8F7D \u2014\u2014 \u5728 ~/.codex/config.toml \u91CC\u4E3A\u672C\u9879\u76EE\u8BBE trust_level="trusted"',
548
568
  "Codex:skill \u8D70 .agents/skills/(\u975E .codex/skills)"
@@ -583,6 +603,12 @@ var init_platforms = __esm({
583
603
  hooksScanDir: null,
584
604
  defaultModel: "anthropic/claude-opus-4-8",
585
605
  memoryBlock: (_role, lang) => agentsMdMemory("OpenCode", lang),
606
+ projectDirEnvVar: "OPENCODE_PROJECT_DIR",
607
+ parseHookInput: (raw) => ({
608
+ filePath: raw?.args?.file_path ?? raw?.args?.filePath
609
+ }),
610
+ respondBlocked: (msg) => ({ exitCode: 2, stderr: msg }),
611
+ formatModel: (providerId, modelId) => `${providerId}/${modelId}`,
586
612
  notes: [
587
613
  "OpenCode:\u6A21\u578B\u4E32\u662F provider/model \u683C\u5F0F(\u89C1 models.dev);API key \u5EFA\u8BAE\u8D70\u73AF\u5883\u53D8\u91CF\u6216 {env:...}"
588
614
  ],
@@ -651,6 +677,15 @@ export const opcflow = async () => ({
651
677
  hooksScanDir: null,
652
678
  defaultModel: "claude-opus-4-8",
653
679
  memoryBlock: (_role, lang) => cursorMemory(lang),
680
+ projectDirEnvVar: "CURSOR_PROJECT_DIR",
681
+ parseHookInput: (raw) => ({
682
+ filePath: raw?.file_path ?? raw?.filePath
683
+ }),
684
+ respondBlocked: (msg) => ({
685
+ exitCode: 0,
686
+ stdout: JSON.stringify({ permission: "deny", userMessage: msg, agentMessage: msg })
687
+ }),
688
+ formatModel: (_providerId, modelId) => modelId,
654
689
  notes: [
655
690
  "Cursor:\u4E3B agent \u6A21\u578B\u7531 UI \u9009,--model \u53EA\u4F5C\u7528\u4E8E\u751F\u6210\u7684 subagent(.cursor/agents/*.md)"
656
691
  ],
@@ -700,6 +735,7 @@ __export(kind_exports, {
700
735
  expandPattern: () => expandPattern,
701
736
  getKindRegistry: () => getKindRegistry,
702
737
  inferKind: () => inferKind,
738
+ kindPathTemplate: () => kindPathTemplate,
703
739
  kindSpec: () => kindSpec,
704
740
  normalizeModule: () => normalizeModule
705
741
  });
@@ -731,6 +767,14 @@ function contractKinds(config) {
731
767
  function expandPattern(pattern, config) {
732
768
  return pattern.replace("{prd}", config.docs.prd).replace("{architecture}", config.docs.architecture).replace("{design}", config.docs.design).replace("{acceptance}", config.docs.acceptance);
733
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
+ }
734
778
  function inferKind(relPath, config) {
735
779
  const p = relPath.replace(/\\/g, "/");
736
780
  const registry = getKindRegistry(config);
@@ -748,7 +792,7 @@ function normalizeModule(module, config) {
748
792
  if (!module) return null;
749
793
  return config.moduleMapping[module] ?? module;
750
794
  }
751
- var DEFAULT_KIND_REGISTRY, META_KINDS, PM_KINDS, KIND_TIERS;
795
+ var DEFAULT_KIND_REGISTRY, META_KINDS, PM_KINDS, KIND_TIERS, COORD_LABELS_ZH;
752
796
  var init_kind = __esm({
753
797
  "core/kind.ts"() {
754
798
  "use strict";
@@ -770,7 +814,7 @@ var init_kind = __esm({
770
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" },
771
815
  "design-system": { level: "endpoint", approval: "human", parents: ["baseline"], drivesStale: true, hashMode: "text-normalize", retrieval: "full", pathPatterns: ["{design}/systems/"], coords: "{endpoint}" },
772
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}" },
773
- 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" },
774
818
  acceptance: { level: "page", approval: "human", parents: ["page-prd", "prototype"], drivesStale: true, hashMode: "text-normalize", retrieval: "full", pathPatterns: ["{acceptance}/"], coords: "{endpoint}/{module}/{page}" },
775
819
  code: { level: "module", approval: "machine", parents: ["baseline", "api-doc", "prototype"], drivesStale: false, hashMode: "directory", retrieval: "semantic" },
776
820
  doc: { level: "module", approval: "none", parents: [], drivesStale: false, hashMode: "text-normalize", retrieval: "summary" }
@@ -789,6 +833,7 @@ var init_kind = __esm({
789
833
  ["prototype", "acceptance"],
790
834
  ["code"]
791
835
  ];
836
+ COORD_LABELS_ZH = { "{module}": "{\u6A21\u5757}", "{endpoint}": "{\u7AEF}", "{page}": "{\u9875\u9762}" };
792
837
  }
793
838
  });
794
839
 
@@ -992,20 +1037,27 @@ function requirementSatisfied(ctx, req, matched) {
992
1037
  (a) => kindSpec(ctx.config, a.kind).approval === "thumbs" ? prototypeEndorsed(ctx.db, a) : reviewStatus(a) === "approved"
993
1038
  );
994
1039
  }
995
- function validateClaim(ctx, task) {
996
- const warnings = [];
997
- const inputs = /* @__PURE__ */ new Map();
998
- const mode = ctx.config.gates.approvalMode;
999
- if (task.role === "qa" && task.type === "qa") {
1000
- const dev = ctx.db.prepare(
1040
+ function siblingRoleBlocking(ctx, task) {
1041
+ for (const rule of ctx.config.taskPreconditions ?? []) {
1042
+ if (rule.role !== task.role) continue;
1043
+ if (rule.type && rule.type !== task.type) continue;
1044
+ const done = ctx.db.prepare(
1001
1045
  `SELECT COUNT(*) AS c FROM tasks
1002
- WHERE role = 'developer' AND status = 'completed' AND module IS ? AND endpoint IS ?
1046
+ WHERE role = ? AND status = 'completed' AND module IS ? AND endpoint IS ?
1003
1047
  AND (? IS NULL OR page IS ?)`
1004
- ).get(task.module, task.endpoint, task.page, task.page);
1005
- if (dev.c === 0) {
1006
- throw new Error(`[\u524D\u7F6E\u6761\u4EF6] \u5BF9\u5E94\u7684 developer ${task.endpoint} \u4EFB\u52A1\u5C1A\u672A\u5B8C\u6210,\u65E0\u6CD5\u9886\u53D6 qa \u4EFB\u52A1\u3002`);
1048
+ ).get(rule.requiresSiblingRoleCompleted, task.module, task.endpoint, task.page, task.page);
1049
+ if (done.c === 0) {
1050
+ return `\u5BF9\u5E94\u7684 ${rule.requiresSiblingRoleCompleted} ${task.endpoint ?? ""} \u4EFB\u52A1\u5C1A\u672A\u5B8C\u6210,\u65E0\u6CD5\u9886\u53D6 ${task.role} \u4EFB\u52A1\u3002`;
1007
1051
  }
1008
1052
  }
1053
+ return null;
1054
+ }
1055
+ function validateClaim(ctx, task) {
1056
+ const warnings = [];
1057
+ const inputs = /* @__PURE__ */ new Map();
1058
+ const mode = ctx.config.gates.approvalMode;
1059
+ const blockMsg = siblingRoleBlocking(ctx, task);
1060
+ if (blockMsg) throw new Error(`[\u524D\u7F6E\u6761\u4EF6] ${blockMsg}`);
1009
1061
  if (task.module && (task.type === "build" || task.type === "hotfix") && !moduleCleared(ctx.db, task.module)) {
1010
1062
  warnings.push(
1011
1063
  `[\u6E05\u7B97] \u6A21\u5757 ${task.module} \u672A\u6E05\u7B97(module-prd \u672A\u5BA1\u6279)\u3002\u5EFA\u8BAE\u5148\u5BF9\u8D26:${ctx.config.cli} audit --module=${task.module}`
@@ -1068,19 +1120,18 @@ function validateComplete(ctx, task, status, opts = {}) {
1068
1120
  warnings.push(msg);
1069
1121
  }
1070
1122
  }
1071
- if (task.role === "developer" || task.role === "architect") {
1072
- const violations = runProtocolLints(ctx, { role: task.role, endpoint: task.endpoint });
1073
- if (violations.length > 0) {
1074
- const detail = violations.slice(0, 10).map((v) => ` ${v.file}:${v.line} [${v.lint}] ${v.message}
1123
+ const violations = runProtocolLints(ctx, { role: task.role, endpoint: task.endpoint });
1124
+ if (violations.length > 0) {
1125
+ const detail = violations.slice(0, 10).map((v) => ` ${v.file}:${v.line} [${v.lint}] ${v.message}
1075
1126
  ${v.text}`).join("\n");
1076
- throw new Error(
1077
- `[\u534F\u8BAE lint \u5931\u8D25] ${violations.length} \u5904\u8FDD\u4F8B,\u4FEE\u590D\u540E\u518D\u5B8C\u6210:
1127
+ throw new Error(
1128
+ `[\u534F\u8BAE lint \u5931\u8D25] ${violations.length} \u5904\u8FDD\u4F8B,\u4FEE\u590D\u540E\u518D\u5B8C\u6210:
1078
1129
  ${detail}${violations.length > 10 ? `
1079
1130
  ...\u7B49 ${violations.length} \u5904` : ""}`
1080
- );
1081
- }
1131
+ );
1082
1132
  }
1083
- if (task.role === "developer" && task.endpoint && ctx.config.machineChecks.enabled) {
1133
+ const producesMachineKind = producedKinds(ctx, task).some((k) => kindSpec(ctx.config, k).approval === "machine");
1134
+ if (producesMachineKind && task.endpoint && ctx.config.machineChecks.enabled) {
1084
1135
  const cmds = ctx.config.machineChecks[task.endpoint];
1085
1136
  if (Array.isArray(cmds)) {
1086
1137
  for (const cmd of cmds) {
@@ -1125,7 +1176,7 @@ function taskBlocked(ctx, task) {
1125
1176
  validateClaim(ctx, task);
1126
1177
  return false;
1127
1178
  } catch (err) {
1128
- return err instanceof Error && err.message.includes("\u4E0D\u5B58\u5728");
1179
+ return err instanceof Error && err.message.includes("[\u524D\u7F6E\u6761\u4EF6]");
1129
1180
  }
1130
1181
  }
1131
1182
  function coordHealth(ctx, artifacts, tasks) {
@@ -1206,6 +1257,7 @@ function buildTree(ctx, opts = {}) {
1206
1257
  eTasks.filter((t) => !t.page)
1207
1258
  );
1208
1259
  for (const child of pageNodes) health2 = worse(health2, child.health);
1260
+ if (coordFailed(ctx, module, endpoint, null)) health2 = "failed";
1209
1261
  return {
1210
1262
  key: `${module}/${endpoint}`,
1211
1263
  title: endpoint,
@@ -1754,16 +1806,16 @@ function ownerRole(ctx, kind) {
1754
1806
  if (kind === "code") return "developer";
1755
1807
  return null;
1756
1808
  }
1757
- function hasOpenReview(ctx, artifactId) {
1758
- const row = ctx.db.prepare(
1759
- `SELECT COUNT(*) AS c FROM tasks t
1809
+ function openReviewTargetKeys(ctx, artifactId) {
1810
+ const rows = ctx.db.prepare(
1811
+ `SELECT t.role, t.endpoint, t.module FROM tasks t
1760
1812
  JOIN task_inputs ti ON ti.task_id = t.id
1761
1813
  WHERE t.type = 'review' AND t.status IN ('pending', 'in_progress') AND ti.artifact_id = ?`
1762
- ).get(artifactId);
1763
- return row.c > 0;
1814
+ ).all(artifactId);
1815
+ return new Set(rows.map((r) => `${r.role}|${r.endpoint ?? ""}|${r.module ?? ""}`));
1764
1816
  }
1765
1817
  function spawnReviews(ctx, source, reason) {
1766
- if (hasOpenReview(ctx, source.id)) return 0;
1818
+ const openKeys = openReviewTargetKeys(ctx, source.id);
1767
1819
  const registry = getKindRegistry(ctx.config);
1768
1820
  const downstream = ctx.db.prepare(`SELECT a.* FROM artifacts a JOIN artifact_edges e ON e.to_id = a.id WHERE e.from_id = ?`).all(source.id);
1769
1821
  const targets = /* @__PURE__ */ new Map();
@@ -1772,6 +1824,7 @@ function spawnReviews(ctx, source, reason) {
1772
1824
  const role = ownerRole(ctx, child.kind);
1773
1825
  if (!role) continue;
1774
1826
  const key = `${role}|${child.endpoint ?? ""}|${child.module ?? source.module ?? ""}`;
1827
+ if (openKeys.has(key)) continue;
1775
1828
  targets.set(key, { role, endpoint: child.endpoint, module: child.module ?? source.module });
1776
1829
  }
1777
1830
  let spawned = 0;
@@ -1969,6 +2022,7 @@ function updateTask(ctx, { id, status, operator, force = false }) {
1969
2022
  throw new Error(`\u53EA\u6709\u6267\u884C\u4EBA\u624D\u80FD\u66F4\u65B0\u4EFB\u52A1\u72B6\u6001`);
1970
2023
  }
1971
2024
  const { warnings } = validateComplete(ctx, task, status, { force });
2025
+ let followupQaId = null;
1972
2026
  const tx = ctx.db.transaction(() => {
1973
2027
  ctx.db.prepare("UPDATE tasks SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(status, id);
1974
2028
  logEvent(ctx.db, {
@@ -1981,15 +2035,15 @@ function updateTask(ctx, { id, status, operator, force = false }) {
1981
2035
  endpoint: task.endpoint,
1982
2036
  page: task.page
1983
2037
  });
2038
+ if (task.type === "rework" && status === "completed") {
2039
+ followupQaId = spawnFollowupQa(ctx, task);
2040
+ }
1984
2041
  });
1985
2042
  tx();
2043
+ if (followupQaId !== null) warnings.push(`[\u590D\u9A8C] \u5DF2\u81EA\u52A8\u6D3E\u65B0\u4E00\u8F6E qa \u4EFB\u52A1 #${followupQaId}`);
1986
2044
  if (task.type === "hotfix" && status === "completed") {
1987
2045
  warnings.push(...checkContractTouch(ctx, task, operator));
1988
2046
  }
1989
- if (task.type === "rework" && status === "completed") {
1990
- const qaId = spawnFollowupQa(ctx, task);
1991
- warnings.push(`[\u590D\u9A8C] \u5DF2\u81EA\u52A8\u6D3E\u65B0\u4E00\u8F6E qa \u4EFB\u52A1 #${qaId}`);
1992
- }
1993
2047
  if (status === "completed" && task.external_ref?.startsWith("gh#")) {
1994
2048
  const closed = closeLinkedIssue(ctx, task.external_ref, task.id);
1995
2049
  warnings.push(closed ? `[issue] \u5DF2\u5173\u95ED ${task.external_ref}` : `[issue] ${task.external_ref} \u5173\u95ED\u5931\u8D25(gh \u4E0D\u53EF\u7528),\u8BF7\u624B\u52A8\u5904\u7406`);
@@ -2938,6 +2992,8 @@ var init_retro_command = __esm({
2938
2992
  });
2939
2993
 
2940
2994
  // core/commands/plan.command.ts
2995
+ import { existsSync as existsSync10 } from "node:fs";
2996
+ import { join as join12 } from "node:path";
2941
2997
  function planModule(ctx, moduleRaw, creator = "product-manager") {
2942
2998
  const module = normalizeModule(moduleRaw, ctx.config);
2943
2999
  const summary = { created: [], skipped: 0, cancelled: 0, warnings: [] };
@@ -2961,7 +3017,7 @@ function planModule(ctx, moduleRaw, creator = "product-manager") {
2961
3017
  if (ctx.config.gates.approvalMode === "enforce") throw new Error(`[\u524D\u7F6E\u6761\u4EF6] ${msg}`);
2962
3018
  summary.warnings.push(msg);
2963
3019
  }
2964
- const pagePrds = byKind("page-prd").filter((a) => a.endpoint && a.page);
3020
+ const pagePrds = byKind("page-prd").filter((a) => a.endpoint && a.page && existsSync10(join12(ctx.root, a.path)));
2965
3021
  const endpoints = [...new Set(pagePrds.map((a) => a.endpoint))];
2966
3022
  const desired = [
2967
3023
  { role: "architect", endpoint: "common", page: null, type: "build", assignee: "architect", content: `\u8BBE\u8BA1 ${module} \u6A21\u5757\u6570\u636E\u5E93` },
@@ -3041,11 +3097,11 @@ var init_plan_command = __esm({
3041
3097
  });
3042
3098
 
3043
3099
  // core/commands/install-hooks.command.ts
3044
- import { chmodSync, existsSync as existsSync10, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
3045
- import { join as join12 } from "node:path";
3100
+ import { chmodSync, existsSync as existsSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
3101
+ import { join as join13 } from "node:path";
3046
3102
  function installGitHooks(ctx) {
3047
- const hooksDir = join12(ctx.root, ".git", "hooks");
3048
- if (!existsSync10(join12(ctx.root, ".git"))) throw new Error("\u4E0D\u662F git \u4ED3\u5E93");
3103
+ const hooksDir = join13(ctx.root, ".git", "hooks");
3104
+ if (!existsSync11(join13(ctx.root, ".git"))) throw new Error("\u4E0D\u662F git \u4ED3\u5E93");
3049
3105
  mkdirSync5(hooksDir, { recursive: true });
3050
3106
  const installed = [];
3051
3107
  const postCommit = `#!/bin/sh
@@ -3053,9 +3109,9 @@ function installGitHooks(ctx) {
3053
3109
  ${WORKBENCH_BIN} postcommit >/dev/null 2>&1 &
3054
3110
  exit 0
3055
3111
  `;
3056
- writeFileSync4(join12(hooksDir, "post-commit"), postCommit);
3112
+ writeFileSync4(join13(hooksDir, "post-commit"), postCommit);
3057
3113
  try {
3058
- chmodSync(join12(hooksDir, "post-commit"), 493);
3114
+ chmodSync(join13(hooksDir, "post-commit"), 493);
3059
3115
  } catch {
3060
3116
  }
3061
3117
  installed.push("post-commit(sync \u5BF9\u8D26)");
@@ -3069,9 +3125,9 @@ if [ -n "$WORKBENCH_TASK_ID" ] && [ "$2" != "merge" ]; then
3069
3125
  fi
3070
3126
  exit 0
3071
3127
  `;
3072
- writeFileSync4(join12(hooksDir, "prepare-commit-msg"), prepare);
3128
+ writeFileSync4(join13(hooksDir, "prepare-commit-msg"), prepare);
3073
3129
  try {
3074
- chmodSync(join12(hooksDir, "prepare-commit-msg"), 493);
3130
+ chmodSync(join13(hooksDir, "prepare-commit-msg"), 493);
3075
3131
  } catch {
3076
3132
  }
3077
3133
  installed.push(`prepare-commit-msg(${ctx.config.git.trailerKey} trailer \u6CE8\u5165)`);
@@ -3086,8 +3142,8 @@ var init_install_hooks_command = __esm({
3086
3142
  });
3087
3143
 
3088
3144
  // core/commands/gen-agents.command.ts
3089
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readdirSync as readdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
3090
- import { dirname as dirname3, join as join13 } from "node:path";
3145
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readdirSync as readdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
3146
+ import { dirname as dirname3, join as join14 } from "node:path";
3091
3147
  function expandPath(ctx, kind) {
3092
3148
  const spec = getKindRegistry(ctx.config)[kind];
3093
3149
  const raw = spec?.pathPatterns?.[0] ?? "";
@@ -3113,8 +3169,8 @@ function parseTemplate(rawInput) {
3113
3169
  function genAgents(ctx, templatesDir) {
3114
3170
  const lang = ctx.config.language;
3115
3171
  const blocks = SHARED[lang] ?? SHARED.zh;
3116
- const dir = templatesDir ?? join13(WORKBENCH_DIR, "templates/agents", lang);
3117
- if (!existsSync11(dir)) throw new Error(`\u6A21\u677F\u76EE\u5F55\u4E0D\u5B58\u5728: ${dir}`);
3172
+ const dir = templatesDir ?? join14(WORKBENCH_DIR, "templates/agents", lang);
3173
+ if (!existsSync12(dir)) throw new Error(`\u6A21\u677F\u76EE\u5F55\u4E0D\u5B58\u5728: ${dir}`);
3118
3174
  const cli = ctx.config.cli;
3119
3175
  const baseTokens = {
3120
3176
  CLI: cli,
@@ -3132,6 +3188,16 @@ function genAgents(ctx, templatesDir) {
3132
3188
  PATH_DESIGN_PROMPTS: expandPath(ctx, "design-prompt"),
3133
3189
  PATH_PROTOTYPES: expandPath(ctx, "prototype"),
3134
3190
  PATH_ACCEPTANCE: expandPath(ctx, "acceptance"),
3191
+ // 路径投影:完整路径模板由 kind 注册表(前缀+coords 文法+ext)推导——
3192
+ // 项目覆盖 coords 后重跑 gen-agents,agent 指示与 scan 解析自动同步(单一真相源)
3193
+ TPL_FLOW: kindPathTemplate(ctx.config, "flow", lang) ?? "",
3194
+ TPL_MODULE_PRD: kindPathTemplate(ctx.config, "module-prd", lang) ?? "",
3195
+ TPL_PAGE_PRD: kindPathTemplate(ctx.config, "page-prd", lang) ?? "",
3196
+ TPL_DB_DOC: kindPathTemplate(ctx.config, "db-doc", lang) ?? "",
3197
+ TPL_DESIGN_SYSTEM: kindPathTemplate(ctx.config, "design-system", lang) ?? "",
3198
+ TPL_DESIGN_PROMPT: kindPathTemplate(ctx.config, "design-prompt", lang) ?? "",
3199
+ TPL_PROTOTYPE: kindPathTemplate(ctx.config, "prototype", lang) ?? "",
3200
+ TPL_ACCEPTANCE: kindPathTemplate(ctx.config, "acceptance", lang) ?? "",
3135
3201
  ENDPOINTS: ctx.config.endpoints.join(" / "),
3136
3202
  PIPELINE: ctx.config.pipeline.join(" \u2192 "),
3137
3203
  CODE_ROOTS: ctx.config.endpoints.map((e) => `| ${e} | ${(ctx.config.codeRoots[e] ?? []).join(blocks.sep) || blocks.todo} |`).join("\n")
@@ -3151,7 +3217,7 @@ function genAgents(ctx, templatesDir) {
3151
3217
  if (!file.endsWith(".md")) continue;
3152
3218
  const role = file.replace(/\.md$/, "");
3153
3219
  if (!pipeline.has(role)) continue;
3154
- const tpl = parseTemplate(readFileSync6(join13(dir, file), "utf-8"));
3220
+ const tpl = parseTemplate(readFileSync6(join14(dir, file), "utf-8"));
3155
3221
  for (const adapter of adapters) {
3156
3222
  const memory = adapter.memoryBlock(role, lang);
3157
3223
  const spec = {
@@ -3162,7 +3228,7 @@ function genAgents(ctx, templatesDir) {
3162
3228
  body: renderTokens(tpl.body, memory)
3163
3229
  };
3164
3230
  const rel = `${adapter.agentsDir}/${adapter.agentFile(role)}`;
3165
- const abs = join13(ctx.root, rel);
3231
+ const abs = join14(ctx.root, rel);
3166
3232
  mkdirSync6(dirname3(abs), { recursive: true });
3167
3233
  writeFileSync5(abs, adapter.renderAgent(spec));
3168
3234
  written.push(rel);
@@ -3246,8 +3312,8 @@ When MCP is registered, prefer the \`wb_*\` typed tools (same source & transacti
3246
3312
  });
3247
3313
 
3248
3314
  // core/commands/audit.command.ts
3249
- import { existsSync as existsSync12 } from "node:fs";
3250
- import { join as join14 } from "node:path";
3315
+ import { existsSync as existsSync13 } from "node:fs";
3316
+ import { join as join15 } from "node:path";
3251
3317
  function auditModule(ctx, moduleRaw) {
3252
3318
  const module = normalizeModule(moduleRaw, ctx.config);
3253
3319
  const kinds = contractKinds(ctx.config);
@@ -3257,9 +3323,9 @@ function auditModule(ctx, moduleRaw) {
3257
3323
  path: a.path,
3258
3324
  status: reviewStatus(a),
3259
3325
  everApproved: everApproved(a),
3260
- onDisk: existsSync12(join14(ctx.root, a.path))
3326
+ onDisk: existsSync13(join15(ctx.root, a.path))
3261
3327
  }));
3262
- const codeDirs = artifacts.filter((a) => a.kind === "code").map((a) => ({ path: a.path, endpoint: a.endpoint, onDisk: existsSync12(join14(ctx.root, a.path)) }));
3328
+ const codeDirs = artifacts.filter((a) => a.kind === "code").map((a) => ({ path: a.path, endpoint: a.endpoint, onDisk: existsSync13(join15(ctx.root, a.path)) }));
3263
3329
  return {
3264
3330
  module,
3265
3331
  cleared: moduleCleared(ctx.db, module),
@@ -3323,28 +3389,28 @@ var init_intake_command = __esm({
3323
3389
  // core/commands/init.command.ts
3324
3390
  import {
3325
3391
  copyFileSync,
3326
- existsSync as existsSync13,
3392
+ existsSync as existsSync14,
3327
3393
  mkdirSync as mkdirSync7,
3328
3394
  readFileSync as readFileSync7,
3329
3395
  readdirSync as readdirSync6,
3330
3396
  writeFileSync as writeFileSync6
3331
3397
  } from "node:fs";
3332
- import { dirname as dirname4, join as join15 } from "node:path";
3398
+ import { dirname as dirname4, join as join16 } from "node:path";
3333
3399
  function deployPreset(root) {
3334
3400
  const deployed = [];
3335
- if (!existsSync13(PRESET_DIR)) return deployed;
3401
+ if (!existsSync14(PRESET_DIR)) return deployed;
3336
3402
  const walk = (rel) => {
3337
- for (const ent of readdirSync6(join15(PRESET_DIR, rel), {
3403
+ for (const ent of readdirSync6(join16(PRESET_DIR, rel), {
3338
3404
  withFileTypes: true
3339
3405
  })) {
3340
- const childRel = rel ? join15(rel, ent.name) : ent.name;
3406
+ const childRel = rel ? join16(rel, ent.name) : ent.name;
3341
3407
  if (ent.isDirectory()) {
3342
3408
  walk(childRel);
3343
3409
  } else if (ent.isFile()) {
3344
- const dest = join15(root, childRel);
3345
- if (existsSync13(dest)) continue;
3410
+ const dest = join16(root, childRel);
3411
+ if (existsSync14(dest)) continue;
3346
3412
  mkdirSync7(dirname4(dest), { recursive: true });
3347
- copyFileSync(join15(PRESET_DIR, childRel), dest);
3413
+ copyFileSync(join16(PRESET_DIR, childRel), dest);
3348
3414
  deployed.push(childRel);
3349
3415
  }
3350
3416
  }
@@ -3353,8 +3419,8 @@ function deployPreset(root) {
3353
3419
  return deployed;
3354
3420
  }
3355
3421
  function ensureRootTsx(root) {
3356
- const pkgPath = join15(root, "package.json");
3357
- if (!existsSync13(pkgPath)) return false;
3422
+ const pkgPath = join16(root, "package.json");
3423
+ if (!existsSync14(pkgPath)) return false;
3358
3424
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
3359
3425
  if (pkg.devDependencies?.tsx) return false;
3360
3426
  pkg.devDependencies = { ...pkg.devDependencies ?? {}, tsx: TSX_VERSION };
@@ -3362,8 +3428,8 @@ function ensureRootTsx(root) {
3362
3428
  return true;
3363
3429
  }
3364
3430
  function initProject(root, opts) {
3365
- const configPath = join15(root, CONFIG_FILENAME);
3366
- if (existsSync13(configPath)) {
3431
+ const configPath = join16(root, CONFIG_FILENAME);
3432
+ if (existsSync14(configPath)) {
3367
3433
  throw new Error(
3368
3434
  `${CONFIG_FILENAME} \u5DF2\u5B58\u5728,init \u53EA\u7528\u4E8E\u7A7A\u9879\u76EE\u5F15\u5BFC(\u6539\u914D\u7F6E\u8BF7\u76F4\u63A5\u7F16\u8F91\u8BE5\u6587\u4EF6)`
3369
3435
  );
@@ -3397,10 +3463,10 @@ function initProject(root, opts) {
3397
3463
  const scaffolded = [];
3398
3464
  if (opts.scaffold !== false) {
3399
3465
  for (const dir of DOC_DIRS) {
3400
- const abs = join15(root, dir);
3401
- if (!existsSync13(abs)) {
3466
+ const abs = join16(root, dir);
3467
+ if (!existsSync14(abs)) {
3402
3468
  mkdirSync7(abs, { recursive: true });
3403
- writeFileSync6(join15(abs, ".gitkeep"), "");
3469
+ writeFileSync6(join16(abs, ".gitkeep"), "");
3404
3470
  scaffolded.push(dir);
3405
3471
  }
3406
3472
  }
@@ -3432,7 +3498,7 @@ function initProject(root, opts) {
3432
3498
  }
3433
3499
  const notes = adapters.flatMap((a) => a.notes);
3434
3500
  let hooks = [];
3435
- if (opts.gitHooks !== false && existsSync13(join15(root, ".git"))) {
3501
+ if (opts.gitHooks !== false && existsSync14(join16(root, ".git"))) {
3436
3502
  try {
3437
3503
  hooks = installGitHooks(ctx);
3438
3504
  } catch {
@@ -3474,7 +3540,7 @@ var init_init_command = __esm({
3474
3540
  "docs/design/prototypes",
3475
3541
  "docs/acceptance"
3476
3542
  ];
3477
- PRESET_DIR = join15(WORKBENCH_DIR, "preset");
3543
+ PRESET_DIR = join16(WORKBENCH_DIR, "preset");
3478
3544
  TSX_VERSION = "^4";
3479
3545
  }
3480
3546
  });
@@ -3532,6 +3598,7 @@ __export(core_exports, {
3532
3598
  initProject: () => initProject,
3533
3599
  installGitHooks: () => installGitHooks,
3534
3600
  intakeIssues: () => intakeIssues,
3601
+ kindPathTemplate: () => kindPathTemplate,
3535
3602
  kindSpec: () => kindSpec,
3536
3603
  listArtifacts: () => listArtifacts,
3537
3604
  listEvents: () => listEvents,
@@ -3762,17 +3829,17 @@ __export(app_exports, {
3762
3829
  import fastifyCors from "@fastify/cors";
3763
3830
  import fastifyStatic from "@fastify/static";
3764
3831
  import Fastify from "fastify";
3765
- import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync8, readdirSync as readdirSync7, statSync as statSync6 } from "node:fs";
3766
- import { isAbsolute as isAbsolute2, join as join16, normalize, relative as relative2 } from "node:path";
3832
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, readFileSync as readFileSync8, readdirSync as readdirSync7, statSync as statSync6 } from "node:fs";
3833
+ import { isAbsolute as isAbsolute2, join as join17, normalize, relative as relative2 } from "node:path";
3767
3834
  function getArtifact(ctx, id) {
3768
3835
  const row = ctx.db.prepare("SELECT * FROM artifacts WHERE id = ?").get(id);
3769
3836
  if (!row) throw Object.assign(new Error(`artifact #${id} \u4E0D\u5B58\u5728`), { statusCode: 404 });
3770
3837
  return row;
3771
3838
  }
3772
3839
  function guardedAbsPath(ctx, artifact, rel) {
3773
- const base = normalize(join16(ctx.root, artifact.path));
3840
+ const base = normalize(join17(ctx.root, artifact.path));
3774
3841
  if (!rel) return base;
3775
- const target = normalize(join16(base, rel));
3842
+ const target = normalize(join17(base, rel));
3776
3843
  const r = relative2(base, target);
3777
3844
  if (r === "" || r === "." || r.startsWith("..") || isAbsolute2(r)) {
3778
3845
  throw Object.assign(new Error("\u8DEF\u5F84\u8D8A\u754C"), { statusCode: 403 });
@@ -3782,11 +3849,11 @@ function guardedAbsPath(ctx, artifact, rel) {
3782
3849
  async function createServer(ctx) {
3783
3850
  const app = Fastify({ logger: false });
3784
3851
  await app.register(fastifyCors, { origin: true });
3785
- const webDist = join16(WORKBENCH_DIR, "web/dist");
3786
- if (existsSync14(webDist)) {
3852
+ const webDist = join17(WORKBENCH_DIR, "web/dist");
3853
+ if (existsSync15(webDist)) {
3787
3854
  await app.register(fastifyStatic, { root: webDist });
3788
3855
  }
3789
- const protoRoot = join16(ctx.root, ctx.config.docs.design, "prototypes");
3856
+ const protoRoot = join17(ctx.root, ctx.config.docs.design, "prototypes");
3790
3857
  mkdirSync8(protoRoot, { recursive: true });
3791
3858
  await app.register(fastifyStatic, { root: protoRoot, prefix: "/proto/", decorateReply: false });
3792
3859
  app.get("/api/meta", async () => ({
@@ -3821,7 +3888,7 @@ async function createServer(ctx) {
3821
3888
  let content = null;
3822
3889
  let isDirectory = false;
3823
3890
  let missing = false;
3824
- if (!existsSync14(abs)) {
3891
+ if (!existsSync15(abs)) {
3825
3892
  missing = true;
3826
3893
  } else if (statSync6(abs).isDirectory()) {
3827
3894
  isDirectory = true;
@@ -3837,20 +3904,20 @@ async function createServer(ctx) {
3837
3904
  const walk = (dir, rel) => {
3838
3905
  for (const name of readdirSync7(dir).sort()) {
3839
3906
  if (name === "node_modules" || name.startsWith(".")) continue;
3840
- const full = join16(dir, name);
3907
+ const full = join17(dir, name);
3841
3908
  const relPath = rel ? `${rel}/${name}` : name;
3842
3909
  const st = statSync6(full);
3843
3910
  if (st.isDirectory()) walk(full, relPath);
3844
3911
  else files.push({ rel: relPath, size: st.size });
3845
3912
  }
3846
3913
  };
3847
- if (existsSync14(base) && statSync6(base).isDirectory()) walk(base, "");
3914
+ if (existsSync15(base) && statSync6(base).isDirectory()) walk(base, "");
3848
3915
  return { files };
3849
3916
  });
3850
3917
  app.get("/api/artifact/:id/file", async (req) => {
3851
3918
  const artifact = getArtifact(ctx, parseInt(req.params.id));
3852
3919
  const abs = guardedAbsPath(ctx, artifact, req.query.rel);
3853
- if (!existsSync14(abs) || statSync6(abs).isDirectory()) {
3920
+ if (!existsSync15(abs) || statSync6(abs).isDirectory()) {
3854
3921
  throw Object.assign(new Error("\u6587\u4EF6\u4E0D\u5B58\u5728"), { statusCode: 404 });
3855
3922
  }
3856
3923
  if (statSync6(abs).size > MAX_INLINE_SIZE) {
@@ -3905,7 +3972,7 @@ data: {"cursor":${cursor2}}
3905
3972
  endpoint: a.endpoint,
3906
3973
  page: a.page,
3907
3974
  review_status: reviewStatus(a),
3908
- missing: !existsSync14(join16(ctx.root, a.path))
3975
+ missing: !existsSync15(join17(ctx.root, a.path))
3909
3976
  }));
3910
3977
  const edges = ctx.db.prepare("SELECT id, from_id, to_id, source FROM artifact_edges").all().filter((e) => ids.has(e.from_id) && ids.has(e.to_id));
3911
3978
  return { nodes, edges };
@@ -3922,7 +3989,7 @@ data: {"cursor":${cursor2}}
3922
3989
  for (const name of readdirSync7(dir).sort()) {
3923
3990
  if (files.length >= 30) return;
3924
3991
  if (name.startsWith(".") || name === "node_modules" || name === "dist" || name === "build") continue;
3925
- const full = join16(dir, name);
3992
+ const full = join17(dir, name);
3926
3993
  const relPath = rel ? `${rel}/${name}` : name;
3927
3994
  if (statSync6(full).isDirectory()) walk(full, relPath);
3928
3995
  else if (relPath.toLowerCase().includes(q) && !registered.has(relPath)) files.push(relPath);
@@ -4022,7 +4089,7 @@ data: {"cursor":${cursor2}}
4022
4089
  app.get("/api/artifact/:id/diff", async (req) => {
4023
4090
  const artifact = getArtifact(ctx, parseInt(req.params.id));
4024
4091
  const abs = guardedAbsPath(ctx, artifact);
4025
- const current = existsSync14(abs) && !statSync6(abs).isDirectory() ? readFileSync8(abs, "utf-8") : null;
4092
+ const current = existsSync15(abs) && !statSync6(abs).isDirectory() ? readFileSync8(abs, "utf-8") : null;
4026
4093
  return { approved: approvedContent(ctx, artifact.id), current };
4027
4094
  });
4028
4095
  app.post(
@@ -4088,15 +4155,17 @@ function hookPlatform() {
4088
4155
  const arg = process.argv.find((a) => a.startsWith("--platform="));
4089
4156
  return arg ? arg.slice("--platform=".length) : "claude";
4090
4157
  }
4091
- function extractFilePath(input) {
4158
+ function extractFilePath(input, adapter) {
4159
+ const fromAdapter = adapter?.parseHookInput(input).filePath;
4160
+ if (fromAdapter) return fromAdapter;
4092
4161
  return input?.tool_input?.file_path ?? // claude PreToolUse/PostToolUse
4093
4162
  input?.tool_input?.filePath ?? input?.file_path ?? // cursor afterFileEdit / 通用
4094
4163
  input?.filePath ?? input?.args?.file_path ?? // opencode tool 参数
4095
4164
  input?.args?.filePath ?? input?.arguments?.file_path ?? // codex 兜底
4096
4165
  input?.arguments?.filePath ?? input?.input?.file_path ?? void 0;
4097
4166
  }
4098
- function hookProjectDir() {
4099
- return process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_PROJECT_DIR ?? process.env.CURSOR_PROJECT_DIR ?? process.env.OPENCODE_PROJECT_DIR ?? process.env.WORKBENCH_PROJECT ?? void 0;
4167
+ function hookProjectDir(adapter) {
4168
+ return (adapter && process.env[adapter.projectDirEnvVar]) ?? process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_PROJECT_DIR ?? process.env.CURSOR_PROJECT_DIR ?? process.env.OPENCODE_PROJECT_DIR ?? process.env.WORKBENCH_PROJECT ?? void 0;
4100
4169
  }
4101
4170
  async function readStdinJson() {
4102
4171
  const chunks = [];
@@ -4120,14 +4189,16 @@ __export(hook_pretooluse_exports, {
4120
4189
  });
4121
4190
  async function writeGateHook(platform) {
4122
4191
  const input = await readStdinJson();
4123
- const filePath = extractFilePath(input);
4192
+ const { getAdapter: getAdapter2, PLATFORM_IDS: PLATFORM_IDS2 } = await Promise.resolve().then(() => (init_platforms(), platforms_exports));
4193
+ const adapter = PLATFORM_IDS2.includes(platform) ? getAdapter2(platform) : void 0;
4194
+ const filePath = extractFilePath(input, adapter);
4124
4195
  if (!filePath) return;
4125
4196
  const { openWorkbench: openWorkbench2 } = await Promise.resolve().then(() => (init_db(), db_exports));
4126
4197
  const { normalizeRelPath: normalizeRelPath2 } = await Promise.resolve().then(() => (init_artifact_commands(), artifact_commands_exports));
4127
4198
  const { reviewStatus: reviewStatus2 } = await Promise.resolve().then(() => (init_derive(), derive_exports));
4128
4199
  const { contractKinds: contractKinds2 } = await Promise.resolve().then(() => (init_kind(), kind_exports));
4129
4200
  const { logEvent: logEvent2 } = await Promise.resolve().then(() => (init_events(), events_exports));
4130
- const ctx = openWorkbench2(hookProjectDir());
4201
+ const ctx = openWorkbench2(hookProjectDir(adapter));
4131
4202
  const mode = ctx.config.gates.writeGate;
4132
4203
  if (mode === "off") return;
4133
4204
  const rel = normalizeRelPath2(ctx, filePath);
@@ -4154,12 +4225,10 @@ async function writeGateHook(platform) {
4154
4225
  const msg = `\u8BE5\u6587\u4EF6\u662F\u5DF2\u5BA1\u6279\u5951\u7EA6(${artifact.kind}): ${rel}\u3002
4155
4226
  \u4FEE\u6539\u524D\u8BF7\u5148\u9886\u53D6\u5BF9\u5E94\u4EFB\u52A1:${ctx.config.cli} list --module=${artifact.module ?? ""} --status=pending
4156
4227
  \u9886\u53D6\u540E\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF WORKBENCH_TASK_ID=<\u4EFB\u52A1id> \u518D\u4FEE\u6539;\u82E5\u5BF9\u5185\u5BB9\u672C\u8EAB\u6709\u5F02\u8BAE,\u7528 dispute \u547D\u4EE4\u7559\u75D5\u5E76\u505C\u6B62\u3002`;
4157
- if (platform === "cursor") {
4158
- process.stdout.write(JSON.stringify({ permission: "deny", userMessage: msg, agentMessage: msg }));
4159
- process.exit(0);
4160
- }
4161
- console.error(msg);
4162
- process.exit(2);
4228
+ const resp = adapter?.respondBlocked(msg) ?? { exitCode: 2, stderr: msg };
4229
+ if (resp.stdout) process.stdout.write(resp.stdout);
4230
+ if (resp.stderr) console.error(resp.stderr);
4231
+ process.exit(resp.exitCode);
4163
4232
  }
4164
4233
  }
4165
4234
  var init_hook_pretooluse = __esm({
@@ -4178,13 +4247,15 @@ var hook_refresh_exports = {};
4178
4247
  __export(hook_refresh_exports, {
4179
4248
  refreshHook: () => refreshHook
4180
4249
  });
4181
- async function refreshHook(_platform) {
4250
+ async function refreshHook(platform) {
4182
4251
  const input = await readStdinJson();
4183
- const filePath = extractFilePath(input);
4252
+ const { getAdapter: getAdapter2, PLATFORM_IDS: PLATFORM_IDS2 } = await Promise.resolve().then(() => (init_platforms(), platforms_exports));
4253
+ const adapter = platform && PLATFORM_IDS2.includes(platform) ? getAdapter2(platform) : void 0;
4254
+ const filePath = extractFilePath(input, adapter);
4184
4255
  if (!filePath) return;
4185
4256
  const { openWorkbench: openWorkbench2 } = await Promise.resolve().then(() => (init_db(), db_exports));
4186
4257
  const { refreshArtifact: refreshArtifact2, normalizeRelPath: normalizeRelPath2 } = await Promise.resolve().then(() => (init_artifact_commands(), artifact_commands_exports));
4187
- const ctx = openWorkbench2(hookProjectDir());
4258
+ const ctx = openWorkbench2(hookProjectDir(adapter));
4188
4259
  const rel = normalizeRelPath2(ctx, filePath);
4189
4260
  const exact = ctx.db.prepare("SELECT id FROM artifacts WHERE path = ?").get(rel);
4190
4261
  if (exact) {
@@ -4284,7 +4355,6 @@ function printTaskDetail(ctx, id) {
4284
4355
  }
4285
4356
  console.log();
4286
4357
  }
4287
- var ALL_PLATFORMS = ["claude", "codex", "opencode", "cursor"];
4288
4358
  var PRIORITY_PROVIDERS = [
4289
4359
  "anthropic",
4290
4360
  "openai",
@@ -4330,7 +4400,7 @@ async function promptInit() {
4330
4400
  const zh = language === "zh";
4331
4401
  const platform = await select({
4332
4402
  message: zh ? "\u5E73\u53F0" : "Platform",
4333
- choices: ALL_PLATFORMS.map((p) => ({ name: p, value: p })),
4403
+ choices: PLATFORM_IDS.map((p) => ({ name: p, value: p })),
4334
4404
  default: "claude"
4335
4405
  });
4336
4406
  const platforms = [platform];
@@ -4358,14 +4428,14 @@ async function promptInit() {
4358
4428
  choices: prov.models.map((m) => ({ name: m.name, value: m.id })),
4359
4429
  pageSize: 12
4360
4430
  });
4361
- if (modelId) modelObj[p] = p === "opencode" ? `${provId}/${modelId}` : modelId;
4431
+ if (modelId) modelObj[p] = getAdapter(p).formatModel(provId, modelId);
4362
4432
  }
4363
4433
  return { platforms, endpoints, model: Object.keys(modelObj).length ? modelObj : void 0, language };
4364
4434
  }
4365
4435
  async function runInit(root, a) {
4366
- const { existsSync: existsSync15 } = await import("node:fs");
4367
- const { join: join17 } = await import("node:path");
4368
- if (existsSync15(join17(root, "workbench.config.json"))) {
4436
+ const { existsSync: existsSync16 } = await import("node:fs");
4437
+ const { join: join18 } = await import("node:path");
4438
+ if (existsSync16(join18(root, "workbench.config.json"))) {
4369
4439
  console.log(chalk.red("\u9519\u8BEF: workbench.config.json \u5DF2\u5B58\u5728,init \u53EA\u7528\u4E8E\u7A7A\u9879\u76EE\u5F15\u5BFC(\u6539\u914D\u7F6E\u8BF7\u76F4\u63A5\u7F16\u8F91\u8BE5\u6587\u4EF6)"));
4370
4440
  process.exit(1);
4371
4441
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dawipong/opcflow",
3
- "version": "0.5.0",
3
+ "version": "0.7.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 完成后):执行验收**