code-workspace-zhuiyi 0.1.0-beta.4 → 0.1.0-beta.5

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/README.md CHANGED
@@ -34,7 +34,7 @@ code-workspace init . \
34
34
  ```
35
35
 
36
36
  Use `--tools claude`, `--tools codex`, or `--tools none` to override the default tool selection. Codex monitoring is enabled by default when Codex is selected; use `--no-monitor` to disable it.
37
- Pass `--coordination` to install the provider-neutral write-coordination Hook artifacts for the selected tools.
37
+ Pass `--coordination` to install the write-coordination Hooks into the selected providers' native configuration.
38
38
 
39
39
  Initialization writes only Workspace-owned state and integrations:
40
40
 
@@ -42,7 +42,7 @@ Initialization writes only Workspace-owned state and integrations:
42
42
  - `USER_GUIDE.md`
43
43
  - `CLAUDE.md` and/or `AGENTS.md`
44
44
  - Workspace-specific commands and skills whose names start with `code-workspace-` or use the `/code-workspace` namespace
45
- - `.codex/hooks.json` when monitoring is enabled
45
+ - `.codex/hooks.json` when monitoring or Codex coordination is enabled, and `.claude/settings.json` when Claude coordination is enabled
46
46
 
47
47
  It does not create `openspec/`, install native `/opsx` commands, or install native `openspec-*` skills.
48
48
 
package/README.zh-CN.md CHANGED
@@ -34,7 +34,7 @@ code-workspace init . \
34
34
  ```
35
35
 
36
36
  可用 `--tools claude`、`--tools codex` 或 `--tools none` 覆盖默认工具选择。选择 Codex 时默认启用监控;可传 `--no-monitor` 关闭。
37
- 传入 `--coordination` 可为选中的工具安装与 Monitor 独立的写入协调 Hook 制品。
37
+ 传入 `--coordination` 可把与 Monitor 独立的写入协调 Hook 安装到选中工具的原生配置中。
38
38
 
39
39
  初始化只写入 Workspace 自有状态和集成:
40
40
 
@@ -42,7 +42,7 @@ code-workspace init . \
42
42
  - `USER_GUIDE.md`
43
43
  - `CLAUDE.md` 和/或 `AGENTS.md`
44
44
  - 名称以 `code-workspace-` 开头或使用 `/code-workspace` 命名空间的 Workspace 专用命令与 Skill
45
- - 启用监控时的 `.codex/hooks.json`
45
+ - 启用监控或 Codex 协调时的 `.codex/hooks.json`,以及启用 Claude 协调时的 `.claude/settings.json`
46
46
 
47
47
  它不会创建 `openspec/`,不会安装原生 `/opsx` 命令,也不会安装原生 `openspec-*` Skill。
48
48
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "releaseVersion": "0.1.0-beta.4",
3
+ "releaseVersion": "0.1.0-beta.5",
4
4
  "requirements": {
5
5
  "node": ">=20.19.0"
6
6
  },
@@ -4,11 +4,32 @@
4
4
  const { runHookStdin } = require("../src/core/task-coordination-protocol");
5
5
 
6
6
  const provider = String(process.argv[2] || "").toLowerCase();
