openshain 0.2.0 → 0.4.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 (46) hide show
  1. package/NOTICE +4 -0
  2. package/dist/bin.js +4 -33
  3. package/dist/commands/init.d.ts +1 -1
  4. package/dist/commands/init.js +7 -6
  5. package/dist/commands/tools.js +1 -2
  6. package/dist/commands/work.d.ts +1 -9
  7. package/dist/commands/work.js +9 -22
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +2 -2
  10. package/dist/labels.d.ts +1 -2
  11. package/dist/labels.js +3 -0
  12. package/dist/preview.d.ts +13 -0
  13. package/dist/preview.js +141 -0
  14. package/dist/report.d.ts +6 -0
  15. package/dist/{commands/run.js → report.js} +6 -38
  16. package/dist/tui/app.js +38 -5
  17. package/dist/tui/banner.d.ts +3 -8
  18. package/dist/tui/banner.js +2 -2
  19. package/dist/tui/controller.d.ts +30 -5
  20. package/dist/tui/controller.js +306 -77
  21. package/dist/tui/index.js +1 -1
  22. package/dist/tui/lines.d.ts +5 -3
  23. package/dist/tui/lines.js +31 -3
  24. package/dist/tui/markdown.d.ts +15 -0
  25. package/dist/tui/markdown.js +199 -0
  26. package/dist/usage.d.ts +1 -1
  27. package/dist/usage.js +1 -1
  28. package/dist/workspace.js +1 -1
  29. package/package.json +9 -7
  30. package/src/bin.ts +4 -38
  31. package/src/commands/init.ts +7 -6
  32. package/src/commands/tools.ts +7 -2
  33. package/src/commands/work.ts +10 -34
  34. package/src/index.ts +1 -10
  35. package/src/labels.ts +4 -2
  36. package/src/preview.ts +157 -0
  37. package/src/{commands/run.ts → report.ts} +6 -59
  38. package/src/tui/app.tsx +75 -13
  39. package/src/tui/banner.ts +4 -10
  40. package/src/tui/controller.ts +338 -77
  41. package/src/tui/index.ts +3 -1
  42. package/src/tui/lines.ts +36 -6
  43. package/src/tui/markdown.ts +214 -0
  44. package/src/usage.ts +1 -2
  45. package/src/workspace.ts +1 -1
  46. package/dist/commands/run.d.ts +0 -22
package/dist/tui/lines.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { displayWidth } from "../format.js";
2
2
  import { logoSegments } from "./banner.js";
3
+ import { markdownRows } from "./markdown.js";
3
4
  /** What starts a line of each kind. The continuation lines of a wrapped entry are indented to match. */
4
5
  const MARKERS = {
5
6
  user: "> ",
@@ -31,6 +32,19 @@ export function wrapText(text, width) {
31
32
  }
32
33
  return out;
33
34
  }
35
+ /**
36
+ * The rows of one reply, kept until the entry goes or the width changes. The screen redraws
37
+ * every entry whenever a line is added, and reading markdown is the expensive part of that.
38
+ */
39
+ const drawn = new WeakMap();
40
+ export function rowsFor(entry, width) {
41
+ const held = drawn.get(entry);
42
+ if (held && held.width === width)
43
+ return held.rows;
44
+ const rows = markdownRows(entry.text, width);
45
+ drawn.set(entry, { width, rows });
46
+ return rows;
47
+ }
34
48
  /** A blank row goes before an entry that starts something new: a message, a reply, a notice, a question. */
