@weiping/wingline-core 0.1.0 → 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.
Files changed (63) hide show
  1. package/dist/audit-table.d.ts +11 -0
  2. package/dist/audit-table.js +85 -0
  3. package/dist/audit.js +1 -1
  4. package/dist/commands/about.d.ts +13 -0
  5. package/dist/commands/about.js +12 -1
  6. package/dist/commands/autostart/sys.d.ts +3 -0
  7. package/dist/commands/autostart/sys.js +23 -0
  8. package/dist/commands/deps.d.ts +2 -0
  9. package/dist/commands/deps.js +2 -0
  10. package/dist/commands/dispatcher/attention.d.ts +2 -0
  11. package/dist/commands/dispatcher/attention.js +10 -9
  12. package/dist/commands/dispatcher/controls.d.ts +1 -1
  13. package/dist/commands/dispatcher/controls.js +5 -3
  14. package/dist/commands/dispatcher/lifecycle.d.ts +4 -0
  15. package/dist/commands/dispatcher/lifecycle.js +7 -1
  16. package/dist/commands/dispatcher/loop.js +2 -0
  17. package/dist/commands/down.js +3 -0
  18. package/dist/commands/init.d.ts +17 -0
  19. package/dist/commands/init.js +106 -0
  20. package/dist/commands/launch.d.ts +10 -2
  21. package/dist/commands/launch.js +14 -3
  22. package/dist/commands/restore/index.js +6 -0
  23. package/dist/commands/restore/posts.d.ts +1 -1
  24. package/dist/commands/restore/posts.js +17 -17
  25. package/dist/commands/restore/resume-command.d.ts +0 -1
  26. package/dist/commands/restore/resume-command.js +0 -3
  27. package/dist/commands/send.js +1 -2
  28. package/dist/commands/spec.d.ts +8 -6
  29. package/dist/commands/spec.js +66 -9
  30. package/dist/commands/task/gate-exec.d.ts +5 -1
  31. package/dist/commands/task/gate-exec.js +29 -5
  32. package/dist/commands/task/notify.d.ts +1 -1
  33. package/dist/commands/task/notify.js +1 -1
  34. package/dist/commands/task/plan.d.ts +1 -0
  35. package/dist/commands/task/plan.js +1 -1
  36. package/dist/commands/task/query.d.ts +2 -0
  37. package/dist/commands/task/query.js +11 -2
  38. package/dist/commands/templates.d.ts +32 -0
  39. package/dist/commands/templates.js +120 -0
  40. package/dist/index.d.ts +7 -0
  41. package/dist/index.js +7 -0
  42. package/dist/orca/client.js +27 -9
  43. package/dist/orca/types.d.ts +6 -0
  44. package/dist/registry/registry.d.ts +20 -0
  45. package/dist/registry/registry.js +35 -2
  46. package/dist/server/api-read.js +55 -37
  47. package/dist/server/api-spec.js +43 -7
  48. package/dist/server/api-write.d.ts +86 -2
  49. package/dist/server/api-write.js +285 -12
  50. package/dist/server/index.js +14 -6
  51. package/dist/server/sse.js +22 -10
  52. package/dist/spec/doc.d.ts +13 -0
  53. package/dist/spec/doc.js +62 -0
  54. package/dist/spec/draft.d.ts +16 -0
  55. package/dist/spec/draft.js +15 -8
  56. package/dist/spec/edit.d.ts +21 -0
  57. package/dist/spec/edit.js +136 -0
  58. package/dist/spec/facade.d.ts +41 -0
  59. package/dist/spec/facade.js +68 -0
  60. package/dist/spec/load.d.ts +1 -0
  61. package/dist/spec/load.js +7 -6
  62. package/dist/spec/plan-change.js +4 -2
  63. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { checkBriefingLimit } from '../../brief/render.js';
3
3
  import { WinglineError } from '../../errors.js';
4
- import { postAddress, requestId } from '../deps.js';
4
+ import { postAddress, requestId, WAIT_SUBMIT_SEC } from '../deps.js';
5
5
  import { briefText } from '../up.js';
