argos-harness 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/assets/hooks/argos-guard-destructive.sh +1 -1
  2. package/assets/managed/disciplina-skills.md +11 -0
  3. package/assets/managed/formato-respuesta.md +2 -2
  4. package/assets/managed/identidad.md +7 -7
  5. package/assets/managed/operaciones-seguras.md +6 -6
  6. package/assets/output-styles/argos.md +12 -12
  7. package/dist/commands/adopt.d.ts +41 -1
  8. package/dist/commands/adopt.d.ts.map +1 -1
  9. package/dist/commands/adopt.js +243 -6
  10. package/dist/commands/adopt.js.map +1 -1
  11. package/dist/commands/doctor.d.ts.map +1 -1
  12. package/dist/commands/doctor.js +36 -2
  13. package/dist/commands/doctor.js.map +1 -1
  14. package/dist/commands/init.d.ts +56 -0
  15. package/dist/commands/init.d.ts.map +1 -1
  16. package/dist/commands/init.js +330 -64
  17. package/dist/commands/init.js.map +1 -1
  18. package/dist/commands/remove.d.ts +24 -0
  19. package/dist/commands/remove.d.ts.map +1 -1
  20. package/dist/commands/remove.js +94 -4
  21. package/dist/commands/remove.js.map +1 -1
  22. package/dist/commands/workspace.d.ts +32 -0
  23. package/dist/commands/workspace.d.ts.map +1 -1
  24. package/dist/commands/workspace.js +97 -3
  25. package/dist/commands/workspace.js.map +1 -1
  26. package/dist/lib/assets.d.ts +5 -3
  27. package/dist/lib/assets.d.ts.map +1 -1
  28. package/dist/lib/assets.js +5 -2
  29. package/dist/lib/assets.js.map +1 -1
  30. package/dist/lib/managed-files.d.ts +19 -5
  31. package/dist/lib/managed-files.d.ts.map +1 -1
  32. package/dist/lib/managed-files.js +18 -6
  33. package/dist/lib/managed-files.js.map +1 -1
  34. package/dist/lib/prompter.d.ts +63 -0
  35. package/dist/lib/prompter.d.ts.map +1 -0
  36. package/dist/lib/prompter.js +30 -0
  37. package/dist/lib/prompter.js.map +1 -0
  38. package/dist/lib/settings-merge.d.ts +78 -0
  39. package/dist/lib/settings-merge.d.ts.map +1 -1
  40. package/dist/lib/settings-merge.js +204 -0
  41. package/dist/lib/settings-merge.js.map +1 -1
  42. package/package.json +1 -1
