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 +2 -2
- package/README.zh-CN.md +2 -2
- package/artifacts/manifest.json +1 -1
- package/bin/code-workspace-task-hook.js +23 -2
- package/package.json +3 -2
- package/src/cli/commands/init.js +3 -2
- package/src/cli/commands/update.js +25 -11
- package/src/core/doctor.js +22 -3
- package/src/core/init.js +3 -0
- package/src/core/initializer.js +29 -5
- package/src/core/managed-files.js +51 -10
- package/src/core/task-coordination-managed.js +33 -5
- package/src/core/task-coordination-protocol.js +17 -5
- package/src/hooks/adapters/claude.js +5 -1
- package/src/hooks/adapters/codex.js +5 -1
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
|
|
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`
|
|
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
|
-
-
|
|
45
|
+
- 启用监控或 Codex 协调时的 `.codex/hooks.json`,以及启用 Claude 协调时的 `.claude/settings.json`
|
|
46
46
|
|
|
47
47
|
它不会创建 `openspec/`,不会安装原生 `/opsx` 命令,也不会安装原生 `openspec-*` Skill。
|
|
48
48
|
|
package/artifacts/manifest.json
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|
package/src/cli/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
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 (
|
|
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
|
-
...(
|
|
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
|
-
|
|
103
|
-
|
|
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 (
|
|
110
|
-
const coordinationIncomplete =
|
|
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
|
|
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);
|
package/src/core/doctor.js
CHANGED
|
@@ -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 ||
|
|
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);
|
package/src/core/initializer.js
CHANGED
|
@@ -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 {
|
|
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
|
-
?
|
|
155
|
+
? installCoordinationHooks(root, options.tools, { force: options.force === true })
|
|
151
156
|
: [];
|
|
152
|
-
return [
|
|
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
|
-
...(
|
|
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, {
|
|
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
|
-
|
|
74
|
-
const
|
|
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
|
-
|
|
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
|
|
35
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
function renderAcknowledgement() {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { provider, target, ABSTRACT_HOOK_EVENTS, EVENT_MAP, nativeEvents, renderDeclaration, eventTypeForNative, renderDecision, renderAcknowledgement };
|