@weiping/wingline-core 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit-table.d.ts +11 -0
- package/dist/audit-table.js +85 -0
- package/dist/audit.js +1 -1
- package/dist/commands/about.d.ts +13 -0
- package/dist/commands/about.js +12 -1
- package/dist/commands/autostart/sys.d.ts +3 -0
- package/dist/commands/autostart/sys.js +23 -0
- package/dist/commands/dispatcher/lifecycle.d.ts +4 -0
- package/dist/commands/dispatcher/lifecycle.js +7 -1
- package/dist/commands/down.js +3 -0
- package/dist/commands/init.d.ts +17 -0
- package/dist/commands/init.js +106 -0
- package/dist/commands/launch.d.ts +10 -1
- package/dist/commands/launch.js +13 -1
- package/dist/commands/spec.d.ts +8 -6
- package/dist/commands/spec.js +66 -9
- package/dist/commands/task/plan.d.ts +1 -0
- package/dist/commands/task/plan.js +1 -1
- package/dist/commands/templates.d.ts +32 -0
- package/dist/commands/templates.js +120 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/orca/client.js +27 -9
- package/dist/orca/types.d.ts +6 -0
- package/dist/registry/registry.d.ts +7 -0
- package/dist/registry/registry.js +12 -0
- package/dist/server/api-read.js +44 -28
- package/dist/server/api-spec.js +43 -7
- package/dist/server/api-write.d.ts +86 -2
- package/dist/server/api-write.js +285 -12
- package/dist/server/index.js +4 -4
- package/dist/spec/doc.d.ts +13 -0
- package/dist/spec/doc.js +62 -0
- package/dist/spec/draft.d.ts +16 -0
- package/dist/spec/draft.js +15 -8
- package/dist/spec/edit.d.ts +21 -0
- package/dist/spec/edit.js +136 -0
- package/dist/spec/facade.d.ts +41 -0
- package/dist/spec/facade.js +68 -0
- package/dist/spec/load.d.ts +1 -0
- package/dist/spec/load.js +7 -6
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AuditTable } from './audit.js';
|
|
2
|
+
import type { CommandDeps } from './commands/deps.js';
|
|
3
|
+
/** 审计表里命令的最小形状:吃 deps 打头,后面参数不限于一个。 */
|
|
4
|
+
type AnyCommand = (deps: CommandDeps, ...a: any[]) => Promise<unknown>;
|
|
5
|
+
/** 取一张按调用方命令集定型的审计表;内容始终是同一张表。 */
|
|
6
|
+
export declare function coreAuditTable<C extends {
|
|
7
|
+
[K in keyof C]: (deps: CommandDeps, ...a: any[]) => Promise<unknown>;
|
|
8
|
+
}>(): AuditTable<C>;
|
|
9
|
+
/** 宽松版:给不想定型的一方(如插件 worker)用。 */
|
|
10
|
+
export declare const CORE_AUDIT: AuditTable<Record<string, AnyCommand>>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** 地址参数取 `@` 之后的团队名;解析不出(没有 `@`)记 `-`。 */
|
|
2
|
+
const ofAddress = (a) => a.address.split('@')[1] ?? '-';
|
|
3
|
+
/** 任务 id 的团队名从注册表查;查不到(任务不存在、已被清理)记 `-`。 */
|
|
4
|
+
const byTask = (idOf) => (_a, ctx) => ctx.deps.registry.getTaskExt(idOf(_a))?.team ?? '-';
|
|
5
|
+
const byGate = (gateId) => (_a, ctx) => ctx.deps.registry.getGate(gateId(_a))?.team ?? '-';
|
|
6
|
+
/** 只有结果里才有团队名(up 成功时);失败或没跑出结果记 `-`。 */
|
|
7
|
+
const byResult = (_a, ctx) => ctx.result?.team ?? '-';
|
|
8
|
+
const id = (a) => a.id;
|
|
9
|
+
const ENTRIES = {
|
|
10
|
+
init: {
|
|
11
|
+
command: 'init',
|
|
12
|
+
team: byResult,
|
|
13
|
+
summary: (a, ctx) => ({ mode: a.mode, template: a.template, created: ctx.result?.created })
|
|
14
|
+
},
|
|
15
|
+
up: {
|
|
16
|
+
command: 'up',
|
|
17
|
+
team: byResult,
|
|
18
|
+
summary: (a) => ({ adoptRun: a.adoptRun, newRun: a.newRun, draft: a.draft })
|
|
19
|
+
},
|
|
20
|
+
down: { command: 'down', team: (a) => a.team, summary: (a) => ({ close: a.close }) },
|
|
21
|
+
restore: {
|
|
22
|
+
command: 'restore',
|
|
23
|
+
team: (a) => a.team,
|
|
24
|
+
skip: (a) => !!a.dryRun,
|
|
25
|
+
summary: (a) => ({ dryRun: !!a.dryRun, noTasks: !!a.noTasks, session: Object.keys(a.sessions ?? {}) })
|
|
26
|
+
},
|
|
27
|
+
send: { command: 'send', team: ofAddress, target: (a) => a.address, summary: (a) => ({ message: a.message }) },
|
|
28
|
+
createTask: {
|
|
29
|
+
command: 'task create',
|
|
30
|
+
team: (a) => a.team,
|
|
31
|
+
target: (_a, ctx) => ctx.result?.id ?? null,
|
|
32
|
+
summary: (a) => ({ title: a.title, post: a.post, tags: a.tags, parent: a.parent, as: a.as })
|
|
33
|
+
},
|
|
34
|
+
claimTask: { command: 'task claim', team: byTask(id), target: id, summary: (a) => ({ as: a.as }) },
|
|
35
|
+
releaseTask: { command: 'task release', team: byTask(id), target: id, summary: (a) => ({ as: a.as }) },
|
|
36
|
+
reassignTask: { command: 'task reassign', team: byTask(id), target: id, summary: (a) => ({ to: a.to }) },
|
|
37
|
+
startTask: { command: 'task start', team: byTask(id), target: id, summary: (a) => ({ as: a.as }) },
|
|
38
|
+
planTask: { command: 'task plan', team: byTask(id), target: id, summary: (a) => ({ text: a.text }) },
|
|
39
|
+
doneTask: { command: 'task done', team: byTask(id), target: id, summary: (a) => ({ as: a.as, resumeEpoch: a.resumeEpoch }) },
|
|
40
|
+
escalateTask: {
|
|
41
|
+
command: 'task escalate',
|
|
42
|
+
team: byTask(id),
|
|
43
|
+
target: (a) => a.id,
|
|
44
|
+
summary: (a) => ({ reason: a.reason, reopen: a.reopen, as: a.as })
|
|
45
|
+
},
|
|
46
|
+
resolveGate: {
|
|
47
|
+
command: 'task resolve',
|
|
48
|
+
team: byGate((a) => a.gateId),
|
|
49
|
+
target: (a) => a.gateId,
|
|
50
|
+
summary: (a) => ({ resolution: a.resolution, reassign: a.reassign, as: a.as })
|
|
51
|
+
},
|
|
52
|
+
recoverTask: { command: 'task recover', team: byTask(id), target: (a) => a.id, summary: (a) => ({ as: a.as }) },
|
|
53
|
+
dispatcherUp: { command: 'dispatcher up', team: (a) => a.team, summary: (a) => ({ platform: a.platform }) },
|
|
54
|
+
dispatcherDown: { command: 'dispatcher down', team: (a) => a.team, summary: (a) => ({ close: a.close }) },
|
|
55
|
+
freezePostCmd: { command: 'dispatcher freeze', team: ofAddress, target: (a) => a.address, summary: (a) => ({ reason: a.reason }) },
|
|
56
|
+
unfreezePostCmd: { command: 'dispatcher unfreeze', team: ofAddress, target: (a) => a.address },
|
|
57
|
+
hushCmd: { command: 'dispatcher hush', team: ofAddress, target: (a) => a.address, summary: (a) => ({ forMs: a.forMs }) },
|
|
58
|
+
unhushCmd: { command: 'dispatcher unhush', team: ofAddress, target: (a) => a.address },
|
|
59
|
+
dispatcherReply: {
|
|
60
|
+
command: 'dispatcher reply',
|
|
61
|
+
team: (a) => a.team,
|
|
62
|
+
target: (a) => a.messageId,
|
|
63
|
+
summary: (a) => ({ body: a.body, again: a.again })
|
|
64
|
+
},
|
|
65
|
+
saveSpecCmd: {
|
|
66
|
+
command: 'spec save',
|
|
67
|
+
team: (a) => a.team,
|
|
68
|
+
target: (_a, ctx) => ctx.result?.path ?? null,
|
|
69
|
+
summary: (a, ctx) => {
|
|
70
|
+
const r = ctx.result;
|
|
71
|
+
return { baseHash: a.baseHash?.slice(0, 16), running: a.running, wrote: r?.wrote, hash: r?.hash?.slice(0, 16) };
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
discardDraftCmd: {
|
|
75
|
+
command: 'spec discard-draft',
|
|
76
|
+
team: (a) => a.team,
|
|
77
|
+
summary: (_a, ctx) => ({ discarded: ctx.result?.discarded })
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
/** 取一张按调用方命令集定型的审计表;内容始终是同一张表。 */
|
|
81
|
+
export function coreAuditTable() {
|
|
82
|
+
return ENTRIES;
|
|
83
|
+
}
|
|
84
|
+
/** 宽松版:给不想定型的一方(如插件 worker)用。 */
|
|
85
|
+
export const CORE_AUDIT = coreAuditTable();
|
package/dist/audit.js
CHANGED
|
@@ -4,7 +4,7 @@ import { actorOf, resolveActor } from './commands/task/identity.js';
|
|
|
4
4
|
export const WRITE_COMMANDS = [
|
|
5
5
|
'up', 'down', 'restore', 'send', 'createTask', 'claimTask', 'releaseTask', 'reassignTask', 'startTask', 'planTask',
|
|
6
6
|
'doneTask', 'escalateTask', 'resolveGate', 'recoverTask', 'dispatcherUp', 'dispatcherDown', 'freezePostCmd',
|
|
7
|
-
'unfreezePostCmd', 'hushCmd', 'unhushCmd', 'dispatcherReply',
|
|
7
|
+
'unfreezePostCmd', 'hushCmd', 'unhushCmd', 'dispatcherReply', 'init',
|
|
8
8
|
];
|
|
9
9
|
/** 换行与控制字符换成空格(一一对应,长度不变),超长截断并标明原长。 */
|
|
10
10
|
const CONTROL = /[\x00-\x1f\x7f]/g;
|
package/dist/commands/about.d.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import type { ConsoleFile } from '../server/control.js';
|
|
2
2
|
import type { CommandDeps } from './deps.js';
|
|
3
3
|
import { type DispatcherHealth } from './dispatcher/lifecycle.js';
|
|
4
|
+
import { type AutostartStatus, type AutostartSys } from './autostart/index.js';
|
|
4
5
|
export interface AboutItem {
|
|
5
6
|
key: string;
|
|
6
7
|
value: string | number | boolean | null;
|
|
7
8
|
source: 'env' | 'default' | 'registry' | 'file' | 'orca';
|
|
8
9
|
note?: string;
|
|
9
10
|
}
|
|
11
|
+
export type AboutAutostart = ({
|
|
12
|
+
supported: true;
|
|
13
|
+
} & AutostartStatus & {
|
|
14
|
+
platform: string;
|
|
15
|
+
user?: string;
|
|
16
|
+
logPath: string;
|
|
17
|
+
}) | {
|
|
18
|
+
supported: false;
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
10
21
|
export interface AboutTeam {
|
|
11
22
|
name: string;
|
|
12
23
|
specPath: string;
|
|
@@ -19,7 +30,9 @@ export declare function about(deps: CommandDeps, o: {
|
|
|
19
30
|
version: string;
|
|
20
31
|
consoleFile: ConsoleFile | null;
|
|
21
32
|
env?: NodeJS.ProcessEnv;
|
|
33
|
+
autostartSys?: AutostartSys;
|
|
22
34
|
}): Promise<{
|
|
23
35
|
items: AboutItem[];
|
|
24
36
|
teams: AboutTeam[];
|
|
37
|
+
autostart: AboutAutostart;
|
|
25
38
|
}>;
|
package/dist/commands/about.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import { WinglineError } from '../errors.js';
|
|
4
4
|
import { draftPathOf } from '../spec/draft.js';
|
|
5
5
|
import { dispatcherHealth } from './dispatcher/lifecycle.js';
|
|
6
|
+
import { autostartStatusOf } from './autostart/index.js';
|
|
7
|
+
import { realAutostartSys } from './autostart/sys.js';
|
|
6
8
|
import { inRange } from './version-range.js';
|
|
7
9
|
/** 目录下所有文件的字节数之和;目录不存在算 0。 */
|
|
8
10
|
function dirBytes(dir) {
|
|
@@ -57,6 +59,15 @@ function consoleItems(f) {
|
|
|
57
59
|
{ key: 'console.startedAt', value: f.startedAt, source: 'file' },
|
|
58
60
|
];
|
|
59
61
|
}
|
|
62
|
+
/** 开机自启状态:取不到(平台不支持 / 权限不足)时显式说原因,不抛错。 */
|
|
63
|
+
async function autostartItem(deps, sys) {
|
|
64
|
+
try {
|
|
65
|
+
return { supported: true, ...(await autostartStatusOf(deps, sys)) };
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return { supported: false, reason: err instanceof Error ? err.message : String(err) };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
60
71
|
/** 控制台“关于与诊断”页的数据:只读,取不到的项目显式标 null,不影响其余各项。 */
|
|
61
72
|
export async function about(deps, o) {
|
|
62
73
|
const env = o.env ?? deps.env;
|
|
@@ -80,5 +91,5 @@ export async function about(deps, o) {
|
|
|
80
91
|
draft: fs.existsSync(draftPathOf(t.specPath)),
|
|
81
92
|
dispatcher: dispatcherHealth(deps, t.name).state,
|
|
82
93
|
}));
|
|
83
|
-
return { items, teams };
|
|
94
|
+
return { items, teams, autostart: await autostartItem(deps, o.autostartSys ?? realAutostartSys()) };
|
|
84
95
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
/** 控制台服务在 core 内,需要同样的宿主信息:系统命令以参数数组启动(不经 shell),入口脚本取符号链接解析后的真实路径。 */
|
|
6
|
+
export function realAutostartSys() {
|
|
7
|
+
const exec = (file, args) => new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn(file, args, { shell: false });
|
|
9
|
+
let stdout = '';
|
|
10
|
+
let stderr = '';
|
|
11
|
+
child.stdout.on('data', (d) => { stdout += String(d); });
|
|
12
|
+
child.stderr.on('data', (d) => { stderr += String(d); });
|
|
13
|
+
child.on('error', reject);
|
|
14
|
+
child.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
15
|
+
});
|
|
16
|
+
const entry = process.argv[1] ?? '';
|
|
17
|
+
let script = path.resolve(entry);
|
|
18
|
+
try {
|
|
19
|
+
script = fs.realpathSync(entry);
|
|
20
|
+
}
|
|
21
|
+
catch { /* 保持 resolve 的结果 */ }
|
|
22
|
+
return { platform: process.platform, homeDir: os.homedir(), uid: process.getuid?.(), user: os.userInfo().username, node: process.execPath, script, exec, fs };
|
|
23
|
+
}
|
|
@@ -21,6 +21,10 @@ export declare function dispatcherLaunchCommand(a: {
|
|
|
21
21
|
export interface UpArgs {
|
|
22
22
|
team: string;
|
|
23
23
|
platform?: NodeJS.Platform;
|
|
24
|
+
/** 覆盖启动命令里的 node 可执行文件。插件 worker 的 process.execPath 是宿主(Orca)的二进制,必须传真实 node。 */
|
|
25
|
+
node?: string;
|
|
26
|
+
/** 覆盖启动命令里的 wingline 入口脚本。插件等宿主进程的 argv[1] 不是 wingline,必须显式传。 */
|
|
27
|
+
script?: string;
|
|
24
28
|
confirmMs?: number;
|
|
25
29
|
pollMs?: number;
|
|
26
30
|
sleep?: (ms: number) => Promise<void>;
|
|
@@ -62,7 +62,13 @@ export async function dispatcherUp(deps, a) {
|
|
|
62
62
|
}
|
|
63
63
|
const platform = a.platform ?? process.platform;
|
|
64
64
|
const command = dispatcherLaunchCommand({
|
|
65
|
-
platform,
|
|
65
|
+
platform,
|
|
66
|
+
node: a.node ?? process.execPath,
|
|
67
|
+
script: a.script ?? process.argv[1] ?? '',
|
|
68
|
+
team: a.team,
|
|
69
|
+
home: deps.home,
|
|
70
|
+
orca: deps.orcaPath,
|
|
71
|
+
nodeOnPath: platform === 'win32' ? nodeOnPath(deps) : undefined,
|
|
66
72
|
});
|
|
67
73
|
const lead = deps.registry.listPosts(a.team).find((p) => p.isLead);
|
|
68
74
|
const { handle } = await deps.orca.terminalCreate({ worktree: lead.worktree, title: address, command });
|
package/dist/commands/down.js
CHANGED
|
@@ -8,6 +8,7 @@ async function closePost(deps, post, specHash) {
|
|
|
8
8
|
const located = await locatePost(orca, address);
|
|
9
9
|
if (!located) {
|
|
10
10
|
registry.setPostStatus(address, 'closed');
|
|
11
|
+
registry.finalizePostRequests(post.team, address.split('@')[0]);
|
|
11
12
|
return { address, outcome: 'already_gone' };
|
|
12
13
|
}
|
|
13
14
|
const id = nextRequestId(registry, post.team, address.split('@')[0], 'close', specHash);
|
|
@@ -18,6 +19,8 @@ async function closePost(deps, post, specHash) {
|
|
|
18
19
|
});
|
|
19
20
|
registry.finishRequest(id, ptyKilled ? 'done' : 'uncertain', { ptyKilled });
|
|
20
21
|
registry.setPostStatus(address, ptyKilled ? 'closed' : 'uncertain');
|
|
22
|
+
if (ptyKilled)
|
|
23
|
+
registry.finalizePostRequests(post.team, address.split('@')[0]);
|
|
21
24
|
return ptyKilled ? { address, outcome: 'closed' } : { address, outcome: 'uncertain', reason: 'Orca 未确认进程已退出' };
|
|
22
25
|
}
|
|
23
26
|
catch (err) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type CommandDeps } from './deps.js';
|
|
2
|
+
import { type TeamTemplateId } from './templates.js';
|
|
3
|
+
export type InitTeamMode = 'template' | 'import';
|
|
4
|
+
export interface InitTeamArgs {
|
|
5
|
+
mode: InitTeamMode;
|
|
6
|
+
name?: string;
|
|
7
|
+
repo?: string;
|
|
8
|
+
template?: TeamTemplateId;
|
|
9
|
+
dir?: string;
|
|
10
|
+
specPath?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface InitTeamResult {
|
|
13
|
+
team: string;
|
|
14
|
+
specPath: string;
|
|
15
|
+
created: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function initTeamCmd(deps: CommandDeps, a: InitTeamArgs): Promise<InitTeamResult>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { WinglineError } from '../errors.js';
|
|
4
|
+
import { specHash } from '../spec/hash.js';
|
|
5
|
+
import { loadTeamSpec } from '../spec/load.js';
|
|
6
|
+
import { postAddress } from './deps.js';
|
|
7
|
+
import { TEAM_TEMPLATES, renderBrief, renderNorms, renderTeamYaml, } from './templates.js';
|
|
8
|
+
const TEAM_NAME_RE = /^[a-z0-9-]+$/;
|
|
9
|
+
function writeTemplate(targetDir, tpl, name, repo) {
|
|
10
|
+
const briefsDir = path.join(targetDir, 'briefs');
|
|
11
|
+
fs.mkdirSync(briefsDir, { recursive: true });
|
|
12
|
+
const normsPath = path.join(targetDir, 'NORMS.md');
|
|
13
|
+
if (!fs.existsSync(normsPath)) {
|
|
14
|
+
fs.writeFileSync(normsPath, renderNorms({ team: name, posts: tpl.posts.map((p) => p.id) }), 'utf8');
|
|
15
|
+
}
|
|
16
|
+
for (const p of tpl.posts) {
|
|
17
|
+
const briefPath = path.join(targetDir, p.brief);
|
|
18
|
+
fs.mkdirSync(path.dirname(briefPath), { recursive: true });
|
|
19
|
+
if (!fs.existsSync(briefPath)) {
|
|
20
|
+
fs.writeFileSync(briefPath, renderBrief(p.role, { team: name, post: p.id }), 'utf8');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const specPath = path.join(targetDir, 'team.yaml');
|
|
24
|
+
fs.writeFileSync(specPath, renderTeamYaml(tpl, { name, repo }), 'utf8');
|
|
25
|
+
return specPath;
|
|
26
|
+
}
|
|
27
|
+
function registerSpec(deps, spec) {
|
|
28
|
+
const hash = specHash(spec);
|
|
29
|
+
deps.registry.upsertTeam({
|
|
30
|
+
name: spec.name,
|
|
31
|
+
specPath: spec.specPath,
|
|
32
|
+
specHash: hash,
|
|
33
|
+
runId: null,
|
|
34
|
+
coordinatorHandle: null,
|
|
35
|
+
status: 'stopped',
|
|
36
|
+
});
|
|
37
|
+
for (const p of spec.posts) {
|
|
38
|
+
deps.registry.upsertPost({
|
|
39
|
+
address: postAddress(p.id, spec.name),
|
|
40
|
+
team: spec.name,
|
|
41
|
+
agent: p.agent,
|
|
42
|
+
worktree: p.worktree,
|
|
43
|
+
handle: null,
|
|
44
|
+
status: 'offline',
|
|
45
|
+
isLead: p.id === spec.lead,
|
|
46
|
+
sessionMarker: null,
|
|
47
|
+
sessionId: null,
|
|
48
|
+
sessionAt: null,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function initFromTemplate(deps, a) {
|
|
53
|
+
const name = a.name ?? '';
|
|
54
|
+
if (!TEAM_NAME_RE.test(name)) {
|
|
55
|
+
throw new WinglineError('input', 'team_name_invalid', '团队名只能用小写字母、数字与连字符');
|
|
56
|
+
}
|
|
57
|
+
const rawRepo = a.repo?.trim() ?? '';
|
|
58
|
+
if (!rawRepo || /[\x00-\x1f\x7f]/.test(a.repo ?? '')) {
|
|
59
|
+
throw new WinglineError('input', 'invalid_param', rawRepo ? '代码库目录不能包含控制字符' : '缺少代码库目录');
|
|
60
|
+
}
|
|
61
|
+
const tplId = a.template ?? 'duo';
|
|
62
|
+
const tpl = TEAM_TEMPLATES[tplId];
|
|
63
|
+
if (!tpl) {
|
|
64
|
+
throw new WinglineError('input', 'invalid_param', `未知团队模板:${String(tplId)}`);
|
|
65
|
+
}
|
|
66
|
+
const targetDir = a.dir ? path.resolve(a.dir) : path.join(deps.home, 'teams', name);
|
|
67
|
+
const specPath = path.join(targetDir, 'team.yaml');
|
|
68
|
+
const repo = rawRepo.startsWith('path:') ? rawRepo : `path:${rawRepo}`;
|
|
69
|
+
const existing = deps.registry.getTeam(name);
|
|
70
|
+
if (existing) {
|
|
71
|
+
if (existing.specPath === specPath && fs.existsSync(specPath)) {
|
|
72
|
+
return { team: name, specPath, created: false };
|
|
73
|
+
}
|
|
74
|
+
throw new WinglineError('input', 'team_already_exists', `团队 ${name} 已存在(${existing.specPath})`);
|
|
75
|
+
}
|
|
76
|
+
if (fs.existsSync(specPath)) {
|
|
77
|
+
throw new WinglineError('input', 'spec_already_exists', `配置文件已存在:${specPath},请改用“导入已有配置”`);
|
|
78
|
+
}
|
|
79
|
+
writeTemplate(targetDir, tpl, name, repo);
|
|
80
|
+
const spec = loadTeamSpec(specPath);
|
|
81
|
+
registerSpec(deps, spec);
|
|
82
|
+
return { team: name, specPath, created: true };
|
|
83
|
+
}
|
|
84
|
+
function initFromImport(deps, a) {
|
|
85
|
+
const rawPath = a.specPath?.trim() ?? '';
|
|
86
|
+
if (!rawPath) {
|
|
87
|
+
throw new WinglineError('input', 'invalid_param', '缺少配置文件路径');
|
|
88
|
+
}
|
|
89
|
+
const spec = loadTeamSpec(rawPath);
|
|
90
|
+
const existing = deps.registry.getTeam(spec.name);
|
|
91
|
+
if (existing) {
|
|
92
|
+
if (existing.specPath === spec.specPath) {
|
|
93
|
+
return { team: spec.name, specPath: spec.specPath, created: false };
|
|
94
|
+
}
|
|
95
|
+
throw new WinglineError('input', 'team_already_exists', `同名团队 ${spec.name} 已登记在另一路径:${existing.specPath}`);
|
|
96
|
+
}
|
|
97
|
+
registerSpec(deps, spec);
|
|
98
|
+
return { team: spec.name, specPath: path.resolve(rawPath), created: true };
|
|
99
|
+
}
|
|
100
|
+
export async function initTeamCmd(deps, a) {
|
|
101
|
+
if (a.mode === 'template')
|
|
102
|
+
return initFromTemplate(deps, a);
|
|
103
|
+
if (a.mode === 'import')
|
|
104
|
+
return initFromImport(deps, a);
|
|
105
|
+
throw new WinglineError('input', 'invalid_param', `未知初始化模式:${String(a.mode)}`);
|
|
106
|
+
}
|
|
@@ -13,7 +13,16 @@ export declare function recorded<T>(reg: Registry, id: string, fn: () => Promise
|
|
|
13
13
|
value: T;
|
|
14
14
|
}>;
|
|
15
15
|
export declare const stepDone: (reg: Registry, id: string) => boolean;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* 终端被关后重新拉起要换新 id,否则 Orca 会按旧 id 回放旧回执。
|
|
18
|
+
*
|
|
19
|
+
* 两段推进:
|
|
20
|
+
* ① brief 段(原语义,located 时也生效):create+brief 都 done 才推进——
|
|
21
|
+
* at_prompt 拒发后 brief 行缺失,重跑必须复用同一代把 brief 补发。
|
|
22
|
+
* ② 重启段(仅 !located,终端已消失):create+wait 都 done 的代整代跳过——
|
|
23
|
+
* 没有 brief 行的岗位(wait 未确认就中断,brief 从未发生)原来会永远卡在
|
|
24
|
+
* 「create 已 done 的代」上,beginRequest 撞 request_already_done。
|
|
25
|
+
*/
|
|
17
26
|
export declare function lifetimeGen(l: Launch, post: string, located: boolean): number;
|
|
18
27
|
/** 会话标记 `WLS-` + 8 位小写十六进制;restore 靠它引号精确搜索找回岗位会话(spec §4)。 */
|
|
19
28
|
export declare function newSessionMarker(rand?: () => string): string;
|
package/dist/commands/launch.js
CHANGED
|
@@ -21,12 +21,24 @@ export async function recorded(reg, id, fn, ok = () => true) {
|
|
|
21
21
|
return { confirmed, value };
|
|
22
22
|
}
|
|
23
23
|
export const stepDone = (reg, id) => reg.getRequest(id)?.state === 'done';
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* 终端被关后重新拉起要换新 id,否则 Orca 会按旧 id 回放旧回执。
|
|
26
|
+
*
|
|
27
|
+
* 两段推进:
|
|
28
|
+
* ① brief 段(原语义,located 时也生效):create+brief 都 done 才推进——
|
|
29
|
+
* at_prompt 拒发后 brief 行缺失,重跑必须复用同一代把 brief 补发。
|
|
30
|
+
* ② 重启段(仅 !located,终端已消失):create+wait 都 done 的代整代跳过——
|
|
31
|
+
* 没有 brief 行的岗位(wait 未确认就中断,brief 从未发生)原来会永远卡在
|
|
32
|
+
* 「create 已 done 的代」上,beginRequest 撞 request_already_done。
|
|
33
|
+
*/
|
|
25
34
|
export function lifetimeGen(l, post, located) {
|
|
26
35
|
const done = (step, g) => stepDone(l.deps.registry, requestId(l.spec.name, post, step, l.hash, g));
|
|
27
36
|
let g = 0;
|
|
28
37
|
while (done('create', g) && done('brief', g))
|
|
29
38
|
g++;
|
|
39
|
+
if (!located)
|
|
40
|
+
while (done('create', g) && done('wait', g))
|
|
41
|
+
g++;
|
|
30
42
|
if (!located && done('create', g))
|
|
31
43
|
g++;
|
|
32
44
|
return g;
|
package/dist/commands/spec.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SpecEditOp } from '../spec/edit.js';
|
|
1
2
|
import { type SpecError } from '../spec/load.js';
|
|
2
3
|
import { type SpecPlan } from '../spec/plan-change.js';
|
|
3
4
|
import type { CommandDeps } from './deps.js';
|
|
@@ -12,8 +13,9 @@ export interface SpecForEdit {
|
|
|
12
13
|
hash: string | null;
|
|
13
14
|
} | null;
|
|
14
15
|
running: boolean;
|
|
16
|
+
files: Record<string, string>;
|
|
15
17
|
}
|
|
16
|
-
/**
|
|
18
|
+
/** 编辑器加载:正式规格与草稿(如果有)各给出文本、解析结果与内容哈希,附上引用文档的正文。 */
|
|
17
19
|
export declare function readSpecForEdit(deps: CommandDeps, team: string): SpecForEdit;
|
|
18
20
|
export interface SpecEditResult {
|
|
19
21
|
yaml: string;
|
|
@@ -21,20 +23,19 @@ export interface SpecEditResult {
|
|
|
21
23
|
errors: SpecError[];
|
|
22
24
|
preview: SpecPlan | null;
|
|
23
25
|
}
|
|
24
|
-
/** 校验一段 YAML
|
|
26
|
+
/** 校验一段 YAML(可先套用编辑页的一处修改)并预演;不写任何文件。`files` 是还没落盘的文档正文。 */
|
|
25
27
|
export declare function validateSpecEdit(deps: CommandDeps, a: {
|
|
26
28
|
team: string;
|
|
27
29
|
yaml: string;
|
|
28
|
-
set?:
|
|
29
|
-
|
|
30
|
-
value: unknown;
|
|
31
|
-
};
|
|
30
|
+
set?: SpecEditOp;
|
|
31
|
+
files?: Record<string, string>;
|
|
32
32
|
}): SpecEditResult;
|
|
33
33
|
export interface SaveSpecArgs {
|
|
34
34
|
team: string;
|
|
35
35
|
yaml: string;
|
|
36
36
|
baseHash: string | null;
|
|
37
37
|
running: boolean;
|
|
38
|
+
files?: Record<string, string>;
|
|
38
39
|
retryRequest?: string;
|
|
39
40
|
}
|
|
40
41
|
/** 保存规格;两次保存相同内容天然幂等,所以 retryRequest 只是被路由校验格式,这里不用。 */
|
|
@@ -42,6 +43,7 @@ export declare function saveSpecCmd(deps: CommandDeps, a: SaveSpecArgs): Promise
|
|
|
42
43
|
wrote: 'draft' | 'spec';
|
|
43
44
|
path: string;
|
|
44
45
|
hash: string;
|
|
46
|
+
docsUpdated: string[];
|
|
45
47
|
}>;
|
|
46
48
|
export declare function discardDraftCmd(deps: CommandDeps, a: {
|
|
47
49
|
team: string;
|
package/dist/commands/spec.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { WinglineError } from '../errors.js';
|
|
3
|
-
import { discardDraft, draftPathOf, fileHash,
|
|
4
|
-
import {
|
|
4
|
+
import { assertSpecSize, checkSaveConflict, discardDraft, draftPathOf, fileHash, saveSpec } from '../spec/draft.js';
|
|
5
|
+
import { DOC_LIMIT_BYTES, docRelsOf, readDocText, resolveDocPath, writeDocAtomic } from '../spec/doc.js';
|
|
6
|
+
import { applySpecOp } from '../spec/edit.js';
|
|
5
7
|
import { loadTeamSpec, validateSpecText } from '../spec/load.js';
|
|
6
8
|
import { planSpecChange } from '../spec/plan-change.js';
|
|
7
9
|
function teamOf(deps, team) {
|
|
@@ -18,7 +20,24 @@ const readText = (file) => {
|
|
|
18
20
|
throw new WinglineError('input', 'spec_not_found', `读不到团队规格 ${file}`, { cause });
|
|
19
21
|
}
|
|
20
22
|
};
|
|
21
|
-
/**
|
|
23
|
+
/** 把规格(与草稿)引用的文档正文读出来;解析不了的相对路径跳过,不让整个编辑页打不开。 */
|
|
24
|
+
function readDocs(dir, parseds) {
|
|
25
|
+
const files = {};
|
|
26
|
+
for (const parsed of parseds) {
|
|
27
|
+
for (const rel of docRelsOf(parsed)) {
|
|
28
|
+
if (rel in files)
|
|
29
|
+
continue;
|
|
30
|
+
try {
|
|
31
|
+
files[rel] = readDocText(resolveDocPath(dir, rel));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// resolveDocPath 拒绝的路径(越界、非 .md):编辑页照样打开,只是不带这份正文
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
/** 编辑器加载:正式规格与草稿(如果有)各给出文本、解析结果与内容哈希,附上引用文档的正文。 */
|
|
22
41
|
export function readSpecForEdit(deps, team) {
|
|
23
42
|
const row = teamOf(deps, team);
|
|
24
43
|
const view = (file) => {
|
|
@@ -26,11 +45,14 @@ export function readSpecForEdit(deps, team) {
|
|
|
26
45
|
return { yaml, parsed: validateSpecText(yaml, row.specPath).parsed, hash: fileHash(file) };
|
|
27
46
|
};
|
|
28
47
|
const draftFile = draftPathOf(row.specPath);
|
|
48
|
+
const spec = view(row.specPath);
|
|
49
|
+
const draft = fs.existsSync(draftFile) ? view(draftFile) : null;
|
|
29
50
|
return {
|
|
30
51
|
specPath: row.specPath,
|
|
31
|
-
...
|
|
32
|
-
draft
|
|
52
|
+
...spec,
|
|
53
|
+
draft,
|
|
33
54
|
running: row.status === 'up',
|
|
55
|
+
files: readDocs(path.dirname(row.specPath), [spec.parsed, draft?.parsed ?? null]),
|
|
34
56
|
};
|
|
35
57
|
}
|
|
36
58
|
function currentSpec(specPath) {
|
|
@@ -43,23 +65,58 @@ function currentSpec(specPath) {
|
|
|
43
65
|
throw err;
|
|
44
66
|
}
|
|
45
67
|
}
|
|
46
|
-
/** 校验一段 YAML
|
|
68
|
+
/** 校验一段 YAML(可先套用编辑页的一处修改)并预演;不写任何文件。`files` 是还没落盘的文档正文。 */
|
|
47
69
|
export function validateSpecEdit(deps, a) {
|
|
48
70
|
const row = teamOf(deps, a.team);
|
|
49
71
|
assertSpecSize(a.yaml);
|
|
50
|
-
|
|
72
|
+
// applySpecOp 已把缺省 op 转给 setField,这里不再分叉
|
|
73
|
+
const yaml = a.set ? applySpecOp(a.yaml, a.set) : a.yaml;
|
|
51
74
|
assertSpecSize(yaml);
|
|
52
|
-
const { spec, parsed, errors } = validateSpecText(yaml, row.specPath);
|
|
75
|
+
const { spec, parsed, errors } = validateSpecText(yaml, row.specPath, { virtualFiles: new Set(Object.keys(a.files ?? {})) });
|
|
53
76
|
const preview = spec ? planSpecChange(currentSpec(row.specPath), spec, { running: row.status === 'up', specPath: row.specPath }) : null;
|
|
54
77
|
return { yaml, parsed, errors, preview };
|
|
55
78
|
}
|
|
79
|
+
const docFiles = (files) => {
|
|
80
|
+
if (files === undefined)
|
|
81
|
+
return {};
|
|
82
|
+
if (!files || typeof files !== 'object' || Array.isArray(files) || !Object.values(files).every((v) => typeof v === 'string')) {
|
|
83
|
+
throw new WinglineError('input', 'invalid_param', '参数 files 不合法');
|
|
84
|
+
}
|
|
85
|
+
return files;
|
|
86
|
+
};
|
|
87
|
+
/** 先校验规格、再查冲突(冲突时一个字节都不写)、最后把文档写到磁盘;team.yaml 由随后的 saveSpec 写。 */
|
|
88
|
+
function writeDocs(specPath, yaml, files, a) {
|
|
89
|
+
const { parsed, errors } = validateSpecText(yaml, specPath, { virtualFiles: new Set(Object.keys(files)) });
|
|
90
|
+
if (errors.length > 0)
|
|
91
|
+
throw new WinglineError('input', 'spec_invalid', errors[0].message, { detail: { errors } });
|
|
92
|
+
checkSaveConflict({ specPath, text: yaml, baseHash: a.baseHash, running: a.running });
|
|
93
|
+
const referenced = new Set(docRelsOf(parsed));
|
|
94
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
95
|
+
if (!referenced.has(rel))
|
|
96
|
+
throw new WinglineError('input', 'doc_not_referenced', `文档 ${rel} 没有被团队规格引用`);
|
|
97
|
+
if (Buffer.byteLength(content, 'utf8') > DOC_LIMIT_BYTES) {
|
|
98
|
+
throw new WinglineError('input', 'doc_too_large', `文档 ${path.basename(rel)} 超过 ${DOC_LIMIT_BYTES / 1024} KB 上限`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const dir = path.dirname(specPath);
|
|
102
|
+
const docsUpdated = [];
|
|
103
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
104
|
+
writeDocAtomic(resolveDocPath(dir, rel), content);
|
|
105
|
+
docsUpdated.push(rel);
|
|
106
|
+
}
|
|
107
|
+
return docsUpdated;
|
|
108
|
+
}
|
|
56
109
|
/** 保存规格;两次保存相同内容天然幂等,所以 retryRequest 只是被路由校验格式,这里不用。 */
|
|
57
110
|
export async function saveSpecCmd(deps, a) {
|
|
58
111
|
const row = teamOf(deps, a.team);
|
|
59
112
|
if (a.running !== (row.status === 'up')) {
|
|
60
113
|
throw new WinglineError('input', 'spec_conflict', '团队状态已变化,请重新加载');
|
|
61
114
|
}
|
|
62
|
-
|
|
115
|
+
assertSpecSize(a.yaml);
|
|
116
|
+
const files = docFiles(a.files);
|
|
117
|
+
const docsUpdated = writeDocs(row.specPath, a.yaml, files, { baseHash: a.baseHash, running: a.running });
|
|
118
|
+
const res = saveSpec({ specPath: row.specPath, text: a.yaml, baseHash: a.baseHash, running: a.running });
|
|
119
|
+
return { ...res, docsUpdated };
|
|
63
120
|
}
|
|
64
121
|
export async function discardDraftCmd(deps, a) {
|
|
65
122
|
return discardDraft(teamOf(deps, a.team).specPath);
|
|
@@ -5,7 +5,7 @@ import { preflightTask } from './preflight.js';
|
|
|
5
5
|
import { findTask } from './query.js';
|
|
6
6
|
export async function planTask(deps, a) {
|
|
7
7
|
const { team } = await preflightTask(deps, a.id);
|
|
8
|
-
const actor = await resolveActor(deps);
|
|
8
|
+
const actor = await resolveActor(deps, a.as);
|
|
9
9
|
await deps.registry.withTeamLock(team.name, async () => {
|
|
10
10
|
const task = await findTask(deps, team.name, a.id);
|
|
11
11
|
if (task.status === 'completed' || task.status === 'failed') {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type TeamTemplateId = 'duo' | 'solo' | 'trio';
|
|
2
|
+
export type RoleKind = 'builder' | 'checker' | 'planner';
|
|
3
|
+
export interface TeamTemplatePost {
|
|
4
|
+
id: string;
|
|
5
|
+
role: RoleKind;
|
|
6
|
+
agent: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
launch?: string;
|
|
9
|
+
brief: string;
|
|
10
|
+
}
|
|
11
|
+
export interface TeamTemplate {
|
|
12
|
+
id: TeamTemplateId;
|
|
13
|
+
label: string;
|
|
14
|
+
summary: string;
|
|
15
|
+
lead: string;
|
|
16
|
+
posts: TeamTemplatePost[];
|
|
17
|
+
links: Record<string, Record<string, string[]>>;
|
|
18
|
+
}
|
|
19
|
+
export declare const TEAM_TEMPLATES: Record<TeamTemplateId, TeamTemplate>;
|
|
20
|
+
export declare const ROLE_BRIEFS: Record<RoleKind, (team: string, post: string) => string>;
|
|
21
|
+
export declare function renderTeamYaml(t: TeamTemplate, a: {
|
|
22
|
+
name: string;
|
|
23
|
+
repo: string;
|
|
24
|
+
}): string;
|
|
25
|
+
export declare function renderNorms(a: {
|
|
26
|
+
team: string;
|
|
27
|
+
posts: string[];
|
|
28
|
+
}): string;
|
|
29
|
+
export declare function renderBrief(role: RoleKind, a: {
|
|
30
|
+
team: string;
|
|
31
|
+
post: string;
|
|
32
|
+
}): string;
|