aptly 0.1.1 → 0.1.2

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.
@@ -12,6 +12,7 @@ export interface SetupClientResult {
12
12
  status: 'configured' | 'unchanged' | 'removed' | 'skipped' | 'error';
13
13
  codes: string[];
14
14
  requires_hook_review?: boolean;
15
+ recovery?: string[];
15
16
  }
16
17
  export interface SetupResult {
17
18
  status: 'complete' | 'partial' | 'skipped';
@@ -24,4 +25,12 @@ export declare function setDefaults(options?: SetupOptions): SetupResult;
24
25
  export declare function setEnvironment(key: string, value: string, options?: SetupOptions): SetupResult;
25
26
  /** Store only the nonsecret source reference; credential values never enter setup. */
26
27
  export declare function setCredentialReference(options?: SetupOptions): SetupResult;
28
+ /** Snapshot-only plan. No file writes, credential access, paths or values in diagnostics. */
29
+ export declare function prepareCredentialReference(options: SetupOptions, client: Client): {
30
+ result: SetupClientResult;
31
+ check: () => SetupClientResult;
32
+ commit: () => SetupClientResult;
33
+ };
34
+ /** Only an explicitly selected, receipt-matching standard registration is eligible. */
35
+ export declare function migrateLauncher(options: SetupOptions, dryRun?: boolean): SetupResult;
27
36
  export {};
package/dist/src/setup.js CHANGED
@@ -188,7 +188,7 @@ function editEnvironment(current, edit, client) {
188
188
  updates.APTLY_SECRET_BACKEND = 'os';
189
189
  updates.APTLY_SECRET_CLIENT = client;
190
190
  }
191
- else {
191
+ else if (!('migrate' in edit)) {
192
192
  validateEnvironmentValue(edit.key, edit.value);
193
193
  updates[edit.key] = edit.value;
194
194
  }
@@ -251,8 +251,11 @@ function ownership(value, client) {
251
251
  /** Compatibility restriction only: this does not authenticate arbitrary launcher code. */
252
252
  function requireCredentialRuntime(entry, root) {
253
253
  const allowed = [join(root, 'dist', 'src', 'server.js'), join(root, 'src', 'server.ts')];
254
- if (entry.command !== process.execPath || !Array.isArray(entry.args) || entry.args.length !== 1 || !allowed.includes(entry.args[0]))
255
- fail('credential_launcher_unverified');
254
+ const incompatible = (reason) => { throw Object.assign(new Error('credential_launcher_unverified'), { reason }); };
255
+ if (entry.command !== process.execPath)
256
+ incompatible('credential_node_mismatch');
257
+ if (!Array.isArray(entry.args) || entry.args.length !== 1 || !allowed.includes(entry.args[0]))
258
+ incompatible('credential_server_mismatch');
256
259
  try {
257
260
  const extension = entry.args[0].endsWith('.ts') ? 'ts' : 'js';
258
261
  const server = read(entry.args[0]).text;
@@ -261,12 +264,12 @@ function requireCredentialRuntime(entry, root) {
261
264
  fail('credential_launcher_unverified');
262
265
  }
263
266
  catch {
264
- fail('credential_launcher_unverified');
267
+ incompatible('credential_runtime_unsupported');
265
268
  }
266
269
  }
267
270
  function prepare(options, client, removing, edit) {
268
271
  const env = options.env ?? process.env, paths = resolved(options, client), { dir, mcpPath, hooksPath, privateDir } = paths;
269
- const changes = [], dirs = [];
272
+ const changes = [], observations = [], dirs = [];
270
273
  noSymlinks(dir);
271
274
  noSymlinks(privateDir);
272
275
  for (const path of [privateDir, join(privateDir, 'state')]) {
@@ -290,16 +293,17 @@ function prepare(options, client, removing, edit) {
290
293
  } })();
291
294
  const result = { client, status: 'unchanged', codes: [] };
292
295
  if (!hasDir && (options.automatic || removing))
293
- return { changes, dirs, result: { client, status: 'skipped', codes: ['host_not_configured'] } };
296
+ return { changes, observations, dirs, result: { client, status: 'skipped', codes: ['host_not_configured'] } };
294
297
  const policyPath = join(privateDir, 'auto-policy.json'), manifestPath = join(privateDir, 'setup-ownership.json'), stateDir = join(privateDir, 'state');
295
298
  const snap = (path) => { const source = read(path); return { path, before: source.text, after: source.text, mode: source.mode }; };
296
299
  const mcp = snap(mcpPath), hooks = snap(hooksPath), policy = snap(policyPath), manifest = snap(manifestPath);
297
300
  for (const privateFile of [policy, manifest])
298
301
  if (privateFile.before !== null)
299
302
  privatePermissions(privateFile.path, 'private_file_permissions');
303
+ observations.push(mcp, hooks, policy, manifest);
300
304
  const oldOwner = manifest.before === null ? undefined : ownership(json(manifest.before), client);
301
305
  if (removing && !oldOwner)
302
- return { changes, dirs, result: { client, status: 'skipped', codes: ['no_owned_setup'] } };
306
+ return { changes, observations, dirs, result: { client, status: 'skipped', codes: ['no_owned_setup'] } };
303
307
  const hookValue = hook(client), hooksObject = json(hooks.before), entries = hooksArray(hooksObject);
304
308
  const config = client === 'codex' ? toml(mcp.before ?? '') : json(mcp.before), servers = map(config, client === 'codex' ? 'mcp_servers' : 'mcpServers');
305
309
  if (removing) {
@@ -354,6 +358,15 @@ function prepare(options, client, removing, edit) {
354
358
  const root = resolve(options.packageRoot ?? join(dirname(fileURLToPath(import.meta.url)), '..', ...(import.meta.url.includes('/dist/src/') ? ['..'] : [])));
355
359
  const additions = { APTLY_HOOK_POLICY_PATH: policyPath, ROUTER_LEDGER_PATH: join(stateDir, 'ledger.json') };
356
360
  let created = false, tomlBlock;
361
+ if (edit && 'migrate' in edit) {
362
+ if (!oldOwner?.mcp_created || !object(servers.aptly) || !isDeepStrictEqual(servers.aptly, oldOwner.mcp))
363
+ fail('launcher_migration_not_owned');
364
+ const entry = servers.aptly;
365
+ if (typeof entry.command !== 'string' || !isAbsolute(entry.command) || !['node', 'node.exe'].includes(basename(entry.command).toLowerCase()) || !Array.isArray(entry.args) || entry.args.length !== 1 || typeof entry.args[0] !== 'string' || !isAbsolute(entry.args[0]) || !/(?:^|[\\/])(?:dist[\\/]src[\\/]server\.js|src[\\/]server\.ts)$/.test(entry.args[0]))
366
+ fail('launcher_migration_not_standard');
367
+ if (!entries.some(entry => isDeepStrictEqual(entry, oldOwner.hook)) || !isDeepStrictEqual(json(policy.before), oldOwner.policy))
368
+ fail('managed_entry_changed');
369
+ }
357
370
  if (servers.aptly === undefined) {
358
371
  created = true;
359
372
  servers.aptly = { ...(client === 'claude' ? { type: 'stdio' } : {}), command: process.execPath, args: [join(root, 'dist', 'src', 'server.js')], ...(client === 'codex' ? { env_vars: ['OPENAI_API_KEY'] } : {}), env: { ROUTER_PROVIDER: edit && 'defaults' in edit ? 'decisions' : env.ROUTER_PROVIDER === 'fixture' ? 'fixture' : 'decisions', ...additions } };
@@ -370,7 +383,11 @@ function prepare(options, client, removing, edit) {
370
383
  Object.assign(currentEnv, missing);
371
384
  mcp.after = client === 'codex' ? addTomlEnv(mcp.before ?? '', missing) : stringify(config);
372
385
  }
373
- if (edit && 'credential' in edit)
386
+ if (edit && 'migrate' in edit) {
387
+ servers.aptly.command = process.execPath;
388
+ servers.aptly.args = [join(root, 'dist', 'src', 'server.js')];
389
+ }
390
+ if (edit && ('credential' in edit || 'migrate' in edit))
374
391
  requireCredentialRuntime(servers.aptly, root);
375
392
  const currentEnv = map(servers.aptly, 'env');
376
393
  const updates = edit ? editEnvironment(currentEnv, edit, client) : {};
@@ -390,7 +407,7 @@ function prepare(options, client, removing, edit) {
390
407
  fail('existing_routing_hook_conflict');
391
408
  if (existingRoute.length === 0)
392
409
  entries.push(hookValue);
393
- hooks.after = stringify(hooksObject);
410
+ hooks.after = edit && 'migrate' in edit ? hooks.before : stringify(hooksObject);
394
411
  const preservedMode = edit && policy.before !== null ? json(policy.before).mode : undefined;
395
412
  const policyValue = { schema_version: 1, kind: 'aptly-auto-hook', mode: options.mode ?? (['defaults', 'advisory'].includes(preservedMode) ? preservedMode : 'defaults'), issuer: 'operator_installation', enabled: true, state_directory: stateDir };
396
413
  if (policy.before !== null) {
@@ -451,7 +468,7 @@ function prepare(options, client, removing, edit) {
451
468
  changes.push(snapshot);
452
469
  if (!removing && changes.length)
453
470
  result.status = 'configured';
454
- return { changes, dirs, result };
471
+ return { changes, observations, dirs, result };
455
472
  }
456
473
  /** Secure sibling staging is short-lived and never a backup; original settings stay in memory. */
457
474
  function replace(snapshot, forward) {
@@ -496,19 +513,19 @@ function replace(snapshot, forward) {
496
513
  }
497
514
  }
498
515
  }
499
- function execute(options, removing, edit) {
516
+ function execute(options, removing, edit, captured) {
500
517
  const clients = [...new Set(options.clients ?? ['codex', 'claude'])];
501
518
  if (clients.some(client => client !== 'codex' && client !== 'claude'))
502
519
  return { status: 'partial', clients: [] };
503
520
  const env = options.env ?? process.env;
504
521
  if (!removing && options.automatic && (env.APTLY_SKIP_SETUP === '1' || !!env.CI))
505
522
  return { status: 'skipped', clients: clients.map(client => ({ client, status: 'skipped', codes: [env.APTLY_SKIP_SETUP === '1' ? 'setup_disabled' : 'ci_environment'] })) };
506
- const prepared = clients.map(client => {
523
+ const prepared = captured ?? clients.map(client => {
507
524
  try {
508
525
  return prepare(options, client, removing, edit);
509
526
  }
510
527
  catch (e) {
511
- return { changes: [], dirs: [], result: { client, status: 'error', codes: [safeCode(e)] } };
528
+ return { changes: [], observations: [], dirs: [], result: { client, status: 'error', codes: [safeCode(e)] } };
512
529
  }
513
530
  });
514
531
  const results = prepared.map(plan => plan.result);
@@ -520,6 +537,7 @@ function execute(options, removing, edit) {
520
537
  continue;
521
538
  const written = [], madeDirs = [];
522
539
  try {
540
+ validateObservations(plan);
523
541
  for (const path of plan.dirs) {
524
542
  noSymlinks(path);
525
543
  try {
@@ -543,9 +561,11 @@ function execute(options, removing, edit) {
543
561
  privatePermissions(path, 'private_directory_permissions');
544
562
  }
545
563
  for (const snapshot of plan.changes) {
564
+ validateObservations(plan, written);
546
565
  replace(snapshot, true);
547
566
  written.push(snapshot);
548
567
  }
568
+ validateObservations(plan, written);
549
569
  }
550
570
  catch (e) {
551
571
  let rollbackConflict = false;
@@ -569,7 +589,7 @@ function execute(options, removing, edit) {
569
589
  }
570
590
  return { status: results.some(result => result.status === 'error') ? 'partial' : results.every(result => result.status === 'skipped') ? 'skipped' : 'complete', clients: results };
571
591
  }
572
- const codes = new Set(['credential_launcher_unverified', 'environment_invalid_value', 'environment_sensitive_value', 'environment_key_unsupported', 'environment_secret_key_unsupported', 'unsafe_directory', 'unsafe_symlink', 'unsafe_configuration_file', 'invalid_json', 'invalid_json_object', 'configuration_shape_conflict', 'hook_shape_conflict', 'invalid_toml', 'unsupported_toml_layout', 'relative_configuration_directory', 'invalid_ownership', 'managed_hook_changed', 'managed_mcp_changed', 'managed_entry_changed', 'managed_policy_changed', 'mcp_entry_conflict', 'mcp_environment_conflict', 'existing_routing_hook_conflict', 'policy_conflict', 'existing_hook_not_owned', 'concurrent_configuration_change', 'private_directory_permissions', 'private_file_permissions']);
592
+ const codes = new Set(['launcher_migration_not_owned', 'launcher_migration_not_standard', 'credential_launcher_unverified', 'environment_invalid_value', 'environment_sensitive_value', 'environment_key_unsupported', 'environment_secret_key_unsupported', 'unsafe_directory', 'unsafe_symlink', 'unsafe_configuration_file', 'invalid_json', 'invalid_json_object', 'configuration_shape_conflict', 'hook_shape_conflict', 'invalid_toml', 'unsupported_toml_layout', 'relative_configuration_directory', 'invalid_ownership', 'managed_hook_changed', 'managed_mcp_changed', 'managed_entry_changed', 'managed_policy_changed', 'mcp_entry_conflict', 'mcp_environment_conflict', 'existing_routing_hook_conflict', 'policy_conflict', 'existing_hook_not_owned', 'concurrent_configuration_change', 'private_directory_permissions', 'private_file_permissions']);
573
593
  function safeCode(error) { return error instanceof Error && codes.has(error.message) ? error.message : 'configuration_io_error'; }
574
594
  export function setupHosts(options = {}) { return execute(options, false); }
575
595
  export function removeSetup(options = {}) { return execute(options, true); }
@@ -585,3 +605,72 @@ export function setEnvironment(key, value, options = {}) {
585
605
  }
586
606
  /** Store only the nonsecret source reference; credential values never enter setup. */
587
607
  export function setCredentialReference(options = {}) { return execute(options, false, { credential: true }); }
608
+ /** Include unchanged files: a no-op reference update must still detect prompt-time edits. */
609
+ function validateObservations(plan, written = []) {
610
+ for (const snapshot of plan.observations) {
611
+ const current = read(snapshot.path);
612
+ const expected = written.includes(snapshot) ? snapshot.after : snapshot.before;
613
+ if (current.text !== expected || (process.platform !== 'win32' && expected !== null && current.mode !== snapshot.mode))
614
+ fail('concurrent_configuration_change');
615
+ }
616
+ }
617
+ function recovery(client) {
618
+ return [`aptly set env OPENAI_API_KEY --client ${client} --check`,
619
+ `aptly setup --client ${client} --migrate-launcher --dry-run`,
620
+ `aptly setup --client ${client} --migrate-launcher`,
621
+ `aptly set env OPENAI_API_KEY --client ${client}`];
622
+ }
623
+ /** Snapshot-only plan. No file writes, credential access, paths or values in diagnostics. */
624
+ export function prepareCredentialReference(options, client) {
625
+ let plan;
626
+ let result;
627
+ try {
628
+ plan = prepare({ ...options, clients: [client] }, client, false, { credential: true });
629
+ result = { client, status: 'unchanged', codes: ['credential_preflight_ok'] };
630
+ }
631
+ catch (error) {
632
+ const reason = error?.reason;
633
+ const reasons = ['credential_node_mismatch', 'credential_server_mismatch', 'credential_runtime_unsupported'];
634
+ const migration = migrateLauncher({ ...options, clients: [client] }, true);
635
+ const canMigrate = migration.status === 'complete';
636
+ result = { client, status: 'error', codes: [safeCode(error), ...(reason && reasons.includes(reason) ? [reason] : []), ...(canMigrate ? ['launcher_migration_available'] : [])],
637
+ recovery: canMigrate ? recovery(client) : [`aptly set env OPENAI_API_KEY --client ${client} --check`, 'Keep custom or modified launchers unchanged. Review the host registration locally; use its secure credential environment or a separately reviewed standard registration.'] };
638
+ }
639
+ const check = () => {
640
+ if (!plan)
641
+ return result;
642
+ try {
643
+ validateObservations(plan);
644
+ // Recheck runtime support, symlinks, private permissions and policy before storage.
645
+ prepare({ ...options, clients: [client] }, client, false, { credential: true });
646
+ return result;
647
+ }
648
+ catch (error) {
649
+ return { client, status: 'error', codes: [safeCode(error)], recovery: recovery(client) };
650
+ }
651
+ };
652
+ return { result, check, commit: () => {
653
+ const checked = check();
654
+ if (checked.status === 'error' || !plan)
655
+ return checked;
656
+ return execute({ ...options, clients: [client] }, false, { credential: true }, [plan]).clients[0];
657
+ } };
658
+ }
659
+ /** Only an explicitly selected, receipt-matching standard registration is eligible. */
660
+ export function migrateLauncher(options, dryRun = false) {
661
+ const client = options.clients?.[0];
662
+ if (options.clients?.length !== 1 || !client || !['codex', 'claude'].includes(client) || options.automatic || options.mode)
663
+ return { status: 'partial', clients: [] };
664
+ try {
665
+ const plan = prepare(options, client, false, { migrate: true });
666
+ if (dryRun)
667
+ return { status: 'complete', clients: [{ client, status: 'unchanged', codes: [plan.changes.length ? 'launcher_migration_available' : 'launcher_current'], requires_hook_review: client === 'codex' }] };
668
+ const result = execute(options, false, { migrate: true }, [plan]);
669
+ if (result.status === 'complete')
670
+ result.clients[0].codes.push('launcher_migrated', 'mcp_restart_required');
671
+ return result;
672
+ }
673
+ catch (error) {
674
+ return { status: 'partial', clients: [{ client, status: 'error', codes: [safeCode(error)] }] };
675
+ }
676
+ }
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.1";
1
+ export declare const VERSION = "0.1.2";
@@ -1 +1 @@
1
- export const VERSION = '0.1.1';
1
+ export const VERSION = '0.1.2';
package/docs/guide/en.md CHANGED
@@ -4,19 +4,21 @@
4
4
 
5
5
  [Back to README](../../README.en.md)
6
6
 
7
- Korean is the canonical documentation. This guide targets Aptly 0.1.1. Package availability does not establish model access for every account. Claude Code or Codex controls native subagent execution, approvals, sandboxing, and MCP permissions.
7
+ Korean is the canonical documentation. This guide covers general Aptly installation and usage. Package availability does not establish model access for every account. Claude Code or Codex controls native subagent execution, approvals, sandboxing, and MCP permissions.
8
8
 
9
9
  ## Installation
10
10
 
11
- Requires Node.js 24 or later and Claude Code or Codex. When 0.1.1 is available on npm, use these global installation commands:
11
+ Requires Node.js 24 or later and Claude Code or Codex. Install the latest stable npm release globally:
12
+
13
+ **Before upgrading an existing persistent ledger to 0.1.2 or later**, stop every host, MCP server, standalone CLI, and older process that can access it. The persistent gate cannot coexist with older writers. Fresh installations need no legacy-lock quarantine. If an old empty lock remains, follow the [ledger transition procedure](#interrupted-processes-and-ledger-locks): quarantine only a confirmed legacy lock while offline and preserve the JSON ledger, `unknown` records, costs, and completed cache.
12
14
 
13
15
  ```sh
14
- npm install -g aptly@0.1.1
16
+ npm install -g aptly@latest
15
17
  aptly setup --client all --mode defaults
16
18
  aptly --version
17
19
  ```
18
20
 
19
- `aptly --version` should print `0.1.1`. Switching Node.js environments may require reinstalling.
21
+ Use `aptly --version` to check the installed version. Switching Node.js environments may require reinstalling.
20
22
 
21
23
  `postinstall` registers the MCP server and a `PreToolUse` Hook in the current user's existing host configuration directories. If you use `CODEX_HOME` or `CLAUDE_CONFIG_DIR`, pass the same environment to installation. Automatic registration skips host directories that do not exist; explicit `setup` can create them. To register only one host, use `--client codex` or `--client claude`.
22
24
 
@@ -56,6 +58,25 @@ A store timeout or malformed acknowledgement may occur after the OS has written
56
58
 
57
59
  Restart MCP, then inspect `provider`, candidates, and `credential_status` with `get_routing_capabilities`. `os_store` means a key was loaded, not that the API authenticated it. Default and public-variable setup continue to preserve custom wrappers. API-key setup does not change host settings for an unverified launch path and restores that host's previous key where possible if storage was already attempted. Do not assume a new key automatically takes effect through an existing wrapper. These commands do not bypass Codex `/hooks` trust or host permissions.
58
60
 
61
+ ### Recover an incompatible launcher
62
+
63
+ These options require Aptly 0.1.2 or later and are not in npm 0.1.1. To test from source, run `npm ci --ignore-scripts` and `npm run build`, then replace `aptly` below with `node bin/aptly.ts`.
64
+
65
+ Key setup diagnoses selected hosts before input or storage. If any host fails, it does not prompt or change any host. `--check` does not access keys or the credential store. `credential_node_mismatch`, `credential_server_mismatch`, and `credential_runtime_unsupported` identify incompatible Node paths, server paths, and OS-store support respectively.
66
+
67
+ If only Claude failed, diagnose that host and review migration only when `launcher_migration_available` is returned:
68
+
69
+ ```sh
70
+ aptly set env OPENAI_API_KEY --client claude --check
71
+ aptly setup --client claude --migrate-launcher --dry-run
72
+ aptly setup --client claude --migrate-launcher
73
+ aptly set env OPENAI_API_KEY --client claude
74
+ ```
75
+
76
+ `--dry-run` makes no changes. Migration replaces the Node/server paths of a receipt-matching, Aptly-owned standard registration with the current build. It preserves environment settings, Hooks, policy and other hosts, accesses no credentials, and never executes the old launcher. Custom wrappers and missing or modified ownership records are rejected. Review such registrations locally; use the wrapper's secure environment or a separately reviewed standard registration. Do not edit or delete ownership records to bypass validation.
77
+
78
+ A successful Codex configuration needs no reset. Retry only the affected host and restart MCP. Codex still requires `/hooks` trust review. Changes after diagnosis stop with `concurrent_configuration_change`. If recovery observes another key or cannot read the current key, it leaves the store unchanged and reports `credential_rollback_failed`. Files and the OS store do not form an atomic transaction; avoid concurrent configuration operations.
79
+
59
80
  ### Keys and restarting
60
81
 
61
82
  Pass the classification key, `OPENAI_API_KEY`, through a secure environment or credential launcher to the MCP process. It is separate from host subscription login. Never write key values into command arguments, requests, configuration files, source, logs, or temporary files.
@@ -129,7 +150,7 @@ See the official [Codex configuration reference](https://learn.chatgpt.com/docs/
129
150
  ### Skipping registration and removing it
130
151
 
131
152
  ```sh
132
- APTLY_SKIP_SETUP=1 npm install -g aptly@0.1.1
153
+ APTLY_SKIP_SETUP=1 npm install -g aptly@latest
133
154
  aptly setup --client codex --mode defaults
134
155
  aptly remove --client all
135
156
  ```
@@ -156,6 +177,20 @@ The CLI without additional configuration, and existing manual configurations, de
156
177
  | `get_routing_precedence` | Explains preserved choices and unverified configuration sources for a call |
157
178
  | `route_spawn_hook` | Selects defaults for eligible calls or passes advice only, according to the installed policy |
158
179
 
180
+ ### Outbound data and disabling automatic sharing
181
+
182
+ For automatic classification, `summary` is an **excerpt assembled from an accepted task label and the clipped first paragraph** of the body. It is not a semantic rewrite or anonymization. Its UTF-8 size is at most `min(4000, max_summary_bytes)`; this is not a limit on the whole HTTP request. The opaque-message fallback in Codex uses only a sufficiently descriptive `task_name`, without decoding or transmitting that body. Labels can also contain sensitive information.
183
+
184
+ Decisions receives `client`, `task.summary`, `task.operation`, `task.context_sufficient`, candidate profile IDs, model/effort descriptions, and configured model facts. The classification API key is sent in `Authorization`; the request ID is sent in `X-Client-Request-Id`. Automatic IDs are hashes; direct recommendation IDs are caller-supplied. Do not put secrets in IDs or candidate descriptions.
185
+
186
+ To disable automatic sharing, change **only `enabled` to `false`** in the existing private policy file referenced by `APTLY_HOOK_POLICY_PATH`. Preserve the other fields and ownership record. Subsequent automatic Hook calls preserve the original call before classification or creation of a call-binding file. This cannot undo a transmission already started or recall information received by the server. Editing a generated policy can cause ownership conflicts in setup, configuration, or removal commands; those commands do not overwrite the disabled policy. Do not edit ownership records to bypass the checks.
187
+
188
+ `mode: advisory` still transmits classification data; it is not a privacy opt-out. Removing `APTLY_HOOK_POLICY_PATH` can activate the older strict path, so do not use removal as an alternative. `enabled: false` does not block explicit `recommend_agent` or `aptly recommend` requests.
189
+
190
+ For control over the shared summary, disable automatic sharing and write only the information needed for a direct recommendation. Aptly does not automatically fetch documents at URLs or attach source files mentioned in the summary. Checks for known key and Bearer patterns do not provide complete secret detection or establish that text is safe to transmit.
191
+
192
+ The ledger and Hook bindings store request metadata, hashes, and cached results, not task excerpts. Aptly diagnostics also exclude raw task text. The local Hook's `updatedInput` retains the original task so the host can continue execution. Aptly does not control the host's own transcript or log retention.
193
+
159
194
  ## Direct recommendations
160
195
 
161
196
  The `recommend_agent` MCP tool and CLI `recommend` accept a task summary and recommend a profile. They do not spawn subagents. Pass this JSON as MCP tool arguments, or save it to a file and provide it to the CLI through standard input.
@@ -254,6 +289,14 @@ These are USD conversions of measured native tokens at the [official Standard AP
254
289
 
255
290
  The measured workflow was **explicit MCP recommendation → native execution with the recommended profile passed as arguments**. It did not test automatic Hook URL retrieval or forwarding. The prior benchmark was reused as the control, with three samples per condition. Full-document reads, matching source quotes, and output structure were checked, but the results do not establish equal quality, statistical significance, or savings for other tasks.
256
291
 
292
+ ### Interrupted processes and ledger locks
293
+
294
+ This locking design requires Aptly `0.1.2` or later and is not included in npm `0.1.1`. The JSON ledger keeps its request IDs, `unknown` records, completed cache, call counts, and reserved costs. Only persistent ledgers use Node.js 24's built-in SQLite. The adjacent `ledger.json.lock` remains after a normal exit; its presence alone does not mean an active lock. With the new design, process exit releases the OS lock so a later request can use the same ledger. It does not retry an existing uncertain ID or refund reserved costs.
295
+
296
+ Keep the ledger and lock in a private directory on a local filesystem that supports hard links, and use one canonical path for each ledger. Network shares, cloud-synced directories, and path aliases are outside the supported scope. Do not delete or replace the new lock or SQLite journal, or open them with another database tool while in use. Age and PID alone do not establish that an owner has exited.
297
+
298
+ An old empty `.lock` cannot establish whether its owner is still alive, so it is never initialized or removed automatically. Stop every relevant host, MCP process, and ledger-accessing process, including older versions, and upgrade all writers. Only after confirming that the file belongs to the legacy locking design may you quarantine that file and restart against the same JSON ledger path. Do not reset or delete the ledger, `unknown` records, costs, or completed results. If ownership or format is uncertain, leave access blocked and investigate.
299
+
257
300
  ## Diagnostics and development
258
301
 
259
302
  ```sh
package/docs/guide/ja.md CHANGED
@@ -4,19 +4,21 @@
4
4
 
5
5
  [README に戻る](../../README.ja.md)
6
6
 
7
- 韓国語版が正本です。このガイドは Aptly 0.1.1 を対象とします。パッケージをインストールできることは、すべてのアカウントでモデルにアクセスできることを意味しません。子エージェントの実行・承認・サンドボックス・MCP 権限は、Claude Code または Codex が管理します。
7
+ 韓国語版が正本です。このガイドでは Aptly の基本的なインストール方法と使い方を説明します。パッケージをインストールできることは、すべてのアカウントでモデルにアクセスできることを意味しません。子エージェントの実行・承認・サンドボックス・MCP 権限は、Claude Code または Codex が管理します。
8
8
 
9
9
  ## インストール
10
10
 
11
- Node.js 24 以上と Claude Code または Codex が必要です。0.1.1 が npm で公開されている場合は、以下のグローバルインストールコマンドを使用してください:
11
+ Node.js 24 以上と Claude Code または Codex が必要です。npm の最新安定版をグローバルにインストールするには、次のコマンドを実行してください:
12
+
13
+ **既存の永続台帳を 0.1.2 以降に更新する前に**、その台帳にアクセスする可能性があるすべてのホスト、MCP サーバー、単独 CLI、旧版プロセスを停止してください。新しい永続ロックは旧版の書き込み側と併用できません。新規インストールでは旧ロックの隔離は不要です。旧版の空のロックが残る場合は、[台帳の移行手順](#プロセス中断と台帳ロック)に従い、確認済みの旧ロックだけをオフラインで隔離し、JSON 台帳、`unknown` 記録、費用、完了済みキャッシュを保持してください。
12
14
 
13
15
  ```sh
14
- npm install -g aptly@0.1.1
16
+ npm install -g aptly@latest
15
17
  aptly setup --client all --mode defaults
16
18
  aptly --version
17
19
  ```
18
20
 
19
- `aptly --version` が `0.1.1` を表示することを確認してください。Node.js 環境を切り替えると再インストールが必要な場合があります。
21
+ `aptly --version` でインストール済みのバージョンを確認してください。Node.js 環境を切り替えると再インストールが必要な場合があります。
20
22
 
21
23
  `postinstall` は、現在のユーザーの既存のホスト設定ディレクトリに MCP と `PreToolUse` Hook を登録します。`CODEX_HOME`・`CLAUDE_CONFIG_DIR` を使う場合は、インストールプロセスにも同じ環境を渡してください。まだ存在しないホストディレクトリは自動登録ではスキップし、明示的な `setup` で作成できます。必要なホストだけを登録するには、`--client codex` または `--client claude` を使用してください。
22
24
 
@@ -56,6 +58,25 @@ API キーは macOS Keychain、インストール済みの Linux `secret-tool`
56
58
 
57
59
  変更後は MCP を再起動し、`get_routing_capabilities` の `provider`、候補、`credential_status` を確認してください。`os_store` はキーを読み込めたことを示し、API 認証の成功を示すものではありません。デフォルト設定と公開変数の設定では、引き続き独自ラッパーを保持します。API キー設定は未確認の起動パスのホスト設定を変更せず、保存を試みていた場合は可能な限りそのホストの旧キーを復元します。既存のラッパーにも新しいキーが自動で適用されるとは想定しないでください。Codex の `/hooks` 信頼承認やホスト権限は迂回しません。
58
60
 
61
+ ### 起動パスのエラーからの復旧
62
+
63
+ 以下のオプションは Aptly 0.1.2 以降で利用でき、npm 0.1.1 には含まれません。ソースから試す場合は `npm ci --ignore-scripts` と `npm run build` を実行し、以下の `aptly` を `node bin/aptly.ts` に置き換えます。
64
+
65
+ キー設定は入力・保存前に対象ホストを診断します。いずれかが失敗すると入力を求めず、全ホストを変更しません。`--check` はキーや資格情報ストアにアクセスしません。`credential_node_mismatch`、`credential_server_mismatch`、`credential_runtime_unsupported` はそれぞれ Node パス、サーバーパス、OS ストア対応の不一致を示します。
66
+
67
+ Claude だけが失敗した場合は次の順序で診断し、`launcher_migration_available` の場合にのみ移行を検討してください。
68
+
69
+ ```sh
70
+ aptly set env OPENAI_API_KEY --client claude --check
71
+ aptly setup --client claude --migrate-launcher --dry-run
72
+ aptly setup --client claude --migrate-launcher
73
+ aptly set env OPENAI_API_KEY --client claude
74
+ ```
75
+
76
+ `--dry-run` は変更しません。移行は所有記録と一致する Aptly の標準登録に限り、Node・サーバーパスを現在のビルドへ更新します。環境設定・Hook・ポリシー・他のホストを保持し、資格情報へのアクセスや古い起動プログラムの実行は行いません。独自ラッパー、所有記録のない登録、変更済みの登録は拒否します。その場合は登録をローカルで確認し、ラッパーの安全な環境変数受け渡しを使うか、別途確認した標準登録へ切り替えてください。検証を回避するために所有記録を編集・削除しないでください。
77
+
78
+ 成功した Codex の再設定は不要です。失敗したホストだけを指定して再試行し、MCP を再起動してください。Codex では引き続き `/hooks` の信頼確認が必要です。診断後の設定変更は `concurrent_configuration_change` で中断します。復旧時に別のキーを検出した場合や現在値を読めない場合は上書きせず、`credential_rollback_failed` を返します。ファイルと OS ストアは原子的なトランザクションではないため、設定操作を同時に実行しないでください。
79
+
59
80
  ### キーと再起動
60
81
 
61
82
  分類用の `OPENAI_API_KEY` は、安全な環境や資格情報ランチャーを通して MCP プロセスに渡してください。ホストのサブスクリプション認証とは別です。キーの値をコマンド引数・リクエスト・設定ファイル・ソース・ログ・一時ファイルに記録しないでください。
@@ -129,7 +150,7 @@ APTLY_CODEX_MODELS = '["gpt-6-luna","gpt-6.1-terra","gpt-6.1-sol"]'
129
150
  ### スキップ・削除
130
151
 
131
152
  ```sh
132
- APTLY_SKIP_SETUP=1 npm install -g aptly@0.1.1
153
+ APTLY_SKIP_SETUP=1 npm install -g aptly@latest
133
154
  aptly setup --client codex --mode defaults
134
155
  aptly remove --client all
135
156
  ```
@@ -156,6 +177,20 @@ aptly remove --client all
156
177
  | `get_routing_precedence` | 呼び出しを保持する根拠と、未確認の設定元を説明します |
157
178
  | `route_spawn_hook` | インストールしたポリシーに応じて、対象呼び出しのデフォルト値を選択するか、推薦だけを伝えます |
158
179
 
180
+ ### 外部送信と自動共有の無効化
181
+
182
+ 自動分類の `summary` は、有効なタスクラベルと本文の**最初の段落を切り詰めて組み合わせた抜粋**です。意味を再要約したものでも、匿名化したものでもありません。UTF-8 のサイズは `min(4000, max_summary_bytes)` 以下ですが、HTTP リクエスト全体の上限ではありません。Codex の opaque メッセージへのフォールバックでは、本文を復号・送信せず、十分に具体的な `task_name` だけを使用します。ラベルにも機密情報が含まれる可能性があります。
183
+
184
+ Decisions には `client`、`task.summary`、`task.operation`、`task.context_sufficient` と、候補プロファイル ID・モデルと effort の説明・設定したモデル特性を送信します。分類用 API キーは `Authorization` ヘッダー、リクエスト ID は `X-Client-Request-Id` ヘッダーで送信します。自動経路の ID はハッシュで、直接推薦の ID は呼び出し元が指定します。ID や候補の説明にも秘密情報を含めないでください。
185
+
186
+ 自動共有を無効にするには、`APTLY_HOOK_POLICY_PATH` が指す既存の非公開ポリシーファイルで、**`enabled` だけを `false` に変更**してください。他のフィールドと所有権の記録は保持してください。その後の自動 Hook 呼び出しは、分類や呼び出しバインディングファイルの作成前に元の呼び出しを保持します。開始済みの送信を取り消したり、サーバーが受信した情報を回収したりすることはできません。生成済みのポリシーを変更すると、登録・設定・削除コマンドの所有権検査で競合する場合があり、それらのコマンドは無効化したポリシーを上書きしません。検査を回避するために所有権の記録を編集しないでください。
187
+
188
+ `mode: advisory` でも分類データは送信されるため、プライバシー上の共有停止にはなりません。`APTLY_HOOK_POLICY_PATH` を削除すると従来の strict 経路が有効になる場合があるため、共有停止の代わりにしないでください。`enabled: false` は、明示的な `recommend_agent` や `aptly recommend` のリクエストを止めるものではありません。
189
+
190
+ 共有する要約を自分で選ぶ場合は、自動共有を無効にしてから直接推薦に必要な情報だけを記述してください。Aptly は要約中の URL の文書や、言及されたソースファイルを自動取得・添付しません。既知のキーや Bearer のパターン検査は、完全な秘密情報の検出や送信の安全性を保証しません。
191
+
192
+ 台帳と Hook バインディングにはリクエストのメタデータ・ハッシュ・キャッシュ結果を保存し、タスクの抜粋は保存しません。Aptly の診断にもタスク原文を含めません。ただし、ローカルの Hook の `updatedInput` は、ホストが実行を続けられるよう元のタスクを保持します。ホスト自身の会話やログの保存は Aptly の制御対象ではありません。
193
+
159
194
  ## 直接推薦
160
195
 
161
196
  `recommend_agent` MCP と CLI の `recommend` は、タスク要約を受け取ってプロファイルを推薦します。子エージェントを直接生成することはありません。以下の JSON を MCP ツールの引数として渡すか、ファイルに保存して CLI の標準入力に渡してください。
@@ -254,6 +289,14 @@ Codex が子エージェントの `message` を暗号文として Hook に渡す
254
289
 
255
290
  測定経路は、**明示的な MCP 推薦 → 推薦プロファイルを引数として渡したホスト上での実行**です。自動 Hook の URL 収集・伝達機能を検証したものではありません。基準群は直前の実測を再利用しており、条件ごとの標本は 3 回です。ドキュメント全体の読み取り・原文の引用一致・出力形式は確認しましたが、品質の同等性、統計的有意性、他のタスクでの費用削減を保証するものではありません。
256
291
 
292
+ ### プロセス中断と台帳ロック
293
+
294
+ このロック方式は Aptly `0.1.2` 以降で利用でき、npm `0.1.1` には含まれません。JSON 台帳のリクエスト ID・`unknown`・完了済みキャッシュ・呼び出し数・予約費用の形式は維持します。永続台帳だけが Node.js 24 内蔵の SQLite を使用します。隣接する `ledger.json.lock` は正常終了後も残るため、ファイルの存在だけではロックが使用中かどうかを判断できません。新方式ではプロセス終了時に OS のロックが解放され、後続のリクエストが同じ台帳を使用できます。結果が不明な既存 ID の再試行や予約費用の返却は行いません。
295
+
296
+ 台帳とロックは、ハードリンクに対応したローカルファイルシステムの非公開ディレクトリに置き、各台帳に同一の正規パスを使用してください。ネットワーク共有・クラウド同期ディレクトリ・パスの別名はサポート範囲外です。新しいロックや SQLite journal を削除・置換したり、使用中に他の DB ツールで開いたりしないでください。ファイルの古さや PID だけでは所有プロセスの終了を確認できません。
297
+
298
+ 旧版の空の `.lock` ファイルでは所有プロセスが生存しているかを確認できないため、自動で初期化・削除しません。旧版を含む関連ホスト・MCP・台帳にアクセスするすべてのプロセスを停止し、書き込み側をすべて新しい版に更新してください。旧方式のロックであると確認できた場合だけ、そのファイルを別途隔離し、同じ JSON 台帳パスで再開します。台帳・`unknown`・費用・完了結果を初期化・削除しないでください。所有者や形式を確認できない場合は、遮断状態を保って調査してください。
299
+
257
300
  ## 診断と開発
258
301
 
259
302
  ```sh
package/docs/guide/ko.md CHANGED
@@ -4,19 +4,21 @@
4
4
 
5
5
  [README로 돌아가기](../../README.md)
6
6
 
7
- 한국어가 정본입니다. 이 안내는 Aptly 0.1.1을 기준으로 합니다. 패키지를 설치할 수 있다는 사실이 모든 계정의 모델 접근 가능성을 뜻하지는 않습니다. 실제 자식 실행·승인·샌드박스·MCP 권한은 Claude Code 또는 Codex가 담당합니다.
7
+ 한국어가 정본입니다. 이 안내는 Aptly의 일반적인 설치·사용 방법을 설명합니다. 패키지를 설치할 수 있다는 사실이 모든 계정의 모델 접근 가능성을 뜻하지는 않습니다. 실제 자식 실행·승인·샌드박스·MCP 권한은 Claude Code 또는 Codex가 담당합니다.
8
8
 
9
9
  ## 설치
10
10
 
11
- Node.js 24 이상과 Claude Code 또는 Codex가 필요합니다. 0.1.1이 npm에 게시된 경우 다음 전역 설치 명령을 사용하십시오:
11
+ Node.js 24 이상과 Claude Code 또는 Codex가 필요합니다. npm의 최신 안정 버전을 전역 설치하려면 다음 명령을 사용하십시오:
12
+
13
+ **기존 영속 원장을 0.1.2 이상으로 업그레이드하기 전에**, 해당 원장을 사용하는 모든 호스트·MCP 서버·별도 CLI와 구버전 프로세스를 정지하십시오. 새 영구 잠금은 구버전 writer와 함께 사용할 수 없습니다. 새 설치에는 기존 잠금 격리가 필요하지 않습니다. 이전 빈 잠금이 남았다면 [원장 잠금 전환 절차](#중단된-프로세스와-원장-잠금)를 따라 확인된 기존 잠금만 오프라인으로 격리하고, JSON 원장·`unknown`·비용·완료 캐시는 보존하십시오.
12
14
 
13
15
  ```sh
14
- npm install -g aptly@0.1.1
16
+ npm install -g aptly@latest
15
17
  aptly setup --client all --mode defaults
16
18
  aptly --version
17
19
  ```
18
20
 
19
- `aptly --version`의 출력은 `0.1.1`이어야 합니다. Node.js 환경을 바꾸면 재설치가 필요할 수 있습니다.
21
+ `aptly --version`으로 설치된 버전을 확인하십시오. Node.js 환경을 바꾸면 재설치가 필요할 수 있습니다.
20
22
 
21
23
  `postinstall`은 현재 사용자의 기존 호스트 설정 디렉터리에 MCP와 `PreToolUse` Hook을 등록합니다. `CODEX_HOME`·`CLAUDE_CONFIG_DIR`을 사용한다면 설치 프로세스에도 같은 환경을 전달하십시오. 아직 없는 호스트 디렉터리는 자동 등록에서 건너뛰고, 명시적인 `setup`으로 만들 수 있습니다. 필요한 호스트만 등록하려면 `--client codex` 또는 `--client claude`를 사용하십시오.
22
24
 
@@ -56,6 +58,25 @@ API 키는 macOS Keychain, Linux의 설치된 `secret-tool`, 또는 Windows Cred
56
58
 
57
59
  변경 후 MCP를 재시작하고 `get_routing_capabilities`의 `provider`, 후보 목록, `credential_status`를 확인하십시오. `os_store`는 저장소에서 키를 읽었다는 뜻이며 API 유효성 검증은 아닙니다. 기본 설정과 일반 변수 설정은 기존 사용자 지정 래퍼를 보존합니다. API 키 설정은 호환성이 확인되지 않은 실행 경로의 호스트 설정을 변경하지 않으며, 이미 저장을 시도했다면 해당 호스트의 이전 키를 가능한 경우 복원합니다. 새 키가 기존 래퍼에도 자동 적용된다고 가정하지 마십시오. 명령은 Codex의 `/hooks` 신뢰 승인이나 호스트 권한을 우회하지 않습니다.
58
60
 
61
+ ### 실행 경로 오류 복구
62
+
63
+ 아래 옵션은 Aptly 0.1.2부터 지원하며 npm 0.1.1에는 없습니다. 소스에서 시험하려면 `npm ci --ignore-scripts`와 `npm run build` 후 `aptly` 대신 `node bin/aptly.ts`를 사용합니다.
64
+
65
+ 키 설정은 입력·저장 전에 선택한 호스트를 진단합니다. 하나라도 실패하면 입력을 받지 않고 모든 호스트를 그대로 둡니다. `--check`는 키나 자격 저장소에 접근하지 않습니다. `credential_node_mismatch`, `credential_server_mismatch`, `credential_runtime_unsupported`는 각각 Node 경로, 서버 경로, OS 저장소 지원의 불일치입니다.
66
+
67
+ Claude만 실패했다면 다음 순서로 진단하고, `launcher_migration_available`일 때만 이전을 검토하십시오:
68
+
69
+ ```sh
70
+ aptly set env OPENAI_API_KEY --client claude --check
71
+ aptly setup --client claude --migrate-launcher --dry-run
72
+ aptly setup --client claude --migrate-launcher
73
+ aptly set env OPENAI_API_KEY --client claude
74
+ ```
75
+
76
+ `--dry-run`은 변경하지 않습니다. 이전 명령은 소유 기록과 일치하는 Aptly 표준 등록의 Node·서버 경로를 현재 빌드로 바꿉니다. 기존 환경·Hook·정책·다른 호스트는 보존하며 키를 읽거나 저장하지 않습니다. 오래된 실행 파일을 실행하지 않습니다. 사용자 래퍼, 소유 기록이 없거나 수정된 등록은 거절합니다. 이 경우 로컬에서 등록을 검토하고 래퍼의 안전한 환경 전달을 사용하거나 별도로 검토한 표준 등록으로 전환하십시오. 소유 기록을 삭제하거나 편집해 검증을 우회하지 마십시오.
77
+
78
+ 이미 성공한 Codex는 재설정할 필요가 없습니다. 실패한 호스트만 지정해 재시도하고 MCP를 재시작하십시오. Codex를 대상으로 하면 `/hooks` 신뢰 검토가 계속 필요합니다. 진단 이후 설정 변경은 `concurrent_configuration_change`로 중단합니다. 저장 실패 후 다른 키가 관찰되거나 현재 키를 읽지 못하면 덮어쓰지 않고 `credential_rollback_failed`를 반환합니다. 파일과 OS 저장소 사이의 원자적 트랜잭션은 아니므로 다른 설정 작업을 동시에 실행하지 마십시오.
79
+
59
80
  ### 키와 재시작
60
81
 
61
82
  분류용 `OPENAI_API_KEY`를 안전한 환경이나 자격 주입 래퍼를 통해 MCP 프로세스에 전달하십시오. 호스트 구독 로그인과 별개입니다. 키 값을 명령 인자·요청·설정 파일·소스·로그·임시 파일에 기록하지 마십시오.
@@ -129,7 +150,7 @@ APTLY_CODEX_MODELS = '["gpt-6-luna","gpt-6.1-terra","gpt-6.1-sol"]'
129
150
  ### 건너뛰기·제거
130
151
 
131
152
  ```sh
132
- APTLY_SKIP_SETUP=1 npm install -g aptly@0.1.1
153
+ APTLY_SKIP_SETUP=1 npm install -g aptly@latest
133
154
  aptly setup --client codex --mode defaults
134
155
  aptly remove --client all
135
156
  ```
@@ -156,6 +177,20 @@ aptly remove --client all
156
177
  | `get_routing_precedence` | 호출의 보존 근거와 미확인 설정 출처를 설명합니다 |
157
178
  | `route_spawn_hook` | 설치한 정책에 따라 대상 호출의 기본값을 선택하거나 추천만 전달합니다 |
158
179
 
180
+ ### 외부 전송과 자동 공유 끄기
181
+
182
+ 자동 분류의 `summary`는 허용된 작업 라벨과 본문의 **첫 문단을 잘라 붙인 발췌문**입니다. 의미를 새로 요약하거나 익명화하지 않습니다. UTF-8 크기는 `min(4000, max_summary_bytes)` 이하이며 전체 HTTP 요청의 크기 제한은 아닙니다. Codex의 opaque 메시지 fallback은 본문을 해독·전송하지 않고 충분히 구체적인 `task_name`만 사용합니다. 라벨에도 민감한 정보가 들어갈 수 있습니다.
183
+
184
+ Decisions에는 `client`, `task.summary`, `task.operation`, `task.context_sufficient`와 후보 프로필 ID·모델·effort 설명 및 설정된 모델 특성이 전달됩니다. 분류용 API 키는 `Authorization` 헤더로, 요청 ID는 `X-Client-Request-Id` 헤더로 전송됩니다. 자동 경로의 ID는 해시이며 직접 추천의 ID는 호출자가 지정합니다. ID나 후보 설명에도 비밀을 넣지 마십시오.
185
+
186
+ 자동 공유를 끄려면 `APTLY_HOOK_POLICY_PATH`가 가리키는 기존 비공개 정책 파일에서 **`enabled`만 `false`로 변경**하십시오. 나머지 필드와 소유권 기록은 유지하십시오. 이후 자동 Hook 호출은 분류 API나 호출 바인딩 파일을 만들기 전에 원래 호출을 보존합니다. 이미 시작한 전송을 되돌리거나 서버가 받은 내용을 회수하지는 못합니다. 생성된 정책을 바꾸면 설치·설정·제거 명령의 소유권 검사에서 충돌할 수 있으며, 해당 명령은 끄기 설정을 덮어쓰지 않습니다. 소유권 기록을 수정해 검사를 우회하지 마십시오.
187
+
188
+ `mode: advisory`도 분류 정보를 전송하므로 개인정보 보호용 비활성화가 아닙니다. `APTLY_HOOK_POLICY_PATH`를 지우면 기존 strict 경로로 전환될 수 있으므로 공유 끄기의 대안으로 사용하지 마십시오. `enabled: false`는 명시적인 `recommend_agent`·`aptly recommend` 요청까지 차단하지 않습니다.
189
+
190
+ 공유할 요약을 직접 선택하려면 자동 공유를 끈 뒤 직접 추천에 필요한 정보만 작성하십시오. Aptly는 요약에 적은 URL의 문서나 소스 파일을 자동으로 읽어 첨부하지 않습니다. 알려진 키·Bearer 패턴을 거르는 검사는 완전한 비밀 탐지나 전송 안전성 보장이 아닙니다.
191
+
192
+ 원장과 Hook 바인딩에는 요청 메타데이터·해시·캐시 결과를 저장하며 작업 발췌문을 저장하지 않습니다. Aptly의 진단 결과에도 원문을 넣지 않습니다. 다만 로컬 호스트가 실행을 이어가도록 Hook의 `updatedInput`에는 원래 작업이 유지됩니다. 호스트 자체의 대화·로그 저장은 Aptly가 제어하지 않습니다.
193
+
159
194
  ## 직접 추천
160
195
 
161
196
  `recommend_agent` MCP와 CLI `recommend`는 작업 요약을 받아 프로필을 추천합니다. 자식을 직접 생성하지 않습니다. 아래 JSON을 MCP 도구 인자로 전달하거나 파일에 저장해 CLI의 표준 입력으로 제공하십시오.
@@ -254,6 +289,14 @@ Codex가 자식 `message`를 암호문으로 Hook에 전달하는 환경에서
254
289
 
255
290
  측정은 **명시적 MCP 추천 → 추천 프로필을 인자로 전달한 호스트의 기본 실행**입니다. 자동 Hook의 URL 수집·전달 기능 검증이 아닙니다. 기준군은 직전 실측을 재사용했고 조건별 표본은 3회입니다. 문서 전체 읽기·원문 인용 일치·출력 형식은 확인했지만 품질 동등성, 통계적 유의성, 다른 업무의 절감을 보장하지 않습니다.
256
291
 
292
+ ### 중단된 프로세스와 원장 잠금
293
+
294
+ 이 잠금 방식은 Aptly `0.1.2`부터 지원하며 npm `0.1.1`에는 없습니다. JSON 원장의 요청 ID·`unknown`·완료 캐시·호출 수·비용 예약 형식은 유지합니다. 영속 원장에만 Node.js 24 내장 SQLite를 사용하며, 원장 옆의 `ledger.json.lock`은 정상 종료 후에도 남는 잠금 파일입니다. 파일이 있다는 사실만으로 활성 잠금을 뜻하지 않습니다. 새 방식에서는 프로세스 종료 시 OS 잠금이 해제되므로 후속 요청이 같은 원장을 다시 사용할 수 있습니다. 결과가 불명확했던 기존 ID를 재호출하거나 예약 비용을 되돌리지는 않습니다.
295
+
296
+ 원장과 잠금 파일은 하드 링크를 지원하는 로컬 파일시스템의 비공개 디렉터리에 두고, 하나의 원장을 동일한 정규 경로로 사용하십시오. 네트워크 공유·클라우드 동기화 디렉터리·경로 별칭은 지원 범위가 아닙니다. 새 잠금 파일과 SQLite journal을 삭제·교체하거나, 사용 중 다른 DB 도구로 열지 마십시오. 잠금 파일의 나이와 PID만으로 소유자가 종료됐다고 판단하지 않습니다.
297
+
298
+ 이전 버전의 빈 `.lock` 파일은 살아 있는 소유자가 있는지 확인할 수 없어 자동으로 초기화하거나 제거하지 않습니다. 구버전을 포함한 모든 관련 호스트·MCP·원장 접근 프로세스를 정지하고, 모든 writer를 새 버전으로 전환하십시오. 파일이 기존 방식의 잠금임을 확인한 경우에만 그 파일을 별도로 격리한 뒤 같은 JSON 원장 경로로 다시 시작합니다. 원장·`unknown`·비용·완료 결과는 초기화하거나 삭제하지 마십시오. 소유자나 형식을 확신할 수 없으면 차단 상태를 유지하고 조사하십시오.
299
+
257
300
  ## 진단과 개발
258
301
 
259
302
  ```sh