xuym-codex-collab 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -5
- package/apps/onboarding/commands.mjs +46 -0
- package/apps/onboarding/guided.mjs +1 -0
- package/apps/onboarding/main.mjs +72 -19
- package/apps/onboarding/manager.mjs +57 -14
- package/apps/onboarding/reset.mjs +3 -1
- package/apps/onboarding/server.mjs +1 -1
- package/apps/onboarding/startup.mjs +61 -0
- package/apps/onboarding/supervisor.mjs +41 -0
- package/apps/onboarding/web/app.js +41 -11
- package/apps/onboarding/web/index.html +14 -15
- package/apps/onboarding/web/style.css +3 -0
- package/apps/onboarding/workspace.mjs +36 -0
- package/apps/peer/desktop.mjs +2 -1
- package/apps/peer/intent.mjs +19 -0
- package/apps/peer/mcp.mjs +4 -2
- package/apps/peer/process.mjs +13 -0
- package/apps/peer/wecom.mjs +6 -4
- package/bin/codex-collab.mjs +21 -9
- package/docs/desktop-poll.md +20 -5
- package/docs/onboarding-audit.md +12 -2
- package/docs/onboarding.md +26 -10
- package/docs/peer.md +5 -3
- package/docs/releases/0.5.0.md +14 -0
- package/package.json +1 -1
- package/packages/codex-kit/templates/.agents/skills/team-collab-desktop/SKILL.md +5 -4
- package/packages/peer/inbox.mjs +11 -4
- package/packages/peer/natural-review.mjs +105 -0
- package/packages/peer/service.mjs +31 -9
- package/packages/peer/store.mjs +28 -3
- package/schemas/peer-inbox-event.schema.json +181 -3
- package/schemas/peer-inbox.schema.json +40 -1
- package/scripts/peer/cli.mjs +12 -3
- package/scripts/peer/refresh-project.mjs +23 -15
- package/scripts/peer/setup-desktop-project.mjs +1 -1
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"clarified",
|
|
62
62
|
"replied",
|
|
63
63
|
"task_created",
|
|
64
|
-
"answered"
|
|
64
|
+
"answered",
|
|
65
|
+
"reviewed"
|
|
65
66
|
]
|
|
66
67
|
},
|
|
67
68
|
"spec_hash": {
|
|
@@ -88,11 +89,31 @@
|
|
|
88
89
|
},
|
|
89
90
|
"task_id": {
|
|
90
91
|
"type": "string",
|
|
91
|
-
"pattern": "^T-[A-
|
|
92
|
+
"pattern": "^T-[A-Z0-9-]{1,62}$"
|
|
92
93
|
},
|
|
93
94
|
"task_hash": {
|
|
94
95
|
"type": "string",
|
|
95
96
|
"pattern": "^[a-f0-9]{64}$"
|
|
97
|
+
},
|
|
98
|
+
"review_seq": {
|
|
99
|
+
"type": "integer",
|
|
100
|
+
"minimum": 2
|
|
101
|
+
},
|
|
102
|
+
"decision": {
|
|
103
|
+
"enum": [
|
|
104
|
+
"approved",
|
|
105
|
+
"changes_requested"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"summary": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"minLength": 1,
|
|
111
|
+
"maxLength": 2000,
|
|
112
|
+
"pattern": "\\S"
|
|
113
|
+
},
|
|
114
|
+
"task_event_hash": {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
96
117
|
}
|
|
97
118
|
},
|
|
98
119
|
"oneOf": [
|
|
@@ -131,6 +152,26 @@
|
|
|
131
152
|
"required": [
|
|
132
153
|
"task_id"
|
|
133
154
|
]
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"required": [
|
|
158
|
+
"review_seq"
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"required": [
|
|
163
|
+
"decision"
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"required": [
|
|
168
|
+
"summary"
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"required": [
|
|
173
|
+
"task_event_hash"
|
|
174
|
+
]
|
|
134
175
|
}
|
|
135
176
|
]
|
|
136
177
|
}
|
|
@@ -170,6 +211,26 @@
|
|
|
170
211
|
"required": [
|
|
171
212
|
"task_id"
|
|
172
213
|
]
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"required": [
|
|
217
|
+
"review_seq"
|
|
218
|
+
]
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"required": [
|
|
222
|
+
"decision"
|
|
223
|
+
]
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"required": [
|
|
227
|
+
"summary"
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"required": [
|
|
232
|
+
"task_event_hash"
|
|
233
|
+
]
|
|
173
234
|
}
|
|
174
235
|
]
|
|
175
236
|
}
|
|
@@ -205,6 +266,26 @@
|
|
|
205
266
|
"required": [
|
|
206
267
|
"task_id"
|
|
207
268
|
]
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"required": [
|
|
272
|
+
"review_seq"
|
|
273
|
+
]
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"required": [
|
|
277
|
+
"decision"
|
|
278
|
+
]
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"required": [
|
|
282
|
+
"summary"
|
|
283
|
+
]
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"required": [
|
|
287
|
+
"task_event_hash"
|
|
288
|
+
]
|
|
208
289
|
}
|
|
209
290
|
]
|
|
210
291
|
}
|
|
@@ -213,6 +294,9 @@
|
|
|
213
294
|
"properties": {
|
|
214
295
|
"type": {
|
|
215
296
|
"const": "task_created"
|
|
297
|
+
},
|
|
298
|
+
"task_id": {
|
|
299
|
+
"pattern": "^T-[A-F0-9]{32}$"
|
|
216
300
|
}
|
|
217
301
|
},
|
|
218
302
|
"required": [
|
|
@@ -236,6 +320,26 @@
|
|
|
236
320
|
"required": [
|
|
237
321
|
"spec_hash"
|
|
238
322
|
]
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"required": [
|
|
326
|
+
"review_seq"
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"required": [
|
|
331
|
+
"decision"
|
|
332
|
+
]
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
"required": [
|
|
336
|
+
"summary"
|
|
337
|
+
]
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
"required": [
|
|
341
|
+
"task_event_hash"
|
|
342
|
+
]
|
|
239
343
|
}
|
|
240
344
|
]
|
|
241
345
|
}
|
|
@@ -275,10 +379,84 @@
|
|
|
275
379
|
"required": [
|
|
276
380
|
"task_id"
|
|
277
381
|
]
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"required": [
|
|
385
|
+
"review_seq"
|
|
386
|
+
]
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"required": [
|
|
390
|
+
"decision"
|
|
391
|
+
]
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"required": [
|
|
395
|
+
"summary"
|
|
396
|
+
]
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
"required": [
|
|
400
|
+
"task_event_hash"
|
|
401
|
+
]
|
|
278
402
|
}
|
|
279
403
|
]
|
|
280
404
|
}
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
"properties": {
|
|
408
|
+
"type": {
|
|
409
|
+
"const": "reviewed"
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
"required": [
|
|
413
|
+
"task_id",
|
|
414
|
+
"review_seq",
|
|
415
|
+
"decision",
|
|
416
|
+
"task_event_hash",
|
|
417
|
+
"response"
|
|
418
|
+
],
|
|
419
|
+
"not": {
|
|
420
|
+
"anyOf": [
|
|
421
|
+
{
|
|
422
|
+
"required": [
|
|
423
|
+
"content"
|
|
424
|
+
]
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
"required": [
|
|
428
|
+
"message_key"
|
|
429
|
+
]
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
"required": [
|
|
433
|
+
"spec_hash"
|
|
434
|
+
]
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
"required": [
|
|
438
|
+
"task_hash"
|
|
439
|
+
]
|
|
440
|
+
}
|
|
441
|
+
]
|
|
442
|
+
},
|
|
443
|
+
"allOf": [
|
|
444
|
+
{
|
|
445
|
+
"if": {
|
|
446
|
+
"properties": {
|
|
447
|
+
"decision": {
|
|
448
|
+
"const": "changes_requested"
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
"then": {
|
|
453
|
+
"required": [
|
|
454
|
+
"summary"
|
|
455
|
+
]
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
]
|
|
281
459
|
}
|
|
282
460
|
],
|
|
283
|
-
"$comment": "Runtime also verifies Ed25519 signatures and historical team hash, receiving-agent authorship, exact sequence, unique IDs and idempotency keys, nondecreasing UTC timestamps, signed spec digest and allowed transitions. received opens pending; clarified waits for a reply; replied returns to pending; task_created and answered resolve pending. A task_created event and its desktop_poll task commit atomically; runtime validates the exact task digest, deterministic task ID, original group, creator and creation key. Replies are scoped to the same bot, group and sender."
|
|
461
|
+
"$comment": "Runtime also verifies Ed25519 signatures and historical team hash, receiving-agent authorship, exact sequence, unique IDs and idempotency keys, nondecreasing UTC timestamps, signed spec digest and allowed transitions. received opens pending; clarified waits for a reply; replied returns to pending; task_created and answered resolve pending. A task_created event and its desktop_poll task commit atomically; runtime validates the exact task digest, deterministic task ID, original group, creator and creation key. Replies are scoped to the same bot, group and sender. reviewed resolves a pending request and atomically appends the matching task reviewed event. Runtime verifies frozen context, explicit owner consent, pinned reviewer, unchanged code revision and the signed task-event digest; task_created keeps the deterministic inbox-derived task ID constraint."
|
|
284
462
|
}
|
|
@@ -57,7 +57,46 @@
|
|
|
57
57
|
},
|
|
58
58
|
"admin_dispatch": {
|
|
59
59
|
"type": "boolean"
|
|
60
|
+
},
|
|
61
|
+
"review_context": {
|
|
62
|
+
"type": "array",
|
|
63
|
+
"maxItems": 100,
|
|
64
|
+
"items": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"required": [
|
|
68
|
+
"task_id",
|
|
69
|
+
"expected_seq",
|
|
70
|
+
"title",
|
|
71
|
+
"commit",
|
|
72
|
+
"stage"
|
|
73
|
+
],
|
|
74
|
+
"properties": {
|
|
75
|
+
"task_id": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"pattern": "^T-[A-Z0-9-]{1,62}$"
|
|
78
|
+
},
|
|
79
|
+
"expected_seq": {
|
|
80
|
+
"type": "integer",
|
|
81
|
+
"minimum": 1
|
|
82
|
+
},
|
|
83
|
+
"title": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"minLength": 1,
|
|
86
|
+
"maxLength": 200,
|
|
87
|
+
"pattern": "\\S"
|
|
88
|
+
},
|
|
89
|
+
"commit": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"pattern": "^[a-f0-9]{40}$"
|
|
92
|
+
},
|
|
93
|
+
"stage": {
|
|
94
|
+
"type": "integer",
|
|
95
|
+
"minimum": 0
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
60
99
|
}
|
|
61
100
|
},
|
|
62
|
-
"$comment": "Runtime also verifies the repo and target against the pinned team, a deterministic request ID derived from target_agent and message_key, and authorization by the bot owner, an enrolled team member, or an explicitly allowed administrator. The original group request is shared in Git; private Codex transcripts, bindings and credentials stay local."
|
|
101
|
+
"$comment": "Runtime also verifies the repo and target against the pinned team, a deterministic request ID derived from target_agent and message_key, and authorization by the bot owner, an enrolled team member, or an explicitly allowed administrator. The original group request is shared in Git; private Codex transcripts, bindings and credentials stay local. Optional review_context freezes pending tasks, sequences and code commits when the authenticated owner message arrives. Natural review interpretation cannot replace this context with a newer version. All peers must support these records before enabling natural reviews."
|
|
63
102
|
}
|
package/scripts/peer/cli.mjs
CHANGED
|
@@ -6,9 +6,11 @@ import { join, resolve } from 'node:path';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
8
8
|
import { loadRuntime, readPeerConfig } from '../../apps/peer/runtime.mjs';
|
|
9
|
-
import { alive, readRunnerLock } from '../../apps/peer/process.mjs';
|
|
9
|
+
import { alive, readRunnerLock, acquireRunnerControl } from '../../apps/peer/process.mjs';
|
|
10
10
|
import { git } from '../../packages/peer/store.mjs';
|
|
11
11
|
|
|
12
|
+
import { readRunnerIntent, writeRunnerIntent } from '../../apps/peer/intent.mjs';
|
|
13
|
+
|
|
12
14
|
const root = fileURLToPath(new URL('../../', import.meta.url));
|
|
13
15
|
const [command, configArg, ...args] = process.argv.slice(2);
|
|
14
16
|
if (!command || !configArg) throw new Error('usage: node scripts/peer/cli.mjs install|doctor|probe|start|stop|status|list|read|review|complete|reopen|desktop-status|desktop-unbind /abs/peer.json [task-id] [decision/reason]');
|
|
@@ -20,6 +22,8 @@ async function running() {
|
|
|
20
22
|
return state && alive(state.pid) ? state : null;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
const releaseControl = ['start', 'recover', 'stop'].includes(command) ? await acquireRunnerControl(config.data_dir, configPath) : null;
|
|
26
|
+
try {
|
|
23
27
|
switch (command) {
|
|
24
28
|
case 'probe': {
|
|
25
29
|
try {
|
|
@@ -58,7 +62,9 @@ switch (command) {
|
|
|
58
62
|
console.log(JSON.stringify({ identity: config.agent_id, auto_execute: config.auto_execute !== false, execution_session: config.execution_mode === 'desktop_poll' ? 'bound desktop heartbeat; adapter never launches SDK (check desktop-status and app heartbeat separately)' : 'new Runner-owned SDK thread; not an existing desktop conversation', config: 'valid; private key matches policy', git: 'shared origin and main readable', origin_configured: !!origin, wecom: config.wecom ? 'configured; live authentication not checked' : 'disabled', codex_provider: config.execution_mode === 'desktop_poll' ? 'uses the existing desktop conversation model; no SDK provider is launched by this adapter' : 'not tested; uses selected CODEX_HOME/provider configuration', test_argv: config.test_argv || ['sh', 'test.sh'] }, null, 2));
|
|
59
63
|
break;
|
|
60
64
|
}
|
|
61
|
-
case 'start': {
|
|
65
|
+
case 'start': case 'recover': {
|
|
66
|
+
if (command === 'recover') { if (!(await readRunnerIntent(config.data_dir))?.enabled) break; }
|
|
67
|
+
else await writeRunnerIntent(config.data_dir, true);
|
|
62
68
|
const current = await running();
|
|
63
69
|
if (current) { console.log(`Runner already online (PID ${current.pid})`); break; }
|
|
64
70
|
await mkdir(config.data_dir, { recursive: true, mode: 0o700 });
|
|
@@ -81,10 +87,11 @@ switch (command) {
|
|
|
81
87
|
if (health.pid === state.pid && new Date(health.updated_at).getTime() >= new Date(state.started_at).getTime() && ['connecting', 'authenticated', 'reconnecting', 'disconnected', 'failed'].includes(health.status)) wecom = health.status;
|
|
82
88
|
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
83
89
|
}
|
|
84
|
-
console.log(JSON.stringify({ identity: config.agent_id, execution_mode: config.execution_mode || 'legacy', auto_execute: config.auto_execute !== false, online: !!state, wecom, pid: state?.pid || null, log: join(config.data_dir, 'runner.log') }, null, 2));
|
|
90
|
+
console.log(JSON.stringify({ identity: config.agent_id, execution_mode: config.execution_mode || 'legacy', auto_execute: config.auto_execute !== false, online: !!state, connection_enabled: (await readRunnerIntent(config.data_dir))?.enabled ?? !!state, wecom, pid: state?.pid || null, log: join(config.data_dir, 'runner.log') }, null, 2));
|
|
85
91
|
break;
|
|
86
92
|
}
|
|
87
93
|
case 'stop': {
|
|
94
|
+
await writeRunnerIntent(config.data_dir, false);
|
|
88
95
|
const state = await running();
|
|
89
96
|
if (!state) { console.log('Runner offline'); break; }
|
|
90
97
|
if (state.config !== configPath) throw new Error('PID configuration mismatch; refusing to signal');
|
|
@@ -114,3 +121,5 @@ switch (command) {
|
|
|
114
121
|
}
|
|
115
122
|
default: throw new Error('unknown peer command');
|
|
116
123
|
}
|
|
124
|
+
|
|
125
|
+
} finally { await releaseControl?.(); }
|
|
@@ -6,14 +6,15 @@ import { readPeerConfig } from '../../apps/peer/runtime.mjs';
|
|
|
6
6
|
import { git } from '../../packages/peer/store.mjs';
|
|
7
7
|
import { bindingPrompt } from './setup-desktop-project.mjs';
|
|
8
8
|
const root = fileURLToPath(new URL('../../', import.meta.url));
|
|
9
|
-
const legacyHashes = new Set(['6e4d1ea6a644fb3078c541e40bcc9e8bf5069462994152f3ec2fc3aa5fedd5fa', 'f98ad425e2ea3c8ab6fb86febef7d9cb288029ef55bea6937dda6de8b467fece']);
|
|
9
|
+
const legacyHashes = new Set(['46543ca4fe00836f0ae0d7d7c12ea2756c9ab0220f01d29c391d608c5e405c53', '6e4d1ea6a644fb3078c541e40bcc9e8bf5069462994152f3ec2fc3aa5fedd5fa', 'f98ad425e2ea3c8ab6fb86febef7d9cb288029ef55bea6937dda6de8b467fece']);
|
|
10
10
|
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
11
11
|
const inside = (parent, child) => { const path = relative(parent, child); return !path || path !== '..' && !path.startsWith('../') && !isAbsolute(path); };
|
|
12
|
-
async function plainPath(base, path) {
|
|
12
|
+
async function plainPath(base, path, { allowMissing = false } = {}) {
|
|
13
13
|
let current = base;
|
|
14
14
|
for (const part of relative(base, path).split('/').filter(Boolean)) {
|
|
15
15
|
current = join(current, part);
|
|
16
|
-
|
|
16
|
+
let info; try { info = await lstat(current); } catch (error) { if (allowMissing && error.code === 'ENOENT') return; throw error; }
|
|
17
|
+
if (info.isSymbolicLink()) throw new Error('协作配置或 Skill 是符号链接,请先手动检查;未覆盖文件。');
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
async function privateWrite(path, contents) {
|
|
@@ -22,14 +23,17 @@ async function privateWrite(path, contents) {
|
|
|
22
23
|
finally { await rm(temporary, { force: true }); }
|
|
23
24
|
}
|
|
24
25
|
// Only recognize our generated MCP entry. Never replace another identity's entry.
|
|
25
|
-
export async function installedProjectConfig({ repoPath, agentId }) {
|
|
26
|
+
export async function installedProjectConfig({ repoPath, agentId, allowMissing = false }) {
|
|
26
27
|
const repo = await realpath(repoPath);
|
|
27
|
-
const path = join(repo, '.codex/config.toml'); await plainPath(repo, path);
|
|
28
|
-
const before = await readFile(path, 'utf8');
|
|
28
|
+
const path = join(repo, '.codex/config.toml'); await plainPath(repo, path, { allowMissing });
|
|
29
|
+
const before = await readFile(path, 'utf8').catch((error) => { if (allowMissing && error.code === 'ENOENT') return null; throw error; });
|
|
29
30
|
const server = `team-collab-${agentId}`;
|
|
30
|
-
const headers = [...before.matchAll(/^\s*\[([^\]\n]+)\][^\n]*$/gm)];
|
|
31
|
+
const headers = [...(before || '').matchAll(/^\s*\[([^\]\n]+)\][^\n]*$/gm)];
|
|
31
32
|
if (headers.some((item) => item[1].startsWith(`mcp_servers.${server}.`))) throw new Error('协作 MCP 有自定义嵌套配置,请手动检查;未覆盖文件。');
|
|
32
33
|
const matching = headers.filter((item) => item[1] === `mcp_servers.${server}`);
|
|
34
|
+
// A missing block can be rebuilt from the private profile, but ambiguous or
|
|
35
|
+
// other identities' collaboration settings always require manual inspection.
|
|
36
|
+
if (!matching.length && allowMissing && !(before || '').includes('team-collab-')) return { repo, path, before, missing: true };
|
|
33
37
|
if (matching.length !== 1) throw new Error('未找到唯一的已安装协作 MCP,请先完成项目接入。');
|
|
34
38
|
const header = matching[0]; const end = headers.find((item) => item.index > header.index)?.index ?? before.length;
|
|
35
39
|
const section = before.slice(header.index, end);
|
|
@@ -46,17 +50,19 @@ export async function refreshProject({ configPath }) {
|
|
|
46
50
|
if (!isAbsolute(configPath || '')) throw new Error('usage: codex-collab refresh-project /absolute/private-peer.json');
|
|
47
51
|
const { config } = await readPeerConfig(configPath);
|
|
48
52
|
if (config.execution_mode !== 'desktop_poll' || config.auto_execute !== false) throw new Error('refresh-project 需要 desktop_poll 配置且 SDK 已关闭。');
|
|
49
|
-
const installed = await installedProjectConfig({ repoPath: config.repo, agentId: config.agent_id });
|
|
53
|
+
const installed = await installedProjectConfig({ repoPath: config.repo, agentId: config.agent_id, allowMissing: true });
|
|
50
54
|
const { repo, path: configFile, before, section } = installed;
|
|
51
|
-
if (await git(repo, 'rev-parse', '--show-toplevel') !== repo || await realpath(installed.config_path) !== await realpath(configPath)) throw new Error('项目 MCP 已绑定其他配置,请使用该 MCP 实际使用的个人配置路径;未更换身份。');
|
|
55
|
+
if (await git(repo, 'rev-parse', '--show-toplevel') !== repo || !installed.missing && await realpath(installed.config_path) !== await realpath(configPath)) throw new Error('项目 MCP 已绑定其他配置,请使用该 MCP 实际使用的个人配置路径;未更换身份。');
|
|
52
56
|
const skillFile = join(repo, '.agents/skills/team-collab-desktop/SKILL.md');
|
|
53
|
-
await plainPath(repo, skillFile);
|
|
57
|
+
await plainPath(repo, skillFile, { allowMissing: true });
|
|
54
58
|
if (await git(repo, 'ls-files', '--', '.codex/config.toml', '.agents/skills/team-collab-desktop')) throw new Error('协作配置或 Skill 已提交到项目 Git,请手动合并;未覆盖共享文件。');
|
|
55
|
-
const oldSkill = await readFile(skillFile, 'utf8');
|
|
59
|
+
const oldSkill = await readFile(skillFile, 'utf8').catch((error) => { if (error.code === 'ENOENT') return null; throw error; });
|
|
56
60
|
const template = await readFile(join(root, 'packages/codex-kit/templates/.agents/skills/team-collab-desktop/SKILL.md'), 'utf8');
|
|
57
|
-
if (oldSkill !== template && !legacyHashes.has(hash(oldSkill))) throw new Error('协作 Skill 已自定义,请手动合并自然语言处理说明;未覆盖文件。');
|
|
58
|
-
const
|
|
59
|
-
const
|
|
61
|
+
if (oldSkill !== null && oldSkill !== template && !legacyHashes.has(hash(oldSkill))) throw new Error('协作 Skill 已自定义,请手动合并自然语言处理说明;未覆盖文件。');
|
|
62
|
+
const command = `command = ${JSON.stringify(process.execPath)}`;
|
|
63
|
+
const args = `args = ${JSON.stringify([join(root, 'apps/peer/mcp-main.mjs'), installed.config_path || configPath])}`;
|
|
64
|
+
const nextSection = installed.missing ? `[mcp_servers.team-collab-${config.agent_id}]\n${command}\n${args}\n` : section.replace(/^command\s*=.*$/m, () => command).replace(/^args\s*=.*$/m, () => args);
|
|
65
|
+
const next = installed.missing ? `${before || ''}\n${nextSection}` : before.replace(section, () => nextSection);
|
|
60
66
|
if (inside(repo, resolve(config.data_dir))) throw new Error('个人数据目录必须位于项目仓库之外。');
|
|
61
67
|
await mkdir(config.data_dir, { recursive: true, mode: 0o700 });
|
|
62
68
|
const dataDir = await realpath(config.data_dir);
|
|
@@ -67,8 +73,10 @@ export async function refreshProject({ configPath }) {
|
|
|
67
73
|
if (changed) {
|
|
68
74
|
backup = join(dataDir, `project-refresh-${randomUUID()}.json`);
|
|
69
75
|
await writeFile(backup, JSON.stringify({ repo, config_toml: before, skill: oldSkill, created_at: new Date().toISOString() }, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
|
|
76
|
+
await mkdir(dirname(skillFile), { recursive: true, mode: 0o700 });
|
|
77
|
+
await mkdir(dirname(configFile), { recursive: true, mode: 0o700 });
|
|
70
78
|
try { await privateWrite(configFile, next); await privateWrite(skillFile, template); }
|
|
71
|
-
catch (error) { await privateWrite(configFile, before); await privateWrite(skillFile, oldSkill); throw error; }
|
|
79
|
+
catch (error) { if (before === null) await rm(configFile, { force: true }); else await privateWrite(configFile, before); if (oldSkill === null) await rm(skillFile, { force: true }); else await privateWrite(skillFile, oldSkill); throw error; }
|
|
72
80
|
}
|
|
73
81
|
await privateWrite(prompt, bindingPrompt(config.agent_id, repo));
|
|
74
82
|
return { agent_id: config.agent_id, repo, changed, mcp_config: configFile, skill: skillFile, backup, binding_prompt: prompt,
|
|
@@ -11,7 +11,7 @@ export function bindingPrompt(agentId, repo) {
|
|
|
11
11
|
return `使用 $team-collab-desktop,启用当前项目的团队协作,将 ${agentId} 机器人绑定到当前对话。项目:${repo}`;
|
|
12
12
|
}
|
|
13
13
|
export function detailedBindingPrompt(agentId, repo) {
|
|
14
|
-
return `请把当前已有 Codex 对话设为 ${agentId} 的企微协作执行对话,不要新建独立执行线程。\n项目:${repo}\n读取 ${join(repo, '.agents/skills/team-collab-desktop/SKILL.md')}。\n1. 用本项目 MCP 的 get_identity 确认身份是 ${agentId},execution_mode 是 desktop_poll;若不匹配,停止并提示重新加载 MCP。\n2. 获取当前真实对话 ID:优先使用已提供的应用上下文,或当前终端 CODEX_THREAD_ID(如果存在);不可猜 ID 或读取/修改应用数据库。没有可用 ID 时请让我复制当前对话 ID/链接。\n3. 调用 bind_desktop_thread 绑定当前真实对话,然后用应用 automation_update 工具在当前已有对话创建一个 heartbeat:每分钟检查(仅当工具接受该周期;否则说明限制),不要创建 cron、新对话或后台 SDK。先查现有 automation,避免重复;不要修改无关 automation 或 CC Switch。\n4. heartbeat 的保存提示词必须包含 ${agentId}、当前真实 thread_id 和该 skill 路径;每次 get_identity → poll_desktop_task。job=null 且 requests 为空 时保持安静;有任务就只在本对话和返回的隔离 worktree 执行本阶段,主动续租、测试、明确提交,然后 finish_desktop_task;失败时按 skill 处理。requests 非空时按 Skill 理解群消息,调用 resolve_desktop_request
|
|
14
|
+
return `请把当前已有 Codex 对话设为 ${agentId} 的企微协作执行对话,不要新建独立执行线程。\n项目:${repo}\n读取 ${join(repo, '.agents/skills/team-collab-desktop/SKILL.md')}。\n1. 用本项目 MCP 的 get_identity 确认身份是 ${agentId},execution_mode 是 desktop_poll;若不匹配,停止并提示重新加载 MCP。\n2. 获取当前真实对话 ID:优先使用已提供的应用上下文,或当前终端 CODEX_THREAD_ID(如果存在);不可猜 ID 或读取/修改应用数据库。没有可用 ID 时请让我复制当前对话 ID/链接。\n3. 调用 bind_desktop_thread 绑定当前真实对话,然后用应用 automation_update 工具在当前已有对话创建一个 heartbeat:每分钟检查(仅当工具接受该周期;否则说明限制),不要创建 cron、新对话或后台 SDK。先查现有 automation,避免重复;不要修改无关 automation 或 CC Switch。\n4. heartbeat 的保存提示词必须包含 ${agentId}、当前真实 thread_id 和该 skill 路径;每次 get_identity → poll_desktop_task。job=null 且 requests 为空 时保持安静;有任务就只在本对话和返回的隔离 worktree 执行本阶段,主动续租、测试、明确提交,然后 finish_desktop_task;失败时按 skill 处理。requests 非空时按 Skill 理解群消息,调用 resolve_desktop_request 创建任务、转达明确的真人审核、追问或回答;使用 team_context 的现有路由、能力与默认分工,不能猜测不存在的同事。解决后再次 poll 开始工作。真人自然语言审核仅能通过 request.review_context 中的固定版本转达;多任务、语义不清或版本变化时先追问,不得自行批准、完成或合并。复用或更新当前对话已有的协作 heartbeat,避免重复创建。禁止启动 SDK/CLI 代替当前对话。仅新任务、结果、失败或需我操作时通知。\n5. 创建完成后立即调用一次 poll_desktop_task 验证,无任务应正常返回,不要创建测试任务。告诉我绑定身份、对话 ID 和 heartbeat 状态;若工具不可用明确说明未启用自动轮询。\n`;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// An explicit project attachment: preserve unrelated TOML settings, keep a local
|