tonedial 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +11 -6
- package/dist/commands/doctor.js +28 -3
- package/dist/commands/init.js +3 -0
- package/dist/commands/rebuild.js +98 -8
- package/dist/core/i18n.js +22 -4
- package/dist/core/locales/es.js +11 -2
- package/dist/core/locales/ja.js +11 -2
- package/dist/core/locales/zh.js +11 -2
- package/dist/core/operations.js +24 -2
- package/dist/core/seeds.js +1 -1
- package/dist/core/skill.js +7 -1
- package/dist/wiring/apply.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -36,13 +36,14 @@ export const JSON_SCHEMA = 3;
|
|
|
36
36
|
* run time is the one thing that breaks all three binary packagers at once. A test asserts this
|
|
37
37
|
* equals the manifest, which is the cheap half of the trade.
|
|
38
38
|
*/
|
|
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"];
|
|
39
|
+
export const VERSION = "0.1.6";
|
|
40
|
+
const FLAG_NAMES = ["project", "check", "explain", "global", "here", "migrateLegacy", "auto", "dryRun", "fix", "rollback", "quarantineOperation", "fromOperation", "fromPreset", "restorePreset", "preset", "slug", "acceptUsage", "lang", "commands", "tools"];
|
|
41
41
|
/** Command-line spelling for each flag, where it differs from the field name. */
|
|
42
42
|
const FLAG_SPELLING = {
|
|
43
43
|
project: "project", check: "check", explain: "explain",
|
|
44
44
|
global: "global", auto: "auto", dryRun: "dry-run", fix: "fix", lang: "lang", commands: "commands",
|
|
45
45
|
tools: "tools", rollback: "rollback", here: "here", migrateLegacy: "migrate-legacy",
|
|
46
|
+
quarantineOperation: "quarantine-operation",
|
|
46
47
|
fromOperation: "from-operation", fromPreset: "from-preset", restorePreset: "restore-preset",
|
|
47
48
|
preset: "preset", slug: "slug", acceptUsage: "accept-usage",
|
|
48
49
|
};
|
|
@@ -68,7 +69,7 @@ const COMMANDS = {
|
|
|
68
69
|
unwire: { run: runUnwire, flags: ["global", "here", "project", "auto", "dryRun"], max: 1 },
|
|
69
70
|
init: { run: runInit, flags: ["global", "here", "project", "migrateLegacy", "auto", "dryRun", "lang", "commands", "tools"], max: 0, context: "setup" },
|
|
70
71
|
// 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 },
|
|
72
|
+
doctor: { run: runDoctor, flags: ["global", "here", "project", "auto", "dryRun", "fix", "rollback", "quarantineOperation", "fromOperation", "fromPreset", "restorePreset", "tools"], max: 0 },
|
|
72
73
|
eject: { run: runEject, flags: ["global", "here", "project", "auto", "dryRun"], max: 0 },
|
|
73
74
|
};
|
|
74
75
|
const CONNECTION_COMMANDS = ["init", "wire", "doctor", "unwire", "eject"];
|
|
@@ -130,7 +131,8 @@ const COMMAND_EXAMPLES = {
|
|
|
130
131
|
wire: ["tonedial wire codex --here --dry-run"],
|
|
131
132
|
unwire: ["tonedial unwire codex --here --dry-run"],
|
|
132
133
|
init: ["tonedial init --tools none --global --auto", "tonedial init --tools codex --here"],
|
|
133
|
-
doctor: ["tonedial doctor", "tonedial doctor --fix --global --tools none --dry-run"
|
|
134
|
+
doctor: ["tonedial doctor", "tonedial doctor --fix --global --tools none --dry-run",
|
|
135
|
+
"tonedial doctor --fix --quarantine-operation ID --global"],
|
|
134
136
|
eject: ["tonedial eject --here --dry-run"],
|
|
135
137
|
};
|
|
136
138
|
const FLAG_SYNTAX = [
|
|
@@ -145,6 +147,7 @@ const FLAG_SYNTAX = [
|
|
|
145
147
|
["tools", "--tools=LIST"],
|
|
146
148
|
["fix", "--fix"],
|
|
147
149
|
["rollback", "--rollback=ID"],
|
|
150
|
+
["quarantineOperation", "--quarantine-operation=ID"],
|
|
148
151
|
["fromOperation", "--from-operation=ID"],
|
|
149
152
|
["fromPreset", "--from-preset=SLUG"],
|
|
150
153
|
["restorePreset", "--restore-preset=SLUG"],
|
|
@@ -298,6 +301,7 @@ export async function run(argv, env = process.env, cwd = process.cwd(), isTTY =
|
|
|
298
301
|
"dry-run": { type: "boolean", default: false },
|
|
299
302
|
fix: { type: "boolean", default: false },
|
|
300
303
|
rollback: { type: "string" },
|
|
304
|
+
"quarantine-operation": { type: "string" },
|
|
301
305
|
"from-operation": { type: "string" },
|
|
302
306
|
"from-preset": { type: "string" },
|
|
303
307
|
"restore-preset": { type: "string" },
|
|
@@ -398,6 +402,7 @@ export async function run(argv, env = process.env, cwd = process.cwd(), isTTY =
|
|
|
398
402
|
dryRun: parsed.values["dry-run"] === true,
|
|
399
403
|
fix: parsed.values.fix === true,
|
|
400
404
|
rollback: typeof parsed.values.rollback === "string" ? parsed.values.rollback : null,
|
|
405
|
+
quarantineOperation: typeof parsed.values["quarantine-operation"] === "string" ? parsed.values["quarantine-operation"] : null,
|
|
401
406
|
fromOperation: typeof parsed.values["from-operation"] === "string" ? parsed.values["from-operation"] : null,
|
|
402
407
|
fromPreset: typeof parsed.values["from-preset"] === "string" ? parsed.values["from-preset"] : null,
|
|
403
408
|
restorePreset: typeof parsed.values["restore-preset"] === "string" ? parsed.values["restore-preset"] : null,
|
|
@@ -424,9 +429,9 @@ export async function run(argv, env = process.env, cwd = process.cwd(), isTTY =
|
|
|
424
429
|
if (command.max !== null && rest.length > command.max) {
|
|
425
430
|
return fail(asJson, say.errors.tooManyArguments(name, command.max, rest.length), 2, uiErr);
|
|
426
431
|
}
|
|
427
|
-
if (name === "doctor" && ((!flags.fix && (flags.fromOperation !== null || flags.fromPreset !== null || flags.restorePreset !== null || flags.tools !== null)) ||
|
|
432
|
+
if (name === "doctor" && ((!flags.fix && (flags.quarantineOperation !== null || flags.fromOperation !== null || flags.fromPreset !== null || flags.restorePreset !== null || flags.tools !== null)) ||
|
|
428
433
|
(flags.restorePreset !== null && flags.fromOperation === null) ||
|
|
429
|
-
(flags.rollback !== null && (flags.fromOperation !== null || flags.fromPreset !== null || flags.restorePreset !== null || flags.tools !== null)))) {
|
|
434
|
+
(flags.rollback !== null && (flags.quarantineOperation !== null || flags.fromOperation !== null || flags.fromPreset !== null || flags.restorePreset !== null || flags.tools !== null)))) {
|
|
430
435
|
return fail(asJson, say.recovery.invalidOptions, 2, uiErr);
|
|
431
436
|
}
|
|
432
437
|
try {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -472,6 +472,20 @@ export async function runDoctor(ctx, _args) {
|
|
|
472
472
|
if (!ctx.flags.fix)
|
|
473
473
|
return doctorReport(ctx);
|
|
474
474
|
const app = await runAppRebuild(ctx);
|
|
475
|
+
if (app.quarantineOnly) {
|
|
476
|
+
const rollbackLines = app.json.applied === true ? recoveryRollbackLines(ctx.t, app.json.operationId, null) : [];
|
|
477
|
+
const remaining = Array.isArray(app.json.remainingInvalidOperations)
|
|
478
|
+
? app.json.remainingInvalidOperations.filter((id) => typeof id === "string") : [];
|
|
479
|
+
const next = typeof app.json.next === "string" ? app.json.next : null;
|
|
480
|
+
return {
|
|
481
|
+
...app,
|
|
482
|
+
lines: [...app.lines, ...rollbackLines, ...(next === null ? [] : [
|
|
483
|
+
` ${ctx.t.recovery.quarantineRemaining(remaining.length)}`, "", ` ${next}`,
|
|
484
|
+
])],
|
|
485
|
+
json: { ...app.json, root: ctx.agent.root, stateRoot: ctx.paths.root, agentRoot: ctx.agent.root,
|
|
486
|
+
live: ctx.agent.live, fixed: app.json.applied === true, appHealthy: false },
|
|
487
|
+
};
|
|
488
|
+
}
|
|
475
489
|
if (ctx.flags.dryRun) {
|
|
476
490
|
const native = await runRebuildNative(ctx, app.legacy, app.proposedConfig);
|
|
477
491
|
return {
|
|
@@ -517,10 +531,14 @@ async function doctorReport(ctx, showRollbackCommands = true, showCodexAction =
|
|
|
517
531
|
if (inspected.lock.state !== "none" || inspected.journals.some(({ state }) => state === "pending" || state === "invalid")) {
|
|
518
532
|
const owner = inspected.lock.owner;
|
|
519
533
|
const blocking = inspected.journals.filter(({ state }) => state === "pending" || state === "invalid");
|
|
534
|
+
const invalid = blocking.filter(({ state }) => state === "invalid");
|
|
535
|
+
const quarantineable = inspected.lock.state === "none" && blocking.length === invalid.length && invalid.length > 0;
|
|
536
|
+
const singleQuarantine = quarantineable && invalid.length === 1;
|
|
520
537
|
const shown = ctx.flags.verbose ? inspected.journals : inspected.journals.filter(({ operationId, state }) => state === "pending" || state === "invalid" || operationId === owner?.operationId);
|
|
521
538
|
const lines = [
|
|
522
|
-
...wrap(ctx.t.doctor.recoveryPending, ctx.ui.width - 2).map(line => ` ${ctx.ui.warn(line)}`),
|
|
523
|
-
...shown.
|
|
539
|
+
...wrap(quarantineable ? ctx.t.doctor.invalidRecoveryPending : ctx.t.doctor.recoveryPending, ctx.ui.width - 2).map(line => ` ${ctx.ui.warn(line)}`),
|
|
540
|
+
...shown.filter(({ operationId }) => !singleQuarantine || operationId !== invalid[0].operationId)
|
|
541
|
+
.map(({ operationId, state }) => ` ${operationId} ${ctx.t.doctor.operationState[state]}`),
|
|
524
542
|
];
|
|
525
543
|
if (owner !== null && !shown.some(({ operationId }) => operationId === owner.operationId))
|
|
526
544
|
lines.push(` ${owner.operationId}`);
|
|
@@ -540,7 +558,14 @@ async function doctorReport(ctx, showRollbackCommands = true, showCodexAction =
|
|
|
540
558
|
}
|
|
541
559
|
}
|
|
542
560
|
}
|
|
543
|
-
|
|
561
|
+
if (quarantineable) {
|
|
562
|
+
lines.push("", ...(singleQuarantine
|
|
563
|
+
? wrap(ctx.t.recovery.quarantineCandidate(invalid[0].operationId), ctx.ui.width - 2)
|
|
564
|
+
: 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));
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
lines.push(...nextStep(`${repairCommand(ctx, "doctor")} --json`, ctx.ui));
|
|
568
|
+
}
|
|
544
569
|
return {
|
|
545
570
|
lines,
|
|
546
571
|
// The damaged config is deliberately not parsed here. `operation` is the only safe context
|
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,7 +11,7 @@ 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";
|
|
@@ -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/core/i18n.js
CHANGED
|
@@ -112,7 +112,7 @@ const enMessages = {
|
|
|
112
112
|
choosePreset: "Choose a preset for values with no valid saved source:",
|
|
113
113
|
sourceRequired: "Saved values are incomplete. Run tonedial doctor --fix interactively, or select --from-preset SLUG or --from-operation ID before unattended recovery.",
|
|
114
114
|
sourceInvalid: "The selected source cannot supply valid settings. The original was kept.",
|
|
115
|
-
invalidOptions: "Recovery source and tool options require doctor --fix. --restore-preset requires --from-operation; --rollback cannot be combined with source, preset, or tool choices.",
|
|
115
|
+
invalidOptions: "Recovery source, quarantine and tool options require doctor --fix. --restore-preset requires --from-operation; --rollback cannot be combined with quarantine, source, preset, or tool choices.",
|
|
116
116
|
keptSettings: "Saved settings and custom presets kept.",
|
|
117
117
|
obsoleteWidthDiscarded: "Discard invalid retired fixed_width input. Output follows terminal width.",
|
|
118
118
|
obsoleteGuardsDiscarded: "Discard invalid retired guards input. ToneDial no longer adds guard requests.",
|
|
@@ -125,6 +125,13 @@ const enMessages = {
|
|
|
125
125
|
rollback: (id) => `Restore the previous files:\ntonedial doctor --fix --rollback ${id}`,
|
|
126
126
|
rollbackOrder: "Restore connections first, then application files:",
|
|
127
127
|
noSnapshot: (id) => `${id}: no valid settings snapshot for this state root`,
|
|
128
|
+
quarantineCandidate: (id) => `Operation ID:\n${id}\nQuarantine it before rebuilding; the original bytes remain available through rollback.`,
|
|
129
|
+
quarantineQuestion: "Type the full operation ID above to quarantine it; Enter cancels:",
|
|
130
|
+
quarantineRequired: (id, command) => `${id}: this unreadable operation record needs explicit quarantine before recovery. No files were changed.\n${command}`,
|
|
131
|
+
quarantineUnavailable: (id) => `${id}: no matching unreadable operation record can be quarantined. No files were changed.`,
|
|
132
|
+
quarantine: (id) => `Quarantine unreadable operation ${id}; keep its original bytes in rollback history.`,
|
|
133
|
+
quarantineComplete: (id) => `Unreadable operation ${id} quarantined.`,
|
|
134
|
+
quarantineRemaining: (count) => `${count} unreadable operation ${count === 1 ? "record remains" : "records remain"}. Quarantine the next record before rebuilding.`,
|
|
128
135
|
retire: (id) => `Retire stopped operation ${id} as history before a new application rebuild. Its external changes are not certified or undone.`,
|
|
129
136
|
resumeOperation: (id) => `Stopped operation:\n${id}`,
|
|
130
137
|
resumeQuestion: "Type the full operation ID above to continue; Enter cancels:",
|
|
@@ -280,6 +287,7 @@ const enMessages = {
|
|
|
280
287
|
seedsNotRun: "no bundled files · init has not run here",
|
|
281
288
|
seedsAttention: "bundled files need attention",
|
|
282
289
|
recoveryPending: "An operation is active or needs recovery. No files were changed by this command.",
|
|
290
|
+
invalidRecoveryPending: "Unreadable operation history is blocking recovery. No files were changed by this command.",
|
|
283
291
|
resumeOperation: "Continue application repair:",
|
|
284
292
|
contextCurrent: "AGENTS.md and TONE.md layout is current.",
|
|
285
293
|
contextRepair: "Managed context files need repair. Check the guide and tone-file diagnostics.",
|
|
@@ -302,7 +310,7 @@ const enMessages = {
|
|
|
302
310
|
changed: "Reader or config changed during inspection. Retry after changes stop.",
|
|
303
311
|
probe: "The controlled probe failed. Check Node and temporary-directory access, then rerun doctor.",
|
|
304
312
|
},
|
|
305
|
-
operationState: { pending: "pending", complete: "complete", "rolled-back": "restored", "retired-for-app-rebuild": "retired for rebuild", invalid: "invalid record" },
|
|
313
|
+
operationState: { pending: "pending", complete: "complete", "rolled-back": "restored", "retired-for-app-rebuild": "retired for rebuild", quarantined: "quarantined", invalid: "invalid record" },
|
|
306
314
|
rollbackStatus: {
|
|
307
315
|
ready: "Approval will restore the selected operation.",
|
|
308
316
|
busy: "Operation still active. Wait for its owner to finish.",
|
|
@@ -470,6 +478,7 @@ const enMessages = {
|
|
|
470
478
|
tools: "agents to connect · init: none = state only",
|
|
471
479
|
fix: "doctor: repair fixable findings",
|
|
472
480
|
rollback: "doctor --fix: restore one operation",
|
|
481
|
+
quarantineOperation: "doctor --fix: quarantine one unreadable operation record",
|
|
473
482
|
fromOperation: "doctor --fix: select saved settings from an operation",
|
|
474
483
|
fromPreset: "doctor --fix: preset for unavailable settings",
|
|
475
484
|
restorePreset: "doctor --fix: restore one missing or damaged user preset",
|
|
@@ -766,7 +775,7 @@ const koMessages = {
|
|
|
766
775
|
choosePreset: "저장된 값이 없는 항목에 사용할 프리셋을 선택하세요:",
|
|
767
776
|
sourceRequired: "저장된 값이 부족합니다. 터미널에서 tonedial doctor --fix를 실행하거나 자동 복구 전에 --from-preset SLUG 또는 --from-operation ID를 선택하세요.",
|
|
768
777
|
sourceInvalid: "선택한 원본에서 올바른 설정을 읽지 못했습니다. 기존 파일은 유지했습니다.",
|
|
769
|
-
invalidOptions: "복구
|
|
778
|
+
invalidOptions: "복구 원본·격리·도구 옵션은 doctor --fix에서 쓰세요. --restore-preset에는 --from-operation이 필요하며, --rollback은 격리·원본·프리셋·도구 선택과 함께 쓸 수 없습니다.",
|
|
770
779
|
keptSettings: "저장된 설정·사용자 프리셋 유지",
|
|
771
780
|
obsoleteWidthDiscarded: "더 이상 쓰지 않는 fixed_width의 잘못된 값을 버립니다. 출력은 터미널 폭에 맞춥니다.",
|
|
772
781
|
obsoleteGuardsDiscarded: "더 이상 쓰지 않는 guards의 잘못된 값을 버립니다. ToneDial은 가드 요청을 넣지 않습니다.",
|
|
@@ -779,6 +788,13 @@ const koMessages = {
|
|
|
779
788
|
rollback: (id) => `이전 파일로 돌아가기:\ntonedial doctor --fix --rollback ${id}`,
|
|
780
789
|
rollbackOrder: "연결 파일을 먼저 되돌린 뒤 앱 파일을 되돌리세요:",
|
|
781
790
|
noSnapshot: (id) => `${id}: 이 상태 경로에서 쓸 수 있는 설정 기록이 없습니다`,
|
|
791
|
+
quarantineCandidate: (id) => `작업 ID:\n${id}\n앱을 복구하려면 이 기록을 먼저 격리해야 합니다. 원본 바이트는 롤백 기록에 보존됩니다.`,
|
|
792
|
+
quarantineQuestion: "격리하려면 위 작업 ID 전체를 입력하세요. Enter를 누르면 취소합니다:",
|
|
793
|
+
quarantineRequired: (id, command) => `${id}: 복구 전에 읽을 수 없는 작업 기록을 명시적으로 격리해야 합니다. 파일은 바꾸지 않았습니다.\n${command}`,
|
|
794
|
+
quarantineUnavailable: (id) => `${id}: 격리할 수 있는 읽기 불가 작업 기록을 찾지 못했습니다. 파일은 바꾸지 않았습니다.`,
|
|
795
|
+
quarantine: (id) => `읽을 수 없는 작업 ${id}를 격리합니다. 원본 바이트는 롤백 기록에 보존합니다.`,
|
|
796
|
+
quarantineComplete: (id) => `읽을 수 없는 작업 ${id}를 격리했습니다.`,
|
|
797
|
+
quarantineRemaining: (count) => `읽을 수 없는 작업 기록 ${count}개가 남았습니다. 다음 기록을 격리한 뒤 복구를 계속하세요.`,
|
|
782
798
|
retire: (id) => `중단된 작업 ${id}를 기록으로 보관한 뒤 새 앱 복구를 시작합니다. 그 작업의 외부 파일 변경은 확인하거나 되돌리지 않습니다.`,
|
|
783
799
|
resumeOperation: (id) => `중단된 작업:\n${id}`,
|
|
784
800
|
resumeQuestion: "계속하려면 위 작업 ID 전체를 입력하세요. Enter를 누르면 취소합니다:",
|
|
@@ -937,6 +953,7 @@ const koMessages = {
|
|
|
937
953
|
seedsNotRun: "기본 파일 없음 · init으로 준비하세요",
|
|
938
954
|
seedsAttention: "기본 파일 확인 필요",
|
|
939
955
|
recoveryPending: "진행 중이거나 복구가 필요한 작업이 있습니다. 이번 실행에서는 파일을 바꾸지 않았습니다.",
|
|
956
|
+
invalidRecoveryPending: "읽을 수 없는 작업 기록 때문에 복구를 진행할 수 없습니다. 이번 실행에서는 파일을 바꾸지 않았습니다.",
|
|
940
957
|
resumeOperation: "앱 복구를 이어가려면:",
|
|
941
958
|
contextCurrent: "AGENTS.md와 TONE.md 구성이 최신입니다.",
|
|
942
959
|
contextRepair: "관리 중인 컨텍스트 파일을 복구해야 합니다. 안내 파일과 톤 파일의 진단을 확인해 주세요.",
|
|
@@ -959,7 +976,7 @@ const koMessages = {
|
|
|
959
976
|
changed: "검사 중 전달 스크립트나 설정이 바뀌었습니다. 변경이 끝난 뒤 다시 시도해 주세요.",
|
|
960
977
|
probe: "제어된 검사에 실패했습니다. Node와 임시 디렉터리 접근 권한을 확인한 뒤 doctor를 다시 실행해 주세요.",
|
|
961
978
|
},
|
|
962
|
-
operationState: { pending: "미완료", complete: "완료", "rolled-back": "복구됨", "retired-for-app-rebuild": "앱 복구를 위해 보관됨", invalid: "잘못된 기록" },
|
|
979
|
+
operationState: { pending: "미완료", complete: "완료", "rolled-back": "복구됨", "retired-for-app-rebuild": "앱 복구를 위해 보관됨", quarantined: "격리됨", invalid: "잘못된 기록" },
|
|
963
980
|
rollbackStatus: {
|
|
964
981
|
ready: "승인하면 선택한 작업을 복구합니다.",
|
|
965
982
|
busy: "아직 진행 중인 작업입니다. 작업이 끝날 때까지 기다려 주세요.",
|
|
@@ -1127,6 +1144,7 @@ const koMessages = {
|
|
|
1127
1144
|
tools: "연결할 에이전트 · init에서 none은 상태만 준비",
|
|
1128
1145
|
fix: "doctor: 고칠 수 있는 문제 복구",
|
|
1129
1146
|
rollback: "doctor --fix: 작업 하나 복구",
|
|
1147
|
+
quarantineOperation: "doctor --fix: 읽을 수 없는 작업 기록 하나 격리",
|
|
1130
1148
|
fromOperation: "doctor --fix: 작업 기록에서 설정 선택",
|
|
1131
1149
|
fromPreset: "doctor --fix: 없는 설정에 사용할 프리셋",
|
|
1132
1150
|
restorePreset: "doctor --fix: 없거나 손상된 사용자 프리셋 하나 복원",
|
package/dist/core/locales/es.js
CHANGED
|
@@ -101,7 +101,7 @@ const messages = {
|
|
|
101
101
|
choosePreset: "Elige un preajuste para los valores sin un origen guardado válido:",
|
|
102
102
|
sourceRequired: "Los valores guardados están incompletos. Ejecuta tonedial doctor --fix de forma interactiva o selecciona --from-preset SLUG o --from-operation ID antes de recuperar sin interacción.",
|
|
103
103
|
sourceInvalid: "El origen elegido no proporciona ajustes válidos. Se conservó el original.",
|
|
104
|
-
invalidOptions: "El origen
|
|
104
|
+
invalidOptions: "El origen, la cuarentena y las herramientas requieren doctor --fix. --restore-preset requiere --from-operation; --rollback no se combina con cuarentena, origen, preajuste ni herramientas.",
|
|
105
105
|
keptSettings: "Se conservaron los ajustes y preajustes personalizados.",
|
|
106
106
|
obsoleteWidthDiscarded: "Se descarta el valor no válido del antiguo fixed_width. La salida se adapta al ancho del terminal.",
|
|
107
107
|
obsoleteGuardsDiscarded: "Se descarta el valor no válido del antiguo guards. ToneDial ya no añade solicitudes de protección.",
|
|
@@ -114,6 +114,13 @@ const messages = {
|
|
|
114
114
|
rollback: (id) => `Restaura los archivos anteriores:\ntonedial doctor --fix --rollback ${id}`,
|
|
115
115
|
rollbackOrder: "Restaura primero las conexiones y después los archivos de la aplicación:",
|
|
116
116
|
noSnapshot: (id) => `${id}: no hay una instantánea válida para este directorio de estado`,
|
|
117
|
+
quarantineCandidate: (id) => `ID de operación:\n${id}\nPara reconstruir, primero debes poner este registro en cuarentena; los bytes originales quedarán en el historial de reversión.`,
|
|
118
|
+
quarantineQuestion: "Escribe el ID completo de arriba para ponerlo en cuarentena; Enter cancela:",
|
|
119
|
+
quarantineRequired: (id, command) => `${id}: debes poner en cuarentena este registro ilegible antes de recuperar. No se cambió ningún archivo.\n${command}`,
|
|
120
|
+
quarantineUnavailable: (id) => `${id}: no hay un registro de operación ilegible que se pueda poner en cuarentena. No se cambió ningún archivo.`,
|
|
121
|
+
quarantine: (id) => `Pon en cuarentena la operación ilegible ${id}; conserva sus bytes originales en el historial de reversión.`,
|
|
122
|
+
quarantineComplete: (id) => `La operación ilegible ${id} quedó en cuarentena.`,
|
|
123
|
+
quarantineRemaining: (count) => `${count === 1 ? "Queda 1 registro de operación ilegible" : `Quedan ${count} registros de operación ilegibles`}. Pon el siguiente en cuarentena antes de continuar la recuperación.`,
|
|
117
124
|
retire: (id) => `Archiva la operación detenida ${id} antes de reconstruir. Sus cambios externos no se certifican ni se deshacen.`,
|
|
118
125
|
resumeOperation: (id) => `Operación detenida:\n${id}`,
|
|
119
126
|
resumeQuestion: "Para continuar, escribe el ID completo de arriba; Enter cancela:",
|
|
@@ -269,6 +276,7 @@ const messages = {
|
|
|
269
276
|
seedsNotRun: "sin archivos incluidos · aquí no se ha ejecutado init",
|
|
270
277
|
seedsAttention: "los archivos incluidos requieren atención",
|
|
271
278
|
recoveryPending: "Hay una operación en curso o que necesita recuperación. Esta ejecución no cambió archivos.",
|
|
279
|
+
invalidRecoveryPending: "Un historial de operaciones ilegible está bloqueando la recuperación. Esta ejecución no cambió archivos.",
|
|
272
280
|
resumeOperation: "Continuar la reparación de la aplicación:",
|
|
273
281
|
contextCurrent: "La estructura de AGENTS.md y TONE.md está al día.",
|
|
274
282
|
contextRepair: "Los archivos de contexto gestionados necesitan reparación. Revisa el diagnóstico de la guía y del tono.",
|
|
@@ -291,7 +299,7 @@ const messages = {
|
|
|
291
299
|
changed: "El lector o los ajustes cambiaron durante la inspección. Reintenta cuando terminen los cambios.",
|
|
292
300
|
probe: "Falló la comprobación controlada. Revisa Node y el acceso al directorio temporal; después repite doctor.",
|
|
293
301
|
},
|
|
294
|
-
operationState: { pending: "pendiente", complete: "completa", "rolled-back": "restaurada", "retired-for-app-rebuild": "archivada para reconstruir", invalid: "registro no válido" },
|
|
302
|
+
operationState: { pending: "pendiente", complete: "completa", "rolled-back": "restaurada", "retired-for-app-rebuild": "archivada para reconstruir", quarantined: "en cuarentena", invalid: "registro no válido" },
|
|
295
303
|
rollbackStatus: {
|
|
296
304
|
ready: "La aprobación restaurará la operación elegida.",
|
|
297
305
|
busy: "La operación sigue activa. Espera a que termine su propietario.",
|
|
@@ -455,6 +463,7 @@ const messages = {
|
|
|
455
463
|
tools: "agentes que conectar · init: none solo crea el estado",
|
|
456
464
|
fix: "doctor: reparar los problemas que se puedan",
|
|
457
465
|
rollback: "doctor --fix: restaurar una operación",
|
|
466
|
+
quarantineOperation: "doctor --fix: poner en cuarentena un registro ilegible",
|
|
458
467
|
fromOperation: "doctor --fix: elegir ajustes guardados de una operación",
|
|
459
468
|
fromPreset: "doctor --fix: preajuste para ajustes no disponibles",
|
|
460
469
|
restorePreset: "doctor --fix: restaurar un preajuste ausente o dañado",
|
package/dist/core/locales/ja.js
CHANGED
|
@@ -101,7 +101,7 @@ const messages = {
|
|
|
101
101
|
choosePreset: "有効な保存元がない値の復元用プリセットを選んでください:",
|
|
102
102
|
sourceRequired: "保存値が不完全です。tonedial doctor --fix を対話的に実行するか、無人復旧の前に --from-preset SLUG または --from-operation ID を選んでください。",
|
|
103
103
|
sourceInvalid: "選択元から有効な設定を取得できません。元のファイルは保持しました。",
|
|
104
|
-
invalidOptions: "
|
|
104
|
+
invalidOptions: "復旧元、隔離、ツールの指定には doctor --fix が必要です。--restore-preset には --from-operation が必要です。--rollback は隔離、復旧元、プリセット、ツール指定と併用できません。",
|
|
105
105
|
keptSettings: "保存済み設定とカスタムプリセットを保持しました。",
|
|
106
106
|
obsoleteWidthDiscarded: "廃止済み fixed_width の無効な値を破棄します。出力はターミナル幅に従います。",
|
|
107
107
|
obsoleteGuardsDiscarded: "廃止済み guards の無効な値を破棄します。ToneDialはガード要求を追加しません。",
|
|
@@ -114,6 +114,13 @@ const messages = {
|
|
|
114
114
|
rollback: (id) => `以前のファイルに戻す:\ntonedial doctor --fix --rollback ${id}`,
|
|
115
115
|
rollbackOrder: "先に接続を復元し、その後アプリケーションのファイルを復元してください:",
|
|
116
116
|
noSnapshot: (id) => `${id}: この状態ルート用の有効な設定スナップショットがありません`,
|
|
117
|
+
quarantineCandidate: (id) => `操作ID:\n${id}\n再構築するには、先にこの記録を隔離する必要があります。元のバイト列はロールバック履歴に保持されます。`,
|
|
118
|
+
quarantineQuestion: "隔離するには上の操作ID全体を入力してください。Enterで中止:",
|
|
119
|
+
quarantineRequired: (id, command) => `${id}: 復旧前に、読み取れない操作記録を明示的に隔離する必要があります。ファイルは変更していません。\n${command}`,
|
|
120
|
+
quarantineUnavailable: (id) => `${id}: 隔離できる読み取り不能な操作記録が見つかりません。ファイルは変更していません。`,
|
|
121
|
+
quarantine: (id) => `読み取れない操作 ${id} を隔離し、元のバイト列をロールバック履歴に保持します。`,
|
|
122
|
+
quarantineComplete: (id) => `読み取れない操作 ${id} を隔離しました。`,
|
|
123
|
+
quarantineRemaining: (count) => `読み取れない操作記録が ${count} 件残っています。次の記録を隔離してから復旧を続けてください。`,
|
|
117
124
|
retire: (id) => `停止した操作 ${id} を履歴に移してから再構築してください。外部への変更は検証も取り消しもされません。`,
|
|
118
125
|
resumeOperation: (id) => `停止した操作:\n${id}`,
|
|
119
126
|
resumeQuestion: "続行するには上の操作ID全体を入力してください。Enterで中止:",
|
|
@@ -269,6 +276,7 @@ const messages = {
|
|
|
269
276
|
seedsNotRun: "同梱ファイルなし · ここでは init 未実行",
|
|
270
277
|
seedsAttention: "同梱ファイルの確認が必要",
|
|
271
278
|
recoveryPending: "実行中または復旧が必要な操作があります。この実行ではファイルを変更していません。",
|
|
279
|
+
invalidRecoveryPending: "読み取れない操作履歴が復旧を妨げています。この実行ではファイルを変更していません。",
|
|
272
280
|
resumeOperation: "アプリ復旧を続けるには:",
|
|
273
281
|
contextCurrent: "AGENTS.md と TONE.md の構成は最新です。",
|
|
274
282
|
contextRepair: "管理対象のコンテキストファイルに修復が必要です。ガイドと口調ファイルの診断を確認してください。",
|
|
@@ -291,7 +299,7 @@ const messages = {
|
|
|
291
299
|
changed: "確認中にリーダーか設定が変更されました。変更が終わってから再試行してください。",
|
|
292
300
|
probe: "制御下の確認に失敗しました。Nodeと一時ディレクトリへのアクセスを確認し、doctor を再実行してください。",
|
|
293
301
|
},
|
|
294
|
-
operationState: { pending: "保留中", complete: "完了", "rolled-back": "復元済み", "retired-for-app-rebuild": "再構築用に履歴へ移行", invalid: "無効な記録" },
|
|
302
|
+
operationState: { pending: "保留中", complete: "完了", "rolled-back": "復元済み", "retired-for-app-rebuild": "再構築用に履歴へ移行", quarantined: "隔離済み", invalid: "無効な記録" },
|
|
295
303
|
rollbackStatus: {
|
|
296
304
|
ready: "承認すると選択した操作を復元します。",
|
|
297
305
|
busy: "まだ実行中です。所有者の完了を待ってください。",
|
|
@@ -455,6 +463,7 @@ const messages = {
|
|
|
455
463
|
tools: "接続するエージェント · init: none は状態のみ",
|
|
456
464
|
fix: "doctor: 修復可能な問題を修復",
|
|
457
465
|
rollback: "doctor --fix: 操作を1つ復元",
|
|
466
|
+
quarantineOperation: "doctor --fix: 読み取れない操作記録を1つ隔離",
|
|
458
467
|
fromOperation: "doctor --fix: 操作から保存済み設定を選択",
|
|
459
468
|
fromPreset: "doctor --fix: 不足した設定用のプリセット",
|
|
460
469
|
restorePreset: "doctor --fix: 不足・破損したユーザープリセットを復元",
|
package/dist/core/locales/zh.js
CHANGED
|
@@ -101,7 +101,7 @@ const messages = {
|
|
|
101
101
|
choosePreset: "请为缺少有效保存来源的值选择预设:",
|
|
102
102
|
sourceRequired: "保存值不完整。请交互运行 tonedial doctor --fix,或在无人值守恢复前选择 --from-preset SLUG 或 --from-operation ID。",
|
|
103
103
|
sourceInvalid: "所选来源无法提供有效设置,已保留原文件。",
|
|
104
|
-
invalidOptions: "
|
|
104
|
+
invalidOptions: "恢复来源、隔离和工具选项需要 doctor --fix。--restore-preset 需要 --from-operation;--rollback 不能与隔离、来源、预设或工具选项同时使用。",
|
|
105
105
|
keptSettings: "已保留保存的设置和自定义预设。",
|
|
106
106
|
obsoleteWidthDiscarded: "丢弃已停用的无效 fixed_width 值。输出将跟随终端宽度。",
|
|
107
107
|
obsoleteGuardsDiscarded: "丢弃已停用的无效 guards 值。ToneDial 不再添加防护请求。",
|
|
@@ -114,6 +114,13 @@ const messages = {
|
|
|
114
114
|
rollback: (id) => `恢复之前的文件:\ntonedial doctor --fix --rollback ${id}`,
|
|
115
115
|
rollbackOrder: "请先恢复连接,再恢复应用文件:",
|
|
116
116
|
noSnapshot: (id) => `${id}:没有此状态根目录的有效设置快照`,
|
|
117
|
+
quarantineCandidate: (id) => `操作 ID:\n${id}\n要重建应用,必须先隔离这条记录;原始字节仍保存在回滚历史中。`,
|
|
118
|
+
quarantineQuestion: "请输入上方完整的操作 ID 以隔离该记录;按 Enter 取消:",
|
|
119
|
+
quarantineRequired: (id, command) => `${id}:恢复前必须明确隔离这条无法读取的操作记录。未更改文件。\n${command}`,
|
|
120
|
+
quarantineUnavailable: (id) => `${id}:找不到可隔离的无法读取操作记录。未更改文件。`,
|
|
121
|
+
quarantine: (id) => `隔离无法读取的操作 ${id};原始字节保留在回滚历史中。`,
|
|
122
|
+
quarantineComplete: (id) => `已隔离无法读取的操作 ${id}。`,
|
|
123
|
+
quarantineRemaining: (count) => `仍有 ${count} 条无法读取的操作记录。请隔离下一条记录后继续恢复。`,
|
|
117
124
|
retire: (id) => `重建应用前,请将已停止的操作 ${id} 归档为历史。其外部变更不会被认证或撤销。`,
|
|
118
125
|
resumeOperation: (id) => `已停止的操作:\n${id}`,
|
|
119
126
|
resumeQuestion: "要继续,请输入上方完整的操作 ID;按 Enter 取消:",
|
|
@@ -269,6 +276,7 @@ const messages = {
|
|
|
269
276
|
seedsNotRun: "无内置文件 · 尚未在此运行 init",
|
|
270
277
|
seedsAttention: "内置文件需要处理",
|
|
271
278
|
recoveryPending: "有操作正在运行或需要恢复。本次执行未更改文件。",
|
|
279
|
+
invalidRecoveryPending: "无法读取的操作历史阻止了恢复。本次执行未更改文件。",
|
|
272
280
|
resumeOperation: "继续应用恢复:",
|
|
273
281
|
contextCurrent: "AGENTS.md 与 TONE.md 布局已是最新。",
|
|
274
282
|
contextRepair: "受管理的上下文文件需要修复,请检查指南和语气文件诊断。",
|
|
@@ -291,7 +299,7 @@ const messages = {
|
|
|
291
299
|
changed: "检查时读取程序或设置发生变化,请等修改结束后重试。",
|
|
292
300
|
probe: "受控探测失败。请检查 Node 和临时目录访问权限,再运行 doctor。",
|
|
293
301
|
},
|
|
294
|
-
operationState: { pending: "待处理", complete: "已完成", "rolled-back": "已恢复", "retired-for-app-rebuild": "已归档以供重建", invalid: "无效记录" },
|
|
302
|
+
operationState: { pending: "待处理", complete: "已完成", "rolled-back": "已恢复", "retired-for-app-rebuild": "已归档以供重建", quarantined: "已隔离", invalid: "无效记录" },
|
|
295
303
|
rollbackStatus: {
|
|
296
304
|
ready: "批准后将恢复所选操作。",
|
|
297
305
|
busy: "操作仍在运行,请等待其所有者完成。",
|
|
@@ -455,6 +463,7 @@ const messages = {
|
|
|
455
463
|
tools: "要连接的代理 · init:none 仅初始化状态",
|
|
456
464
|
fix: "doctor:修复可处理的问题",
|
|
457
465
|
rollback: "doctor --fix:恢复一次操作",
|
|
466
|
+
quarantineOperation: "doctor --fix:隔离一条无法读取的操作记录",
|
|
458
467
|
fromOperation: "doctor --fix:从操作中选择保存设置",
|
|
459
468
|
fromPreset: "doctor --fix:为不可用设置选择预设",
|
|
460
469
|
restorePreset: "doctor --fix:恢复一个缺失或损坏的用户预设",
|
package/dist/core/operations.js
CHANGED
|
@@ -387,6 +387,27 @@ export function readJournal(paths, operationIdValue) {
|
|
|
387
387
|
const journal = readJson(path);
|
|
388
388
|
return validJournal(journal, paths, operationIdValue) ? journal : null;
|
|
389
389
|
}
|
|
390
|
+
function readOperationQuarantine(paths, operationIdValue) {
|
|
391
|
+
if (!existingOperationsDirectory(paths))
|
|
392
|
+
return null;
|
|
393
|
+
const value = readJson(operationJournalPath(paths, operationIdValue));
|
|
394
|
+
return isPlainObject(value) && hasExactKeys(value, ["kind", "operationId", "originalSha256", "root", "schema"]) &&
|
|
395
|
+
value.schema === 1 && value.kind === "quarantined-operation" && value.operationId === operationIdValue &&
|
|
396
|
+
value.root === paths.root && /^[0-9a-f]{64}$/.test(value.originalSha256)
|
|
397
|
+
? value : null;
|
|
398
|
+
}
|
|
399
|
+
/** Replace an unreadable journal with a nonblocking marker; the enclosing repair journal keeps its original bytes. */
|
|
400
|
+
export function serializeOperationQuarantine(paths, operationIdValue, original) {
|
|
401
|
+
if (!validOperationId(operationIdValue))
|
|
402
|
+
throw new OperationError("invalid", "operation id is not valid");
|
|
403
|
+
return `${JSON.stringify({
|
|
404
|
+
schema: 1,
|
|
405
|
+
kind: "quarantined-operation",
|
|
406
|
+
operationId: operationIdValue,
|
|
407
|
+
root: paths.root,
|
|
408
|
+
originalSha256: createHash("sha256").update(original).digest("hex"),
|
|
409
|
+
}, null, 2)}\n`;
|
|
410
|
+
}
|
|
390
411
|
function journalTime(journal) {
|
|
391
412
|
const updated = Date.parse(journal.updatedAt);
|
|
392
413
|
if (Number.isFinite(updated))
|
|
@@ -889,12 +910,13 @@ export function inspectOperations(paths) {
|
|
|
889
910
|
continue;
|
|
890
911
|
const id = match.groups.id;
|
|
891
912
|
const journal = readJournal(paths, id);
|
|
913
|
+
const quarantine = journal === null ? readOperationQuarantine(paths, id) : null;
|
|
892
914
|
journals.push({
|
|
893
915
|
path: join(paths.operations, entry.name),
|
|
894
916
|
operationId: id,
|
|
895
|
-
state: journal?.state ?? "invalid",
|
|
917
|
+
state: journal?.state ?? (quarantine === null ? "invalid" : "quarantined"),
|
|
896
918
|
journal,
|
|
897
|
-
...(journal === null ? { message: "mutation journal is invalid" } : {}),
|
|
919
|
+
...(journal === null ? { message: quarantine === null ? "mutation journal is invalid" : "invalid journal is quarantined in rollback history" } : {}),
|
|
898
920
|
});
|
|
899
921
|
}
|
|
900
922
|
const ownerOnly = ownerOnlyInspection(paths, lock);
|
package/dist/core/seeds.js
CHANGED
|
@@ -11,7 +11,7 @@ import { captureRawEntry, caseRenameMatchesCurrent, isNoop, planChange, planEntr
|
|
|
11
11
|
import { BUNDLED, builtinPresetDir, LEGACY_BUILTIN_SLUGS, RETIRED_PRESETS } from "./presets.js";
|
|
12
12
|
import { SKILL_MD, SKILL_NAME, SKILL_TRAITS_MD } from "./skill.js";
|
|
13
13
|
// Bump for changed generated content; freshness also checks each managed seed's bytes/provenance.
|
|
14
|
-
export const SEED_VERSION =
|
|
14
|
+
export const SEED_VERSION = 23;
|
|
15
15
|
/** The state-root operating boundaries. The compiler owns the current tone separately. */
|
|
16
16
|
export const GUIDE_MD = `${GUIDE_FORMAT_MARKER}
|
|
17
17
|
ToneDial changes manner only; it never expands task scope or permissions.
|
package/dist/core/skill.js
CHANGED
|
@@ -195,6 +195,12 @@ If repair fails, report its phase, affected path, safe error code and operation
|
|
|
195
195
|
Some file changes may already be applied; a failure is not evidence that all files stayed unchanged.
|
|
196
196
|
Preserve the operation record and follow the reported scoped read-only diagnosis.
|
|
197
197
|
Default doctor output focuses on blockers; \`--verbose\` and JSON retain the operation history.
|
|
198
|
+
If doctor reports an unreadable operation record, use its exact scoped
|
|
199
|
+
\`doctor --fix --quarantine-operation <id>\` command. Preview or approve that replacement through
|
|
200
|
+
the normal repair gate; never delete the record directly. The replacement stops the damaged
|
|
201
|
+
record from blocking recovery, while the repair journal keeps its original bytes and mode for
|
|
202
|
+
explicit rollback. If several records are unreadable, run the next exact command doctor reports;
|
|
203
|
+
each invocation quarantines one record, and the last continues the application rebuild.
|
|
198
204
|
For an explicitly selected stopped operation, \`doctor --fix --from-operation <id>\` continues
|
|
199
205
|
application repair using valid current settings first; it does not undo the earlier changes.
|
|
200
206
|
Preview that exact ID and scope with \`--dry-run\` before applying the approved repair.
|
|
@@ -452,7 +458,7 @@ selected by critique or persona. Bravado grades effort claims without replacing
|
|
|
452
458
|
* command name stays `/tone` even though the tool is `tonedial`: it is what a person types
|
|
453
459
|
* mid-sentence, and the two extra characters are paid every time.
|
|
454
460
|
*/
|
|
455
|
-
export const TONE_COMMAND_VERSION =
|
|
461
|
+
export const TONE_COMMAND_VERSION = 15;
|
|
456
462
|
export const TONE_COMMAND_MD = `---
|
|
457
463
|
description: Check, change, or reload this agent's tone, preset, or ToneDial settings
|
|
458
464
|
---
|
package/dist/wiring/apply.js
CHANGED
|
@@ -832,7 +832,7 @@ function validateEntryRebuildAction(action, prepared = false) {
|
|
|
832
832
|
const value = snapshotRecord(action.metadata ?? null);
|
|
833
833
|
if (action.kind !== "entry-rebuild" || !normalizedPath(action.path) || action.realPath !== action.path || value === null ||
|
|
834
834
|
Object.keys(value).sort().join(",") !== ["schema", "reason", "boundary", "boundaryIdentity", "parents", "stash", "stashIdentity", "archive", "prepared", "preparedIdentity", "createdParents", ...(Object.hasOwn(value, "recipes") ? ["recipes"] : [])].sort().join(",") ||
|
|
835
|
-
value.schema !== 1 || !["generated", "restore-config", "restore-preset"].includes(value.reason) ||
|
|
835
|
+
value.schema !== 1 || !["generated", "restore-config", "restore-preset", "quarantine-operation"].includes(value.reason) ||
|
|
836
836
|
!normalizedPath(value.boundary) || !lexicallyInside(action.path, value.boundary) || !identityTextValue(value.boundaryIdentity) ||
|
|
837
837
|
!Array.isArray(value.parents) || value.parents.length === 0 || !normalizedPath(value.stash) ||
|
|
838
838
|
dirname(value.stash) !== dirname(action.path) || !new RegExp(`^\\.${basename(action.path).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.tonedial-rebuild-[0-9a-f]{32}$`).test(basename(value.stash)) ||
|