7
+ let workspaceRoot = process.cwd();
8
+ for (let index = 3; index < process.argv.length; index += 1) {
9
+ const argument = process.argv[index];
10
+ if (argument === "--workspace-root-b64") {
11
+ const encoded = process.argv[++index];
12
+ try {
13
+ workspaceRoot = Buffer.from(String(encoded || ""), "base64url").toString("utf8");
14
+ if (!workspaceRoot) throw new Error("empty path");
15
+ } catch {
16
+ process.stdout.write(JSON.stringify({ decision: "block", reason: "Invalid task coordination Hook workspace root." }) + "\n");
17
+ process.exitCode = 2;
18
+ break;
19
+ }
20
+ } else if (argument === "--workspace-root") {
21
+ workspaceRoot = String(process.argv[++index] || "");
22
+ } else {
23
+ process.stdout.write(JSON.stringify({ decision: "block", reason: `Unsupported task coordination Hook option: ${argument}` }) + "\n");
24
+ process.exitCode = 2;
25
+ break;
26
+ }
27
+ }
7
28
  if (!["codex", "claude"].includes(provider)) {
8
29
  process.stdout.write(JSON.stringify({ decision: "block", reason: "Unsupported task coordination Hook provider." }) + "\n");
9
30
  process.exitCode = 2;
10
- } else {
11
- runHookStdin(provider, { workspaceRoot: process.cwd() })
31
+ } else if (process.exitCode !== 2) {
32
+ runHookStdin(provider, { workspaceRoot })
12
33
  .then((output) => process.stdout.write(`${JSON.stringify(output)}\n`))
13
34
  .catch((error) => {
14
35
  process.stdout.write(`${JSON.stringify({ decision: "block", reason: `Task coordination Hook failed closed: ${error.message}` })}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-workspace-zhuiyi",
3
- "version": "0.1.0-beta.4",
3
+ "version": "0.1.0-beta.5",
4
4
  "description": "Local multi-project registry and safety layer for AI coding workspaces",
5
5
  "author": "icebearx-ai",
6
6
  "license": "MIT",
@@ -13,7 +13,8 @@
13
13
  },
14
14
  "bin": {
15
15
  "code-workspace": "bin/code-workspace.js",
16
- "code-w": "bin/code-workspace.js"
16
+ "code-w": "bin/code-workspace.js",
17
+ "code-workspace-task-hook": "bin/code-workspace-task-hook.js"
17
18
  },
18
19
  "files": [
19
20
  "assets",
@@ -111,6 +111,7 @@ async function executeInitUnlocked(invocation, root) {
111
111
  const extensionPreparation = prepareExtensionPlans(extensionCatalogResult, requestedExtensions, { tools, state: extensionState, stateError: extensionStateInspection.error });
112
112
  const extensionPlans = extensionPreparation.plans;
113
113
  const toolSelection = { tools, source: plan ? (options.tools !== undefined ? "cli" : "interactive") : resolvedTools.source };
114
+ const coordination = options.coordination === true || existingState?.coordination === true;
114
115
  const result = await initializeWorkspace(root, {
115
116
  run,
116
117
  tools,
@@ -120,7 +121,7 @@ async function executeInitUnlocked(invocation, root) {
120
121
  workspaceUuid: plan?.workspace.uuid,
121
122
  monitor: plan?.monitor.enable ?? (options.monitor === true ? true : options["no-monitor"] === true ? false : undefined),
122
123
  monitorUrl: plan?.monitor.url || options["monitor-url"],
123
- coordination: options.coordination === true,
124
+ coordination,
124
125
  language: plan?.language || options.language,
125
126
  interactive: false,
126
127
  initPlan: plan,
@@ -173,7 +174,7 @@ async function executeInitUnlocked(invocation, root) {
173
174
  monitor: result.workspaceConfig.monitor,
174
175
  language: result.language,
175
176
  tools: toolSelection,
176
- coordination: options.coordination === true,
177
+ coordination,
177
178
  migration: migrationData(result.migration),
178
179
  permissions: result.permissions,
179
180
  verification: result.verification,
@@ -13,7 +13,7 @@ const { WorkspaceError } = require("../../core/errors");
13
13
  const { commitUpdateState, loadInitManifest } = require("../../core/init");
14
14
  const { workspaceGuide } = require("../../core/language");
15
15
  const { inspectManagedFiles, installManagedFiles, planManagedFiles } = require("../../core/managed-files");
16
- const { installCoordinationArtifacts } = require("../../core/task-coordination-managed");
16
+ const { installCoordinationHooks, removeCoordinationHooks } = require("../../core/task-coordination-managed");
17
17
  const { planWorkspaceMaintenance } = require("../../core/migration");
18
18
  const { inspectProjectPermissions } = require("../../core/permissions");
19
19
  const { resolveWorkspaceTools } = require("../../core/tools");
@@ -21,8 +21,8 @@ const { createFileTransaction } = require("../../core/transaction");
21
21
  const { success } = require("../result");
22
22
  const { migrationData } = require("./init");
23
23
 
24
- function verifyUpdatedManagedFiles(root, manifest, tools, capabilities, variables) {
25
- const inspection = inspectManagedFiles(root, manifest, tools, capabilities, variables);
24
+ function verifyUpdatedManagedFiles(root, manifest, tools, capabilities, variables, options = {}) {
25
+ const inspection = inspectManagedFiles(root, manifest, tools, capabilities, variables, options);
26
26
  const incomplete = [
27
27
  ...inspection.managedOld.map((target) => ({ target, state: "managed-old" })),
28
28
  ...inspection.replaceable.map((target) => ({ target, state: "replaceable" })),
@@ -55,6 +55,10 @@ function updateWorkspace(root, options = {}) {
55
55
  manifestTools: manifest.tools,
56
56
  });
57
57
  const tools = toolSelection.tools;
58
+ const previousCoordination = initialState.coordination === true;
59
+ const previousTools = Array.isArray(initialState.tools) ? initialState.tools : [];
60
+ const removedCoordinationTools = previousCoordination ? previousTools.filter((tool) => !tools.includes(tool)) : [];
61
+ const coordination = options.coordination === true || initialState.coordination === true;
58
62
  const migration = planWorkspaceMaintenance(root, {
59
63
  language: options.language,
60
64
  allowLegacy: true,
@@ -64,7 +68,7 @@ function updateWorkspace(root, options = {}) {
64
68
  const config = loadConfig(root, { defaultLanguage: language });
65
69
  const nextConfig = { ...config, workspace: { ...config.workspace, language } };
66
70
  const capabilities = nextConfig.monitor.enable ? ["monitor"] : [];
67
- if (options.coordination === true) capabilities.push("coordination");
71
+ if (coordination) capabilities.push("coordination");
68
72
  const variables = { WORKSPACE_LANGUAGE: language, WORKSPACE_USER_GUIDE: workspaceGuide(language) };
69
73
  let managedPlan;
70
74
  let obsoletePlan;
@@ -72,6 +76,7 @@ function updateWorkspace(root, options = {}) {
72
76
  managedPlan = planManagedFiles(root, manifest, tools, {
73
77
  force: options.force === true,
74
78
  capabilities,
79
+ coordination,
75
80
  variables,
76
81
  });
77
82
  obsoletePlan = planObsoleteAssets(root, tools, { force: options.force === true });
@@ -88,7 +93,9 @@ function updateWorkspace(root, options = {}) {
88
93
  resolveProjectConfigPath(root),
89
94
  statePath(root),
90
95
  ...managedPlan.plans.map((plan) => plan.target),
91
- ...(options.coordination === true ? [path.join(root, ".codex", "task-coordination-hooks.json"), path.join(root, ".claude", "task-coordination-settings.json")] : []),
96
+ ...(coordination || removedCoordinationTools.length > 0
97
+ ? [path.join(root, ".codex", "hooks.json"), path.join(root, ".claude", "settings.json")]
98
+ : []),
92
99
  ...obsoletePlan.map((plan) => path.join(root, plan.target)),
93
100
  ]);
94
101
  try {
@@ -97,25 +104,31 @@ function updateWorkspace(root, options = {}) {
97
104
  const managedFiles = installManagedFiles(root, manifest, tools, {
98
105
  force: options.force === true,
99
106
  capabilities,
107
+ coordination,
100
108
  variables,
101
109
  });
102
- if (options.coordination === true) {
103
- managedFiles.push(...installCoordinationArtifacts(root, tools, { force: options.force === true }).map((entry) => ({ ...entry, id: `task-coordination-${entry.tool}` })));
110
+ managedFiles.push(...removeCoordinationHooks(root, removedCoordinationTools).map((entry) => ({ ...entry, id: `task-coordination-remove-${entry.tool}` })));
111
+ if (coordination) {
112
+ managedFiles.push(...installCoordinationHooks(root, tools, { force: options.force === true }).map((entry) => ({ ...entry, id: `task-coordination-${entry.tool}` })));
104
113
  }
105
114
  options.injectFailure?.("after-managed-install", managedFiles);
106
115
  saveConfig(root, nextConfig);
107
116
  options.injectFailure?.("after-config-save", nextConfig);
108
- verifyUpdatedManagedFiles(root, manifest, tools, capabilities, variables);
109
- if (options.coordination === true) {
110
- const coordinationIncomplete = installCoordinationArtifacts(root, tools, { dryRun: true }).filter((entry) => entry.action !== "skip");
117
+ verifyUpdatedManagedFiles(root, manifest, tools, capabilities, variables, { coordination });
118
+ if (coordination) {
119
+ const coordinationIncomplete = installCoordinationHooks(root, tools, { dryRun: true }).filter((entry) => entry.action !== "skip");
111
120
  if (coordinationIncomplete.length > 0) {
112
121
  throw new WorkspaceError("UPDATE_POSTCONDITION_FAILED", "Coordination Hook artifacts failed verification.", { files: coordinationIncomplete, remediation: "Run code-w update --coordination again after reviewing the reported files." });
113
122
  }
114
123
  }
124
+ const removedCoordinationIncomplete = removeCoordinationHooks(root, removedCoordinationTools, { dryRun: true }).filter((entry) => entry.action !== "skip");
125
+ if (removedCoordinationIncomplete.length > 0) {
126
+ throw new WorkspaceError("UPDATE_POSTCONDITION_FAILED", "Obsolete coordination Hook entries failed verification.", { files: removedCoordinationIncomplete, remediation: "Run code-w update again after reviewing the reported files." });
127
+ }
115
128
  const result = {
116
129
  language,
117
130
  tools: toolSelection,
118
- coordination: options.coordination === true,
131
+ coordination,
119
132
  migration: migrationData(migration),
120
133
  obsoleteFiles,
121
134
  managedFiles,
@@ -141,6 +154,7 @@ function updateWorkspace(root, options = {}) {
141
154
  options.injectFailure?.("after-verify", result);
142
155
  const state = commitUpdateState(root, manifest, {
143
156
  tools,
157
+ coordination,
144
158
  removeLegacyWorkspaceLanguage: true,
145
159
  });
146
160
  options.injectFailure?.("after-state-save", state);
@@ -1,6 +1,7 @@
1
1
  const { CURRENT_CONFIG_VERSION, inspectConfigDomains, loadState } = require("./config");
2
2
  const { add, result } = require("./diagnostics");
3
3
  const { inspectManagedFiles } = require("./managed-files");
4
+ const { installCoordinationHooks } = require("./task-coordination-managed");
4
5
  const { DEFAULT_WORKSPACE_LANGUAGE, workspaceGuide } = require("./language");
5
6
  const { inspectProjectPermissions } = require("./permissions");
6
7
  const { validateProjects } = require("./validation");
@@ -36,13 +37,17 @@ function doctorWorkspace(root, manifest, options = {}) {
36
37
  add(output, "error", "WORKSPACE_IDENTITY_MISSING", "Local workspace name and UUID are missing. Re-run `code-w init .` to generate them.");
37
38
  }
38
39
 
40
+ const state = loadState(root);
39
41
  const toolSelection = options.toolSelection || resolveWorkspaceTools({
40
42
  explicit: options.tools,
41
43
  state: loadState(root),
42
44
  manifestTools: manifest.tools,
43
45
  });
44
46
  const tools = toolSelection.tools;
45
- const capabilities = options.capabilities || (inspection.monitor.valid && config.monitor?.enable ? ["monitor"] : []);
47
+ const capabilities = options.capabilities || [
48
+ ...(inspection.monitor.valid && config.monitor?.enable ? ["monitor"] : []),
49
+ ...(state?.coordination === true ? ["coordination"] : []),
50
+ ];
46
51
  if (inspection.monitor.valid && config.monitor?.enable && !tools.includes("codex")) {
47
52
  add(output, "error", "MONITOR_CODEX_REQUIRED", "Agent monitoring is enabled, but Codex is not one of the selected tools.");
48
53
  }
@@ -56,7 +61,7 @@ function doctorWorkspace(root, manifest, options = {}) {
56
61
  const managed = inspectManagedFiles(root, manifest, tools, capabilities, {
57
62
  WORKSPACE_LANGUAGE: language || DEFAULT_WORKSPACE_LANGUAGE,
58
63
  WORKSPACE_USER_GUIDE: workspaceGuide(language || DEFAULT_WORKSPACE_LANGUAGE),
59
- });
64
+ }, { coordination: capabilities.includes("coordination") });
60
65
  for (const file of [...managed.managedOld, ...managed.replaceable]) {
61
66
  add(output, "error", "MANAGED_FILE_OUTDATED", `Managed file is not at the desired version: ${file}`);
62
67
  }
@@ -71,6 +76,21 @@ function doctorWorkspace(root, manifest, options = {}) {
71
76
  } catch (error) {
72
77
  add(output, "error", "MANAGED_FILE_MANIFEST_INVALID", error.message);
73
78
  }
79
+ if (capabilities.includes("coordination")) {
80
+ try {
81
+ const coordination = installCoordinationHooks(root, tools, { dryRun: true });
82
+ output.coordinationHooks = coordination;
83
+ for (const entry of coordination.filter((item) => item.action !== "skip")) {
84
+ add(output, "error", "TASK_HOOK_NOT_INSTALLED", `Coordination Hook is not installed for ${entry.tool}: ${entry.target}`, {
85
+ tool: entry.tool,
86
+ target: entry.target,
87
+ remediation: "Run code-w update --coordination and retry.",
88
+ });
89
+ }
90
+ } catch (error) {
91
+ add(output, "error", error.code || "TASK_HOOK_INSPECTION_FAILED", error.message, error.details || {});
92
+ }
93
+ }
74
94
  if (inspection.projects.valid && config.projects.length > 0) {
75
95
  try {
76
96
  output.permissionInspection = inspectProjectPermissions({ root, tools, projects: config.projects });
@@ -89,7 +109,6 @@ function doctorWorkspace(root, manifest, options = {}) {
89
109
  }
90
110
  }
91
111
 
92
- const state = loadState(root);
93
112
  if (!options.allowIncompleteState) {
94
113
  if (!state || state.status !== "healthy") add(output, "error", "INIT_STATE_UNHEALTHY", "Local initialization state is missing or unhealthy.");
95
114
  else {
package/src/core/init.js CHANGED
@@ -113,6 +113,7 @@ function installWorkspaceDependencies(root, options = {}) {
113
113
 
114
114
  function commitInitializationState(root, manifest, options = {}) {
115
115
  const current = loadState(root) || {};
116
+ const coordination = options.coordination === undefined ? current.coordination === true : options.coordination === true;
116
117
  const state = {
117
118
  ...current,
118
119
  schemaVersion: 2,
@@ -120,6 +121,7 @@ function commitInitializationState(root, manifest, options = {}) {
120
121
  appliedReleaseVersion: manifest.releaseVersion,
121
122
  appliedManifestSha256: sha256(fs.readFileSync(options.manifestFile || MANIFEST_FILE)),
122
123
  tools: options.tools || manifest.tools,
124
+ coordination,
123
125
  };
124
126
  delete state.workspaceLanguage;
125
127
  saveState(root, state);
@@ -140,6 +142,7 @@ function commitUpdateState(root, manifest, options = {}) {
140
142
  appliedReleaseVersion: manifest.releaseVersion,
141
143
  appliedManifestSha256: sha256(fs.readFileSync(options.manifestFile || MANIFEST_FILE)),
142
144
  ...(options.tools !== undefined ? { tools: options.tools } : {}),
145
+ ...(options.coordination !== undefined ? { coordination: options.coordination === true } : {}),
143
146
  };
144
147
  if (options.removeLegacyWorkspaceLanguage === true) delete state.workspaceLanguage;
145
148
  saveState(root, state);
@@ -11,6 +11,7 @@ const {
11
11
  projectConfigPath,
12
12
  ensureLocalIgnore,
13
13
  loadConfig,
14
+ loadState,
14
15
  normalizeConfig,
15
16
  resolveProjectConfigPath,
16
17
  saveConfig,
@@ -28,7 +29,7 @@ const {
28
29
  } = require("./init");
29
30
  const { applyPermissionPlan, permissionTargets, planPermissionChanges } = require("./permissions");
30
31
  const { installManagedFiles } = require("./managed-files");
31
- const { installCoordinationArtifacts } = require("./task-coordination-managed");
32
+ const { installCoordinationHooks, removeCoordinationHooks } = require("./task-coordination-managed");
32
33
  const { DEFAULT_WORKSPACE_LANGUAGE, workspaceGuide } = require("./language");
33
34
  const { planWorkspaceMaintenance } = require("./migration");
34
35
  const { createFileTransaction } = require("./transaction");
@@ -144,12 +145,20 @@ async function initializeWorkspaceStages(rootInput, options = {}) {
144
145
  const core = installManagedFiles(root, manifest, options.tools, {
145
146
  force: options.force === true,
146
147
  capabilities,
148
+ coordination: options.coordination === true,
147
149
  variables: { WORKSPACE_LANGUAGE: language, WORKSPACE_USER_GUIDE: workspaceGuide(language) },
148
150
  });
151
+ const removedCoordination = options.previousCoordination === true
152
+ ? removeCoordinationHooks(root, (options.previousTools || []).filter((tool) => !options.tools.includes(tool)))
153
+ : [];
149
154
  const coordination = options.coordination === true
150
- ? installCoordinationArtifacts(root, options.tools, { force: options.force === true })
155
+ ? installCoordinationHooks(root, options.tools, { force: options.force === true })
151
156
  : [];
152
- return [...core, ...coordination.map((entry) => ({ ...entry, id: `task-coordination-${entry.tool}` }))];
157
+ return [
158
+ ...core,
159
+ ...removedCoordination.map((entry) => ({ ...entry, id: `task-coordination-remove-${entry.tool}` })),
160
+ ...coordination.map((entry) => ({ ...entry, id: `task-coordination-${entry.tool}` })),
161
+ ];
153
162
  });
154
163
 
155
164
  const localConfig = await stage("Prepare local workspace configuration", () => {
@@ -189,6 +198,7 @@ async function initializeWorkspaceStages(rootInput, options = {}) {
189
198
  manifestFile: options.manifestFile || MANIFEST_FILE,
190
199
  tools: options.tools,
191
200
  language,
201
+ coordination: options.coordination === true,
192
202
  })
193
203
  );
194
204
 
@@ -219,6 +229,12 @@ async function initializeWorkspaceStages(rootInput, options = {}) {
219
229
 
220
230
  async function initializeWorkspace(rootInput, options = {}) {
221
231
  const root = path.resolve(rootInput || ".");
232
+ const existingState = loadState(root);
233
+ const coordination = options.coordination === true || existingState?.coordination === true;
234
+ const previousTools = Array.isArray(existingState?.tools) ? existingState.tools : [];
235
+ const removedCoordinationTools = existingState?.coordination === true
236
+ ? previousTools.filter((tool) => !options.tools?.includes(tool))
237
+ : [];
222
238
  const manifest = loadInitManifest(options.manifestFile || MANIFEST_FILE);
223
239
  const projectFile = fs.existsSync(configPath(root))
224
240
  ? resolveProjectConfigPath(root)
@@ -229,14 +245,22 @@ async function initializeWorkspace(rootInput, options = {}) {
229
245
  projectFile,
230
246
  path.join(root, ".gitignore"),
231
247
  path.join(root, ".codex", "config.toml"),
232
- ...(options.coordination === true ? [path.join(root, ".codex", "task-coordination-hooks.json"), path.join(root, ".claude", "task-coordination-settings.json")] : []),
248
+ ...(coordination || removedCoordinationTools.length > 0
249
+ ? [path.join(root, ".codex", "hooks.json"), path.join(root, ".claude", "settings.json")]
250
+ : []),
233
251
  ...permissionTargets(root, options.tools),
234
252
  ...manifest.managedFiles.map((entry) => path.join(root, entry.target)),
235
253
  ...OBSOLETE_ASSETS.map((target) => path.join(root, target)),
236
254
  ];
237
255
  const transaction = createFileTransaction(files);
238
256
  try {
239
- const result = await initializeWorkspaceStages(root, { ...options, transaction });
257
+ const result = await initializeWorkspaceStages(root, {
258
+ ...options,
259
+ coordination,
260
+ previousCoordination: existingState?.coordination === true,
261
+ previousTools,
262
+ transaction,
263
+ });
240
264
  transaction.commit();
241
265
  return result;
242
266
  } catch (error) {
@@ -8,6 +8,7 @@ const { atomicWrite, sha256 } = require("./fs");
8
8
  const { DEFAULT_WORKSPACE_LANGUAGE, workspaceGuide } = require("./language");
9
9
  const { CODEX_HOOKS_TARGET, composeHookContent } = require("./extension-artifacts");
10
10
  const { composeHookDocument } = require("./hooks");
11
+ const { coordinationFragment, mergeHooks } = require("./task-coordination-managed");
11
12
 
12
13
  const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
13
14
  const ARTIFACTS_ROOT = path.join(PACKAGE_ROOT, "artifacts");
@@ -66,14 +67,34 @@ function hasExtensionHooks(state) {
66
67
  );
67
68
  }
68
69
 
69
- function desiredManagedContent(root, entry, variables, extensionState, includeCore = true) {
70
+ function desiredManagedContent(root, entry, variables, extensionState, includeCore = true, options = {}) {
70
71
  if (entry.target !== CODEX_HOOKS_TARGET) return renderManagedContent(entry, variables);
71
72
  const target = path.join(root, entry.target);
72
- if (extensionState === null && fs.existsSync(target)) return fs.readFileSync(target);
73
- const base = includeCore ? renderManagedContent(entry, variables) : Buffer.from('{\n "hooks": {}\n}\n');
74
- const legacy = composeHookContent(base, extensionState || { extensions: {} });
73
+ if (extensionState === null && fs.existsSync(target) && options.coordination !== true) return fs.readFileSync(target);
74
+ let base;
75
+ const preserveExisting = options.coordination === true && fs.existsSync(target);
76
+ if (preserveExisting) {
77
+ let existing;
78
+ try { existing = JSON.parse(fs.readFileSync(target, "utf8")); }
79
+ catch (error) { throw new WorkspaceError("MANAGED_FILE_UNKNOWN", `Cannot parse managed Hook target: ${target}: ${error.message}`, { target }); }
80
+ base = includeCore
81
+ ? mergeHooks(existing, JSON.parse(renderManagedContent(entry, variables).toString("utf8")), "codex")
82
+ : existing;
83
+ } else {
84
+ base = includeCore ? renderManagedContent(entry, variables) : Buffer.from('{\n "hooks": {}\n}\n');
85
+ }
86
+ if (preserveExisting) {
87
+ const withCoordination = mergeHooks(base, coordinationFragment("codex", root), "codex");
88
+ return Buffer.from(`${JSON.stringify(withCoordination, null, 2)}\n`);
89
+ }
90
+ const legacy = composeHookContent(
91
+ typeof base === "string" || Buffer.isBuffer(base) ? base : JSON.stringify(base),
92
+ extensionState || { extensions: {} }
93
+ );
75
94
  const composed = composeHookDocument(legacy, "codex", { extensions: {} }, extensionState || { extensions: {} });
76
- return Buffer.from(`${JSON.stringify(composed, null, 2)}\n`);
95
+ if (options.coordination !== true) return Buffer.from(`${JSON.stringify(composed, null, 2)}\n`);
96
+ const withCoordination = mergeHooks(composed, coordinationFragment("codex", root), "codex");
97
+ return Buffer.from(`${JSON.stringify(withCoordination, null, 2)}\n`);
77
98
  }
78
99
 
79
100
  function resolveArtifact(relativePath) {
@@ -196,12 +217,19 @@ function previousInstalledSha(state, entry) {
196
217
  return previous?.installedSha256 || previous?.sha256 || null;
197
218
  }
198
219
 
199
- function classifyManagedFile(root, entry, state, variables = {}, extensionState, includeCore = true) {
220
+ function classifyManagedFile(root, entry, state, variables = {}, extensionState, includeCore = true, options = {}) {
200
221
  const target = path.join(root, entry.target);
201
- const desiredSha256 = sha256(desiredManagedContent(root, entry, variables, extensionState, includeCore));
222
+ const desiredSha256 = sha256(desiredManagedContent(root, entry, variables, extensionState, includeCore, options));
202
223
  if (!fs.existsSync(target)) return { state: "missing", target, sha256: null, desiredSha256 };
203
224
  const actualSha256 = sha256(fs.readFileSync(target));
204
225
  if (actualSha256 === desiredSha256) return { state: "current", target, sha256: actualSha256, desiredSha256 };
226
+ // Provider Hook documents are shared with the user. When coordination is
227
+ // enabled, the desired document is built by merging managed contributions
228
+ // into the current document, so arbitrary user entries must not be treated
229
+ // as an unknown managed-file overwrite.
230
+ if (entry.target === CODEX_HOOKS_TARGET && options.coordination === true) {
231
+ return { state: "replaceable", target, sha256: actualSha256, desiredSha256 };
232
+ }
205
233
  if (actualSha256 === previousInstalledSha(state, entry)) return { state: "managed-old", target, sha256: actualSha256, desiredSha256 };
206
234
  if ((entry.replaceable || []).some((accepted) => accepted.sha256 === actualSha256)) {
207
235
  return { state: "replaceable", target, sha256: actualSha256, desiredSha256 };
@@ -218,7 +246,7 @@ function inspectManagedFiles(root, manifest, tools, capabilities = [], variables
218
246
  if (hookEntry && (hasExtensionHooks(hooksState) || (hooksState === null && fs.existsSync(path.join(root, CODEX_HOOKS_TARGET))))) selected.add(hookEntry.id);
219
247
  const output = { current: [], managedOld: [], replaceable: [], missing: [], unknown: [], files: [] };
220
248
  for (const entry of manifest.managedFiles.filter((entry) => selected.has(entry.id))) {
221
- const classified = classifyManagedFile(root, entry, state, variables, hooksState, coreSelected.has(entry.id));
249
+ const classified = classifyManagedFile(root, entry, state, variables, hooksState, coreSelected.has(entry.id), options);
222
250
  const key = classified.state === "managed-old" ? "managedOld" : classified.state;
223
251
  output[key].push(entry.target);
224
252
  output.files.push({
@@ -244,6 +272,19 @@ function planManagedFiles(root, manifest, tools, options = {}) {
244
272
  for (const entry of manifest.managedFiles) {
245
273
  if (!selected.has(entry.id)) {
246
274
  const previousSha = previousInstalledSha(state, entry);
275
+ if (entry.target === CODEX_HOOKS_TARGET && options.coordination === true) {
276
+ if (previousSha) {
277
+ plans.push({
278
+ entry,
279
+ target: path.join(root, entry.target),
280
+ action: "forget",
281
+ previous: null,
282
+ reason: "coordination-managed",
283
+ desiredSha256: null,
284
+ });
285
+ }
286
+ continue;
287
+ }
247
288
  if (!previousSha) continue;
248
289
  const target = path.join(root, entry.target);
249
290
  const exists = fs.existsSync(target);
@@ -261,7 +302,7 @@ function planManagedFiles(root, manifest, tools, options = {}) {
261
302
  });
262
303
  continue;
263
304
  }
264
- const classified = classifyManagedFile(root, entry, state, options.variables, hooksState, coreSelected.has(entry.id));
305
+ const classified = classifyManagedFile(root, entry, state, options.variables, hooksState, coreSelected.has(entry.id), options);
265
306
  if (classified.state === "current") {
266
307
  plans.push({ entry, target: classified.target, action: "skip", previous: null, reason: "current", desiredSha256: classified.desiredSha256 });
267
308
  continue;
@@ -278,7 +319,7 @@ function planManagedFiles(root, manifest, tools, options = {}) {
278
319
  action: "write",
279
320
  reason: classified.state,
280
321
  previous: fs.existsSync(classified.target) ? fs.readFileSync(classified.target) : null,
281
- content: desiredManagedContent(root, entry, options.variables, hooksState, coreSelected.has(entry.id)),
322
+ content: desiredManagedContent(root, entry, options.variables, hooksState, coreSelected.has(entry.id), options),
282
323
  desiredSha256: classified.desiredSha256,
283
324
  });
284
325
  }
@@ -24,15 +24,43 @@ function loadJson(file, fallback) {
24
24
  }
25
25
 
26
26
  function hookCommand(entry, provider) {
27
- return entry?.hooks?.some((hook) => hook?.type === "command" && String(hook.command || "").includes(`code-workspace-task-hook ${provider}`));
27
+ const marker = new RegExp(`(?:^|\\s)code-workspace-task-hook\\s+${provider}(?:\\s|$)`);
28
+ return entry?.hooks?.some((hook) => hook?.type === "command" && marker.test(String(hook.command || "")));
29
+ }
30
+
31
+ function coordinationCommand(provider, root) {
32
+ const command = `code-workspace-task-hook ${provider}`;
33
+ if (!root) return command;
34
+ const resolvedRoot = path.resolve(root);
35
+ let canonicalRoot = resolvedRoot;
36
+ try { canonicalRoot = fs.realpathSync.native(resolvedRoot); } catch { /* the root is created by the caller before installation */ }
37
+ const encodedRoot = Buffer.from(canonicalRoot).toString("base64url");
38
+ return `${command} --workspace-root-b64 ${encodedRoot}`;
39
+ }
40
+
41
+ function coordinationFragment(provider, root) {
42
+ const source = JSON.parse(fs.readFileSync(provider === "codex" ? CODEX_TEMPLATE : CLAUDE_TEMPLATE, "utf8"));
43
+ if (!root) return source;
44
+ for (const entries of Object.values(source.hooks || {})) {
45
+ for (const entry of Array.isArray(entries) ? entries : []) {
46
+ for (const hook of Array.isArray(entry.hooks) ? entry.hooks : []) {
47
+ if (hook?.type === "command" && String(hook.command || "").includes(`code-workspace-task-hook ${provider}`)) {
48
+ hook.command = coordinationCommand(provider, root);
49
+ }
50
+ }
51
+ }
52
+ }
53
+ return source;
28
54
  }
29
55
 
30
56
  function mergeHooks(document, fragment, provider) {
31
57
  const output = { ...document, hooks: { ...(document.hooks || {}) } };
32
58
  for (const [event, entries] of Object.entries(fragment.hooks || {})) {
33
59
  const current = Array.isArray(output.hooks[event]) ? [...output.hooks[event]] : [];
34
- const additions = (Array.isArray(entries) ? entries : []).filter((entry) => !current.some((existing) => hookCommand(existing, provider) && JSON.stringify(existing) === JSON.stringify(entry)));
35
- output.hooks[event] = [...current, ...additions];
60
+ const retained = current.filter((existing) => !hookCommand(existing, provider));
61
+ const additions = (Array.isArray(entries) ? entries : [])
62
+ .filter((entry) => !retained.some((existing) => JSON.stringify(existing) === JSON.stringify(entry)));
63
+ output.hooks[event] = [...retained, ...additions.map((entry) => structuredClone(entry))];
36
64
  }
37
65
  return output;
38
66
  }
@@ -58,7 +86,7 @@ function installCoordinationHooks(root, tools = ["codex", "claude"], options = {
58
86
  const plans = [];
59
87
  for (const tool of selected) {
60
88
  const target = path.join(root, tool === "codex" ? CODEX_TARGET : CLAUDE_TARGET);
61
- const fragment = JSON.parse(fs.readFileSync(tool === "codex" ? CODEX_TEMPLATE : CLAUDE_TEMPLATE, "utf8"));
89
+ const fragment = coordinationFragment(tool, root);
62
90
  const before = loadJson(target, {});
63
91
  const after = mergeHooks(before, fragment, tool);
64
92
  plans.push({ tool, target, action: JSON.stringify(before) === JSON.stringify(after) ? "skip" : "write", before, after });
@@ -109,4 +137,4 @@ function installCoordinationArtifacts(root, tools = ["codex", "claude"], options
109
137
  return plans.map(({ content: _content, ...plan }) => plan);
110
138
  }
111
139
 
112
- module.exports = { CODEX_TARGET, CLAUDE_TARGET, CODEX_TEMPLATE, CLAUDE_TEMPLATE, mergeHooks, stripHooks, installCoordinationHooks, removeCoordinationHooks, installCoordinationArtifacts };
140
+ module.exports = { CODEX_TARGET, CLAUDE_TARGET, CODEX_TEMPLATE, CLAUDE_TEMPLATE, coordinationFragment, mergeHooks, stripHooks, installCoordinationHooks, removeCoordinationHooks, installCoordinationArtifacts };
@@ -118,6 +118,11 @@ function renderNativeDecision(provider, result) {
118
118
  return providerAdapter.renderDecision({ ...result, decision, remediation: reason });
119
119
  }
120
120
 
121
+ function renderNativeResponse(provider, eventType, result) {
122
+ if (eventType === "write.before") return renderNativeDecision(provider, result);
123
+ return getHookAdapter(provider).renderAcknowledgement(eventType, result);
124
+ }
125
+
121
126
  function createAdapter(provider, options = {}) {
122
127
  const providerAdapter = getHookAdapter(provider);
123
128
  return {
@@ -157,16 +162,22 @@ async function runHook(provider, input, options = {}) {
157
162
  } catch (error) {
158
163
  const adapter = createAdapter(provider, options);
159
164
  const result = { decision: "RETRY_COORDINATION_FAILURE", error: { code: error.code || "HOOK_WORKSPACE_CONFIG_INVALID", message: error.message, details: error.details || {} }, remediation: "The coordination Hook could not load Workspace identity/projects; repair the Workspace configuration and retry." };
160
- return { envelope: null, result, native: adapter.render(result) };
165
+ const eventType = adapter.nativeEvents ? getHookAdapter(provider).eventTypeForNative(input?.hook_event_name || input?.hookEventName || input?.event_name || input?.eventName || input?.type || "Unknown") : null;
166
+ return { envelope: null, result, native: renderNativeResponse(provider, eventType, result) };
161
167
  }
162
168
  const adapter = createAdapter(provider, effectiveOptions);
163
169
  try {
164
170
  const envelope = adapter.normalize(input, effectiveOptions);
165
171
  if (!effectiveOptions.projectRealPath && Array.isArray(effectiveOptions.projects)) {
166
- const cwd = path.resolve(envelope.cwd || process.cwd());
172
+ const canonical = (value) => {
173
+ const resolved = path.resolve(value);
174
+ try { return fs.realpathSync.native(resolved); } catch { return resolved; }
175
+ };
176
+ const cwd = canonical(envelope.cwd || process.cwd());
167
177
  const matching = effectiveOptions.projects
168
178
  .map((project) => ({ ...project, realPath: project.realPath || project.location }))
169
- .filter((project) => project.realPath && (cwd === path.resolve(project.realPath) || cwd.startsWith(`${path.resolve(project.realPath)}${path.sep}`)))
179
+ .map((project) => ({ ...project, realPath: project.realPath ? canonical(project.realPath) : null }))
180
+ .filter((project) => project.realPath && (cwd === project.realPath || cwd.startsWith(`${project.realPath}${path.sep}`)))
170
181
  .sort((left, right) => right.realPath.length - left.realPath.length)[0];
171
182
  if (matching) effectiveOptions.projectRealPath = matching.realPath;
172
183
  }
@@ -177,10 +188,11 @@ async function runHook(provider, input, options = {}) {
177
188
  });
178
189
  }
179
190
  const result = await processEnvelope(envelope, effectiveOptions);
180
- return { envelope, result, native: adapter.render(result) };
191
+ return { envelope, result, native: renderNativeResponse(provider, envelope.eventType, result) };
181
192
  } catch (error) {
182
193
  const result = { decision: "RETRY_COORDINATION_FAILURE", error: { code: error.code || "HOOK_INTERNAL_ERROR", message: error.message, details: error.details || {} }, remediation: "The coordination Hook failed closed. Inspect the error and retry after fixing the workspace state." };
183
- return { envelope: null, result, native: adapter.render(result) };
194
+ const eventType = getHookAdapter(provider).eventTypeForNative(input?.hook_event_name || input?.hookEventName || input?.event_name || input?.eventName || input?.type || "Unknown");
195
+ return { envelope: null, result, native: renderNativeResponse(provider, eventType, result) };
184
196
  }
185
197
  }
186
198
 
@@ -59,4 +59,8 @@ function renderDecision(result) {
59
59
  };
60
60
  }
61
61
 
62
- module.exports = { provider, target, ABSTRACT_HOOK_EVENTS, EVENT_MAP, nativeEvents, renderDeclaration, eventTypeForNative, renderDecision };
62
+ function renderAcknowledgement() {
63
+ return { continue: true };
64
+ }
65
+
66
+ module.exports = { provider, target, ABSTRACT_HOOK_EVENTS, EVENT_MAP, nativeEvents, renderDeclaration, eventTypeForNative, renderDecision, renderAcknowledgement };
@@ -56,4 +56,8 @@ function renderDecision(result) {
56
56
  };
57
57
  }
58
58
 
59
- module.exports = { provider, target, ABSTRACT_HOOK_EVENTS, EVENT_MAP, nativeEvents, renderDeclaration, eventTypeForNative, renderDecision };
59
+ function renderAcknowledgement() {
60
+ return {};
61
+ }
62
+
63
+ module.exports = { provider, target, ABSTRACT_HOOK_EVENTS, EVENT_MAP, nativeEvents, renderDeclaration, eventTypeForNative, renderDecision, renderAcknowledgement };