@dawipong/opcflow 0.5.0 → 0.6.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.
Files changed (2) hide show
  1. package/dist/cli.mjs +149 -100
  2. package/package.json +1 -1
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
  ],
@@ -992,20 +1027,27 @@ function requirementSatisfied(ctx, req, matched) {
992
1027
  (a) => kindSpec(ctx.config, a.kind).approval === "thumbs" ? prototypeEndorsed(ctx.db, a) : reviewStatus(a) === "approved"
993
1028
  );
994
1029
  }
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(
1030
+ function siblingRoleBlocking(ctx, task) {
1031
+ for (const rule of ctx.config.taskPreconditions ?? []) {
1032
+ if (rule.role !== task.role) continue;
1033
+ if (rule.type && rule.type !== task.type) continue;
1034
+ const done = ctx.db.prepare(
1001
1035
  `SELECT COUNT(*) AS c FROM tasks
1002
- WHERE role = 'developer' AND status = 'completed' AND module IS ? AND endpoint IS ?
1036
+ WHERE role = ? AND status = 'completed' AND module IS ? AND endpoint IS ?
1003
1037
  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`);
1038
+ ).get(rule.requiresSiblingRoleCompleted, task.module, task.endpoint, task.page, task.page);
1039
+ if (done.c === 0) {
1040
+ return `\u5BF9\u5E94\u7684 ${rule.requiresSiblingRoleCompleted} ${task.endpoint ?? ""} \u4EFB\u52A1\u5C1A\u672A\u5B8C\u6210,\u65E0\u6CD5\u9886\u53D6 ${task.role} \u4EFB\u52A1\u3002`;
1007
1041
  }
1008
1042
  }
1043
+ return null;
1044
+ }
1045
+ function validateClaim(ctx, task) {
1046
+ const warnings = [];
1047
+ const inputs = /* @__PURE__ */ new Map();
1048
+ const mode = ctx.config.gates.approvalMode;
1049
+ const blockMsg = siblingRoleBlocking(ctx, task);
1050
+ if (blockMsg) throw new Error(`[\u524D\u7F6E\u6761\u4EF6] ${blockMsg}`);
1009
1051
  if (task.module && (task.type === "build" || task.type === "hotfix") && !moduleCleared(ctx.db, task.module)) {
1010
1052
  warnings.push(
1011
1053
  `[\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 +1110,18 @@ function validateComplete(ctx, task, status, opts = {}) {
1068
1110
  warnings.push(msg);
1069
1111
  }
1070
1112
  }
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}
1113
+ const violations = runProtocolLints(ctx, { role: task.role, endpoint: task.endpoint });
1114
+ if (violations.length > 0) {
1115
+ const detail = violations.slice(0, 10).map((v) => ` ${v.file}:${v.line} [${v.lint}] ${v.message}
1075
1116
  ${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:
1117
+ throw new Error(
1118
+ `[\u534F\u8BAE lint \u5931\u8D25] ${violations.length} \u5904\u8FDD\u4F8B,\u4FEE\u590D\u540E\u518D\u5B8C\u6210:
1078
1119
  ${detail}${violations.length > 10 ? `
1079
1120
  ...\u7B49 ${violations.length} \u5904` : ""}`
1080
- );
1081
- }
1121
+ );
1082
1122
  }
1083
- if (task.role === "developer" && task.endpoint && ctx.config.machineChecks.enabled) {
1123
+ const producesMachineKind = producedKinds(ctx, task).some((k) => kindSpec(ctx.config, k).approval === "machine");
1124
+ if (producesMachineKind && task.endpoint && ctx.config.machineChecks.enabled) {
1084
1125
  const cmds = ctx.config.machineChecks[task.endpoint];
1085
1126
  if (Array.isArray(cmds)) {
1086
1127
  for (const cmd of cmds) {
@@ -1125,7 +1166,7 @@ function taskBlocked(ctx, task) {
1125
1166
  validateClaim(ctx, task);
1126
1167
  return false;
1127
1168
  } catch (err) {
1128
- return err instanceof Error && err.message.includes("\u4E0D\u5B58\u5728");
1169
+ return err instanceof Error && err.message.includes("[\u524D\u7F6E\u6761\u4EF6]");
1129
1170
  }
1130
1171
  }
1131
1172
  function coordHealth(ctx, artifacts, tasks) {
@@ -1206,6 +1247,7 @@ function buildTree(ctx, opts = {}) {
1206
1247
  eTasks.filter((t) => !t.page)
1207
1248
  );
1208
1249
  for (const child of pageNodes) health2 = worse(health2, child.health);
1250
+ if (coordFailed(ctx, module, endpoint, null)) health2 = "failed";
1209
1251
  return {
1210
1252
  key: `${module}/${endpoint}`,
1211
1253
  title: endpoint,
@@ -1754,16 +1796,16 @@ function ownerRole(ctx, kind) {
1754
1796
  if (kind === "code") return "developer";
1755
1797
  return null;
1756
1798
  }
1757
- function hasOpenReview(ctx, artifactId) {
1758
- const row = ctx.db.prepare(
1759
- `SELECT COUNT(*) AS c FROM tasks t
1799
+ function openReviewTargetKeys(ctx, artifactId) {
1800
+ const rows = ctx.db.prepare(
1801
+ `SELECT t.role, t.endpoint, t.module FROM tasks t
1760
1802
  JOIN task_inputs ti ON ti.task_id = t.id
1761
1803
  WHERE t.type = 'review' AND t.status IN ('pending', 'in_progress') AND ti.artifact_id = ?`
1762
- ).get(artifactId);
1763
- return row.c > 0;
1804
+ ).all(artifactId);
1805
+ return new Set(rows.map((r) => `${r.role}|${r.endpoint ?? ""}|${r.module ?? ""}`));
1764
1806
  }
1765
1807
  function spawnReviews(ctx, source, reason) {
1766
- if (hasOpenReview(ctx, source.id)) return 0;
1808
+ const openKeys = openReviewTargetKeys(ctx, source.id);
1767
1809
  const registry = getKindRegistry(ctx.config);
1768
1810
  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
1811
  const targets = /* @__PURE__ */ new Map();
@@ -1772,6 +1814,7 @@ function spawnReviews(ctx, source, reason) {
1772
1814
  const role = ownerRole(ctx, child.kind);
1773
1815
  if (!role) continue;
1774
1816
  const key = `${role}|${child.endpoint ?? ""}|${child.module ?? source.module ?? ""}`;
1817
+ if (openKeys.has(key)) continue;
1775
1818
  targets.set(key, { role, endpoint: child.endpoint, module: child.module ?? source.module });
1776
1819
  }
1777
1820
  let spawned = 0;
@@ -1969,6 +2012,7 @@ function updateTask(ctx, { id, status, operator, force = false }) {
1969
2012
  throw new Error(`\u53EA\u6709\u6267\u884C\u4EBA\u624D\u80FD\u66F4\u65B0\u4EFB\u52A1\u72B6\u6001`);
1970
2013
  }
1971
2014
  const { warnings } = validateComplete(ctx, task, status, { force });
2015
+ let followupQaId = null;
1972
2016
  const tx = ctx.db.transaction(() => {
1973
2017
  ctx.db.prepare("UPDATE tasks SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(status, id);
1974
2018
  logEvent(ctx.db, {
@@ -1981,15 +2025,15 @@ function updateTask(ctx, { id, status, operator, force = false }) {
1981
2025
  endpoint: task.endpoint,
1982
2026
  page: task.page
1983
2027
  });
2028
+ if (task.type === "rework" && status === "completed") {
2029
+ followupQaId = spawnFollowupQa(ctx, task);
2030
+ }
1984
2031
  });
1985
2032
  tx();
2033
+ if (followupQaId !== null) warnings.push(`[\u590D\u9A8C] \u5DF2\u81EA\u52A8\u6D3E\u65B0\u4E00\u8F6E qa \u4EFB\u52A1 #${followupQaId}`);
1986
2034
  if (task.type === "hotfix" && status === "completed") {
1987
2035
  warnings.push(...checkContractTouch(ctx, task, operator));
1988
2036
  }
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
2037
  if (status === "completed" && task.external_ref?.startsWith("gh#")) {
1994
2038
  const closed = closeLinkedIssue(ctx, task.external_ref, task.id);
1995
2039
  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 +2982,8 @@ var init_retro_command = __esm({
2938
2982
  });
2939
2983
 
2940
2984
  // core/commands/plan.command.ts
2985
+ import { existsSync as existsSync10 } from "node:fs";
2986
+ import { join as join12 } from "node:path";
2941
2987
  function planModule(ctx, moduleRaw, creator = "product-manager") {
2942
2988
  const module = normalizeModule(moduleRaw, ctx.config);
2943
2989
  const summary = { created: [], skipped: 0, cancelled: 0, warnings: [] };
@@ -2961,7 +3007,7 @@ function planModule(ctx, moduleRaw, creator = "product-manager") {
2961
3007
  if (ctx.config.gates.approvalMode === "enforce") throw new Error(`[\u524D\u7F6E\u6761\u4EF6] ${msg}`);
2962
3008
  summary.warnings.push(msg);
2963
3009
  }
2964
- const pagePrds = byKind("page-prd").filter((a) => a.endpoint && a.page);
3010
+ const pagePrds = byKind("page-prd").filter((a) => a.endpoint && a.page && existsSync10(join12(ctx.root, a.path)));
2965
3011
  const endpoints = [...new Set(pagePrds.map((a) => a.endpoint))];
2966
3012
  const desired = [
2967
3013
  { role: "architect", endpoint: "common", page: null, type: "build", assignee: "architect", content: `\u8BBE\u8BA1 ${module} \u6A21\u5757\u6570\u636E\u5E93` },
@@ -3041,11 +3087,11 @@ var init_plan_command = __esm({
3041
3087
  });
3042
3088
 
3043
3089
  // 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";
3090
+ import { chmodSync, existsSync as existsSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
3091
+ import { join as join13 } from "node:path";
3046
3092
  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");
3093
+ const hooksDir = join13(ctx.root, ".git", "hooks");
3094
+ if (!existsSync11(join13(ctx.root, ".git"))) throw new Error("\u4E0D\u662F git \u4ED3\u5E93");
3049
3095
  mkdirSync5(hooksDir, { recursive: true });
3050
3096
  const installed = [];
3051
3097
  const postCommit = `#!/bin/sh
@@ -3053,9 +3099,9 @@ function installGitHooks(ctx) {
3053
3099
  ${WORKBENCH_BIN} postcommit >/dev/null 2>&1 &
3054
3100
  exit 0
3055
3101
  `;
3056
- writeFileSync4(join12(hooksDir, "post-commit"), postCommit);
3102
+ writeFileSync4(join13(hooksDir, "post-commit"), postCommit);
3057
3103
  try {
3058
- chmodSync(join12(hooksDir, "post-commit"), 493);
3104
+ chmodSync(join13(hooksDir, "post-commit"), 493);
3059
3105
  } catch {
3060
3106
  }
3061
3107
  installed.push("post-commit(sync \u5BF9\u8D26)");
@@ -3069,9 +3115,9 @@ if [ -n "$WORKBENCH_TASK_ID" ] && [ "$2" != "merge" ]; then
3069
3115
  fi
3070
3116
  exit 0
3071
3117
  `;
3072
- writeFileSync4(join12(hooksDir, "prepare-commit-msg"), prepare);
3118
+ writeFileSync4(join13(hooksDir, "prepare-commit-msg"), prepare);
3073
3119
  try {
3074
- chmodSync(join12(hooksDir, "prepare-commit-msg"), 493);
3120
+ chmodSync(join13(hooksDir, "prepare-commit-msg"), 493);
3075
3121
  } catch {
3076
3122
  }
3077
3123
  installed.push(`prepare-commit-msg(${ctx.config.git.trailerKey} trailer \u6CE8\u5165)`);
@@ -3086,8 +3132,8 @@ var init_install_hooks_command = __esm({
3086
3132
  });
3087
3133
 
3088
3134
  // 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";
3135
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readdirSync as readdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
3136
+ import { dirname as dirname3, join as join14 } from "node:path";
3091
3137
  function expandPath(ctx, kind) {
3092
3138
  const spec = getKindRegistry(ctx.config)[kind];
3093
3139
  const raw = spec?.pathPatterns?.[0] ?? "";
@@ -3113,8 +3159,8 @@ function parseTemplate(rawInput) {
3113
3159
  function genAgents(ctx, templatesDir) {
3114
3160
  const lang = ctx.config.language;
3115
3161
  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}`);
3162
+ const dir = templatesDir ?? join14(WORKBENCH_DIR, "templates/agents", lang);
3163
+ if (!existsSync12(dir)) throw new Error(`\u6A21\u677F\u76EE\u5F55\u4E0D\u5B58\u5728: ${dir}`);
3118
3164
  const cli = ctx.config.cli;
3119
3165
  const baseTokens = {
3120
3166
  CLI: cli,
@@ -3151,7 +3197,7 @@ function genAgents(ctx, templatesDir) {
3151
3197
  if (!file.endsWith(".md")) continue;
3152
3198
  const role = file.replace(/\.md$/, "");
3153
3199
  if (!pipeline.has(role)) continue;
3154
- const tpl = parseTemplate(readFileSync6(join13(dir, file), "utf-8"));
3200
+ const tpl = parseTemplate(readFileSync6(join14(dir, file), "utf-8"));
3155
3201
  for (const adapter of adapters) {
3156
3202
  const memory = adapter.memoryBlock(role, lang);
3157
3203
  const spec = {
@@ -3162,7 +3208,7 @@ function genAgents(ctx, templatesDir) {
3162
3208
  body: renderTokens(tpl.body, memory)
3163
3209
  };
3164
3210
  const rel = `${adapter.agentsDir}/${adapter.agentFile(role)}`;
3165
- const abs = join13(ctx.root, rel);
3211
+ const abs = join14(ctx.root, rel);
3166
3212
  mkdirSync6(dirname3(abs), { recursive: true });
3167
3213
  writeFileSync5(abs, adapter.renderAgent(spec));
3168
3214
  written.push(rel);
@@ -3246,8 +3292,8 @@ When MCP is registered, prefer the \`wb_*\` typed tools (same source & transacti
3246
3292
  });
3247
3293
 
3248
3294
  // core/commands/audit.command.ts
3249
- import { existsSync as existsSync12 } from "node:fs";
3250
- import { join as join14 } from "node:path";
3295
+ import { existsSync as existsSync13 } from "node:fs";
3296
+ import { join as join15 } from "node:path";
3251
3297
  function auditModule(ctx, moduleRaw) {
3252
3298
  const module = normalizeModule(moduleRaw, ctx.config);
3253
3299
  const kinds = contractKinds(ctx.config);
@@ -3257,9 +3303,9 @@ function auditModule(ctx, moduleRaw) {
3257
3303
  path: a.path,
3258
3304
  status: reviewStatus(a),
3259
3305
  everApproved: everApproved(a),
3260
- onDisk: existsSync12(join14(ctx.root, a.path))
3306
+ onDisk: existsSync13(join15(ctx.root, a.path))
3261
3307
  }));
3262
- const codeDirs = artifacts.filter((a) => a.kind === "code").map((a) => ({ path: a.path, endpoint: a.endpoint, onDisk: existsSync12(join14(ctx.root, a.path)) }));
3308
+ const codeDirs = artifacts.filter((a) => a.kind === "code").map((a) => ({ path: a.path, endpoint: a.endpoint, onDisk: existsSync13(join15(ctx.root, a.path)) }));
3263
3309
  return {
3264
3310
  module,
3265
3311
  cleared: moduleCleared(ctx.db, module),
@@ -3323,28 +3369,28 @@ var init_intake_command = __esm({
3323
3369
  // core/commands/init.command.ts
3324
3370
  import {
3325
3371
  copyFileSync,
3326
- existsSync as existsSync13,
3372
+ existsSync as existsSync14,
3327
3373
  mkdirSync as mkdirSync7,
3328
3374
  readFileSync as readFileSync7,
3329
3375
  readdirSync as readdirSync6,
3330
3376
  writeFileSync as writeFileSync6
3331
3377
  } from "node:fs";
3332
- import { dirname as dirname4, join as join15 } from "node:path";
3378
+ import { dirname as dirname4, join as join16 } from "node:path";
3333
3379
  function deployPreset(root) {
3334
3380
  const deployed = [];
3335
- if (!existsSync13(PRESET_DIR)) return deployed;
3381
+ if (!existsSync14(PRESET_DIR)) return deployed;
3336
3382
  const walk = (rel) => {
3337
- for (const ent of readdirSync6(join15(PRESET_DIR, rel), {
3383
+ for (const ent of readdirSync6(join16(PRESET_DIR, rel), {
3338
3384
  withFileTypes: true
3339
3385
  })) {
3340
- const childRel = rel ? join15(rel, ent.name) : ent.name;
3386
+ const childRel = rel ? join16(rel, ent.name) : ent.name;
3341
3387
  if (ent.isDirectory()) {
3342
3388
  walk(childRel);
3343
3389
  } else if (ent.isFile()) {
3344
- const dest = join15(root, childRel);
3345
- if (existsSync13(dest)) continue;
3390
+ const dest = join16(root, childRel);
3391
+ if (existsSync14(dest)) continue;
3346
3392
  mkdirSync7(dirname4(dest), { recursive: true });
3347
- copyFileSync(join15(PRESET_DIR, childRel), dest);
3393
+ copyFileSync(join16(PRESET_DIR, childRel), dest);
3348
3394
  deployed.push(childRel);
3349
3395
  }
3350
3396
  }
@@ -3353,8 +3399,8 @@ function deployPreset(root) {
3353
3399
  return deployed;
3354
3400
  }
3355
3401
  function ensureRootTsx(root) {
3356
- const pkgPath = join15(root, "package.json");
3357
- if (!existsSync13(pkgPath)) return false;
3402
+ const pkgPath = join16(root, "package.json");
3403
+ if (!existsSync14(pkgPath)) return false;
3358
3404
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
3359
3405
  if (pkg.devDependencies?.tsx) return false;
3360
3406
  pkg.devDependencies = { ...pkg.devDependencies ?? {}, tsx: TSX_VERSION };
@@ -3362,8 +3408,8 @@ function ensureRootTsx(root) {
3362
3408
  return true;
3363
3409
  }
3364
3410
  function initProject(root, opts) {
3365
- const configPath = join15(root, CONFIG_FILENAME);
3366
- if (existsSync13(configPath)) {
3411
+ const configPath = join16(root, CONFIG_FILENAME);
3412
+ if (existsSync14(configPath)) {
3367
3413
  throw new Error(
3368
3414
  `${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
3415
  );
@@ -3397,10 +3443,10 @@ function initProject(root, opts) {
3397
3443
  const scaffolded = [];
3398
3444
  if (opts.scaffold !== false) {
3399
3445
  for (const dir of DOC_DIRS) {
3400
- const abs = join15(root, dir);
3401
- if (!existsSync13(abs)) {
3446
+ const abs = join16(root, dir);
3447
+ if (!existsSync14(abs)) {
3402
3448
  mkdirSync7(abs, { recursive: true });
3403
- writeFileSync6(join15(abs, ".gitkeep"), "");
3449
+ writeFileSync6(join16(abs, ".gitkeep"), "");
3404
3450
  scaffolded.push(dir);
3405
3451
  }
3406
3452
  }
@@ -3432,7 +3478,7 @@ function initProject(root, opts) {
3432
3478
  }
3433
3479
  const notes = adapters.flatMap((a) => a.notes);
3434
3480
  let hooks = [];
3435
- if (opts.gitHooks !== false && existsSync13(join15(root, ".git"))) {
3481
+ if (opts.gitHooks !== false && existsSync14(join16(root, ".git"))) {
3436
3482
  try {
3437
3483
  hooks = installGitHooks(ctx);
3438
3484
  } catch {
@@ -3474,7 +3520,7 @@ var init_init_command = __esm({
3474
3520
  "docs/design/prototypes",
3475
3521
  "docs/acceptance"
3476
3522
  ];
3477
- PRESET_DIR = join15(WORKBENCH_DIR, "preset");
3523
+ PRESET_DIR = join16(WORKBENCH_DIR, "preset");
3478
3524
  TSX_VERSION = "^4";
3479
3525
  }
3480
3526
  });
@@ -3762,17 +3808,17 @@ __export(app_exports, {
3762
3808
  import fastifyCors from "@fastify/cors";
3763
3809
  import fastifyStatic from "@fastify/static";
3764
3810
  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";
3811
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, readFileSync as readFileSync8, readdirSync as readdirSync7, statSync as statSync6 } from "node:fs";
3812
+ import { isAbsolute as isAbsolute2, join as join17, normalize, relative as relative2 } from "node:path";
3767
3813
  function getArtifact(ctx, id) {
3768
3814
  const row = ctx.db.prepare("SELECT * FROM artifacts WHERE id = ?").get(id);
3769
3815
  if (!row) throw Object.assign(new Error(`artifact #${id} \u4E0D\u5B58\u5728`), { statusCode: 404 });
3770
3816
  return row;
3771
3817
  }
3772
3818
  function guardedAbsPath(ctx, artifact, rel) {
3773
- const base = normalize(join16(ctx.root, artifact.path));
3819
+ const base = normalize(join17(ctx.root, artifact.path));
3774
3820
  if (!rel) return base;
3775
- const target = normalize(join16(base, rel));
3821
+ const target = normalize(join17(base, rel));
3776
3822
  const r = relative2(base, target);
3777
3823
  if (r === "" || r === "." || r.startsWith("..") || isAbsolute2(r)) {
3778
3824
  throw Object.assign(new Error("\u8DEF\u5F84\u8D8A\u754C"), { statusCode: 403 });
@@ -3782,11 +3828,11 @@ function guardedAbsPath(ctx, artifact, rel) {
3782
3828
  async function createServer(ctx) {
3783
3829
  const app = Fastify({ logger: false });
3784
3830
  await app.register(fastifyCors, { origin: true });
3785
- const webDist = join16(WORKBENCH_DIR, "web/dist");
3786
- if (existsSync14(webDist)) {
3831
+ const webDist = join17(WORKBENCH_DIR, "web/dist");
3832
+ if (existsSync15(webDist)) {
3787
3833
  await app.register(fastifyStatic, { root: webDist });
3788
3834
  }
3789
- const protoRoot = join16(ctx.root, ctx.config.docs.design, "prototypes");
3835
+ const protoRoot = join17(ctx.root, ctx.config.docs.design, "prototypes");
3790
3836
  mkdirSync8(protoRoot, { recursive: true });
3791
3837
  await app.register(fastifyStatic, { root: protoRoot, prefix: "/proto/", decorateReply: false });
3792
3838
  app.get("/api/meta", async () => ({
@@ -3821,7 +3867,7 @@ async function createServer(ctx) {
3821
3867
  let content = null;
3822
3868
  let isDirectory = false;
3823
3869
  let missing = false;
3824
- if (!existsSync14(abs)) {
3870
+ if (!existsSync15(abs)) {
3825
3871
  missing = true;
3826
3872
  } else if (statSync6(abs).isDirectory()) {
3827
3873
  isDirectory = true;
@@ -3837,20 +3883,20 @@ async function createServer(ctx) {
3837
3883
  const walk = (dir, rel) => {
3838
3884
  for (const name of readdirSync7(dir).sort()) {
3839
3885
  if (name === "node_modules" || name.startsWith(".")) continue;
3840
- const full = join16(dir, name);
3886
+ const full = join17(dir, name);
3841
3887
  const relPath = rel ? `${rel}/${name}` : name;
3842
3888
  const st = statSync6(full);
3843
3889
  if (st.isDirectory()) walk(full, relPath);
3844
3890
  else files.push({ rel: relPath, size: st.size });
3845
3891
  }
3846
3892
  };
3847
- if (existsSync14(base) && statSync6(base).isDirectory()) walk(base, "");
3893
+ if (existsSync15(base) && statSync6(base).isDirectory()) walk(base, "");
3848
3894
  return { files };
3849
3895
  });
3850
3896
  app.get("/api/artifact/:id/file", async (req) => {
3851
3897
  const artifact = getArtifact(ctx, parseInt(req.params.id));
3852
3898
  const abs = guardedAbsPath(ctx, artifact, req.query.rel);
3853
- if (!existsSync14(abs) || statSync6(abs).isDirectory()) {
3899
+ if (!existsSync15(abs) || statSync6(abs).isDirectory()) {
3854
3900
  throw Object.assign(new Error("\u6587\u4EF6\u4E0D\u5B58\u5728"), { statusCode: 404 });
3855
3901
  }
3856
3902
  if (statSync6(abs).size > MAX_INLINE_SIZE) {
@@ -3905,7 +3951,7 @@ data: {"cursor":${cursor2}}
3905
3951
  endpoint: a.endpoint,
3906
3952
  page: a.page,
3907
3953
  review_status: reviewStatus(a),
3908
- missing: !existsSync14(join16(ctx.root, a.path))
3954
+ missing: !existsSync15(join17(ctx.root, a.path))
3909
3955
  }));
3910
3956
  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
3957
  return { nodes, edges };
@@ -3922,7 +3968,7 @@ data: {"cursor":${cursor2}}
3922
3968
  for (const name of readdirSync7(dir).sort()) {
3923
3969
  if (files.length >= 30) return;
3924
3970
  if (name.startsWith(".") || name === "node_modules" || name === "dist" || name === "build") continue;
3925
- const full = join16(dir, name);
3971
+ const full = join17(dir, name);
3926
3972
  const relPath = rel ? `${rel}/${name}` : name;
3927
3973
  if (statSync6(full).isDirectory()) walk(full, relPath);
3928
3974
  else if (relPath.toLowerCase().includes(q) && !registered.has(relPath)) files.push(relPath);
@@ -4022,7 +4068,7 @@ data: {"cursor":${cursor2}}
4022
4068
  app.get("/api/artifact/:id/diff", async (req) => {
4023
4069
  const artifact = getArtifact(ctx, parseInt(req.params.id));
4024
4070
  const abs = guardedAbsPath(ctx, artifact);
4025
- const current = existsSync14(abs) && !statSync6(abs).isDirectory() ? readFileSync8(abs, "utf-8") : null;
4071
+ const current = existsSync15(abs) && !statSync6(abs).isDirectory() ? readFileSync8(abs, "utf-8") : null;
4026
4072
  return { approved: approvedContent(ctx, artifact.id), current };
4027
4073
  });
4028
4074
  app.post(
@@ -4088,15 +4134,17 @@ function hookPlatform() {
4088
4134
  const arg = process.argv.find((a) => a.startsWith("--platform="));
4089
4135
  return arg ? arg.slice("--platform=".length) : "claude";
4090
4136
  }
4091
- function extractFilePath(input) {
4137
+ function extractFilePath(input, adapter) {
4138
+ const fromAdapter = adapter?.parseHookInput(input).filePath;
4139
+ if (fromAdapter) return fromAdapter;
4092
4140
  return input?.tool_input?.file_path ?? // claude PreToolUse/PostToolUse
4093
4141
  input?.tool_input?.filePath ?? input?.file_path ?? // cursor afterFileEdit / 通用
4094
4142
  input?.filePath ?? input?.args?.file_path ?? // opencode tool 参数
4095
4143
  input?.args?.filePath ?? input?.arguments?.file_path ?? // codex 兜底
4096
4144
  input?.arguments?.filePath ?? input?.input?.file_path ?? void 0;
4097
4145
  }
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;
4146
+ function hookProjectDir(adapter) {
4147
+ 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
4148
  }
4101
4149
  async function readStdinJson() {
4102
4150
  const chunks = [];
@@ -4120,14 +4168,16 @@ __export(hook_pretooluse_exports, {
4120
4168
  });
4121
4169
  async function writeGateHook(platform) {
4122
4170
  const input = await readStdinJson();
4123
- const filePath = extractFilePath(input);
4171
+ const { getAdapter: getAdapter2, PLATFORM_IDS: PLATFORM_IDS2 } = await Promise.resolve().then(() => (init_platforms(), platforms_exports));
4172
+ const adapter = PLATFORM_IDS2.includes(platform) ? getAdapter2(platform) : void 0;
4173
+ const filePath = extractFilePath(input, adapter);
4124
4174
  if (!filePath) return;
4125
4175
  const { openWorkbench: openWorkbench2 } = await Promise.resolve().then(() => (init_db(), db_exports));
4126
4176
  const { normalizeRelPath: normalizeRelPath2 } = await Promise.resolve().then(() => (init_artifact_commands(), artifact_commands_exports));
4127
4177
  const { reviewStatus: reviewStatus2 } = await Promise.resolve().then(() => (init_derive(), derive_exports));
4128
4178
  const { contractKinds: contractKinds2 } = await Promise.resolve().then(() => (init_kind(), kind_exports));
4129
4179
  const { logEvent: logEvent2 } = await Promise.resolve().then(() => (init_events(), events_exports));
4130
- const ctx = openWorkbench2(hookProjectDir());
4180
+ const ctx = openWorkbench2(hookProjectDir(adapter));
4131
4181
  const mode = ctx.config.gates.writeGate;
4132
4182
  if (mode === "off") return;
4133
4183
  const rel = normalizeRelPath2(ctx, filePath);
@@ -4154,12 +4204,10 @@ async function writeGateHook(platform) {
4154
4204
  const msg = `\u8BE5\u6587\u4EF6\u662F\u5DF2\u5BA1\u6279\u5951\u7EA6(${artifact.kind}): ${rel}\u3002
4155
4205
  \u4FEE\u6539\u524D\u8BF7\u5148\u9886\u53D6\u5BF9\u5E94\u4EFB\u52A1:${ctx.config.cli} list --module=${artifact.module ?? ""} --status=pending
4156
4206
  \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);
4207
+ const resp = adapter?.respondBlocked(msg) ?? { exitCode: 2, stderr: msg };
4208
+ if (resp.stdout) process.stdout.write(resp.stdout);
4209
+ if (resp.stderr) console.error(resp.stderr);
4210
+ process.exit(resp.exitCode);
4163
4211
  }
4164
4212
  }
4165
4213
  var init_hook_pretooluse = __esm({
@@ -4178,13 +4226,15 @@ var hook_refresh_exports = {};
4178
4226
  __export(hook_refresh_exports, {
4179
4227
  refreshHook: () => refreshHook
4180
4228
  });
4181
- async function refreshHook(_platform) {
4229
+ async function refreshHook(platform) {
4182
4230
  const input = await readStdinJson();
4183
- const filePath = extractFilePath(input);
4231
+ const { getAdapter: getAdapter2, PLATFORM_IDS: PLATFORM_IDS2 } = await Promise.resolve().then(() => (init_platforms(), platforms_exports));
4232
+ const adapter = platform && PLATFORM_IDS2.includes(platform) ? getAdapter2(platform) : void 0;
4233
+ const filePath = extractFilePath(input, adapter);
4184
4234
  if (!filePath) return;
4185
4235
  const { openWorkbench: openWorkbench2 } = await Promise.resolve().then(() => (init_db(), db_exports));
4186
4236
  const { refreshArtifact: refreshArtifact2, normalizeRelPath: normalizeRelPath2 } = await Promise.resolve().then(() => (init_artifact_commands(), artifact_commands_exports));
4187
- const ctx = openWorkbench2(hookProjectDir());
4237
+ const ctx = openWorkbench2(hookProjectDir(adapter));
4188
4238
  const rel = normalizeRelPath2(ctx, filePath);
4189
4239
  const exact = ctx.db.prepare("SELECT id FROM artifacts WHERE path = ?").get(rel);
4190
4240
  if (exact) {
@@ -4284,7 +4334,6 @@ function printTaskDetail(ctx, id) {
4284
4334
  }
4285
4335
  console.log();
4286
4336
  }
4287
- var ALL_PLATFORMS = ["claude", "codex", "opencode", "cursor"];
4288
4337
  var PRIORITY_PROVIDERS = [
4289
4338
  "anthropic",
4290
4339
  "openai",
@@ -4330,7 +4379,7 @@ async function promptInit() {
4330
4379
  const zh = language === "zh";
4331
4380
  const platform = await select({
4332
4381
  message: zh ? "\u5E73\u53F0" : "Platform",
4333
- choices: ALL_PLATFORMS.map((p) => ({ name: p, value: p })),
4382
+ choices: PLATFORM_IDS.map((p) => ({ name: p, value: p })),
4334
4383
  default: "claude"
4335
4384
  });
4336
4385
  const platforms = [platform];
@@ -4358,14 +4407,14 @@ async function promptInit() {
4358
4407
  choices: prov.models.map((m) => ({ name: m.name, value: m.id })),
4359
4408
  pageSize: 12
4360
4409
  });
4361
- if (modelId) modelObj[p] = p === "opencode" ? `${provId}/${modelId}` : modelId;
4410
+ if (modelId) modelObj[p] = getAdapter(p).formatModel(provId, modelId);
4362
4411
  }
4363
4412
  return { platforms, endpoints, model: Object.keys(modelObj).length ? modelObj : void 0, language };
4364
4413
  }
4365
4414
  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"))) {
4415
+ const { existsSync: existsSync16 } = await import("node:fs");
4416
+ const { join: join18 } = await import("node:path");
4417
+ if (existsSync16(join18(root, "workbench.config.json"))) {
4369
4418
  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
4419
  process.exit(1);
4371
4420
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dawipong/opcflow",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",