tonedial 0.1.5 → 0.1.7
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.es.md +37 -26
- package/README.ja.md +37 -26
- package/README.ko.md +37 -26
- package/README.md +37 -26
- package/README.zh.md +37 -26
- package/dist/cli.js +22 -14
- package/dist/commands/compile.js +1 -1
- package/dist/commands/doctor.js +34 -5
- package/dist/commands/get.js +3 -1
- package/dist/commands/init.js +3 -0
- package/dist/commands/rebuild.js +99 -9
- package/dist/commands/show.js +3 -1
- package/dist/commands/test.js +2 -1
- package/dist/commands/toggle.js +23 -0
- package/dist/core/anchors.js +2 -0
- package/dist/core/compile.js +7 -2
- package/dist/core/config.js +8 -2
- package/dist/core/i18n.js +48 -12
- package/dist/core/legacy-tone.js +7 -2
- package/dist/core/locales/es.js +24 -6
- package/dist/core/locales/ja.js +24 -6
- package/dist/core/locales/zh.js +24 -6
- package/dist/core/operations.js +24 -2
- package/dist/core/reader.js +16 -2
- package/dist/core/resolve.js +1 -0
- package/dist/core/seeds.js +3 -3
- package/dist/core/skill.js +43 -10
- package/dist/wiring/apply.js +1 -1
- package/package.json +1 -1
package/README.zh.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**给编程代理一个角色。**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
ToneDial 是本地 CLI,用于调整编程代理的语气并在不同项目中复用。
|
|
6
6
|
|
|
7
7
|
**7 个角色 · 12 个语气轴 · 5 种 CLI 语言 · 零运行时依赖**
|
|
8
8
|
|
|
@@ -19,9 +19,36 @@
|
|
|
19
19
|
|
|
20
20
|
**[查看全部 7 个角色 →](https://github.com/awesometic/tonedial/blob/main/docs/EXAMPLES.zh.md)**
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## 同一模型,更精简的计划
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
在 `gpt-5.6-sol` / `high` 下,一个全新会话使用 Codex 默认设置,另一个使用下方完整的 `vibe-coder` 配置。
|
|
25
|
+
`verbosity=1` 只是该配置的一部分,并非唯一差异。
|
|
26
|
+
|
|
27
|
+
**指令**
|
|
28
|
+
|
|
29
|
+
> I want to make ToneDial easier for first-time users. The README should get them from installation through `tonedial init` to their first preview with `tonedial test codex --preset <slug>`, and the CLI should make that next step obvious after `init`. Give me a prioritized three-step improvement plan for the README and first-run CLI flow.
|
|
30
|
+
|
|
31
|
+
<img src="https://raw.githubusercontent.com/awesometic/tonedial/main/docs/media/readme/zh/verbosity-comparison.svg" alt="GPT-5.6 Sol high 使用 Codex 默认设置和完整 vibe-coder 配置回答同一条 ToneDial 改进计划指令;配置后的英文回复从 148 个词缩短到 82 个词" width="760">
|
|
32
|
+
|
|
33
|
+
<sub>同一指令 · 每个新会话的首次完整回复 · 英文回复 148 个词 → 82 个词。这是一次示例性对比,并非只隔离单个轴的基准测试。回复用于展示语气差异;产品行为和命令请以下方指南为准。 <a href="https://github.com/awesometic/tonedial/blob/main/docs/media/readme/verbosity-comparison.json">输入与生成记录</a>.</sub>
|
|
34
|
+
|
|
35
|
+
## 开始试用
|
|
36
|
+
|
|
37
|
+
需要 **Node 24 或更高版本**和 npm。
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install --global tonedial
|
|
41
|
+
tonedial init --tools none --global --lang zh
|
|
42
|
+
tonedial set
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
设置时会先展示文件变更并请求批准。无需连接代理即可先打开面板试用。
|
|
46
|
+
|
|
47
|
+
[连接代理](https://github.com/awesometic/tonedial/blob/main/docs/USAGE.md#connecting-an-agent) · [使用临时设置试用](https://github.com/awesometic/tonedial/blob/main/docs/TESTING.md#isolated-candidate)
|
|
48
|
+
|
|
49
|
+
## 复制一种语气
|
|
50
|
+
|
|
51
|
+
复制完整配置,或把它作为自定义设置的起点。
|
|
25
52
|
每条命令都会保存并应用完整配置;`--auto` 会批准显示的更改。
|
|
26
53
|
|
|
27
54
|
实际 `tonedial test codex` 回复 · `gpt-6.1-sol` / `high` · 每组配置保留问题的首次回答 · 仅对话,不编辑文件。 [输入与生成记录](https://github.com/awesometic/tonedial/blob/main/docs/media/readme/zh/use-cases.json).
|
|
@@ -35,7 +62,7 @@
|
|
|
35
62
|
话少、直接选定推荐方案,简短汇报做完了什么、还剩什么的编程搭档。
|
|
36
63
|
|
|
37
64
|
<details>
|
|
38
|
-
<summary
|
|
65
|
+
<summary>配置与实际回复</summary>
|
|
39
66
|
|
|
40
67
|
```bash
|
|
41
68
|
tonedial create '彻底放手的氛围编程' --slug vibe-coder --auto \
|
|
@@ -51,8 +78,6 @@ tonedial create '彻底放手的氛围编程' --slug vibe-coder --auto \
|
|
|
51
78
|
|
|
52
79
|
试试这个语气: `tonedial test codex --preset vibe-coder`
|
|
53
80
|
|
|
54
|
-
</details>
|
|
55
|
-
|
|
56
81
|
**Codex**
|
|
57
82
|
|
|
58
83
|
> 老板,我选把 `null` 和 `undefined` 转为空字符串,避免 `trim()` 报错:
|
|
@@ -63,13 +88,14 @@ tonedial create '彻底放手的氛围编程' --slug vibe-coder --auto \
|
|
|
63
88
|
> }
|
|
64
89
|
> ```
|
|
65
90
|
|
|
91
|
+
</details>
|
|
66
92
|
|
|
67
93
|
### 需要安慰的时候
|
|
68
94
|
|
|
69
95
|
温柔、爱夸人,偶尔说几句安慰的话,像个让人安心的搭档。
|
|
70
96
|
|
|
71
97
|
<details>
|
|
72
|
-
<summary
|
|
98
|
+
<summary>配置与实际回复</summary>
|
|
73
99
|
|
|
74
100
|
```bash
|
|
75
101
|
tonedial create '需要安慰的时候' --slug steady-companion --auto \
|
|
@@ -85,8 +111,6 @@ tonedial create '需要安慰的时候' --slug steady-companion --auto \
|
|
|
85
111
|
|
|
86
112
|
试试这个语气: `tonedial test codex --preset steady-companion`
|
|
87
113
|
|
|
88
|
-
</details>
|
|
89
|
-
|
|
90
114
|
**Codex**
|
|
91
115
|
|
|
92
116
|
> 我的选择:搭档,把 `null` 和 `undefined` 当作空字符串,其他输入保留原来的处理方式。
|
|
@@ -101,13 +125,14 @@ tonedial create '需要安慰的时候' --slug steady-companion --auto \
|
|
|
101
125
|
>
|
|
102
126
|
> 你贴出的函数很清楚,指出触发条件很细致,聚焦一个方案很明智,要求直接替换的版本也很周到。辛苦了,我陪你拆问题,慢慢来,放心先喘口气——这个空值小坑已经有块补丁了。
|
|
103
127
|
|
|
128
|
+
</details>
|
|
104
129
|
|
|
105
130
|
### 就喜欢嘴狠的
|
|
106
131
|
|
|
107
132
|
脏话照说,嘲讽照给,活也照样干利索的狠角色。
|
|
108
133
|
|
|
109
134
|
<details>
|
|
110
|
-
<summary
|
|
135
|
+
<summary>配置与实际回复</summary>
|
|
111
136
|
|
|
112
137
|
```bash
|
|
113
138
|
tonedial create '就喜欢嘴狠的' --slug rough-but-reliable --auto \
|
|
@@ -123,8 +148,6 @@ tonedial create '就喜欢嘴狠的' --slug rough-but-reliable --auto \
|
|
|
123
148
|
|
|
124
149
|
试试这个语气: `tonedial test codex --preset rough-but-reliable`
|
|
125
150
|
|
|
126
|
-
</details>
|
|
127
|
-
|
|
128
151
|
**Codex**
|
|
129
152
|
|
|
130
153
|
> 喂,老子选空值返回空字符串这招,一行就把这他妈的崩溃点收拾利索:
|
|
@@ -137,23 +160,11 @@ tonedial create '就喜欢嘴狠的' --slug rough-but-reliable --auto \
|
|
|
137
160
|
>
|
|
138
161
|
> `null` 或 `undefined` 返回 `""`,`" Ada "` 返回 `"Ada"`。
|
|
139
162
|
|
|
163
|
+
</details>
|
|
164
|
+
|
|
140
165
|
Codex 开始对话后,输入上面的问题即可。
|
|
141
166
|
模型与推理强度由 Codex 设置,与 ToneDial 配置分开。
|
|
142
167
|
|
|
143
|
-
## 开始试用
|
|
144
|
-
|
|
145
|
-
需要 **Node 24 或更高版本**和 npm。
|
|
146
|
-
|
|
147
|
-
```bash
|
|
148
|
-
npm install --global tonedial
|
|
149
|
-
tonedial init --tools none --global --lang zh
|
|
150
|
-
tonedial set
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
设置时会先展示文件变更并请求批准。无需连接代理即可先打开面板试用。
|
|
154
|
-
|
|
155
|
-
[连接代理](https://github.com/awesometic/tonedial/blob/main/docs/USAGE.md#connecting-an-agent) · [使用临时设置试用](https://github.com/awesometic/tonedial/blob/main/docs/TESTING.md#isolated-candidate)
|
|
156
|
-
|
|
157
168
|
## 选择你的角色
|
|
158
169
|
|
|
159
170
|
| 预设 | 你 → 代理 |
|
package/dist/cli.js
CHANGED
|
@@ -30,24 +30,28 @@ import { runEject } from "./commands/eject.js";
|
|
|
30
30
|
import { runInit } from "./commands/init.js";
|
|
31
31
|
import { runUnwire, runWire } from "./commands/wire.js";
|
|
32
32
|
import { runTest } from "./commands/test.js";
|
|
33
|
+
import { runOff, runOn } from "./commands/toggle.js";
|
|
33
34
|
export const JSON_SCHEMA = 3;
|
|
34
35
|
/**
|
|
35
36
|
* Stated here rather than read from `package.json`, because reading a package-relative file at
|
|
36
37
|
* run time is the one thing that breaks all three binary packagers at once. A test asserts this
|
|
37
38
|
* equals the manifest, which is the cheap half of the trade.
|
|
38
39
|
*/
|
|
39
|
-
export const VERSION = "0.1.
|
|
40
|
-
const FLAG_NAMES = ["project", "check", "explain", "global", "here", "migrateLegacy", "auto", "dryRun", "fix", "rollback", "fromOperation", "fromPreset", "restorePreset", "preset", "slug", "acceptUsage", "lang", "commands", "tools"];
|
|
40
|
+
export const VERSION = "0.1.7";
|
|
41
|
+
const FLAG_NAMES = ["project", "check", "explain", "global", "here", "migrateLegacy", "auto", "dryRun", "fix", "rollback", "quarantineOperation", "fromOperation", "fromPreset", "restorePreset", "preset", "slug", "acceptUsage", "lang", "commands", "tools"];
|
|
41
42
|
/** Command-line spelling for each flag, where it differs from the field name. */
|
|
42
43
|
const FLAG_SPELLING = {
|
|
43
44
|
project: "project", check: "check", explain: "explain",
|
|
44
45
|
global: "global", auto: "auto", dryRun: "dry-run", fix: "fix", lang: "lang", commands: "commands",
|
|
45
46
|
tools: "tools", rollback: "rollback", here: "here", migrateLegacy: "migrate-legacy",
|
|
47
|
+
quarantineOperation: "quarantine-operation",
|
|
46
48
|
fromOperation: "from-operation", fromPreset: "from-preset", restorePreset: "restore-preset",
|
|
47
49
|
preset: "preset", slug: "slug", acceptUsage: "accept-usage",
|
|
48
50
|
};
|
|
49
51
|
/** Each command declares its accepted flags and positional limit; null means unlimited. */
|
|
50
52
|
const COMMANDS = {
|
|
53
|
+
on: { run: runOn, flags: [], max: 0, context: "state" },
|
|
54
|
+
off: { run: runOff, flags: [], max: 0, context: "state" },
|
|
51
55
|
set: { run: runSet, flags: [], max: null, context: "state" },
|
|
52
56
|
get: { run: runGet, flags: [], max: 1 },
|
|
53
57
|
test: { run: runTest, flags: ["project", "dryRun", "preset", "acceptUsage"], max: 1 },
|
|
@@ -68,7 +72,7 @@ const COMMANDS = {
|
|
|
68
72
|
unwire: { run: runUnwire, flags: ["global", "here", "project", "auto", "dryRun"], max: 1 },
|
|
69
73
|
init: { run: runInit, flags: ["global", "here", "project", "migrateLegacy", "auto", "dryRun", "lang", "commands", "tools"], max: 0, context: "setup" },
|
|
70
74
|
// Doctor checks mutation permission after its recovery-first diagnosis, even with bad config.
|
|
71
|
-
doctor: { run: runDoctor, flags: ["global", "here", "project", "auto", "dryRun", "fix", "rollback", "fromOperation", "fromPreset", "restorePreset", "tools"], max: 0 },
|
|
75
|
+
doctor: { run: runDoctor, flags: ["global", "here", "project", "auto", "dryRun", "fix", "rollback", "quarantineOperation", "fromOperation", "fromPreset", "restorePreset", "tools"], max: 0 },
|
|
72
76
|
eject: { run: runEject, flags: ["global", "here", "project", "auto", "dryRun"], max: 0 },
|
|
73
77
|
};
|
|
74
78
|
const CONNECTION_COMMANDS = ["init", "wire", "doctor", "unwire", "eject"];
|
|
@@ -80,6 +84,8 @@ export const COMMAND_NAMES = Object.keys(COMMANDS);
|
|
|
80
84
|
/** Intent groups share one command inventory between compact and full help. */
|
|
81
85
|
const COMMAND_GROUPS = [
|
|
82
86
|
["tone", [
|
|
87
|
+
["on", "on"],
|
|
88
|
+
["off", "off"],
|
|
83
89
|
["set", "set <key>=<value> …"],
|
|
84
90
|
["get", "get [key]"],
|
|
85
91
|
["address", "address [self user]"],
|
|
@@ -111,6 +117,8 @@ const COMMAND_GROUPS = [
|
|
|
111
117
|
];
|
|
112
118
|
const COMMAND_SYNTAX = COMMAND_GROUPS.flatMap(([, rows]) => rows);
|
|
113
119
|
const COMMAND_EXAMPLES = {
|
|
120
|
+
on: ["tonedial on"],
|
|
121
|
+
off: ["tonedial off"],
|
|
114
122
|
set: ["tonedial set", "tonedial set warmth=5 critique=4", "tonedial set persona.agent=reviewer"],
|
|
115
123
|
get: ["tonedial get", "tonedial get language", "tonedial get speech_level --json"],
|
|
116
124
|
test: ["tonedial test codex --dry-run", "tonedial test claude --project .", "tonedial test codex --preset my-tone --accept-usage"],
|
|
@@ -130,7 +138,8 @@ const COMMAND_EXAMPLES = {
|
|
|
130
138
|
wire: ["tonedial wire codex --here --dry-run"],
|
|
131
139
|
unwire: ["tonedial unwire codex --here --dry-run"],
|
|
132
140
|
init: ["tonedial init --tools none --global --auto", "tonedial init --tools codex --here"],
|
|
133
|
-
doctor: ["tonedial doctor", "tonedial doctor --fix --global --tools none --dry-run"
|
|
141
|
+
doctor: ["tonedial doctor", "tonedial doctor --fix --global --tools none --dry-run",
|
|
142
|
+
"tonedial doctor --fix --quarantine-operation ID --global"],
|
|
134
143
|
eject: ["tonedial eject --here --dry-run"],
|
|
135
144
|
};
|
|
136
145
|
const FLAG_SYNTAX = [
|
|
@@ -145,6 +154,7 @@ const FLAG_SYNTAX = [
|
|
|
145
154
|
["tools", "--tools=LIST"],
|
|
146
155
|
["fix", "--fix"],
|
|
147
156
|
["rollback", "--rollback=ID"],
|
|
157
|
+
["quarantineOperation", "--quarantine-operation=ID"],
|
|
148
158
|
["fromOperation", "--from-operation=ID"],
|
|
149
159
|
["fromPreset", "--from-preset=SLUG"],
|
|
150
160
|
["restorePreset", "--restore-preset=SLUG"],
|
|
@@ -271,16 +281,12 @@ function columns(env, stream, isTTY) {
|
|
|
271
281
|
return isTTY ? stream.columns : undefined;
|
|
272
282
|
}
|
|
273
283
|
/**
|
|
274
|
-
* Words a person reaches for when they want to stop,
|
|
275
|
-
*
|
|
276
|
-
* There is no off switch to give them, so the answer names the acts that do exist and says so.
|
|
284
|
+
* Words a person reaches for when they want to stop, but which are not command spellings.
|
|
277
285
|
*
|
|
278
286
|
* A deliberate ceiling: the list is guessed, not measured — nobody has reported typing any of
|
|
279
|
-
* them. Replace it with a measured one when a user reports a word that is not here
|
|
280
|
-
* if the answer turns out to be the list of seventeen after all. The cost of being wrong is one
|
|
281
|
-
* string; the cost of being right is that `tonedial off` stops dead-ending in that list.
|
|
287
|
+
* them. Replace it with a measured one when a user reports a word that is not here.
|
|
282
288
|
*/
|
|
283
|
-
const STOPPING = ["
|
|
289
|
+
const STOPPING = ["disable", "stop", "quit", "uninstall", "remove", "delete"];
|
|
284
290
|
export async function run(argv, env = process.env, cwd = process.cwd(), isTTY = process.stdout.isTTY === true, isErrTTY = process.stderr.isTTY === true) {
|
|
285
291
|
const options = {
|
|
286
292
|
json: { type: "boolean", default: false },
|
|
@@ -298,6 +304,7 @@ export async function run(argv, env = process.env, cwd = process.cwd(), isTTY =
|
|
|
298
304
|
"dry-run": { type: "boolean", default: false },
|
|
299
305
|
fix: { type: "boolean", default: false },
|
|
300
306
|
rollback: { type: "string" },
|
|
307
|
+
"quarantine-operation": { type: "string" },
|
|
301
308
|
"from-operation": { type: "string" },
|
|
302
309
|
"from-preset": { type: "string" },
|
|
303
310
|
"restore-preset": { type: "string" },
|
|
@@ -383,7 +390,7 @@ export async function run(argv, env = process.env, cwd = process.cwd(), isTTY =
|
|
|
383
390
|
// one: `stop` is two edits from `show` and `delete` three from `reset`, so the match would
|
|
384
391
|
// answer a person trying to leave with the command they were leaving.
|
|
385
392
|
if (STOPPING.includes(name))
|
|
386
|
-
return fail(asJson, say.errors.
|
|
393
|
+
return fail(asJson, say.errors.stoppingHelp, 2, uiErr);
|
|
387
394
|
return fail(asJson, say.errors.notACommand(asJson ? name : humanText(name), COMMAND_NAMES, nearest(name, COMMAND_NAMES)), 2, uiErr);
|
|
388
395
|
}
|
|
389
396
|
const flags = {
|
|
@@ -398,6 +405,7 @@ export async function run(argv, env = process.env, cwd = process.cwd(), isTTY =
|
|
|
398
405
|
dryRun: parsed.values["dry-run"] === true,
|
|
399
406
|
fix: parsed.values.fix === true,
|
|
400
407
|
rollback: typeof parsed.values.rollback === "string" ? parsed.values.rollback : null,
|
|
408
|
+
quarantineOperation: typeof parsed.values["quarantine-operation"] === "string" ? parsed.values["quarantine-operation"] : null,
|
|
401
409
|
fromOperation: typeof parsed.values["from-operation"] === "string" ? parsed.values["from-operation"] : null,
|
|
402
410
|
fromPreset: typeof parsed.values["from-preset"] === "string" ? parsed.values["from-preset"] : null,
|
|
403
411
|
restorePreset: typeof parsed.values["restore-preset"] === "string" ? parsed.values["restore-preset"] : null,
|
|
@@ -424,9 +432,9 @@ export async function run(argv, env = process.env, cwd = process.cwd(), isTTY =
|
|
|
424
432
|
if (command.max !== null && rest.length > command.max) {
|
|
425
433
|
return fail(asJson, say.errors.tooManyArguments(name, command.max, rest.length), 2, uiErr);
|
|
426
434
|
}
|
|
427
|
-
if (name === "doctor" && ((!flags.fix && (flags.fromOperation !== null || flags.fromPreset !== null || flags.restorePreset !== null || flags.tools !== null)) ||
|
|
435
|
+
if (name === "doctor" && ((!flags.fix && (flags.quarantineOperation !== null || flags.fromOperation !== null || flags.fromPreset !== null || flags.restorePreset !== null || flags.tools !== null)) ||
|
|
428
436
|
(flags.restorePreset !== null && flags.fromOperation === null) ||
|
|
429
|
-
(flags.rollback !== null && (flags.fromOperation !== null || flags.fromPreset !== null || flags.restorePreset !== null || flags.tools !== null)))) {
|
|
437
|
+
(flags.rollback !== null && (flags.quarantineOperation !== null || flags.fromOperation !== null || flags.fromPreset !== null || flags.restorePreset !== null || flags.tools !== null)))) {
|
|
430
438
|
return fail(asJson, say.recovery.invalidOptions, 2, uiErr);
|
|
431
439
|
}
|
|
432
440
|
try {
|
package/dist/commands/compile.js
CHANGED
|
@@ -107,7 +107,7 @@ export async function runCompile(ctx) {
|
|
|
107
107
|
` ${ctx.ui.dim(artifactSize(size, ctx.t))}`,
|
|
108
108
|
...nextStep(check && !current ? "tonedial compile" : "tonedial show", ctx.ui),
|
|
109
109
|
],
|
|
110
|
-
json: { path: ctx.paths.tone, agentsPath: ctx.paths.agents, written: !check, ...size },
|
|
110
|
+
json: { path: ctx.paths.tone, agentsPath: ctx.paths.agents, enabled: config.enabled, written: !check, ...size },
|
|
111
111
|
warnings: [],
|
|
112
112
|
};
|
|
113
113
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -11,6 +11,7 @@ import { join, relative } from "node:path";
|
|
|
11
11
|
import { AGENT_TOOLS, CONTEXT_ARTIFACT_FILENAME, CONTEXT_GUIDE_FILENAME, UPDATE_COMMAND, connectionScopeFlag, onPath, readerPath, resolveAgentPaths, shimScript, installShape, canonicalDestination, shellQuote } from "../core/paths.js";
|
|
12
12
|
import { SKILL_NAME } from "../core/skill.js";
|
|
13
13
|
import { GUIDE_FORMAT_MARKER } from "../core/reader.js";
|
|
14
|
+
import { DELIVERY_OFF_MARKER } from "../core/anchors.js";
|
|
14
15
|
import { inspect as inspectSkill, skillPath } from "../wiring/skill.js";
|
|
15
16
|
import { candidateRcPaths, pathLineState, rcPath, withPathLine } from "../wiring/shell.js";
|
|
16
17
|
import { SEED_VERSION, inspectContextLayout, inspectSeeds, recordedSeedSelection, } from "../core/seeds.js";
|
|
@@ -94,7 +95,9 @@ function readerHealth(ctx, config, configRead, seeds, pairRequired) {
|
|
|
94
95
|
temporary = mkdtempSync(join(tmpdir(), "tonedial-reader-check-"));
|
|
95
96
|
temporary = realpathSync(temporary);
|
|
96
97
|
const guide = `${GUIDE_FORMAT_MARKER}\nToneDial diagnostic guide.\n`;
|
|
97
|
-
const artifact =
|
|
98
|
+
const artifact = config.enabled
|
|
99
|
+
? "ToneDial diagnostic tone — 한글 🌿.\n"
|
|
100
|
+
: `<!-- generated by tonedial · preset: unsaved -->\n<!-- tonedial-style:1 verbosity=3 care=3 -->\n${DELIVERY_OFF_MARKER}\nToneDial diagnostic tone — 한글 🌿.\n`;
|
|
98
101
|
writeFileSync(join(temporary, CONTEXT_GUIDE_FILENAME), guide, { flag: "wx", mode: 0o600 });
|
|
99
102
|
writeFileSync(join(temporary, CONTEXT_ARTIFACT_FILENAME), artifact, { flag: "wx", mode: 0o600 });
|
|
100
103
|
const body = guide + "\n\n" + artifact;
|
|
@@ -110,7 +113,8 @@ function readerHealth(ctx, config, configRead, seeds, pairRequired) {
|
|
|
110
113
|
maxBuffer: 8192,
|
|
111
114
|
});
|
|
112
115
|
const expected = provider === "antigravity"
|
|
113
|
-
? JSON.stringify({ injectSteps: [{ ephemeralMessage: body }] }) + "\n"
|
|
116
|
+
? JSON.stringify({ injectSteps: config.enabled ? [{ ephemeralMessage: body }] : [] }) + "\n"
|
|
117
|
+
: config.enabled ? body : "";
|
|
114
118
|
if (probe.error || probe.status !== 0 || probe.stderr !== "" || probe.stdout !== expected)
|
|
115
119
|
return result;
|
|
116
120
|
}
|
|
@@ -472,6 +476,20 @@ export async function runDoctor(ctx, _args) {
|
|
|
472
476
|
if (!ctx.flags.fix)
|
|
473
477
|
return doctorReport(ctx);
|
|
474
478
|
const app = await runAppRebuild(ctx);
|
|
479
|
+
if (app.quarantineOnly) {
|
|
480
|
+
const rollbackLines = app.json.applied === true ? recoveryRollbackLines(ctx.t, app.json.operationId, null) : [];
|
|
481
|
+
const remaining = Array.isArray(app.json.remainingInvalidOperations)
|
|
482
|
+
? app.json.remainingInvalidOperations.filter((id) => typeof id === "string") : [];
|
|
483
|
+
const next = typeof app.json.next === "string" ? app.json.next : null;
|
|
484
|
+
return {
|
|
485
|
+
...app,
|
|
486
|
+
lines: [...app.lines, ...rollbackLines, ...(next === null ? [] : [
|
|
487
|
+
` ${ctx.t.recovery.quarantineRemaining(remaining.length)}`, "", ` ${next}`,
|
|
488
|
+
])],
|
|
489
|
+
json: { ...app.json, root: ctx.agent.root, stateRoot: ctx.paths.root, agentRoot: ctx.agent.root,
|
|
490
|
+
live: ctx.agent.live, fixed: app.json.applied === true, appHealthy: false },
|
|
491
|
+
};
|
|
492
|
+
}
|
|
475
493
|
if (ctx.flags.dryRun) {
|
|
476
494
|
const native = await runRebuildNative(ctx, app.legacy, app.proposedConfig);
|
|
477
495
|
return {
|
|
@@ -517,10 +535,14 @@ async function doctorReport(ctx, showRollbackCommands = true, showCodexAction =
|
|
|
517
535
|
if (inspected.lock.state !== "none" || inspected.journals.some(({ state }) => state === "pending" || state === "invalid")) {
|
|
518
536
|
const owner = inspected.lock.owner;
|
|
519
537
|
const blocking = inspected.journals.filter(({ state }) => state === "pending" || state === "invalid");
|
|
538
|
+
const invalid = blocking.filter(({ state }) => state === "invalid");
|
|
539
|
+
const quarantineable = inspected.lock.state === "none" && blocking.length === invalid.length && invalid.length > 0;
|
|
540
|
+
const singleQuarantine = quarantineable && invalid.length === 1;
|
|
520
541
|
const shown = ctx.flags.verbose ? inspected.journals : inspected.journals.filter(({ operationId, state }) => state === "pending" || state === "invalid" || operationId === owner?.operationId);
|
|
521
542
|
const lines = [
|
|
522
|
-
...wrap(ctx.t.doctor.recoveryPending, ctx.ui.width - 2).map(line => ` ${ctx.ui.warn(line)}`),
|
|
523
|
-
...shown.
|
|
543
|
+
...wrap(quarantineable ? ctx.t.doctor.invalidRecoveryPending : ctx.t.doctor.recoveryPending, ctx.ui.width - 2).map(line => ` ${ctx.ui.warn(line)}`),
|
|
544
|
+
...shown.filter(({ operationId }) => !singleQuarantine || operationId !== invalid[0].operationId)
|
|
545
|
+
.map(({ operationId, state }) => ` ${operationId} ${ctx.t.doctor.operationState[state]}`),
|
|
524
546
|
];
|
|
525
547
|
if (owner !== null && !shown.some(({ operationId }) => operationId === owner.operationId))
|
|
526
548
|
lines.push(` ${owner.operationId}`);
|
|
@@ -540,7 +562,14 @@ async function doctorReport(ctx, showRollbackCommands = true, showCodexAction =
|
|
|
540
562
|
}
|
|
541
563
|
}
|
|
542
564
|
}
|
|
543
|
-
|
|
565
|
+
if (quarantineable) {
|
|
566
|
+
lines.push("", ...(singleQuarantine
|
|
567
|
+
? wrap(ctx.t.recovery.quarantineCandidate(invalid[0].operationId), ctx.ui.width - 2)
|
|
568
|
+
: wrap(ctx.t.recovery.quarantineRemaining(invalid.length), ctx.ui.width - 2)).map(line => ` ${line}`), ...nextStep(repairCommand(ctx, `doctor --fix --quarantine-operation ${invalid[0].operationId}`), ctx.ui).slice(1));
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
lines.push(...nextStep(`${repairCommand(ctx, "doctor")} --json`, ctx.ui));
|
|
572
|
+
}
|
|
544
573
|
return {
|
|
545
574
|
lines,
|
|
546
575
|
// The damaged config is deliberately not parsed here. `operation` is the only safe context
|
package/dist/commands/get.js
CHANGED
|
@@ -33,9 +33,11 @@ export function runGet(ctx, args) {
|
|
|
33
33
|
return [` ${ctx.ui.accent(padEnd(key, column))} ${first ?? ""}`,
|
|
34
34
|
...rest.map(line => `${" ".repeat(column + 4)}${line}`)];
|
|
35
35
|
});
|
|
36
|
+
if (requested === undefined && !config.enabled)
|
|
37
|
+
lines.unshift(` ${ctx.ui.bold(ctx.t.delivery.status(false))}`, "");
|
|
36
38
|
if (requested !== undefined) {
|
|
37
39
|
const copy = catalogue(isLanguage(config.language) ? config.language : detectLanguage(ctx.env));
|
|
38
40
|
lines.push("", ...settingHelp(requested, copy).flatMap(hint => wrap(hint, ctx.ui.width - 2).map(line => ` ${ctx.ui.dim(line)}`)));
|
|
39
41
|
}
|
|
40
|
-
return { lines, json: { values } };
|
|
42
|
+
return { lines, json: { enabled: config.enabled, values } };
|
|
41
43
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -297,6 +297,9 @@ async function rebuildExisting(entry) {
|
|
|
297
297
|
const resultHeading = ctx.canPrompt && !ctx.flags.auto && !ctx.flags.dryRun
|
|
298
298
|
? section(copy.init.resultHeading, ctx.ui) : [];
|
|
299
299
|
const app = await runAppRebuild(ctx);
|
|
300
|
+
if (app.quarantineOnly) {
|
|
301
|
+
return { ...app, lines: [...(ctx.canPrompt ? [] : introduction), ...resultHeading, ...app.lines] };
|
|
302
|
+
}
|
|
300
303
|
if (ctx.flags.dryRun) {
|
|
301
304
|
const answers = await ask(ctx, copy, false, selection);
|
|
302
305
|
const tools = selection ?? answers.tools;
|
package/dist/commands/rebuild.js
CHANGED
|
@@ -11,14 +11,14 @@ import { recoverLegacyTone } from "../core/legacy-tone.js";
|
|
|
11
11
|
import { connectionScopeFlag, exportAliasesState, stateOverlapsInstructions } from "../core/paths.js";
|
|
12
12
|
import { applyPreset, matchesPreset, assertPresetSelection, BUNDLED, customPresetDir, inspectPresetLibrary, parsePreset, PRESET_MAX_BYTES, resolvePreset, serializePreset, SLUG_PATTERN, } from "../core/presets.js";
|
|
13
13
|
import { contextArtifactTargets, generatedSeedSelection, planGeneratedSeedRebuild, seedFiles, SEED_VERSION, } from "../core/seeds.js";
|
|
14
|
-
import { inspectOperations, readJournal, retireOperationForAppRebuild, } from "../core/operations.js";
|
|
14
|
+
import { inspectOperations, readJournal, retireOperationForAppRebuild, serializeOperationQuarantine, } from "../core/operations.js";
|
|
15
15
|
import { assertFresh, captureRawEntry, gate, planEntryRebuild, rawReadCheck, validateFileAction, } from "../wiring/apply.js";
|
|
16
16
|
import { confirm } from "../ui/prompt.js";
|
|
17
17
|
import { wrap } from "../ui/width.js";
|
|
18
18
|
import { parseToolSelection } from "../wiring/targets.js";
|
|
19
19
|
import { riskWarnings } from "../ui/format.js";
|
|
20
20
|
const CONFIG_MAX_BYTES = 1024 * 1024;
|
|
21
|
-
const scalarFields = ["preset", "unsaved", "safeword", "speech_level", "language"];
|
|
21
|
+
const scalarFields = ["enabled", "preset", "unsaved", "safeword", "speech_level", "language"];
|
|
22
22
|
const personaFields = ["age", "gender", "note", "user", "agent"];
|
|
23
23
|
function capturedText(capture) {
|
|
24
24
|
if (capture.type !== "regular" || capture.bytes === null)
|
|
@@ -218,13 +218,83 @@ function privateCapture(capture) {
|
|
|
218
218
|
boundary: capture.boundary, boundaryIdentity: capture.boundaryIdentity,
|
|
219
219
|
parents: capture.parents.map(parent => ({ ...parent })) };
|
|
220
220
|
}
|
|
221
|
+
function planOperationQuarantine(ctx, entry) {
|
|
222
|
+
const capture = captureRawEntry(entry.path, ctx.t, { within: ctx.paths.root });
|
|
223
|
+
if (capture.type !== "regular" || capture.bytes === null)
|
|
224
|
+
throw new ConfigError(ctx.t.recovery.quarantineUnavailable(entry.operationId));
|
|
225
|
+
return planEntryRebuild(capture, serializeOperationQuarantine(ctx.paths, entry.operationId, capture.bytes), ctx.t, { reason: "quarantine-operation", mode: capture.mode ?? 0o600 }).action;
|
|
226
|
+
}
|
|
227
|
+
async function quarantineOneOperation(ctx, entry, remaining) {
|
|
228
|
+
const action = planOperationQuarantine(ctx, entry);
|
|
229
|
+
const presentation = [ctx.t.recovery.quarantine(entry.operationId)]
|
|
230
|
+
.flatMap(line => wrap(line, ctx.ui.width - 2).map(part => ` ${part}`));
|
|
231
|
+
const result = await gate([], {
|
|
232
|
+
paths: ctx.paths, operationActions: [action], retention: { kind: "migration", reason: "app-rebuild" },
|
|
233
|
+
auto: ctx.flags.auto, dryRun: ctx.flags.dryRun, verbose: ctx.flags.verbose, ui: ctx.ui, t: ctx.t,
|
|
234
|
+
heading: ctx.t.recovery.heading, interactive: () => ctx.canPrompt, ...(ctx.canPrompt ? {} : { out: () => { } }),
|
|
235
|
+
recoveryPresentation: { lines: presentation,
|
|
236
|
+
failureMessage: (id, phase, path, code) => `${ctx.t.recovery.failure(id, phase, path, code)}\ntonedial doctor ${connectionScopeFlag(ctx.agent)} --json` },
|
|
237
|
+
verify: () => {
|
|
238
|
+
const state = inspectOperations(ctx.paths).journals.find(({ operationId }) => operationId === entry.operationId)?.state;
|
|
239
|
+
if (state !== "quarantined")
|
|
240
|
+
throw new ConfigError(ctx.t.recovery.sourceInvalid);
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
const lines = [...result.lines];
|
|
244
|
+
if (result.reason === "written") {
|
|
245
|
+
lines.push(` ${ctx.t.recovery.quarantineComplete(entry.operationId)}`);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
lines.push(` ${result.reason === "dry-run" ? ctx.t.wiring.dryRun : ctx.t.wiring.declined}`);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
lines,
|
|
252
|
+
json: {
|
|
253
|
+
appRebuilt: false, applied: result.applied, reason: result.reason, operationId: result.operationId ?? null,
|
|
254
|
+
root: ctx.paths.root, quarantinedOperation: entry.operationId, remainingInvalidOperations: [...remaining],
|
|
255
|
+
next: result.applied && remaining.length > 0
|
|
256
|
+
? `tonedial doctor --fix --quarantine-operation ${remaining[0]} ${connectionScopeFlag(ctx.agent)}` : null,
|
|
257
|
+
nativeAttention: false, activationVerified: false, diff: result.diff,
|
|
258
|
+
},
|
|
259
|
+
rebuiltConfig: null,
|
|
260
|
+
proposedConfig: null,
|
|
261
|
+
legacy: null,
|
|
262
|
+
quarantineOnly: true,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
221
265
|
export async function runAppRebuild(ctx) {
|
|
222
266
|
const t = ctx.t;
|
|
223
267
|
const operations = inspectOperations(ctx.paths);
|
|
224
|
-
const pending = operations.journals.filter(entry => entry.state === "pending"
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
268
|
+
const pending = operations.journals.filter(entry => entry.state === "pending");
|
|
269
|
+
const invalid = operations.journals.filter(entry => entry.state === "invalid");
|
|
270
|
+
let quarantined = invalid.find(entry => entry.operationId === ctx.flags.quarantineOperation) ?? null;
|
|
271
|
+
if (ctx.flags.quarantineOperation && quarantined === null)
|
|
272
|
+
throw new ConfigError(t.recovery.quarantineUnavailable(ctx.flags.quarantineOperation));
|
|
273
|
+
if (invalid.length > 0 && quarantined === null && operations.lock.state === "none" && pending.length === 0 && invalid.length === 1 &&
|
|
274
|
+
!ctx.flags.auto && ctx.canPrompt) {
|
|
275
|
+
const candidate = invalid[0];
|
|
276
|
+
const input = createInterface({ input: process.stdin, output: process.stdout });
|
|
277
|
+
try {
|
|
278
|
+
const notice = t.recovery.quarantineCandidate(candidate.operationId).split("\n").flatMap(line => line === candidate.operationId ? [line] : wrap(line, ctx.ui.width - 2));
|
|
279
|
+
process.stdout.write(`${notice.map(line => ` ${line}`).join("\n")}\n`);
|
|
280
|
+
const question = wrap(t.recovery.quarantineQuestion, ctx.ui.width - 3).map(line => ` ${line}`).join("\n");
|
|
281
|
+
const answer = (await input.question(`${question} `)).trim();
|
|
282
|
+
if (answer === "")
|
|
283
|
+
throw new ConfigError(t.recovery.cancelled);
|
|
284
|
+
if (answer !== candidate.operationId)
|
|
285
|
+
throw new ConfigError(t.recovery.operationMismatch);
|
|
286
|
+
quarantined = candidate;
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
input.close();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (invalid.length > 0 && quarantined === null) {
|
|
293
|
+
const candidate = invalid[0];
|
|
294
|
+
const command = `tonedial doctor --fix --quarantine-operation ${candidate.operationId} ${connectionScopeFlag(ctx.agent)}`;
|
|
295
|
+
throw new ConfigError(t.recovery.quarantineRequired(candidate.operationId, command));
|
|
296
|
+
}
|
|
297
|
+
if (quarantined !== null && (operations.lock.state !== "none" || pending.length > 0)) {
|
|
228
298
|
throw new ConfigError(t.doctor.recoveryPending);
|
|
229
299
|
}
|
|
230
300
|
const scopeExplicit = ctx.scopeExplicit ?? (ctx.flags.global || ctx.flags.here === true || ctx.flags.project !== null);
|
|
@@ -232,6 +302,14 @@ export async function runAppRebuild(ctx) {
|
|
|
232
302
|
throw new ConfigError(t.errors.scopeRequired);
|
|
233
303
|
if (ctx.flags.tools !== null)
|
|
234
304
|
parseToolSelection(ctx.flags.tools, true, t);
|
|
305
|
+
if (quarantined !== null && invalid.length > 1) {
|
|
306
|
+
return quarantineOneOperation(ctx, quarantined, invalid.filter(entry => entry !== quarantined).map(entry => entry.operationId));
|
|
307
|
+
}
|
|
308
|
+
const blocked = operations.lock.state !== "none" || pending.length > 0;
|
|
309
|
+
if (blocked && (operations.lock.state === "live" || operations.lock.state === "unknown" ||
|
|
310
|
+
pending.some(entry => entry.journal === null) || (!ctx.flags.fromOperation && (ctx.flags.auto || !ctx.canPrompt)))) {
|
|
311
|
+
throw new ConfigError(t.doctor.recoveryPending);
|
|
312
|
+
}
|
|
235
313
|
const configCapture = captureRawEntry(ctx.paths.config, t, { within: ctx.paths.root, maxBytes: CONFIG_MAX_BYTES });
|
|
236
314
|
const selected = ctx.flags.fromOperation ? selectedJournal(ctx, ctx.flags.fromOperation) : null;
|
|
237
315
|
let retirementId = null;
|
|
@@ -405,7 +483,11 @@ export async function runAppRebuild(ctx) {
|
|
|
405
483
|
config.managedSeeds = seeds.records;
|
|
406
484
|
config.seedVersion = SEED_VERSION;
|
|
407
485
|
const artifact = compile(toCompileInput(config));
|
|
408
|
-
|
|
486
|
+
let quarantinePlan = null;
|
|
487
|
+
if (quarantined !== null) {
|
|
488
|
+
quarantinePlan = planOperationQuarantine(ctx, quarantined);
|
|
489
|
+
}
|
|
490
|
+
const operationActions = [...(quarantinePlan ? [quarantinePlan] : []), ...seeds.operationActions];
|
|
409
491
|
for (const target of artifacts) {
|
|
410
492
|
const capture = target.capture ?? captureRawEntry(target.path, t, { within: ctx.paths.root, allowLink: true });
|
|
411
493
|
const plan = planEntryRebuild(capture, artifact, t, { reason: "generated" });
|
|
@@ -457,6 +539,7 @@ export async function runAppRebuild(ctx) {
|
|
|
457
539
|
...(missingFields.length ? [` ${t.recovery.missing(missingFields)}`] : []),
|
|
458
540
|
...(copies.length ? [` ${t.recovery.copies(copies)}`] : []),
|
|
459
541
|
...(nativeAttention ? [` ${t.recovery.nativePending}`] : []),
|
|
542
|
+
...(quarantined ? [` ${t.recovery.quarantine(quarantined.operationId)}`] : []),
|
|
460
543
|
...(retirementId ? [` ${t.recovery.retire(retirementId)}`] : []),
|
|
461
544
|
].flatMap(line => wrap(line.trimStart(), ctx.ui.width - 2).map(part => ` ${part}`));
|
|
462
545
|
const options = {
|
|
@@ -484,6 +567,12 @@ export async function runAppRebuild(ctx) {
|
|
|
484
567
|
if (capturedText(captureRawEntry(target.path, t, { within: ctx.paths.root })) !== artifact)
|
|
485
568
|
throw mismatch();
|
|
486
569
|
}
|
|
570
|
+
if (quarantined !== null) {
|
|
571
|
+
verificationPath = quarantined.path;
|
|
572
|
+
const entry = inspectOperations(ctx.paths).journals.find(({ operationId }) => operationId === quarantined.operationId);
|
|
573
|
+
if (entry?.state !== "quarantined")
|
|
574
|
+
throw mismatch();
|
|
575
|
+
}
|
|
487
576
|
verificationPath = ctx.paths.tone;
|
|
488
577
|
if (capturedText(captureRawEntry(ctx.paths.tone, t, { within: ctx.paths.root })) !== artifact)
|
|
489
578
|
throw mismatch();
|
|
@@ -521,7 +610,7 @@ export async function runAppRebuild(ctx) {
|
|
|
521
610
|
if (!ctx.flags.auto) {
|
|
522
611
|
process.stdout.write(`${(preview.reason === "nothing" ? [...preview.lines, ...presentation] : preview.lines).join("\n")}\n`);
|
|
523
612
|
if (await confirm(` ${t.wiring.applyQuestion(preview.diff.length)}`) === "no") {
|
|
524
|
-
return { lines: [` ${t.wiring.declined}`], json: { appRebuilt: false, applied: false, reason: "declined", diff: preview.diff }, rebuiltConfig: null, proposedConfig: config, legacy };
|
|
613
|
+
return { lines: [` ${t.wiring.declined}`], json: { appRebuilt: false, applied: false, reason: "declined", diff: preview.diff }, rebuiltConfig: null, proposedConfig: config, legacy, quarantineOnly: false };
|
|
525
614
|
}
|
|
526
615
|
}
|
|
527
616
|
const snapshots = [configCapture, ...seeds.reads, ...artifacts.flatMap(target => target.capture === null ? [] : [target.capture]),
|
|
@@ -551,12 +640,13 @@ export async function runAppRebuild(ctx) {
|
|
|
551
640
|
lines.push(...presentation);
|
|
552
641
|
lines.push(` ${applied ? result.reason === "nothing" ? t.recovery.unchanged : t.recovery.rebuilt : result.reason === "dry-run" ? t.wiring.dryRun : t.wiring.declined}`);
|
|
553
642
|
return {
|
|
554
|
-
lines, rebuiltConfig: applied ? config : null, proposedConfig: config, legacy,
|
|
643
|
+
lines, rebuiltConfig: applied ? config : null, proposedConfig: config, legacy, quarantineOnly: false,
|
|
555
644
|
json: {
|
|
556
645
|
appRebuilt: applied, applied: result.applied, reason: result.reason, operationId: result.operationId ?? null,
|
|
557
646
|
root: ctx.paths.root, source, recovered: [...kept], defaulted: [...defaulted], invalid: damagedFields,
|
|
558
647
|
seeded: seeds.labels, chars: [...artifact].length, riskAxes: riskyAxes(config.axes),
|
|
559
648
|
recoveredPresets: copies, restoredPreset: restoredPreset?.slug ?? null, retiredOperation: retirementId,
|
|
649
|
+
quarantinedOperation: quarantined?.operationId ?? null,
|
|
560
650
|
retiredExternalActions,
|
|
561
651
|
nativeAttention, activationVerified: false, diff: result.diff,
|
|
562
652
|
},
|
package/dist/commands/show.js
CHANGED
|
@@ -69,7 +69,8 @@ export function runShow(ctx, args) {
|
|
|
69
69
|
const gauges = [gaugeHeader(ui, ctx.t), ...gaugeBlock(axes, ui)];
|
|
70
70
|
const gaugeWidth = Math.max(...gauges.map((line) => maxCells(stripVTControlCharacters(line))));
|
|
71
71
|
const detailWidth = ui.terminal && ui.width >= 119 ? ui.width - gaugeWidth - 4 : ui.width - 2;
|
|
72
|
-
const details = [ui.bold(ctx.t.artifact.settingsHeading)
|
|
72
|
+
const details = [ui.bold(ctx.t.artifact.settingsHeading),
|
|
73
|
+
...(config.enabled ? [] : [ctx.t.delivery.status(false)])];
|
|
73
74
|
if (personaSet) {
|
|
74
75
|
const bits = [
|
|
75
76
|
age ? humanText(age) : null,
|
|
@@ -163,6 +164,7 @@ export function runShow(ctx, args) {
|
|
|
163
164
|
json: {
|
|
164
165
|
preset: preview ? preview.slug : config.preset,
|
|
165
166
|
preview: preview !== null,
|
|
167
|
+
enabled: config.enabled,
|
|
166
168
|
activeKnown: preview !== null || !activeConflict,
|
|
167
169
|
// Layered rather than flattened: the skill has to tell a forbidden contradiction from a
|
|
168
170
|
// cosmetic one, and a joined string cannot carry that.
|
package/dist/commands/test.js
CHANGED
|
@@ -58,7 +58,8 @@ export async function runTest(ctx, args) {
|
|
|
58
58
|
}
|
|
59
59
|
input = toCompileInput(withPreset(config, preview));
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
// A test is an explicit one-shot preview, independent of the persistent native delivery switch.
|
|
62
|
+
const tone = compile({ ...input, enabled: true });
|
|
62
63
|
const installed = PROVIDERS.map((provider) => ({ provider, program: onPath(provider, ctx.env) }))
|
|
63
64
|
.filter((entry) => entry.program !== null);
|
|
64
65
|
let selected = requested === undefined ? undefined : installed.find((entry) => entry.provider === requested);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Pause or resume native tone delivery while preserving the saved settings and full artifact.
|
|
2
|
+
import { serializeConfig } from "../core/config.js";
|
|
3
|
+
import { wrap } from "../ui/width.js";
|
|
4
|
+
import { applyPlanned, planArtifact, planConfig } from "./compile.js";
|
|
5
|
+
async function setDelivery(ctx, enabled) {
|
|
6
|
+
const { change: configChange, config } = planConfig(ctx);
|
|
7
|
+
const changed = config.enabled !== enabled;
|
|
8
|
+
config.enabled = enabled;
|
|
9
|
+
const { change: toneChange, size } = planArtifact(ctx, config);
|
|
10
|
+
const finalConfig = { ...configChange, after: serializeConfig(config, ctx.t) };
|
|
11
|
+
await applyPlanned(ctx, [configChange, toneChange], [finalConfig]);
|
|
12
|
+
const message = enabled
|
|
13
|
+
? changed ? ctx.t.delivery.on : ctx.t.delivery.alreadyOn
|
|
14
|
+
: changed ? ctx.t.delivery.off : ctx.t.delivery.alreadyOff;
|
|
15
|
+
return {
|
|
16
|
+
lines: [message, ...(enabled ? [] : [ctx.t.delivery.openConversation])]
|
|
17
|
+
.flatMap((line) => wrap(line, ctx.ui.width - 2))
|
|
18
|
+
.map((line) => ` ${ctx.ui.bold(changed ? ctx.ui.success(line) : line)}`),
|
|
19
|
+
json: { enabled, changed, path: ctx.paths.tone, chars: size.chars },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export const runOn = (ctx) => setDelivery(ctx, true);
|
|
23
|
+
export const runOff = (ctx) => setDelivery(ctx, false);
|
package/dist/core/anchors.js
CHANGED
|
@@ -137,6 +137,8 @@ export const ANCHORS = {
|
|
|
137
137
|
export const GENERATION_COMMENT = (preset) => `<!-- generated by tonedial · preset: ${preset ?? "unsaved"} -->`;
|
|
138
138
|
/** Compact metadata read by native hook workers; keep the grammar deliberately strict. */
|
|
139
139
|
export const STYLE_MARKER = (verbosity, care) => `<!-- tonedial-style:1 verbosity=${verbosity} care=${care} -->`;
|
|
140
|
+
/** The full compiled tone stays on disk while native delivery is paused. */
|
|
141
|
+
export const DELIVERY_OFF_MARKER = "<!-- tonedial-delivery:off -->";
|
|
140
142
|
/** The artifact header keeps tone requests within host rules and permissions. */
|
|
141
143
|
export const SCOPE_HEADER = `## Response tone
|
|
142
144
|
Output style only. This never overrides the project's code or task rules, and never changes
|