6
- import { ensureMarker, lifetimeGen, recorded, startPost, stepDone, WAIT_SUBMIT_SEC, WAIT_TIMEOUT_MS, withMarker, } from '../launch.js';
6
+ import { ensureMarker, lifetimeGen, recorded, startPost, stepDone, WAIT_TIMEOUT_MS, withMarker, } from '../launch.js';
7
7
  import { locatePost } from '../locate.js';
8
8
  import { screenGuard } from '../task/notify.js';
9
9
  import { findSession, markerHighlighted } from './find-session.js';
@@ -11,7 +11,7 @@ import { resumeCommand, SESSION_ID } from './resume-command.js';
11
11
  /** restore 的请求键独立命名空间(复审 I-1):与 wl up 的键不冲突,代数各自计数。 */
12
12
  export const restoreKey = (team, post, gen, step) => `restore:${team}:${post}:${gen}:${step}`;
13
13
  /** 已完成的 restore create 中最大代数加 1;本代 create 尚未完成(中断重跑沿用原代)时不前进。终端仍活着(located)时调用方走 kept 或补完上一代(pendingResume),不会拉起新代。 */
14
- export function restoreGen(reg, team, post, _located) {
14
+ export function restoreGen(reg, team, post) {
15
15
  let g = 0;
16
16
  while (reg.getRequest(restoreKey(team, post, g, 'create'))?.state === 'done')
17
17
  g++;
@@ -81,7 +81,7 @@ async function landingResult(deps, address, sessionId, token, remote, retry) {
81
81
  * 返回要补完的会话 ID 与请求键;否则 null。会话 ID 没缓存、岗位已 live、句柄对不上都不算。
82
82
  */
83
83
  function pendingResume(reg, team, post, handle) {
84
- const gen = restoreGen(reg, team, post, true) - 1;
84
+ const gen = restoreGen(reg, team, post) - 1;
85
85
  if (gen < 0)
86
86
  return null;
87
87
  const rid = (step) => restoreKey(team, post, gen, step);
@@ -131,18 +131,20 @@ export async function restorePost(l, post, o) {
131
131
  const retry = `wl restore ${l.spec.name}`;
132
132
  try {
133
133
  const located = await locatePost(orca, address);
134
- if (located && located.connected) {
135
- // 上次接回没走完(终端是 restore 拉起的、空闲等待或接回提示未定案):补完它,而不是报 kept
134
+ if (located) {
135
+ // 上次接回没走完(终端是 restore 拉起或复用的、空闲等待或接回提示未定案):补完它,而不是报 kept 或再开新代
136
136
  const pending = pendingResume(registry, l.spec.name, post.id, located.handle);
137
137
  if (pending && o.dryRun)
138
138
  return { address, outcome: 'resumed', sessionId: pending.sessionId };
139
139
  if (pending)
140
140
  return await settleResume(l, address, located.handle, pending.sessionId, pending.rid, retry, remote);
141
- if (!o.dryRun) {
142
- registry.setPostHandle(address, located.handle);
143
- registry.setPostStatus(address, 'live');
141
+ if (located.connected) {
142
+ if (!o.dryRun) {
143
+ registry.setPostHandle(address, located.handle);
144
+ registry.setPostStatus(address, 'live');
145
+ }
146
+ return { address, outcome: 'kept' };
144
147
  }
145
- return { address, outcome: 'kept' };
146
148
  }
147
149
  const row = registry.getPost(address);
148
150
  // path: 固定值直接用;选择器(阶段 3 之前的行)现解析一次
@@ -175,14 +177,12 @@ export async function restorePost(l, post, o) {
175
177
  if (sessionId !== null && command !== null) {
176
178
  if (o.dryRun)
177
179
  return { address, outcome: 'resumed', sessionId };
178
- const gen = restoreGen(registry, l.spec.name, post.id, located !== null);
180
+ const gen = restoreGen(registry, l.spec.name, post.id);
179
181
  const rid = (step) => restoreKey(l.spec.name, post.id, gen, step);
180
- let handle = stepDone(registry, rid('create')) ? row.handle : null;
181
- if (!handle) {
182
- const created = await recorded(registry, rid('create'), () => orca.terminalCreate({ worktree: post.worktree, title: address, command }));
183
- handle = created.value.handle;
184
- registry.setPostHandle(address, handle);
185
- }
182
+ // 终端还在(哪怕暂时断连)就复用它并同样记入 create 账本,避免把同一会话接进两个终端或下一代重用旧代数
183
+ const created = await recorded(registry, rid('create'), async () => located ? { handle: located.handle } : orca.terminalCreate({ worktree: post.worktree, title: address, command }));
184
+ const handle = created.value.handle;
185
+ registry.setPostHandle(address, handle);
186
186
  registry.setSessionId(address, sessionId);
187
187
  return await settleResume(l, address, handle, sessionId, rid, retry, remote);
188
188
  }
@@ -1,5 +1,4 @@
1
1
  /** 会话 ID 形如 Orca 分配的 UUID / 内部 ID:字母、数字、连字符之外的字符一律拒绝。 */
2
2
  export declare const SESSION_ID: RegExp;
3
- export declare function supportsResume(agent: string): boolean;
4
3
  /** 拼出该 agent 的接回命令;不支持接回的 agent 返回 null(调用方走 fresh)。 */
5
4
  export declare function resumeCommand(agent: string, launch: string, sessionId: string): string | null;
@@ -5,9 +5,6 @@ export const SESSION_ID = /^[A-Za-z0-9-]+$/;
5
5
  const RESUME_TEMPLATES = {
6
6
  claude: (launch, sessionId) => `${launch} --resume ${sessionId}`,
7
7
  };
8
- export function supportsResume(agent) {
9
- return agent in RESUME_TEMPLATES;
10
- }
11
8
  /** 拼出该 agent 的接回命令;不支持接回的 agent 返回 null(调用方走 fresh)。 */
12
9
  export function resumeCommand(agent, launch, sessionId) {
13
10
  if (!SESSION_ID.test(sessionId)) {
@@ -1,9 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { WinglineError } from '../errors.js';
3
- import { nextRequestId, preflight } from './deps.js';
3
+ import { nextRequestId, preflight, WAIT_SUBMIT_SEC } from './deps.js';
4
4
  import { locatePost } from './locate.js';
5
5
  const MAX_MESSAGE = 4000;
6
- const WAIT_SUBMIT_SEC = 10;
7
6
  function checkMessage(deps, message) {
8
7
  if (message.length > MAX_MESSAGE) {
9
8
  throw new WinglineError('input', 'message_too_long', `消息 ${message.length} 字符,超过上限 ${MAX_MESSAGE}`);
@@ -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
- path: Array<string | number>;
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;
@@ -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, assertSpecSize, saveSpec } from '../spec/draft.js';
4
- import { setField } from '../spec/edit.js';
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
- ...view(row.specPath),
32
- draft: fs.existsSync(draftFile) ? view(draftFile) : null,
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
- const yaml = a.set ? setField(a.yaml, a.set.path, a.set.value) : a.yaml;
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
- return saveSpec({ specPath: row.specPath, text: a.yaml, baseHash: a.baseHash, running: a.running });
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);
@@ -1,12 +1,16 @@
1
1
  import type { CommandDeps } from '../deps.js';
2
2
  export interface GateExecResult {
3
3
  passed: boolean;
4
- /** 完整输出(不截断)。 */
4
+ /** 输出正文;超过 GATE_OUTPUT_LIMIT_BYTES 时只留尾部,并在末尾标明被截断。 */
5
5
  output: string;
6
6
  timedOut: boolean;
7
7
  /** 被 Ctrl-C / SIGTERM 中断(不计为 gate 失败)。 */
8
8
  interrupted?: boolean;
9
+ /** 输出超过上限,gate 被终止并计为失败。 */
10
+ overflowed?: boolean;
9
11
  }
12
+ /** 输出只保留尾部这么多字节:`yes`、`tail -f` 这类跑飞的 gate 不能把内存吃光。 */
13
+ export declare const GATE_OUTPUT_LIMIT_BYTES: number;
10
14
  export declare function runGate(command: string | null, opts: {
11
15
  cwd: string;
12
16
  timeoutMs: number;
@@ -2,9 +2,12 @@ import { spawn } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { WinglineError } from '../../errors.js';
5
+ /** 输出只保留尾部这么多字节:`yes`、`tail -f` 这类跑飞的 gate 不能把内存吃光。 */
6
+ export const GATE_OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;
5
7
  const TIMEOUT_MARK = '\n(gate 超时,已终止)';
8
+ const OVERFLOW_MARK = `\n(gate 输出超过 ${GATE_OUTPUT_LIMIT_BYTES / 1024 / 1024} MB,已终止,只保留尾部)`;
6
9
  const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
7
- /** 超时要杀整棵进程树:POSIX 杀进程组,Windows 用 taskkill /T。 */
10
+ /** 超时、输出超限都要杀整棵进程树:POSIX 杀进程组,Windows 用 taskkill /T。 */
8
11
  function killTree(pid) {
9
12
  try {
10
13
  if (process.platform === 'win32') {
@@ -26,8 +29,10 @@ function spawnGate(command, opts) {
26
29
  // gate 是用户在 team.yaml 里写的完整 shell 命令行,必须经 shell 解释(2A spec 第 3 节约束 6,总纲 §2.4);全项目只此一处。
27
30
  const child = spawn(command, { shell: true, cwd: opts.cwd, detached: process.platform !== 'win32', windowsHide: true });
28
31
  const chunks = [];
32
+ let kept = 0;
29
33
  let timedOut = false;
30
34
  let interrupted = false;
35
+ let overflowed = false;
31
36
  // 运行期间 Ctrl-C / SIGTERM:把 gate 的整个进程组一起杀掉,不留孤儿
32
37
  const onSignal = () => {
33
38
  interrupted = true;
@@ -42,16 +47,35 @@ function spawnGate(command, opts) {
42
47
  SIGNALS.forEach((s) => process.off(s, onSignal));
43
48
  };
44
49
  SIGNALS.forEach((s) => process.on(s, onSignal));
45
- child.stdout.on('data', (d) => chunks.push(d));
46
- child.stderr.on('data', (d) => chunks.push(d));
50
+ /** 只留尾部:超过上限就丢最前面的块,并终止 gate(整份输出既没必要也存不住)。 */
51
+ const collect = (d) => {
52
+ chunks.push(d);
53
+ kept += d.length;
54
+ if (kept <= GATE_OUTPUT_LIMIT_BYTES)
55
+ return;
56
+ if (!overflowed) {
57
+ overflowed = true;
58
+ killTree(child.pid);
59
+ }
60
+ while (chunks.length > 1 && kept > GATE_OUTPUT_LIMIT_BYTES)
61
+ kept -= chunks.shift().length;
62
+ };
63
+ child.stdout.on('data', collect);
64
+ child.stderr.on('data', collect);
47
65
  child.on('error', (err) => {
48
66
  cleanup();
49
67
  resolve({ passed: false, output: `无法启动 gate:${err.message}`, timedOut: false });
50
68
  });
51
69
  child.on('close', (code) => {
52
70
  cleanup();
53
- const output = Buffer.concat(chunks).toString('utf8') + (timedOut ? TIMEOUT_MARK : '');
54
- resolve({ passed: code === 0 && !timedOut && !interrupted, output, timedOut, ...(interrupted ? { interrupted } : {}) });
71
+ const output = Buffer.concat(chunks).toString('utf8') + (timedOut ? TIMEOUT_MARK : '') + (overflowed ? OVERFLOW_MARK : '');
72
+ resolve({
73
+ passed: code === 0 && !timedOut && !interrupted && !overflowed,
74
+ output,
75
+ timedOut,
76
+ ...(interrupted ? { interrupted } : {}),
77
+ ...(overflowed ? { overflowed } : {}),
78
+ });
55
79
  });
56
80
  });
57
81
  }
@@ -1,4 +1,4 @@
1
- import type { CommandDeps } from '../deps.js';
1
+ import { type CommandDeps } from '../deps.js';
2
2
  export type ScreenState = 'clear' | 'at_prompt' | 'shell' | 'unknown';
3
3
  /** 逐条对应 spec 第 7.4 节:agent 或包装器正停在需要人确认的画面上。 */
4
4
  export declare const PROMPT_PATTERNS: readonly RegExp[];
@@ -1,4 +1,5 @@
1
1
  import { WinglineError } from '../../errors.js';
2
+ import { WAIT_SUBMIT_SEC } from '../deps.js';
2
3
  import { locatePost } from '../locate.js';
3
4
  /** 逐条对应 spec 第 7.4 节:agent 或包装器正停在需要人确认的画面上。 */
4
5
  export const PROMPT_PATTERNS = [
@@ -19,7 +20,6 @@ export const PROMPT_PATTERNS = [
19
20
  /** 终端已退回 shell:在那里打字会执行命令。只看最后一个非空行。 */
20
21
  const SHELL_PROMPT = /(?:(?:[$#]|(?<!\d)%)\s*$)|(?:^PS .*>\s*$)|(?:^[A-Za-z]:\\.*>\s*$)/;
21
22
  export const SCREEN_TAIL_LINES = 8;
22
- const WAIT_SUBMIT_SEC = 10;
23
23
  /** 纯函数:取 preview 最后 8 行,任一行命中 PROMPT_PATTERNS → at_prompt;否则最后一个非空行像 shell 提示符 → shell;空 → unknown;其余 clear。 */
24
24
  export function classifyPreview(preview) {
25
25
  const lines = preview.split('\n');
@@ -2,4 +2,5 @@ import type { CommandDeps } from '../deps.js';
2
2
  export declare function planTask(deps: CommandDeps, a: {
3
3
  id: string;
4
4
  text: string;
5
+ as?: string;
5
6
  }): Promise<void>;
@@ -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') {
@@ -56,6 +56,8 @@ export declare function showTask(deps: CommandDeps, a: {
56
56
  history?: boolean;
57
57
  }): Promise<ShownTask>;
58
58
  export type StaleReason = 'done_unfinished' | 'call_uncertain' | 'dead_assignee' | 'owner_idle' | 'bypassed' | 'blocked_waiting';
59
+ /** 卡住原因的短标签:TUI 与文档共用(控制台前端只 import type,标签在 web/src/labels.ts 另放一份)。 */
60
+ export declare const STALE_REASON_LABEL: Record<StaleReason, string>;
59
61
  export interface StaleItem {
60
62
  taskId: string;
61
63
  title: string;
@@ -116,6 +116,15 @@ export async function showTask(deps, a) {
116
116
  ...(a.history ? { history: deps.registry.listTaskLog(a.id) } : {}),
117
117
  };
118
118
  }
119
+ /** 卡住原因的短标签:TUI 与文档共用(控制台前端只 import type,标签在 web/src/labels.ts 另放一份)。 */
120
+ export const STALE_REASON_LABEL = {
121
+ done_unfinished: '已完成但未收尾',
122
+ call_uncertain: '调用结果不确定',
123
+ dead_assignee: '派发失主',
124
+ owner_idle: '主人迟迟未开始',
125
+ bypassed: '绕过闸门上报',
126
+ blocked_waiting: '等待决策',
127
+ };
119
128
  export const DEFAULT_STALE = { ownerIdleMinutes: 30, uncertainMinutes: 1 };
120
129
  const MINUTE_MS = 60_000;
121
130
  const as = (owner) => owner ?? '<主人>';
@@ -130,7 +139,7 @@ function doneUnfinished(x) {
130
139
  });
131
140
  }
132
141
  /** 账本键 → 任务与重跑命令;oneshot:* 不归任何任务。 */
133
- function commandOfKey(x, key, t) {
142
+ function commandOfKey(key, t) {
134
143
  const [kind] = key.split(':');
135
144
  if (kind === 'dispatch')
136
145
  return `wl task start ${t.id} --as ${as(t.claimedBy)}`;
@@ -178,7 +187,7 @@ function callUncertain(x, now, th) {
178
187
  return [];
179
188
  if (task && epochOfKey(r.id) < task.epoch)
180
189
  return [];
181
- const command = task ? commandOfKey(x, r.id, task) : null;
190
+ const command = task ? commandOfKey(r.id, task) : null;
182
191
  return task && command ? [item(task, 'call_uncertain', r.updatedAt, command)] : [];
183
192
  });
184
193
  }
@@ -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;
@@ -0,0 +1,120 @@
1
+ const CHECKER_LAUNCH = 'codex --model gpt-6-astra -c model_reasoning_effort="high"';
2
+ export const TEAM_TEMPLATES = {
3
+ duo: {
4
+ id: 'duo',
5
+ label: '双人协作',
6
+ summary: 'builder 负责实现,checker 负责独立审查',
7
+ lead: 'builder',
8
+ posts: [
9
+ { id: 'builder', role: 'builder', agent: 'claude', model: 'opus', brief: 'briefs/builder.md' },
10
+ { id: 'checker', role: 'checker', agent: 'codex', launch: CHECKER_LAUNCH, brief: 'briefs/checker.md' },
11
+ ],
12
+ links: { builder: { reviewed_by: ['checker'] } },
13
+ },
14
+ solo: {
15
+ id: 'solo',
16
+ label: '单人',
17
+ summary: '单岗位 builder 独立完成实现与自检',
18
+ lead: 'builder',
19
+ posts: [
20
+ { id: 'builder', role: 'builder', agent: 'claude', model: 'opus', brief: 'briefs/builder.md' },
21
+ ],
22
+ links: {},
23
+ },
24
+ trio: {
25
+ id: 'trio',
26
+ label: '三人流水线',
27
+ summary: 'planner 规划拆解,移交 builder 实现,checker 负责审查',
28
+ lead: 'planner',
29
+ posts: [
30
+ { id: 'planner', role: 'planner', agent: 'claude', model: 'opus', brief: 'briefs/planner.md' },
31
+ { id: 'builder', role: 'builder', agent: 'claude', model: 'opus', brief: 'briefs/builder.md' },
32
+ { id: 'checker', role: 'checker', agent: 'codex', launch: CHECKER_LAUNCH, brief: 'briefs/checker.md' },
33
+ ],
34
+ links: {
35
+ planner: { hands_to: ['builder'] },
36
+ builder: { reviewed_by: ['checker'] },
37
+ },
38
+ },
39
+ };
40
+ export const ROLE_BRIEFS = {
41
+ builder: (team, _post) => [
42
+ `你是 ${team} 团队的实现岗,负责把需求落成代码。`,
43
+ '',
44
+ '- 先读懂需求和现有代码,再说明方案,再实现。',
45
+ '- 优先写测试,再写实现;每次改动保持测试通过。',
46
+ `- 完成一个可审查的改动后,用 \`wl send checker@${team} "<摘要和文件列表>"\` 请审查岗审查。`,
47
+ '- 收到审查意见后,逐条回应:修改,或说明为什么不改。',
48
+ '- 需求不清时向牵头岗或用户确认,不要自行扩大范围。',
49
+ ].join('\n'),
50
+ checker: (team, _post) => [
51
+ `你是 ${team} 团队的审查岗,负责独立审查改动。`,
52
+ '',
53
+ '- 收到审查请求后,先看改动本身和相关测试,再给意见。',
54
+ '- 重点看:正确性、边界情况、测试是否真的覆盖了行为、是否引入不必要的复杂度。',
55
+ `- 意见分级:必须修改 / 建议修改 / 仅供参考;用 \`wl send builder@${team} "<意见>"\` 回复。`,
56
+ '- 你可以运行测试和只读命令,但不要直接改实现岗的代码;需要改动时把建议发给对方。',
57
+ '- 没有问题时明确回复"通过",并说明你检查了什么。',
58
+ ].join('\n'),
59
+ planner: (team, _post) => [
60
+ `你是 ${team} 团队的规划岗,负责拆解需求与验收标准。`,
61
+ '',
62
+ '- 先读懂需求与现有代码结构,给出清晰的分步方案与边界条件。',
63
+ '- 将拆解好的子任务与验收标准写清楚,避免模糊描述。',
64
+ `- 方案就绪后,用 \`wl send builder@${team} "<方案与任务说明>"\` 移交实现岗。`,
65
+ '- 实现或审查过程中遇到范围变更时,及时更新方案并同步相关岗位。',
66
+ '- 拿不准的产品或架构取舍先向用户确认,不要替用户拍板。',
67
+ ].join('\n'),
68
+ };
69
+ export function renderTeamYaml(t, a) {
70
+ const repo = a.repo.startsWith('path:') ? a.repo : `path:${a.repo}`;
71
+ const lines = [
72
+ 'version: 1',
73
+ `name: ${a.name}`,
74
+ `repo: ${JSON.stringify(repo)}`,
75
+ 'norms: NORMS.md',
76
+ `lead: ${t.lead}`,
77
+ 'posts:',
78
+ ];
79
+ for (const p of t.posts) {
80
+ lines.push(` - id: ${p.id}`, ` agent: ${p.agent}`);
81
+ if (p.model)
82
+ lines.push(` model: ${p.model}`);
83
+ if (p.launch)
84
+ lines.push(` launch: '${p.launch}'`);
85
+ lines.push(' worktree: current', ` brief: [${p.brief}]`);
86
+ }
87
+ const linkEntries = Object.entries(t.links);
88
+ if (linkEntries.length > 0) {
89
+ lines.push('links:');
90
+ for (const [post, rels] of linkEntries) {
91
+ lines.push(` ${post}:`);
92
+ for (const [rel, targets] of Object.entries(rels)) {
93
+ lines.push(` ${rel}: [${targets.join(', ')}]`);
94
+ }
95
+ }
96
+ }
97
+ return `${lines.join('\n')}\n`;
98
+ }
99
+ export function renderNorms(a) {
100
+ const lead = a.posts[0] ?? 'builder';
101
+ const peer = a.posts[1] ?? lead;
102
+ return [
103
+ `# ${a.team} 团队规范`,
104
+ '',
105
+ `在岗角色:${a.posts.join('、')}。`,
106
+ '',
107
+ '1. 改动前先用一两句话说明你打算怎么做,再动手。',
108
+ '2. 改完先自己跑测试,确认通过再汇报。',
109
+ `3. 需要其他岗位审查或回答时,用 \`wl send <地址> "<消息>"\` 联系,地址形如 \`${peer}@${a.team}\`。`,
110
+ '4. 消息写清楚:改了什么、在哪个文件、希望对方做什么。',
111
+ '5. 不修改其他岗位正在改的文件;拿不准时先问。',
112
+ '6. 小步提交,提交信息说明为什么改。',
113
+ `7. 遇到无法判断的事(需求不清、权限、破坏性操作),停下来问牵头岗 \`${lead}@${a.team}\`,不要猜。`,
114
+ '8. 收到消息后尽快回复;暂时无法处理时说明原因。',
115
+ '',
116
+ ].join('\n');
117
+ }
118
+ export function renderBrief(role, a) {
119
+ return `# ${a.post} 岗位说明\n\n${ROLE_BRIEFS[role](a.team, a.post)}\n`;
120
+ }
package/dist/index.d.ts CHANGED
@@ -9,18 +9,22 @@ export * from './orca/types.js';
9
9
  export * from './spec/hash.js';
10
10
  export * from './spec/load.js';
11
11
  export * from './spec/draft.js';
12
+ export * from './spec/doc.js';
12
13
  export * from './spec/edit.js';
13
14
  export * from './spec/plan-change.js';
15
+ export * from './spec/facade.js';
14
16
  export * from './commands/spec.js';
15
17
  export * from './commands/about.js';
16
18
  export * from './registry/registry.js';
17
19
  export * from './brief/render.js';
18
20
  export * from './audit.js';
21
+ export * from './audit-table.js';
19
22
  export * from './commands/deps.js';
20
23
  export * from './commands/locate.js';
21
24
  export * from './commands/up.js';
22
25
  export * from './commands/restore/index.js';
23
26
  export * from './commands/autostart/index.js';
27
+ export * from './commands/autostart/sys.js';
24
28
  export * from './commands/version-range.js';
25
29
  export * from './commands/ps.js';
26
30
  export * from './commands/send.js';
@@ -48,9 +52,12 @@ export * from './commands/task/escalate.js';
48
52
  export { HEARTBEAT_STALE_MS, HEARTBEAT_TAKEOVER_MS, dispatcherHealth, dispatcherLaunchCommand, dispatcherUp, dispatcherDown, type DispatcherHealth, type UpArgs as DispatcherUpArgs, } from './commands/dispatcher/lifecycle.js';
49
53
  export * from './commands/dispatcher/loop.js';
50
54
  export * from './commands/dispatcher/attention.js';
55
+ export * from './commands/dispatcher/signal-summary.js';
51
56
  export * from './commands/dispatcher/controls.js';
52
57
  export * from './commands/dispatcher/inbox.js';
53
58
  export * from './commands/dispatcher/outlets.js';
54
59
  export * from './commands/dispatcher/signals.js';
55
60
  export * from './commands/dispatcher/advance.js';
56
61
  export * from './commands/dispatcher/orca-sync.js';
62
+ export * from './commands/templates.js';
63
+ export * from './commands/init.js';