35
49
  function startsBlock(kind, previous) {
36
50
  if (previous === undefined)
@@ -52,14 +66,28 @@ export function screenLines(entries, width) {
52
66
  lines.push({ kind: "blank", text: "" });
53
67
  if (entry.kind === "logo") {
54
68
  // Never wrapped: a cut row of the wordmark reads better than a broken one.
55
- lines.push({ kind: "logo", text: entry.text, segments: logoSegments(entry.text) });
69
+ lines.push({ kind: "logo", text: entry.text, spans: logoSegments(entry.text) });
56
70
  previous = entry.kind;
57
71
  continue;
58
72
  }
59
73
  const marker = MARKERS[entry.kind];
60
74
  const indent = " ".repeat(displayWidth(marker));
61
- const body = wrapText(entry.text, Math.max(8, width - displayWidth(marker)));
62
- for (const [i, text] of body.entries()) {
75
+ const room = Math.max(8, width - displayWidth(marker));
76
+ if (entry.kind === "assistant") {
77
+ // The reply is written in markdown; the screen draws it rather than showing its marks.
78
+ for (const [i, row] of rowsFor(entry, room).entries()) {
79
+ // A row with nothing on it is drawn as an empty one: no marker, no indent, no pieces.
80
+ if (row.length === 0) {
81
+ lines.push({ kind: entry.kind, text: "" });
82
+ continue;
83
+ }
84
+ const spans = [{ text: i === 0 ? marker : indent }, ...row];
85
+ lines.push({ kind: entry.kind, text: spans.map((s) => s.text).join(""), spans });
86
+ }
87
+ previous = entry.kind;
88
+ continue;
89
+ }
90
+ for (const [i, text] of wrapText(entry.text, room).entries()) {
63
91
  lines.push({ kind: entry.kind, text: (i === 0 ? marker : indent) + text });
64
92
  }
65
93
  previous = entry.kind;
@@ -0,0 +1,15 @@
1
+ /** A piece of a row that carries one style. A row is a list of these, drawn left to right. */
2
+ export interface Span {
3
+ text: string;
4
+ color?: string;
5
+ bold?: boolean;
6
+ italic?: boolean;
7
+ dim?: boolean;
8
+ strikethrough?: boolean;
9
+ }
10
+ /**
11
+ * A reply as rows of styled pieces. The model writes markdown, so the screen shows the emphasis
12
+ * and the structure instead of the characters that mark them. What this does not draw yet is
13
+ * shown as it was written, never dropped.
14
+ */
15
+ export declare function markdownRows(source: string, width: number): Span[][];
@@ -0,0 +1,199 @@
1
+ import { marked } from "marked";
2
+ import { displayWidth } from "../format.js";
3
+ /** What each part of a reply looks like on the screen. */
4
+ const STYLE = {
5
+ heading: { bold: true, color: "cyan" },
6
+ code: { color: "green" },
7
+ link: { color: "blue" },
8
+ quote: { dim: true },
9
+ rule: { dim: true },
10
+ };
11
+ const QUOTE_MARKER = "▎ ";
12
+ const CODE_MARKER = "│ ";
13
+ const BULLETS = ["•", "◦", "‣"];
14
+ function styled(text, style) {
15
+ return { text, ...style };
16
+ }
17
+ /** The inline tokens of one block, flattened into styled pieces. */
18
+ function inline(tokens, style) {
19
+ if (!tokens)
20
+ return [];
21
+ const spans = [];
22
+ for (const token of tokens) {
23
+ switch (token.type) {
24
+ case "strong":
25
+ spans.push(...inline(token.tokens, { ...style, bold: true }));
26
+ break;
27
+ case "em":
28
+ spans.push(...inline(token.tokens, { ...style, italic: true }));
29
+ break;
30
+ case "del":
31
+ spans.push(...inline(token.tokens, { ...style, strikethrough: true }));
32
+ break;
33
+ case "codespan":
34
+ spans.push(styled(token.text, { ...style, ...STYLE.code }));
35
+ break;
36
+ case "link": {
37
+ const link = token;
38
+ spans.push(...inline(link.tokens, style));
39
+ // The label alone hides where the link goes, so the address follows it.
40
+ if (link.href && link.href !== textOf(link.tokens)) {
41
+ spans.push(styled(` (${link.href})`, { ...style, ...STYLE.link }));
42
+ }
43
+ break;
44
+ }
45
+ case "br":
46
+ spans.push(styled("\n", style));
47
+ break;
48
+ case "escape":
49
+ case "text":
50
+ spans.push(...(token.tokens
51
+ ? inline(token.tokens, style)
52
+ : [styled(token.text, style)]));
53
+ break;
54
+ default:
55
+ spans.push(styled(token.raw, style));
56
+ }
57
+ }
58
+ return spans;
59
+ }
60
+ function textOf(tokens) {
61
+ return inline(tokens, {})
62
+ .map((s) => s.text)
63
+ .join("");
64
+ }
65
+ /**
66
+ * Breaks styled pieces into rows no wider than `width` display columns, the way the rest of the
67
+ * screen breaks plain text: at the character, so Japanese wraps where it should. Rows after the
68
+ * first start with `hanging`, which keeps a list item under its own marker.
69
+ */
70
+ function wrap(spans, width, hanging = "") {
71
+ const limit = Math.max(4, width);
72
+ const rows = [];
73
+ let row = [];
74
+ let used = 0;
75
+ const indent = () => (hanging === "" ? [] : [{ text: hanging }]);
76
+ const start = () => {
77
+ rows.push(row);
78
+ row = indent();
79
+ used = displayWidth(hanging);
80
+ };
81
+ for (const span of spans) {
82
+ for (const [i, part] of span.text.split("\n").entries()) {
83
+ // A line break inside a block starts a row of its own.
84
+ if (i > 0)
85
+ start();
86
+ let piece = "";
87
+ for (const ch of part) {
88
+ const w = displayWidth(ch);
89
+ if (used + w > limit && (row.length > 0 || piece !== "")) {
90
+ if (piece !== "")
91
+ row.push({ ...span, text: piece });
92
+ piece = "";
93
+ start();
94
+ }
95
+ piece += ch;
96
+ used += w;
97
+ }
98
+ if (piece !== "")
99
+ row.push({ ...span, text: piece });
100
+ }
101
+ }
102
+ rows.push(row);
103
+ return rows;
104
+ }
105
+ /** Puts `prefix` in front of every row, for a quote bar or a code bar. */
106
+ function prefixed(rows, prefix) {
107
+ return rows.map((row) => [prefix, ...row]);
108
+ }
109
+ function blockRows(tokens, width) {
110
+ const rows = [];
111
+ for (const token of tokens) {
112
+ switch (token.type) {
113
+ case "space":
114
+ rows.push([]);
115
+ break;
116
+ case "heading":
117
+ rows.push(...wrap(inline(token.tokens, STYLE.heading), width));
118
+ break;
119
+ case "paragraph":
120
+ case "text":
121
+ rows.push(...wrap(inline(token.tokens ?? [], {}), width));
122
+ break;
123
+ case "code": {
124
+ const marker = styled(CODE_MARKER, STYLE.quote);
125
+ const body = token.text.split("\n");
126
+ for (const line of body) {
127
+ rows.push(...prefixed(wrap([styled(line, STYLE.code)], width - 2), marker));
128
+ }
129
+ break;
130
+ }
131
+ case "blockquote": {
132
+ const inner = blockRows(token.tokens ?? [], width - 2);
133
+ rows.push(...prefixed(inner, styled(QUOTE_MARKER, STYLE.quote)));
134
+ break;
135
+ }
136
+ case "list":
137
+ rows.push(...listRows(token, width, 0));
138
+ break;
139
+ case "hr":
140
+ rows.push([styled("─".repeat(Math.max(4, width)), STYLE.rule)]);
141
+ break;
142
+ case "table":
143
+ // Aligning columns is its own piece of work; until then the source rows are shown as
144
+ // they were written, so nothing the model put in the table is lost.
145
+ for (const line of token.raw.trimEnd().split("\n")) {
146
+ rows.push(...wrap([{ text: line }], width));
147
+ }
148
+ break;
149
+ default:
150
+ for (const line of (token.raw ?? "").trimEnd().split("\n")) {
151
+ rows.push(...wrap([{ text: line }], width));
152
+ }
153
+ }
154
+ }
155
+ return rows;
156
+ }
157
+ function listRows(list, width, depth) {
158
+ const rows = [];
159
+ let number = Number(list.start || 1);
160
+ for (const item of list.items) {
161
+ const marker = list.ordered ? `${number++}. ` : `${BULLETS[depth % BULLETS.length]} `;
162
+ const indent = " ".repeat(displayWidth(marker));
163
+ const inner = [];
164
+ for (const token of item.tokens) {
165
+ if (token.type === "list") {
166
+ inner.push(...listRows(token, width - displayWidth(marker), depth + 1));
167
+ }
168
+ else {
169
+ inner.push(...blockRows([token], width - displayWidth(marker)));
170
+ }
171
+ }
172
+ for (const [i, row] of inner.entries()) {
173
+ rows.push([{ text: i === 0 ? marker : indent }, ...row]);
174
+ }
175
+ }
176
+ return rows;
177
+ }
178
+ /**
179
+ * How much of a reply is read as markdown. Reading it costs more than the square of its length
180
+ * (10,000 characters take about 0.14 seconds, 20,000 about 0.46, 140,000 over a minute), and the
181
+ * screen draws on one thread, so a longer reply would hold it. Above this the reply is shown as
182
+ * plain text: every character is still there, with its marks.
183
+ */
184
+ const MAX_SOURCE = 20_000;
185
+ /**
186
+ * A reply as rows of styled pieces. The model writes markdown, so the screen shows the emphasis
187
+ * and the structure instead of the characters that mark them. What this does not draw yet is
188
+ * shown as it was written, never dropped.
189
+ */
190
+ export function markdownRows(source, width) {
191
+ if (source.length > MAX_SOURCE)
192
+ return wrap([{ text: source }], width);
193
+ const rows = blockRows(marked.lexer(source), width);
194
+ while (rows.length > 0 && (rows[0]?.length ?? 0) === 0)
195
+ rows.shift();
196
+ while (rows.length > 0 && (rows.at(-1)?.length ?? 0) === 0)
197
+ rows.pop();
198
+ return rows.length > 0 ? rows : [[]];
199
+ }
package/dist/usage.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AnyEvent } from "@openshain/core";
1
+ import { type AnyEvent } from "@openshain/core";
2
2
  export interface UsageSummary {
3
3
  modelCalls: number;
4
4
  toolCalls: number;
package/dist/usage.js CHANGED
@@ -1,4 +1,4 @@
1
- import { countToolCalls } from "@openshain/agent";
1
+ import { countToolCalls } from "@openshain/core";
2
2
  /** Totals over a work's events: calls and tokens. */
3
3
  export function summarizeUsage(events) {
4
4
  const summary = {
package/dist/workspace.js CHANGED
@@ -12,7 +12,7 @@ export async function findWorkspace(start) {
12
12
  catch {
13
13
  const parent = dirname(dir);
14
14
  if (parent === dir) {
15
- throw new OpenshainError("config", `${CONFIG_FILE_NAME} が見つかりません。${resolve(start)} から上に向かって探しました。openshain init で作れます。`);
15
+ throw new OpenshainError("config", `${CONFIG_FILE_NAME} が見つかりません。${resolve(start)} から上に向かって探しました。openshain init で作成します。`);
16
16
  }
17
17
  dir = parent;
18
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openshain",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Reference CLI of the openshain agent harness",
5
5
  "keywords": [
6
6
  "openshain",
@@ -30,7 +30,8 @@
30
30
  "!src/**/*.test.ts",
31
31
  "!src/**/*.test.tsx",
32
32
  "README.md",
33
- "LICENSE"
33
+ "LICENSE",
34
+ "NOTICE"
34
35
  ],
35
36
  "exports": {
36
37
  ".": {
@@ -40,7 +41,7 @@
40
41
  }
41
42
  },
42
43
  "bin": {
43
- "openshain": "./dist/bin.js"
44
+ "openshain": "dist/bin.js"
44
45
  },
45
46
  "scripts": {
46
47
  "build": "../../node_modules/.bin/tsc -p tsconfig.build.json",
@@ -48,11 +49,12 @@
48
49
  },
49
50
  "dependencies": {
50
51
  "@modelcontextprotocol/sdk": "1.30.0",
51
- "@openshain/agent": "0.2.0",
52
- "@openshain/core": "0.2.0",
53
- "@openshain/mcp": "0.2.0",
54
- "@openshain/tools": "0.2.0",
52
+ "@openshain/agent": "0.4.0",
53
+ "@openshain/core": "0.4.0",
54
+ "@openshain/mcp": "0.4.0",
55
+ "@openshain/tools": "0.4.0",
55
56
  "ink": "7.1.1",
57
+ "marked": "18.0.12",
56
58
  "react": "19.2.8"
57
59
  },
58
60
  "devDependencies": {
package/src/bin.ts CHANGED
@@ -1,14 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { createInterface } from "node:readline/promises";
3
2
  import { parseArgs } from "node:util";
4
3
  import { anthropicProvider, openaiCompatibleProvider } from "@openshain/agent";
5
4
  import { isOpenshainError, type RuntimeProviders } from "@openshain/core";
6
5
  import { standardTools } from "@openshain/tools";
7
6
  import { init } from "./commands/init.ts";
8
7
  import { mcp } from "./commands/mcp.ts";
9
- import { run } from "./commands/run.ts";
10
8
  import { toolsList } from "./commands/tools.ts";
11
- import { workList, workResume, workShow } from "./commands/work.ts";
9
+ import { workList, workShow } from "./commands/work.ts";
12
10
  import { plain } from "./format.ts";
13
11
  import { errorLabel } from "./labels.ts";
14
12
  import { startTui } from "./tui/index.ts";
@@ -17,11 +15,9 @@ import { findWorkspace } from "./workspace.ts";
17
15
  const USAGE = `使い方:
18
16
  openshain 端末で対話を始める
19
17
  openshain init openshain.yaml のひな型を書く
20
- openshain run "<依頼>" 依頼を Work として進める
21
18
  openshain tools list 使える Tool の一覧
22
19
  openshain work list Work の一覧
23
20
  openshain work show <id> Work の詳細
24
- openshain work resume <id> 途中で止まった Work を続ける
25
21
  openshain mcp MCP Server を stdio で起動する
26
22
 
27
23
  --workspace <dir> 起点のディレクトリ。省略時はカレントディレクトリ
@@ -67,17 +63,6 @@ async function main(argv: string[]): Promise<number> {
67
63
  case "init":
68
64
  await init({ workspaceRoot: values.workspace ?? process.cwd(), write });
69
65
  return 0;
70
- case "run": {
71
- const objective = rest.join(" ").trim();
72
- if (!objective) {
73
- write('依頼の文を指定してください。openshain run "今月の経理を進めて" のように。');
74
- return 2;
75
- }
76
- const workspaceRoot = await findWorkspace(values.workspace ?? process.cwd());
77
- return withTerminal((ask) =>
78
- run({ workspaceRoot, providers, objective, write, ...(ask && { ask }) }),
79
- );
80
- }
81
66
  case "mcp": {
82
67
  const workspaceRoot = await findWorkspace(values.workspace ?? process.cwd());
83
68
  await mcp({ workspaceRoot, providers });
@@ -95,7 +80,7 @@ async function main(argv: string[]): Promise<number> {
95
80
  case "work": {
96
81
  const sub = rest[0];
97
82
  const id = rest[1] ?? "";
98
- if (!(sub === "list" || ((sub === "show" || sub === "resume") && id))) {
83
+ if (!(sub === "list" || (sub === "show" && id))) {
99
84
  write(USAGE);
100
85
  return 2;
101
86
  }
@@ -104,13 +89,8 @@ async function main(argv: string[]): Promise<number> {
104
89
  await workList({ workspaceRoot, write });
105
90
  return 0;
106
91
  }
107
- if (sub === "show") {
108
- await workShow({ workspaceRoot, id, write });
109
- return 0;
110
- }
111
- return withTerminal((ask) =>
112
- workResume({ workspaceRoot, providers, id, write, ...(ask && { ask }) }),
113
- );
92
+ await workShow({ workspaceRoot, id, write });
93
+ return 0;
114
94
  }
115
95
  default:
116
96
  write(`不明なコマンド ${command}`);
@@ -119,20 +99,6 @@ async function main(argv: string[]): Promise<number> {
119
99
  }
120
100
  }
121
101
 
122
- /** Runs `fn` with a way to ask the person when a terminal is there to answer; otherwise without one. */
123
- async function withTerminal(
124
- fn: (ask?: (question: string) => Promise<string>) => Promise<number>,
125
- ): Promise<number> {
126
- // Without a terminal there is no one to answer; the work waits instead.
127
- if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) return fn();
128
- const rl = createInterface({ input: process.stdin, output: process.stdout });
129
- try {
130
- return await fn((question) => rl.question(`${plain(question)}\n> `));
131
- } finally {
132
- rl.close();
133
- }
134
- }
135
-
136
102
  main(process.argv.slice(2)).then(
137
103
  (code) => process.exit(code),
138
104
  (err: unknown) => {
@@ -21,7 +21,7 @@ profession:
21
21
  id: generic
22
22
  # model への指示。100,000 文字まで
23
23
  instructions: |
24
- あなたはこの会社の事務担当です。依頼された作業を、workspace 内のファイルだけを使って進めてください。
24
+ あなたはこの会社の一般事務の社員エージェントです。依頼された作業を、workspace 内のファイルだけを使って進めてください。
25
25
  model:
26
26
  provider: anthropic # anthropic | openai-compatible
27
27
  model: claude-opus-5
@@ -52,7 +52,7 @@ export const MCP_TEMPLATE = `${JSON.stringify(
52
52
  /** What an outside agent reads before working in the folder. Codex reads AGENTS.md; Claude Code reads it through CLAUDE.md. */
53
53
  export const AGENTS_TEMPLATE = `# この会社フォルダで働くエージェントへ
54
54
 
55
- このフォルダは openshain の Company Workspace です。この指示は、Claude Code や Codex のような外部のエージェントが MCP 経由でこのフォルダを扱うときのものです。\`openshain run\` Runtime 自身が動くときは、Work の作成と完了を Runtime が行うので、下の work_* の手順は当てはまりません。
55
+ このフォルダは openshain の Company Workspace です。この指示は、Claude Code や Codex のような外部のエージェントが MCP 経由でこのフォルダを扱うときのものです。openshain の対話型 CLI も同じ手順で Runtime を使います。
56
56
 
57
57
  会社のファイルの読み書きと集計は openshain の MCP tool で行います。Claude Code や Codex 自身の Read、Write、Bash は会社のファイルには使いません。Runtime を通らなかった操作は記録に残らないためです。
58
58
 
@@ -60,7 +60,8 @@ export const AGENTS_TEMPLATE = `# この会社フォルダで働くエージェ
60
60
  - ファイルは \`fs_list\`、\`fs_search\`、\`fs_read\`、\`csv_read\`、\`markdown_read\` で見る。合計や件数は \`csv_aggregate\` に任せ、自分で合計しない
61
61
  - 書くときは \`fs_write\` か \`csv_write\`
62
62
  - 終わったら \`work_complete\` に、何をしたかと、書いたファイルを渡す。続けられないときは \`work_fail\`
63
- - \`openshain.yaml\` と \`work/\` は Runtime のもの。変更しない
63
+ - \`openshain.yaml\`、\`work/\`、\`principals/\`、\`authority/\` は Runtime のもの。変更しない
64
+ - 呼び出しの結果が \`pending\` なら、会社の権限の規則がその呼び出しを止めている。承認は会社の人が openshain の画面で決める。自分で \`approval_decide\` や \`review_decide\` を呼んで通さない。人に伝えて、決まるまで別の作業をする
64
65
  `;
65
66
 
66
67
  export const CLAUDE_TEMPLATE = "@AGENTS.md\n";
@@ -110,7 +111,7 @@ export async function init({ workspaceRoot, write }: InitOptions): Promise<void>
110
111
  }
111
112
  }
112
113
  write(
113
- "company と principal を自分の会社に合わせ、api_key_env に書いた環境変数を設定してから openshain run を実行してください。",
114
+ "company と principal を自分の会社に合わせ、api_key_env に書いた環境変数を設定してから openshain を実行してください。",
114
115
  );
115
116
  }
116
117
 
@@ -123,10 +124,10 @@ async function addToMcpConfig(path: string): Promise<string> {
123
124
  try {
124
125
  parsed = JSON.parse(await readFile(path, "utf8"));
125
126
  } catch {
126
- return `${path} はすでにあり、JSON として読めないので変更しません。openshain を手で登録してください。`;
127
+ return `${path} はすでにあり、JSON として読めないので変更しません。openshain を手動で登録してください。`;
127
128
  }
128
129
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
129
- return `${path} はすでにあり、形が違うので変更しません。openshain を手で登録してください。`;
130
+ return `${path} はすでにあり、形が違うので変更しません。openshain を手動で登録してください。`;
130
131
  }
131
132
  const config = parsed as { mcpServers?: unknown };
132
133
  const servers =
@@ -1,5 +1,10 @@
1
- import { ASK_USER, RUNTIME_PROVIDER_ID } from "@openshain/agent";
2
- import { createToolRegistry, loadConfig, type RuntimeProviders } from "@openshain/core";
1
+ import {
2
+ ASK_USER,
3
+ createToolRegistry,
4
+ loadConfig,
5
+ RUNTIME_PROVIDER_ID,
6
+ type RuntimeProviders,
7
+ } from "@openshain/core";
3
8
 
4
9
  export interface ToolsListOptions {
5
10
  workspaceRoot: string;
@@ -1,19 +1,16 @@
1
- import { pendingQuestions } from "@openshain/agent";
2
1
  import {
3
2
  type AnyEvent,
4
- createRuntime,
5
3
  type Event,
6
- isTerminal,
7
4
  parseWorkId,
8
- type RuntimeProviders,
9
- SESSION_WORK_TYPE,
5
+ pendingApprovals,
6
+ pendingQuestions,
10
7
  type Work,
11
8
  WorkStore,
12
9
  } from "@openshain/core";
13
10
  import { describeInput, padDisplay } from "../format.ts";
14
11
  import { errorLabel, failureLabel, rejectionLabel, statusLabel } from "../labels.ts";
12
+ import { nextActor } from "../report.ts";
15
13
  import { formatUsage, summarizeUsage } from "../usage.ts";
16
- import { type DriveOptions, drive, nextActor } from "./run.ts";
17
14
 
18
15
  export interface WorkListOptions {
19
16
  workspaceRoot: string;
@@ -24,7 +21,7 @@ export interface WorkListOptions {
24
21
  export async function workList({ workspaceRoot, write }: WorkListOptions): Promise<void> {
25
22
  const { works, problems } = await new WorkStore(workspaceRoot).list();
26
23
  if (works.length === 0 && problems.length === 0) {
27
- write('Work はまだありません。openshain run "<依頼>" で始められます。');
24
+ write("Work はまだありません。openshain で社員エージェントに依頼すると始まります。");
28
25
  return;
29
26
  }
30
27
  for (const work of works) {
@@ -75,6 +72,12 @@ export function describeWork(work: Work, events: AnyEvent[]): string[] {
75
72
  if (work.status === "waiting_input") {
76
73
  for (const { question } of pendingQuestions(events)) lines.push(`質問 ${question}`);
77
74
  }
75
+ if (work.status === "waiting_approval") {
76
+ for (const a of pendingApprovals(events)) {
77
+ const who = a.kind === "review" ? `${a.reviewer?.role ?? "資格者"}の判断待ち` : "承認待ち";
78
+ lines.push(`${who} ${a.call.name} ${describeInput(a.call.input)} (${a.approvalId})`);
79
+ }
80
+ }
78
81
 
79
82
  const calls = toolLines(events);
80
83
  if (calls.length > 0) {
@@ -86,33 +89,6 @@ export function describeWork(work: Work, events: AnyEvent[]): string[] {
86
89
  return lines;
87
90
  }
88
91
 
89
- export interface WorkResumeOptions extends DriveOptions {
90
- workspaceRoot: string;
91
- providers: RuntimeProviders;
92
- id: string;
93
- }
94
-
95
- /** Continues a work that stopped before its end, answering its questions when the caller can. */
96
- export async function workResume(options: WorkResumeOptions): Promise<number> {
97
- const workId = parseWorkId(options.id);
98
- const work = await new WorkStore(options.workspaceRoot).get(workId);
99
- if (work.type === SESSION_WORK_TYPE) {
100
- options.write(
101
- `${work.id} は会話の記録のため、再開できません。会話は openshain で始め直してください。`,
102
- );
103
- return 1;
104
- }
105
- if (isTerminal(work.status)) {
106
- options.write(`${work.id} は${statusLabel(work.status)}のため、再開できません。`);
107
- return 1;
108
- }
109
- const runtime = await createRuntime({
110
- workspaceRoot: options.workspaceRoot,
111
- providers: options.providers,
112
- });
113
- return drive(runtime, workId, options);
114
- }
115
-
116
92
  /** One line per tool call, in log order, with its outcome when it was rejected or failed. */
117
93
  function toolLines(events: AnyEvent[]): string[] {
118
94
  const lines = new Map<string, string>();
package/src/index.ts CHANGED
@@ -8,22 +8,12 @@ export {
8
8
  MCP_TEMPLATE,
9
9
  } from "./commands/init.ts";
10
10
  export { type McpOptions, mcp } from "./commands/mcp.ts";
11
- export {
12
- type DriveOptions,
13
- drive,
14
- nextActor,
15
- type RunOptions,
16
- report,
17
- run,
18
- } from "./commands/run.ts";
19
11
  export { type ToolsListOptions, toolsList } from "./commands/tools.ts";
20
12
  export {
21
13
  describeWork,
22
14
  type WorkListOptions,
23
- type WorkResumeOptions,
24
15
  type WorkShowOptions,
25
16
  workList,
26
- workResume,
27
17
  workShow,
28
18
  } from "./commands/work.ts";
29
19
  export {
@@ -36,5 +26,6 @@ export {
36
26
  STATUS_LABELS,
37
27
  statusLabel,
38
28
  } from "./labels.ts";
29
+ export { nextActor, progressLine, report } from "./report.ts";
39
30
  export { formatUsage, summarizeUsage, type UsageSummary } from "./usage.ts";
40
31
  export { findWorkspace } from "./workspace.ts";
package/src/labels.ts CHANGED
@@ -1,5 +1,4 @@
1
- import type { FailureReason } from "@openshain/agent";
2
- import type { ErrorCode, ToolRejectionCode, WorkStatus } from "@openshain/core";
1
+ import type { ErrorCode, FailureReason, ToolRejectionCode, WorkStatus } from "@openshain/core";
3
2
 
4
3
  /** The words shown for a work's status. The log keeps the original value. */
5
4
  export const STATUS_LABELS: Record<WorkStatus, string> = {
@@ -28,6 +27,9 @@ export const REJECTION_LABELS: Record<ToolRejectionCode, string> = {
28
27
  reserved_path: "予約されたパス",
29
28
  outside_workspace: "workspace の外",
30
29
  invalid_path: "不正なパス",
30
+ limit_reached: "Tool 呼び出しの上限",
31
+ denied: "権限の表で不許可",
32
+ rejected_by_person: "承認されなかった",
31
33
  };
32
34
 
33
35
  /** A heading for a runtime error, before the original message. */