@@ -6,9 +6,10 @@ import { listAgentIds, listSkillFiles, listSkillIds, MANAGED_BLOCK_IDS, readAsse
6
6
  import { writeFileAtomic } from "../lib/atomic-write.js";
7
7
  import { createBackup } from "../lib/backup.js";
8
8
  import { injectBlock, listBlocks } from "../lib/markers.js";
9
- import { hasArgosFileMarker, writeManagedFile, writeManagedShellFile } from "../lib/managed-files.js";
9
+ import { hasArgosFileMarker, hasArgosShellFileMarker, writeManagedFile, writeManagedShellFile } from "../lib/managed-files.js";
10
10
  import { resolveArgosHome, resolveClaudeDir } from "../lib/paths.js";
11
- import { mergeHooksIntoSettings } from "../lib/settings-merge.js";
11
+ import { applyOutputStylePolicy, isNavoriOutputStyle, mergeHooksIntoSettings } from "../lib/settings-merge.js";
12
+ import { isInteractive, clackPrompter } from "../lib/prompter.js";
12
13
  import { readCliVersion } from "../lib/version.js";
13
14
  /**
14
15
  * Ids (basenames under assets/hooks/) of the 2 global hooks argos init
@@ -32,6 +33,7 @@ const STATUS_COUNT_ORDER = [
32
33
  "updated",
33
34
  "unchanged",
34
35
  "skipped-foreign",
36
+ "overwritten-foreign",
35
37
  "error"
36
38
  ];
37
39
  function summarize(rows) {
@@ -40,6 +42,7 @@ function summarize(rows) {
40
42
  updated: 0,
41
43
  unchanged: 0,
42
44
  "skipped-foreign": 0,
45
+ "overwritten-foreign": 0,
43
46
  error: 0
44
47
  };
45
48
  for (const row of rows)counts[row.status]++;
@@ -70,6 +73,23 @@ function errorMessage(err) {
70
73
  writeFileAtomic(destPath, sourceContent);
71
74
  return "updated";
72
75
  }
76
+ /**
77
+ * `--force` counterpart to `writePlainFile`, used only for a supporting file
78
+ * (e.g. `references/core.md`) that lives under a skill directory whose
79
+ * `SKILL.md` was just detected as foreign (no `argos:file` marker). These
80
+ * plain subfiles never carry a marker of their own — the whole skill
81
+ * directory's ownership is decided once from `SKILL.md` (see the skills loop
82
+ * below) — so this always overwrites unconditionally: `overwritten-foreign`
83
+ * when something was already there to replace, `created` when the motor
84
+ * ships a subfile the foreign directory never had.
85
+ */ function writeForcedPlainFile(destPath, sourceContent) {
86
+ const existed = existsSync(destPath);
87
+ if (!existed) mkdirSync(dirname(destPath), {
88
+ recursive: true
89
+ });
90
+ writeFileAtomic(destPath, sourceContent);
91
+ return existed ? "overwritten-foreign" : "created";
92
+ }
73
93
  /** Inject one managed CLAUDE.md block, reporting created/updated/unchanged. */ function injectAndReport(claudeMd, id, version, content) {
74
94
  const hadBlock = listBlocks(claudeMd).some((b)=>b.id === id);
75
95
  const after = injectBlock(claudeMd, id, version, content);
@@ -86,6 +106,9 @@ function errorMessage(err) {
86
106
  * filesystem — no process.exit, no console output.
87
107
  */ export function runInit(options = {}) {
88
108
  const language = options.language ?? "es";
109
+ const installAgents = options.installAgents ?? true;
110
+ const installHooks = options.installHooks ?? true;
111
+ const force = options.force ?? false;
89
112
  const version = readCliVersion();
90
113
  const claudeDir = resolveClaudeDir();
91
114
  const assetsDir = resolveAssetsDir();
@@ -117,7 +140,7 @@ function errorMessage(err) {
117
140
  exitCode: 1
118
141
  };
119
142
  }
120
- // 1. CLAUDE.md — 5 managed blocks, in order.
143
+ // 1. CLAUDE.md — MANAGED_BLOCK_IDS.length managed blocks, in order.
121
144
  const claudeMdPath = join(claudeDir, "CLAUDE.md");
122
145
  let claudeMd = existsSync(claudeMdPath) ? readFileSync(claudeMdPath, "utf-8") : "";
123
146
  const blockResults = [];
@@ -147,22 +170,27 @@ function errorMessage(err) {
147
170
  detail
148
171
  });
149
172
  }
150
- // 2. Full-file assets: output-style, agents.
173
+ // 2. Full-file assets: output-style always, agents gated by the
174
+ // `installAgents` toggle (default true; the wizard's "agentes sí/no"
175
+ // step — spec 0004). Skills below are NEVER gated: they load on-demand
176
+ // and trimming the set breaks the arsenal.
151
177
  const fullFiles = [
152
178
  [
153
179
  "output-styles",
154
180
  "argos.md"
155
181
  ],
156
- ...listAgentIds(assetsDir).map((id)=>[
182
+ ...installAgents ? listAgentIds(assetsDir).map((id)=>[
157
183
  "agents",
158
184
  `${id}.md`
159
- ])
185
+ ]) : []
160
186
  ];
161
187
  for (const relPath of fullFiles){
162
188
  const source = readAsset(assetsDir, ...relPath);
163
189
  const dest = join(claudeDir, ...relPath);
164
190
  try {
165
- const status = writeManagedFile(dest, source, version);
191
+ const status = writeManagedFile(dest, source, version, {
192
+ force
193
+ });
166
194
  rows.push({
167
195
  path: join(...relPath),
168
196
  status
@@ -182,14 +210,18 @@ function errorMessage(err) {
182
210
  // file the skill ships.
183
211
  // - SKILL.md present WITHOUT the marker (foreign/user-modified) → skip
184
212
  // every file under that skill dir, untouched — same policy as a single
185
- // foreign full-file asset above, extended to the whole directory.
213
+ // foreign full-file asset above, extended to the whole directory. Under
214
+ // `--force`, every file in that directory is overwritten instead
215
+ // (`overwritten-foreign`) — SKILL.md gets its marker stamped via
216
+ // `writeManagedFile`'s own `force` path, and each plain subfile is
217
+ // force-written via `writeForcedPlainFile` (see its own doc comment).
186
218
  for (const skillId of listSkillIds(assetsDir)){
187
219
  const skillMdDest = join(claudeDir, "skills", skillId, "SKILL.md");
188
220
  const isForeignSkill = existsSync(skillMdDest) && !hasArgosFileMarker(readFileSync(skillMdDest, "utf-8"));
189
221
  for (const relFile of listSkillFiles(assetsDir, skillId)){
190
222
  const relParts = relFile.split("/");
191
223
  const relPath = join("skills", skillId, ...relParts);
192
- if (isForeignSkill) {
224
+ if (isForeignSkill && !force) {
193
225
  rows.push({
194
226
  path: relPath,
195
227
  status: "skipped-foreign"
@@ -199,7 +231,9 @@ function errorMessage(err) {
199
231
  const source = readAsset(assetsDir, "skills", skillId, ...relParts);
200
232
  const dest = join(claudeDir, "skills", skillId, ...relParts);
201
233
  try {
202
- const status = relFile === "SKILL.md" ? writeManagedFile(dest, source, version) : writePlainFile(dest, source);
234
+ const status = relFile === "SKILL.md" ? writeManagedFile(dest, source, version, {
235
+ force
236
+ }) : isForeignSkill ? writeForcedPlainFile(dest, source) : writePlainFile(dest, source);
203
237
  rows.push({
204
238
  path: relPath,
205
239
  status
@@ -219,62 +253,92 @@ function errorMessage(err) {
219
253
  // write threw must NEVER get a settings.json entry (see 2c below): a
220
254
  // dangling PreToolUse entry pointing at a script that isn't there hard-
221
255
  // blocks every subsequent Bash call.
256
+ //
257
+ // Gated by the `installHooks` toggle (default true; the wizard's "hooks
258
+ // sí/no" step — spec 0004): when disabled, this run neither writes nor
259
+ // touches any pre-existing hook script/settings.json entry — a full
260
+ // uninstall of previously-installed hooks is `argos remove`'s job, not a
261
+ // side effect of toggling this flag off on a later `init` run.
222
262
  const hookWriteFailed = new Map();
223
- for (const id of HOOK_IDS){
224
- const relPath = [
225
- "hooks",
226
- `${id}.sh`
227
- ];
228
- const source = readAsset(assetsDir, ...relPath);
229
- const dest = join(claudeDir, ...relPath);
230
- try {
231
- const status = writeManagedShellFile(dest, source, version);
232
- rows.push({
233
- path: join(...relPath),
234
- status
235
- });
236
- hookWriteFailed.set(id, false);
237
- } catch (err) {
238
- rows.push({
239
- path: join(...relPath),
240
- status: "error",
241
- detail: errorMessage(err)
242
- });
243
- hookWriteFailed.set(id, true);
263
+ if (installHooks) {
264
+ for (const id of HOOK_IDS){
265
+ const relPath = [
266
+ "hooks",
267
+ `${id}.sh`
268
+ ];
269
+ const source = readAsset(assetsDir, ...relPath);
270
+ const dest = join(claudeDir, ...relPath);
271
+ try {
272
+ const status = writeManagedShellFile(dest, source, version, {
273
+ force
274
+ });
275
+ rows.push({
276
+ path: join(...relPath),
277
+ status
278
+ });
279
+ hookWriteFailed.set(id, false);
280
+ } catch (err) {
281
+ rows.push({
282
+ path: join(...relPath),
283
+ status: "error",
284
+ detail: errorMessage(err)
285
+ });
286
+ hookWriteFailed.set(id, true);
287
+ }
244
288
  }
289
+ // 2c. settings.json — surgical merge of the 2 PreToolUse hook entries.
290
+ // Only writes/updates entries whose command targets one of the 2 scripts
291
+ // above; every other key and hook in the user's settings.json is left
292
+ // untouched (see lib/settings-merge.ts). Only hooks whose script write
293
+ // actually succeeded get an entry built for them at all.
294
+ const settingsPath = join(claudeDir, "settings.json");
295
+ const allHookSpecs = {
296
+ "argos-guard-destructive": {
297
+ scriptPath: join(claudeDir, "hooks", "argos-guard-destructive.sh"),
298
+ matcher: "Bash",
299
+ timeout: 10,
300
+ statusMessage: "argos: guard-destructive"
301
+ },
302
+ "argos-quality-gate": {
303
+ scriptPath: join(claudeDir, "hooks", "argos-quality-gate.sh"),
304
+ matcher: "Bash",
305
+ timeout: QUALITY_GATE_OUTER_TIMEOUT_SECONDS,
306
+ statusMessage: "argos: quality-gate"
307
+ }
308
+ };
309
+ const hookSpecs = HOOK_IDS.filter((id)=>!hookWriteFailed.get(id)).map((id)=>allHookSpecs[id]);
310
+ // Any hook whose write just failed also gets its (possibly pre-existing,
311
+ // from an earlier successful run) settings.json entry stripped out — a
312
+ // script that's gone or broken must never be left with a live entry.
313
+ const failedScriptPaths = HOOK_IDS.filter((id)=>hookWriteFailed.get(id)).map((id)=>allHookSpecs[id].scriptPath);
314
+ const mergeResult = mergeHooksIntoSettings(settingsPath, hookSpecs, {
315
+ removeScriptPaths: failedScriptPaths
316
+ });
317
+ rows.push({
318
+ path: "settings.json",
319
+ status: mergeResult.status,
320
+ detail: mergeResult.detail
321
+ });
245
322
  }
246
- // 2c. settings.json surgical merge of the 2 PreToolUse hook entries.
247
- // Only writes/updates entries whose command targets one of the 2 scripts
248
- // above; every other key and hook in the user's settings.json is left
249
- // untouched (see lib/settings-merge.ts). Only hooks whose script write
250
- // actually succeeded get an entry built for them at all.
251
- const settingsPath = join(claudeDir, "settings.json");
252
- const allHookSpecs = {
253
- "argos-guard-destructive": {
254
- scriptPath: join(claudeDir, "hooks", "argos-guard-destructive.sh"),
255
- matcher: "Bash",
256
- timeout: 10,
257
- statusMessage: "argos: guard-destructive"
258
- },
259
- "argos-quality-gate": {
260
- scriptPath: join(claudeDir, "hooks", "argos-quality-gate.sh"),
261
- matcher: "Bash",
262
- timeout: QUALITY_GATE_OUTER_TIMEOUT_SECONDS,
263
- statusMessage: "argos: quality-gate"
264
- }
265
- };
266
- const hookSpecs = HOOK_IDS.filter((id)=>!hookWriteFailed.get(id)).map((id)=>allHookSpecs[id]);
267
- // Any hook whose write just failed also gets its (possibly pre-existing,
268
- // from an earlier successful run) settings.json entry stripped out — a
269
- // script that's gone or broken must never be left with a live entry.
270
- const failedScriptPaths = HOOK_IDS.filter((id)=>hookWriteFailed.get(id)).map((id)=>allHookSpecs[id].scriptPath);
271
- const mergeResult = mergeHooksIntoSettings(settingsPath, hookSpecs, {
272
- removeScriptPaths: failedScriptPaths
323
+ // 2d. Voice activation (spec 0004 "Activación de la voz"):
324
+ // settings.json.outputStyle. Absent set to "Argos". Matching the
325
+ // predecessor harness's voice (navori) takeover per
326
+ // `takeoverNavoriVoice` (default true, i.e. unconditional replace under
327
+ // --yes/no-TTY; the interactive wizard passes `false` when the user
328
+ // declines). Any other value → never touched. Reported separately from
329
+ // the hooks settings.json row above so a takeover/foreign-voice detail is
330
+ // never conflated with the hooks merge outcome.
331
+ const outputStyleResult = applyOutputStylePolicy(join(claudeDir, "settings.json"), {
332
+ takeoverNavori: options.takeoverNavoriVoice ?? true
273
333
  });
334
+ // "untouched" (a foreign non-navori voice, or a declined navori takeover)
335
+ // maps to the same "skipped-foreign" row status used elsewhere for
336
+ // "found something not ours, left it byte-identical".
337
+ const outputStyleRowStatus = outputStyleResult.status === "untouched" ? "skipped-foreign" : outputStyleResult.status;
274
338
  rows.push({
275
- path: "settings.json",
276
- status: mergeResult.status,
277
- detail: mergeResult.detail
339
+ path: "settings.json#outputStyle",
340
+ status: outputStyleRowStatus,
341
+ detail: outputStyleResult.detail
278
342
  });
279
343
  // 3. ~/.argos/global.json
280
344
  const argosHome = resolveArgosHome();
@@ -309,6 +373,194 @@ function errorMessage(err) {
309
373
  backupPath
310
374
  };
311
375
  }
376
+ /**
377
+ * A cancelled wizard's report: identical shape to a run that touched
378
+ * nothing. `exitCode: 1` — a cancel is neither a successful write nor
379
+ * silently indistinguishable from one by exit code alone (matches
380
+ * `runWorkspaceLinkInteractive`'s convention for its own cancel paths).
381
+ */ function cancelledInitReport() {
382
+ return {
383
+ rows: [],
384
+ summary: "argos init: cancelado — no se tocó nada.",
385
+ exitCode: 1
386
+ };
387
+ }
388
+ /**
389
+ * Read-only peek at `settings.json.outputStyle`, used only to decide whether
390
+ * the interactive wizard needs to ask about a navori takeover. Never throws,
391
+ * never writes — any missing file, unreadable file, or invalid JSON just
392
+ * reads as "no value" (`undefined`), which is safely a non-match for
393
+ * `isNavoriOutputStyle`. The actual write happens inside `runInit` via
394
+ * `applyOutputStylePolicy`, which re-does this read itself under its own
395
+ * mtime-guarded contract.
396
+ */ function peekOutputStyleValue(settingsPath) {
397
+ if (!existsSync(settingsPath)) return undefined;
398
+ try {
399
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
400
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
401
+ return parsed.outputStyle;
402
+ } catch {
403
+ return undefined;
404
+ }
405
+ }
406
+ /**
407
+ * Read-only `--force` pre-flight: counts how many on-disk paths at motor
408
+ * paths (output-style, agents, skills incl. every manifest subfile, hooks)
409
+ * are foreign (no `argos:file`/shell marker) and would therefore be
410
+ * overwritten by a forced `runInit`. Used only by the interactive wizard to
411
+ * size its confirm prompt — the actual overwrite decision and write happen
412
+ * inside `runInit` itself, independently, so this can never drift into
413
+ * writing anything. Mirrors the same directory-ownership rule `runInit`'s
414
+ * own skills loop uses (SKILL.md's marker decides the whole directory), and
415
+ * — like `doctor.ts`'s own read-only checks — intentionally duplicates that
416
+ * iteration shape rather than sharing private state with the writer.
417
+ */ function countForceOverwrites(claudeDir, assetsDir, installAgents, installHooks) {
418
+ let count = 0;
419
+ const fullFiles = [
420
+ [
421
+ "output-styles",
422
+ "argos.md"
423
+ ],
424
+ ...installAgents ? listAgentIds(assetsDir).map((id)=>[
425
+ "agents",
426
+ `${id}.md`
427
+ ]) : []
428
+ ];
429
+ for (const relPath of fullFiles){
430
+ const dest = join(claudeDir, ...relPath);
431
+ if (existsSync(dest) && !hasArgosFileMarker(readFileSync(dest, "utf-8"))) count++;
432
+ }
433
+ for (const skillId of listSkillIds(assetsDir)){
434
+ const skillMdDest = join(claudeDir, "skills", skillId, "SKILL.md");
435
+ const isForeignSkill = existsSync(skillMdDest) && !hasArgosFileMarker(readFileSync(skillMdDest, "utf-8"));
436
+ if (!isForeignSkill) continue;
437
+ for (const relFile of listSkillFiles(assetsDir, skillId)){
438
+ const dest = join(claudeDir, "skills", skillId, ...relFile.split("/"));
439
+ if (existsSync(dest)) count++;
440
+ }
441
+ }
442
+ if (installHooks) {
443
+ for (const id of HOOK_IDS){
444
+ const dest = join(claudeDir, "hooks", `${id}.sh`);
445
+ if (existsSync(dest) && !hasArgosShellFileMarker(readFileSync(dest, "utf-8"))) count++;
446
+ }
447
+ }
448
+ return count;
449
+ }
450
+ /**
451
+ * Interactive layer over `runInit` (spec 0004 F5 "argos init"). A pure
452
+ * additive wrapper — the core `runInit` never changes behavior or contract.
453
+ * Without a real TTY, or with `--yes`, this delegates to `runInit(options)`
454
+ * unchanged: no prompt library call is ever reached on that path. With a
455
+ * TTY, it runs a 3-step wizard (language, agents/hooks toggles, summary +
456
+ * final confirm) before calling `runInit` with the gathered choices;
457
+ * cancelling at any step touches nothing and returns a no-op report.
458
+ */ export async function runInitInteractive(options = {}) {
459
+ if (!isInteractive({
460
+ yes: options.yes
461
+ })) {
462
+ return runInit(options);
463
+ }
464
+ const prompter = options.prompter ?? clackPrompter;
465
+ prompter.intro("argos init — instalación interactiva del motor");
466
+ const language = await prompter.select({
467
+ message: "Idioma del motor",
468
+ options: [
469
+ {
470
+ value: "es",
471
+ label: "es — español"
472
+ },
473
+ {
474
+ value: "en",
475
+ label: "en — English"
476
+ }
477
+ ],
478
+ initialValue: options.language ?? "es"
479
+ });
480
+ if (prompter.isCancel(language)) {
481
+ prompter.cancel("argos init cancelado — no se tocó nada.");
482
+ return cancelledInitReport();
483
+ }
484
+ const installAgents = await prompter.confirm({
485
+ message: "¿Instalar los agentes del motor (agents/)?",
486
+ initialValue: options.installAgents ?? true
487
+ });
488
+ if (prompter.isCancel(installAgents)) {
489
+ prompter.cancel("argos init cancelado — no se tocó nada.");
490
+ return cancelledInitReport();
491
+ }
492
+ const installHooks = await prompter.confirm({
493
+ message: "¿Instalar los hooks globales (guard-destructive + quality-gate)?",
494
+ initialValue: options.installHooks ?? true
495
+ });
496
+ if (prompter.isCancel(installHooks)) {
497
+ prompter.cancel("argos init cancelado — no se tocó nada.");
498
+ return cancelledInitReport();
499
+ }
500
+ const claudeDir = resolveClaudeDir();
501
+ // `--force` pre-flight (see `InitOptions.force`'s doc comment): flag-driven,
502
+ // not itself a wizard question. When active, count how many on-disk paths
503
+ // are foreign and about to be overwritten (read-only — see
504
+ // `countForceOverwrites`) and, only if that count is > 0, require an
505
+ // explicit confirm before proceeding. Zero foreign paths means force has
506
+ // nothing to do this run, so no extra prompt.
507
+ const force = options.force ?? false;
508
+ if (force) {
509
+ const foreignCount = countForceOverwrites(claudeDir, resolveAssetsDir(), installAgents, installHooks);
510
+ if (foreignCount > 0) {
511
+ const confirmForce = await prompter.confirm({
512
+ message: `${foreignCount} archivos ajenos serán sobrescritos por versiones del motor — backup previo en ~/.argos/backups. ¿Continuar?`,
513
+ initialValue: false
514
+ });
515
+ if (prompter.isCancel(confirmForce) || !confirmForce) {
516
+ prompter.cancel("argos init cancelado — no se tocó nada.");
517
+ return cancelledInitReport();
518
+ }
519
+ }
520
+ }
521
+ // Voice activation (spec 0004): if the current settings.json.outputStyle
522
+ // matches the predecessor harness's voice, ask before taking it over —
523
+ // this peek is read-only and never writes; the actual takeover only
524
+ // happens inside runInit below, once the user has confirmed everything.
525
+ let takeoverNavoriVoice = options.takeoverNavoriVoice ?? true;
526
+ const currentOutputStyle = peekOutputStyleValue(join(claudeDir, "settings.json"));
527
+ if (isNavoriOutputStyle(currentOutputStyle)) {
528
+ const takeover = await prompter.confirm({
529
+ message: `settings.json.outputStyle apunta a la voz del harness predecesor ('${String(currentOutputStyle)}') — ¿reemplazarla por Argos?`,
530
+ initialValue: true
531
+ });
532
+ if (prompter.isCancel(takeover)) {
533
+ prompter.cancel("argos init cancelado — no se tocó nada.");
534
+ return cancelledInitReport();
535
+ }
536
+ takeoverNavoriVoice = takeover;
537
+ }
538
+ prompter.note([
539
+ `idioma: ${language}`,
540
+ `agentes: ${installAgents ? "sí" : "no"}`,
541
+ "hooks: " + (installHooks ? "sí" : "no"),
542
+ "skills: sí (siempre — cargan on-demand)",
543
+ `destino: ${claudeDir}`,
544
+ "se hace un backup antes de escribir nada"
545
+ ].join("\n"), "Resumen");
546
+ const proceed = await prompter.confirm({
547
+ message: "¿Proceder a escribir estos cambios?",
548
+ initialValue: true
549
+ });
550
+ if (prompter.isCancel(proceed) || !proceed) {
551
+ prompter.cancel("argos init cancelado — no se tocó nada.");
552
+ return cancelledInitReport();
553
+ }
554
+ const report = runInit({
555
+ language,
556
+ installAgents,
557
+ installHooks,
558
+ takeoverNavoriVoice,
559
+ force
560
+ });
561
+ prompter.outro(report.summary);
562
+ return report;
563
+ }
312
564
  export const initCommand = defineCommand({
313
565
  meta: {
314
566
  name: "init",
@@ -323,17 +575,31 @@ export const initCommand = defineCommand({
323
575
  ],
324
576
  default: "es",
325
577
  description: "Idioma del motor (global.json)."
578
+ },
579
+ yes: {
580
+ type: "boolean",
581
+ default: false,
582
+ description: "Fuerza modo no interactivo aunque haya una TTY real (defaults + flags, sin wizard)."
583
+ },
584
+ force: {
585
+ type: "boolean",
586
+ default: false,
587
+ description: "Sobrescribe con la versión del motor los archivos ajenos (sin marker argos) en agents/skills/output-styles/hooks. No toca prosa ajena de CLAUDE.md ni entradas ajenas de settings.json."
326
588
  }
327
589
  },
328
- run ({ args }) {
329
- const report = runInit({
330
- language: args.language
590
+ async run ({ args }) {
591
+ const report = await runInitInteractive({
592
+ language: args.language,
593
+ yes: Boolean(args.yes),
594
+ force: Boolean(args.force)
331
595
  });
332
596
  const colorize = (status)=>{
333
597
  const padded = status.padEnd(18);
334
598
  switch(status){
335
599
  case "skipped-foreign":
336
600
  return pc.yellow(padded);
601
+ case "overwritten-foreign":
602
+ return pc.magenta(padded);
337
603
  case "created":
338
604
  return pc.green(padded);
339
605
  case "updated":