@firatcand/roster 0.4.0 → 1.0.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 (76) hide show
  1. package/README.md +79 -219
  2. package/agents/lesson-drafter.md +3 -8
  3. package/agents/pattern-detector.md +0 -1
  4. package/bin/roster.js +1407 -217
  5. package/package.json +2 -3
  6. package/skills/chief-of-staff/SKILL.md +62 -78
  7. package/skills/dreamer/SKILL.md +8 -7
  8. package/skills/roster-orchestrator/SKILL.md +53 -25
  9. package/templates/CLAUDE.project.template.md +1 -1
  10. package/templates/CONTEXT.template.md +2 -2
  11. package/templates/gitignore-defaults.txt +2 -0
  12. package/templates/scaffold/chief-of-staff/README.md +16 -24
  13. package/templates/scaffold/chief-of-staff/agent.md +22 -32
  14. package/templates/scaffold/chief-of-staff/plans/audit-agent.yaml +4 -4
  15. package/templates/scaffold/chief-of-staff/plans/audit-repo.yaml +5 -4
  16. package/templates/scaffold/chief-of-staff/plans/create-agent.yaml +5 -34
  17. package/templates/scaffold/config/project.yaml.template +10 -0
  18. package/templates/scaffold/conventions.md +159 -171
  19. package/templates/scaffold/dreamer/README.md +2 -2
  20. package/templates/scaffold/dreamer/agent.md +0 -1
  21. package/templates/scaffold/dreamer/plans/nightly-reflection.yaml +23 -37
  22. package/templates/scaffold/dreamer/subagents/lesson-drafter.md +2 -7
  23. package/templates/scaffold/{projects/_demo/guidelines → guidelines}/asset-links.md +4 -0
  24. package/templates/scaffold/{projects/_demo/guidelines → guidelines}/brand-book.md +4 -0
  25. package/templates/scaffold/{projects/_demo/guidelines → guidelines}/messaging.md +4 -0
  26. package/templates/scaffold/{projects/_demo/guidelines → guidelines}/voice.md +4 -0
  27. package/templates/scaffold/scripts/audit-agent.sh +74 -47
  28. package/templates/scaffold/scripts/audit-repo.sh +27 -49
  29. package/templates/scaffold/scripts/create-function.sh +1 -1
  30. package/templates/scaffold/scripts/lib/README.md +1 -1
  31. package/templates/scaffold/scripts/lib/bindings-prompt.sh +43 -124
  32. package/templates/scaffold/scripts/new-agent.sh +99 -91
  33. package/templates/scaffold/scripts/rename-agent.sh +91 -0
  34. package/templates/scaffold/scripts/save-state.sh +32 -0
  35. package/agents/critic.md +0 -74
  36. package/agents/enricher.md +0 -56
  37. package/agents/promotion-arbiter.md +0 -71
  38. package/agents/prospector.md +0 -51
  39. package/agents/writer.md +0 -58
  40. package/skills/sdr/SKILL.md +0 -147
  41. package/templates/scaffold/chief-of-staff/plans/add-agent-to-project.yaml +0 -45
  42. package/templates/scaffold/chief-of-staff/plans/archive-project.yaml +0 -51
  43. package/templates/scaffold/chief-of-staff/plans/audit-project.yaml +0 -34
  44. package/templates/scaffold/chief-of-staff/plans/create-project.yaml +0 -65
  45. package/templates/scaffold/chief-of-staff/plans/remove-agent-from-project.yaml +0 -50
  46. package/templates/scaffold/chief-of-staff/plans/rename-project.yaml +0 -62
  47. package/templates/scaffold/chief-of-staff/plans/unarchive-project.yaml +0 -41
  48. package/templates/scaffold/dreamer/subagents/promotion-arbiter.md +0 -64
  49. package/templates/scaffold/gtm/sdr/.claude/settings.json +0 -3
  50. package/templates/scaffold/gtm/sdr/.mcp.json +0 -21
  51. package/templates/scaffold/gtm/sdr/README.md +0 -41
  52. package/templates/scaffold/gtm/sdr/agent.md +0 -136
  53. package/templates/scaffold/gtm/sdr/plans/cold-outreach.yaml +0 -92
  54. package/templates/scaffold/gtm/sdr/projects/_demo/asset-references.md +0 -7
  55. package/templates/scaffold/gtm/sdr/projects/_demo/config/default.yaml +0 -69
  56. package/templates/scaffold/gtm/sdr/projects/_demo/log/feedback/.gitkeep +0 -0
  57. package/templates/scaffold/gtm/sdr/projects/_demo/log/runs/.gitkeep +0 -0
  58. package/templates/scaffold/gtm/sdr/projects/_demo/playbook/.gitkeep +0 -0
  59. package/templates/scaffold/gtm/sdr/subagents/critic.md +0 -67
  60. package/templates/scaffold/gtm/sdr/subagents/enricher.md +0 -49
  61. package/templates/scaffold/gtm/sdr/subagents/prospector.md +0 -44
  62. package/templates/scaffold/gtm/sdr/subagents/writer.md +0 -51
  63. package/templates/scaffold/projects/_demo/CLAUDE.md +0 -35
  64. package/templates/scaffold/projects/_demo/README.md +0 -16
  65. package/templates/scaffold/projects/_demo/assets/.gitkeep +0 -0
  66. package/templates/scaffold/projects/_demo/config/default.yaml +0 -28
  67. package/templates/scaffold/projects/_demo/state.md +0 -11
  68. package/templates/scaffold/scripts/archive-project.sh +0 -98
  69. package/templates/scaffold/scripts/audit-project.sh +0 -361
  70. package/templates/scaffold/scripts/new-agent-instance.sh +0 -114
  71. package/templates/scaffold/scripts/new-project.sh +0 -125
  72. package/templates/scaffold/scripts/remove-agent-from-project.sh +0 -67
  73. package/templates/scaffold/scripts/rename-project.sh +0 -118
  74. package/templates/scaffold/scripts/unarchive-project.sh +0 -115
  75. /package/templates/scaffold/gtm/{sdr/playbook/.gitkeep → .gitkeep} +0 -0
  76. /package/templates/scaffold/{projects/_demo/guidelines → guidelines}/icps/_persona-template.md +0 -0
package/bin/roster.js CHANGED
@@ -10,6 +10,7 @@ import { z } from "zod";
10
10
  import { createHash, randomBytes } from "node:crypto";
11
11
  import YAML, { parse } from "yaml";
12
12
  import { execFileSync, spawn, spawnSync } from "node:child_process";
13
+ import { createInterface } from "node:readline";
13
14
  //#region src/lib/paths.ts
14
15
  function findRosterRoot() {
15
16
  const here = dirname(fileURLToPath(import.meta.url));
@@ -99,12 +100,13 @@ var RosterError = class extends Error {
99
100
  function isRosterError(err) {
100
101
  return err instanceof RosterError;
101
102
  }
102
- function permissionError(targetPath, cause) {
103
+ function permissionError(targetPath, cause, scope) {
103
104
  const syscall = cause.syscall ? ` (${cause.syscall})` : "";
105
+ const remedy = scope === "project" ? ` Check filesystem permissions on the workspace, or run: chmod -R u+w ${targetPath}` : ` Re-run with sudo, or run: sudo chown -R "$USER" ${targetPath}`;
104
106
  return new RosterError({
105
107
  header: `${chalk.red.bold("roster:")} permission denied`,
106
108
  body: ` ${cause.code ?? "EACCES"}${syscall} writing ${targetPath}`,
107
- remedy: ` Re-run with sudo, or run: sudo chown -R "$USER" ${targetPath}`,
109
+ remedy,
108
110
  exitCode: 1
109
111
  });
110
112
  }
@@ -141,6 +143,19 @@ function missingScaffoldError(scaffoldPath) {
141
143
  exitCode: 1
142
144
  });
143
145
  }
146
+ function v04WorkspaceDetectedError(paths) {
147
+ const list = paths.map((p) => ` - ${p}`).join("\n");
148
+ return new RosterError({
149
+ header: `${chalk.red.bold("roster:")} detected v0.4 workspace`,
150
+ body: [
151
+ " v1.0 is a breaking change with no automatic migration.",
152
+ " Found:",
153
+ list
154
+ ].join("\n"),
155
+ remedy: " Re-scaffold in a fresh directory; see docs/CHANGELOG.md#v1.0.0.",
156
+ exitCode: 2
157
+ });
158
+ }
144
159
  function userCancelledInit() {
145
160
  return new RosterError({
146
161
  header: `${chalk.dim("roster:")} cancelled`,
@@ -157,6 +172,32 @@ function userCancelledInstall() {
157
172
  exitCode: 2
158
173
  });
159
174
  }
175
+ function workspaceRequiredError(cwd) {
176
+ return new RosterError({
177
+ header: `${chalk.red.bold("roster:")} project-level install requires a roster workspace`,
178
+ body: [` CWD: ${cwd}`, ` Expected: config/project.yaml (created by ${chalk.bold("roster init")})`].join("\n"),
179
+ remedy: [
180
+ ` Either:`,
181
+ ` - cd into a roster workspace, or`,
182
+ ` - re-run with ${chalk.bold("--scope user")} to install to your home directory.`
183
+ ].join("\n"),
184
+ exitCode: 2
185
+ });
186
+ }
187
+ function toolsNotDetectedError(requestedKeys, detectedKeys) {
188
+ const missing = requestedKeys.filter((k) => !detectedKeys.includes(k));
189
+ const detectedLabel = detectedKeys.length === 0 ? "(none)" : detectedKeys.join(", ");
190
+ return new RosterError({
191
+ header: `${chalk.red.bold("roster:")} requested tool${missing.length === 1 ? "" : "s"} not detected: ${missing.join(", ")}`,
192
+ body: [` --tool requested: ${requestedKeys.join(", ")}`, ` detected on this machine: ${detectedLabel}`].join("\n"),
193
+ remedy: [
194
+ ` Either:`,
195
+ ` - install the missing tool${missing.length === 1 ? "" : "s"} first, or`,
196
+ ` - drop the missing key${missing.length === 1 ? "" : "s"} from ${chalk.bold("--tool")}.`
197
+ ].join("\n"),
198
+ exitCode: 3
199
+ });
200
+ }
160
201
  function linuxClaudeUnsupportedError() {
161
202
  return new RosterError({
162
203
  header: `${chalk.red.bold("roster:")} Claude Desktop scheduling is not available on Linux`,
@@ -447,7 +488,7 @@ async function prepareTargetForWrite(targetPath, kind, logger, confirm) {
447
488
  }
448
489
  return true;
449
490
  }
450
- async function copyOne(srcPath, targetPath, kind, logger, confirm) {
491
+ async function copyOne(srcPath, targetPath, kind, logger, confirm, scope) {
451
492
  try {
452
493
  if (!await prepareTargetForWrite(targetPath, kind, logger, confirm)) return false;
453
494
  await copy(srcPath, targetPath, {
@@ -456,17 +497,17 @@ async function copyOne(srcPath, targetPath, kind, logger, confirm) {
456
497
  });
457
498
  return true;
458
499
  } catch (err) {
459
- if (isEacces(err)) throw permissionError(targetPath, err);
500
+ if (isEacces(err)) throw permissionError(targetPath, err, scope);
460
501
  throw err;
461
502
  }
462
503
  }
463
- async function writeRenderedOne(targetPath, contents, kind, logger, confirm) {
504
+ async function writeRenderedOne(targetPath, contents, kind, logger, confirm, scope) {
464
505
  try {
465
506
  if (!await prepareTargetForWrite(targetPath, kind, logger, confirm)) return false;
466
507
  writeFileSync(targetPath, contents);
467
508
  return true;
468
509
  } catch (err) {
469
- if (isEacces(err)) throw permissionError(targetPath, err);
510
+ if (isEacces(err)) throw permissionError(targetPath, err, scope);
470
511
  throw err;
471
512
  }
472
513
  }
@@ -483,7 +524,7 @@ async function installToTool(tool, opts) {
483
524
  await ensureDir(tool.skillsTarget);
484
525
  if (tool.agentsTarget) await ensureDir(tool.agentsTarget);
485
526
  } catch (err) {
486
- if (isEacces(err)) throw permissionError(tool.skillsTarget, err);
527
+ if (isEacces(err)) throw permissionError(tool.skillsTarget, err, opts.scope);
487
528
  throw err;
488
529
  }
489
530
  let skillsCount = 0;
@@ -501,7 +542,7 @@ async function installToTool(tool, opts) {
501
542
  const renderedSkillMd = join(targetPath, "SKILL.md");
502
543
  assertWithinRoot(targetPath, tool.configRoot, "skill targetPath");
503
544
  info(chalk.dim(` + skill ${dirent.name} -> ${targetPath}`));
504
- if (await copyOne(srcPath, targetPath, "skill", logger, confirm)) {
545
+ if (await copyOne(srcPath, targetPath, "skill", logger, confirm, opts.scope)) {
505
546
  renderSkillFrontmatter(renderedSkillMd, tool.key);
506
547
  skillsCount++;
507
548
  }
@@ -530,15 +571,15 @@ async function installToTool(tool, opts) {
530
571
  throw err;
531
572
  }
532
573
  info(chalk.dim(` + agent ${dirent.name} -> ${tomlTarget}`));
533
- const wroteToml = await writeRenderedOne(tomlTarget, rendered.toml, "agent", logger, confirm);
534
- const wrotePersona = await writeRenderedOne(personaTarget, rendered.personaBody, "agent persona", logger, confirm);
574
+ const wroteToml = await writeRenderedOne(tomlTarget, rendered.toml, "agent", logger, confirm, opts.scope);
575
+ const wrotePersona = await writeRenderedOne(personaTarget, rendered.personaBody, "agent persona", logger, confirm, opts.scope);
535
576
  if (wroteToml && wrotePersona) agentsCount++;
536
577
  continue;
537
578
  }
538
579
  const targetPath = join(tool.agentsTarget, dirent.name);
539
580
  assertWithinRoot(targetPath, tool.configRoot, "agent targetPath");
540
581
  info(chalk.dim(` + agent ${dirent.name} -> ${targetPath}`));
541
- if (await copyOne(srcPath, targetPath, "agent", logger, confirm)) agentsCount++;
582
+ if (await copyOne(srcPath, targetPath, "agent", logger, confirm, opts.scope)) agentsCount++;
542
583
  }
543
584
  }
544
585
  return {
@@ -556,19 +597,46 @@ const KNOWN_TOOL_KEYS = [
556
597
  "gemini"
557
598
  ];
558
599
  const TOOL_LIST$1 = KNOWN_TOOL_KEYS.join(" | ");
600
+ const SCOPE_LIST$1 = "(project | user)";
559
601
  function isToolKey(value) {
560
602
  return KNOWN_TOOL_KEYS.includes(value);
561
603
  }
604
+ function isScope$1(value) {
605
+ return value === "project" || value === "user";
606
+ }
607
+ function parseToolValue(value) {
608
+ const parts = value.split(",").map((s) => s.trim());
609
+ if (parts.some((p) => p.length === 0)) return {
610
+ ok: false,
611
+ message: `--tool received an empty value (check for stray commas)`
612
+ };
613
+ const keys = [];
614
+ for (const part of parts) {
615
+ if (!isToolKey(part)) return {
616
+ ok: false,
617
+ message: `unknown tool '${part}'; expected one of: ${TOOL_LIST$1}`
618
+ };
619
+ if (!keys.includes(part)) keys.push(part);
620
+ }
621
+ return {
622
+ ok: true,
623
+ keys
624
+ };
625
+ }
562
626
  function parseInstallArgs(args) {
563
627
  let silent = false;
564
628
  let verbose = false;
629
+ let yes = false;
565
630
  let all = false;
566
631
  let toolValue = null;
567
632
  let toolFlagSeen = false;
633
+ let scopeValue = null;
634
+ let scopeFlagSeen = false;
568
635
  for (let i = 0; i < args.length; i++) {
569
636
  const arg = args[i];
570
637
  if (arg === "--silent") silent = true;
571
638
  else if (arg === "--verbose") verbose = true;
639
+ else if (arg === "--yes" || arg === "-y") yes = true;
572
640
  else if (arg === "--all") all = true;
573
641
  else if (arg === "--tool") {
574
642
  toolFlagSeen = true;
@@ -587,57 +655,150 @@ function parseInstallArgs(args) {
587
655
  message: `--tool requires a tool name (${TOOL_LIST$1})`
588
656
  };
589
657
  toolValue = value;
658
+ } else if (arg === "--scope") {
659
+ scopeFlagSeen = true;
660
+ const next = args[i + 1];
661
+ if (next === void 0 || next.startsWith("-")) return {
662
+ kind: "err",
663
+ message: `--scope requires a value: ${SCOPE_LIST$1}`
664
+ };
665
+ scopeValue = next;
666
+ i++;
667
+ } else if (arg.startsWith("--scope=")) {
668
+ scopeFlagSeen = true;
669
+ const value = arg.slice(8);
670
+ if (value === "") return {
671
+ kind: "err",
672
+ message: `--scope requires a value: ${SCOPE_LIST$1}`
673
+ };
674
+ scopeValue = value;
590
675
  }
591
676
  }
592
677
  if (all && toolFlagSeen) return {
593
678
  kind: "err",
594
679
  message: "flags --all and --tool are mutually exclusive"
595
680
  };
681
+ let scope = null;
682
+ if (scopeFlagSeen) {
683
+ if (scopeValue === null || !isScope$1(scopeValue)) return {
684
+ kind: "err",
685
+ message: `unknown scope '${scopeValue ?? ""}'; expected one of: ${SCOPE_LIST$1}`
686
+ };
687
+ scope = scopeValue;
688
+ }
689
+ let target;
596
690
  if (toolFlagSeen && toolValue !== null) {
597
- if (!isToolKey(toolValue)) return {
691
+ const parsed = parseToolValue(toolValue);
692
+ if (!parsed.ok) return {
598
693
  kind: "err",
599
- message: `unknown tool '${toolValue}'; expected one of: ${TOOL_LIST$1}`
694
+ message: parsed.message
600
695
  };
601
- return {
602
- kind: "ok",
603
- silent,
604
- verbose,
605
- target: {
606
- mode: "tool",
607
- key: toolValue
608
- }
696
+ target = {
697
+ mode: "tools",
698
+ keys: parsed.keys
609
699
  };
610
- }
611
- if (all) return {
700
+ } else if (all) target = { mode: "all" };
701
+ else target = { mode: "interactive" };
702
+ return {
612
703
  kind: "ok",
613
704
  silent,
614
705
  verbose,
615
- target: { mode: "all" }
706
+ yes,
707
+ scope,
708
+ target
616
709
  };
710
+ }
711
+ //#endregion
712
+ //#region src/lib/install-scope.ts
713
+ function detectWorkspace(cwd) {
714
+ return existsSync(join(cwd, "config", "project.yaml"));
715
+ }
716
+ function defaultScopeForContext(workspaceExists) {
717
+ return workspaceExists ? "project" : "user";
718
+ }
719
+ function projectPathsFor(toolKey, workspaceRoot, hasAgents) {
720
+ const agentsPath = (root) => hasAgents ? join(workspaceRoot, root, "agents") : null;
721
+ switch (toolKey) {
722
+ case "claude": return {
723
+ configRoot: join(workspaceRoot, ".claude"),
724
+ skillsTarget: join(workspaceRoot, ".claude", "skills"),
725
+ agentsTarget: agentsPath(".claude")
726
+ };
727
+ case "codex": return {
728
+ configRoot: join(workspaceRoot, ".codex"),
729
+ skillsTarget: join(workspaceRoot, ".codex", "skills"),
730
+ agentsTarget: agentsPath(".codex")
731
+ };
732
+ case "gemini": return {
733
+ configRoot: join(workspaceRoot, ".gemini"),
734
+ skillsTarget: join(workspaceRoot, ".gemini", "extensions"),
735
+ agentsTarget: agentsPath(".gemini")
736
+ };
737
+ }
738
+ }
739
+ function toolForScope(tool, scope, workspaceRoot) {
740
+ if (scope === "user") return tool;
741
+ if (workspaceRoot === void 0) throw new Error("toolForScope: workspaceRoot is required when scope is \"project\"");
742
+ const hasAgents = tool.agentsTarget !== null;
743
+ const paths = projectPathsFor(tool.key, workspaceRoot, hasAgents);
617
744
  return {
618
- kind: "ok",
619
- silent,
620
- verbose,
621
- target: { mode: "interactive" }
745
+ ...tool,
746
+ ...paths
622
747
  };
623
748
  }
624
749
  //#endregion
625
750
  //#region src/lib/doctor-args.ts
751
+ const SCOPE_LIST = "(project | user)";
752
+ function isScope(value) {
753
+ return value === "project" || value === "user";
754
+ }
626
755
  function parseDoctorArgs(args) {
627
756
  let json = false;
628
757
  let silent = false;
629
758
  let fix = false;
630
759
  let dryRun = false;
631
- for (const arg of args) if (arg === "--json") json = true;
632
- else if (arg === "--silent") silent = true;
633
- else if (arg === "--fix") fix = true;
634
- else if (arg === "--dry-run") dryRun = true;
760
+ let scopeValue = null;
761
+ let scopeFlagSeen = false;
762
+ for (let i = 0; i < args.length; i++) {
763
+ const arg = args[i];
764
+ if (arg === "--json") json = true;
765
+ else if (arg === "--silent") silent = true;
766
+ else if (arg === "--fix") fix = true;
767
+ else if (arg === "--dry-run") dryRun = true;
768
+ else if (arg === "--scope") {
769
+ scopeFlagSeen = true;
770
+ const next = args[i + 1];
771
+ if (next === void 0 || next.startsWith("-")) return {
772
+ kind: "err",
773
+ message: `--scope requires a value: ${SCOPE_LIST}`
774
+ };
775
+ scopeValue = next;
776
+ i++;
777
+ } else if (arg.startsWith("--scope=")) {
778
+ scopeFlagSeen = true;
779
+ const value = arg.slice(8);
780
+ if (value === "") return {
781
+ kind: "err",
782
+ message: `--scope requires a value: ${SCOPE_LIST}`
783
+ };
784
+ scopeValue = value;
785
+ }
786
+ }
787
+ let scope = null;
788
+ if (scopeFlagSeen) {
789
+ if (scopeValue === null || !isScope(scopeValue)) return {
790
+ kind: "err",
791
+ message: `unknown scope '${scopeValue ?? ""}'; expected one of: ${SCOPE_LIST}`
792
+ };
793
+ scope = scopeValue;
794
+ }
635
795
  return {
636
796
  kind: "ok",
637
797
  json,
638
798
  silent,
639
799
  fix,
640
- dryRun
800
+ dryRun,
801
+ scope
641
802
  };
642
803
  }
643
804
  const KEBAB_RE$1 = /^[a-z0-9]+(-[a-z0-9]+)*$/;
@@ -747,8 +908,6 @@ function isValidIanaTimezone(tz) {
747
908
  }
748
909
  }
749
910
  const kebabString = (label) => z.string().min(1, { message: `${label}: required` }).refine((s) => KEBAB_RE$1.test(s), { message: `${label}: must be kebab-case (lowercase letters, digits, hyphens)` });
750
- const PROJECT_SLUG_RE = /^_?[a-z0-9]+(-[a-z0-9]+)*$/;
751
- const projectString = (label) => z.string().min(1, { message: `${label}: required` }).refine((s) => PROJECT_SLUG_RE.test(s), { message: `${label}: must be kebab-case (optionally prefixed with '_' for scaffold templates)` });
752
911
  const cronString = z.string().min(1, { message: "cron: required" }).transform((s) => s.trim()).superRefine((expr, ctx) => {
753
912
  const result = validateCronExpression(expr);
754
913
  if (!result.ok) ctx.addIssue({
@@ -769,11 +928,10 @@ const subscriptionAttestationSchema = z.object({
769
928
  env_policy: z.literal("cleared"),
770
929
  codex_home: z.string().min(1, { message: "subscription_attestation.codex_home: required" })
771
930
  }).strict();
772
- const scheduleEntrySchema = z.object({
931
+ const scheduleEntryShape = z.object({
773
932
  name: kebabString("name"),
774
933
  agent: kebabString("agent"),
775
934
  plan: kebabString("plan"),
776
- project: projectString("project"),
777
935
  cron: cronString,
778
936
  tool: z.enum(TOOL_VALUES, { error: (issue) => {
779
937
  const base = `tool: must be one of ${TOOL_VALUES.map((v) => `'${v}'`).join(" | ")}`;
@@ -815,6 +973,17 @@ const scheduleEntrySchema = z.object({
815
973
  message: "capture_events: requires install_mode=via-cron (ui-handoff routes through the Codex app; no wrapper to redirect stdout)"
816
974
  });
817
975
  });
976
+ const scheduleEntrySchema = z.preprocess((raw, ctx) => {
977
+ if (raw !== null && typeof raw === "object" && "project" in raw) {
978
+ ctx.addIssue({
979
+ code: "custom",
980
+ path: ["project"],
981
+ message: "v0.4 schedule entry detected — see CHANGELOG#breaking-v1"
982
+ });
983
+ return z.NEVER;
984
+ }
985
+ return raw;
986
+ }, scheduleEntryShape);
818
987
  const scheduleFileSchema = z.object({
819
988
  version: z.number().int({ message: "version: must be an integer" }).refine((n) => n === 1, { message: `version: unsupported schema version (expected 1)` }),
820
989
  schedules: z.array(scheduleEntrySchema)
@@ -1010,7 +1179,6 @@ function parseInstall(rest) {
1010
1179
  let toolRaw;
1011
1180
  let viaRaw;
1012
1181
  let name;
1013
- let project;
1014
1182
  let dryRun = false;
1015
1183
  let cloudRoutine = false;
1016
1184
  let json = false;
@@ -1088,21 +1256,11 @@ function parseInstall(rest) {
1088
1256
  message: "flag --name specified more than once"
1089
1257
  };
1090
1258
  name = arg.slice(7);
1091
- } else if (arg === "--project") {
1092
- const r = consumeValue("--project", project, rest[i + 1]);
1093
- if (!r.ok) return {
1094
- kind: "err",
1095
- message: r.message
1096
- };
1097
- project = r.value;
1098
- i++;
1099
- } else if (arg.startsWith("--project=")) {
1100
- if (project !== void 0) return {
1101
- kind: "err",
1102
- message: "flag --project specified more than once"
1103
- };
1104
- project = arg.slice(10);
1105
- } else if (arg === "--cwd") {
1259
+ } else if (arg === "--project" || arg.startsWith("--project=")) return {
1260
+ kind: "err",
1261
+ message: "--project removed in v1.0 see CHANGELOG"
1262
+ };
1263
+ else if (arg === "--cwd") {
1106
1264
  const r = consumeValue("--cwd", cwd, rest[i + 1]);
1107
1265
  if (!r.ok) return {
1108
1266
  kind: "err",
@@ -1168,7 +1326,6 @@ function parseInstall(rest) {
1168
1326
  functionName,
1169
1327
  agent,
1170
1328
  plan,
1171
- project: project ?? "_demo",
1172
1329
  cron,
1173
1330
  tool: toolRaw,
1174
1331
  via,
@@ -1950,7 +2107,6 @@ function renderFailedExitItem(args) {
1950
2107
  `function: ${schedule.functionName}`,
1951
2108
  `agent: ${schedule.entry.agent}`,
1952
2109
  `plan: ${schedule.entry.plan}`,
1953
- `project: ${schedule.entry.project}`,
1954
2110
  `cron: '${schedule.entry.cron}'`,
1955
2111
  `exit_code: ${code}`,
1956
2112
  `exit_path: ${exit.exitPath}`,
@@ -1988,7 +2144,6 @@ function renderStaleItem(args) {
1988
2144
  `function: ${schedule.functionName}`,
1989
2145
  `agent: ${schedule.entry.agent}`,
1990
2146
  `plan: ${schedule.entry.plan}`,
1991
- `project: ${schedule.entry.project}`,
1992
2147
  `cron: '${schedule.entry.cron}'`,
1993
2148
  `expected_before: '${expectedBeforeUtc}'`,
1994
2149
  `last_run: '${lastRun?.timestamp ?? ""}'`,
@@ -2471,6 +2626,14 @@ const FORGE_MARKERS = [
2471
2626
  "spec/PRD.md",
2472
2627
  "plans/phases.yaml"
2473
2628
  ];
2629
+ const V04_SCAN_SKIP = new Set([
2630
+ "node_modules",
2631
+ "dist",
2632
+ "build",
2633
+ "coverage",
2634
+ "lib",
2635
+ "bin"
2636
+ ]);
2474
2637
  const TEMPLATE_SUFFIX_RE = /\.template(\.[^.]+)?$/;
2475
2638
  function readTemplate(name) {
2476
2639
  return readFileSync(join(ROSTER_ROOT, "templates", name), "utf8");
@@ -2481,6 +2644,35 @@ function substitute(template, vars) {
2481
2644
  function detectForgeMarkers(cwd) {
2482
2645
  return FORGE_MARKERS.filter((m) => existsSync(join(cwd, m)));
2483
2646
  }
2647
+ function detectV04Workspace(cwd) {
2648
+ const hits = [];
2649
+ if (existsSync(join(cwd, "projects"))) hits.push("projects/");
2650
+ let topEntries;
2651
+ try {
2652
+ topEntries = readdirSync(cwd, { withFileTypes: true });
2653
+ } catch {
2654
+ return hits;
2655
+ }
2656
+ for (const top of topEntries) {
2657
+ if (!top.isDirectory()) continue;
2658
+ if (top.name.startsWith(".")) continue;
2659
+ if (V04_SCAN_SKIP.has(top.name)) continue;
2660
+ const topPath = join(cwd, top.name);
2661
+ if (existsSync(join(topPath, "projects"))) hits.push(`${top.name}/projects/`);
2662
+ let subEntries;
2663
+ try {
2664
+ subEntries = readdirSync(topPath, { withFileTypes: true });
2665
+ } catch {
2666
+ continue;
2667
+ }
2668
+ for (const sub of subEntries) {
2669
+ if (!sub.isDirectory()) continue;
2670
+ if (sub.name.startsWith(".")) continue;
2671
+ if (existsSync(join(topPath, sub.name, "projects"))) hits.push(`${top.name}/${sub.name}/projects/`);
2672
+ }
2673
+ }
2674
+ return hits;
2675
+ }
2484
2676
  function entryAtPath(path) {
2485
2677
  try {
2486
2678
  return {
@@ -2576,6 +2768,8 @@ async function executeInit(opts) {
2576
2768
  const info = (msg) => {
2577
2769
  if (!silent) logger.log(msg);
2578
2770
  };
2771
+ const v04Hits = detectV04Workspace(opts.cwd);
2772
+ if (v04Hits.length > 0) throw v04WorkspaceDetectedError(v04Hits);
2579
2773
  const forgeMarkers = detectForgeMarkers(opts.cwd);
2580
2774
  const contextMdPath = join(opts.cwd, "CONTEXT.md");
2581
2775
  const claudeMdPath = join(opts.cwd, "CLAUDE.md");
@@ -2621,7 +2815,10 @@ async function executeInit(opts) {
2621
2815
  const warnings = [];
2622
2816
  const scaffoldSrc = join(ROSTER_ROOT, "templates", "scaffold");
2623
2817
  if (!existsSync(scaffoldSrc)) throw missingScaffoldError(scaffoldSrc);
2624
- const scaffoldFiles = walkScaffold(scaffoldSrc, opts.cwd, { PROJECT_NAME: projectName });
2818
+ const scaffoldFiles = walkScaffold(scaffoldSrc, opts.cwd, {
2819
+ PROJECT_NAME: projectName,
2820
+ DISPLAY_NAME: projectName
2821
+ });
2625
2822
  filesWritten.push(...scaffoldFiles);
2626
2823
  if (isMigration && migrate) {
2627
2824
  const existingClaudeMd = readFileSync(claudeMdPath, "utf8");
@@ -2667,7 +2864,7 @@ async function executeInit(opts) {
2667
2864
  const totalChanged = filesWritten.length + filesUpdated.length + filesLinked.length;
2668
2865
  if (!silent) {
2669
2866
  info("");
2670
- info(`${chalk.green("✓")} Initialized ${chalk.bold(projectName)} in ${opts.cwd}`);
2867
+ info(`${chalk.green("✓")} Initialized roster workspace ${chalk.bold(`'${projectName}'`)} in ${opts.cwd} ${chalk.dim("(current directory)")}`);
2671
2868
  if (totalChanged > 8) info(chalk.dim(`Files: ${totalChanged} written/linked`));
2672
2869
  else {
2673
2870
  const changed = [
@@ -2680,7 +2877,7 @@ async function executeInit(opts) {
2680
2877
  for (const w of warnings) info(chalk.yellow(w));
2681
2878
  if (gitInitialized) info(chalk.dim("Git: initialized .git/"));
2682
2879
  info("");
2683
- info(`${chalk.dim("Next: ")}${chalk.bold("open in Claude Code")}${chalk.dim(" and run ")}${chalk.bold("/chief-of-staff audit-repo")}`);
2880
+ info(`${chalk.dim("Next: ")}you are already in this directory — ${chalk.bold("open Claude Code here")}${chalk.dim(" and run ")}${chalk.bold("/chief-of-staff audit-repo")}`);
2684
2881
  }
2685
2882
  return {
2686
2883
  status: "ok",
@@ -3101,6 +3298,90 @@ function validateSchedulesInCwd(cwd) {
3101
3298
  };
3102
3299
  }
3103
3300
  //#endregion
3301
+ //#region src/lib/dotenv-parse.ts
3302
+ const KEY_RE$1 = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*/;
3303
+ function parseEnvFile(content) {
3304
+ const out = /* @__PURE__ */ new Map();
3305
+ if (content.charCodeAt(0) === 65279) content = content.slice(1);
3306
+ for (const rawLine of content.split(/\r?\n/)) {
3307
+ const trimmed = rawLine.replace(/^\s+/, "");
3308
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
3309
+ const candidate = trimmed.startsWith("export ") ? trimmed.slice(7).replace(/^\s+/, "") : trimmed;
3310
+ const m = candidate.match(KEY_RE$1);
3311
+ if (m === null) continue;
3312
+ const key = m[1];
3313
+ const parsed = parseValue(candidate.slice(m[0].length));
3314
+ if (parsed === null) continue;
3315
+ out.set(key, parsed);
3316
+ }
3317
+ return out;
3318
+ }
3319
+ function parseEnvKeys(content) {
3320
+ return Array.from(parseEnvFile(content).keys());
3321
+ }
3322
+ function parseValue(src) {
3323
+ if (src.length === 0) return "";
3324
+ const first = src.charAt(0);
3325
+ if (first === "\"") return parseDoubleQuoted(src);
3326
+ if (first === "'") return parseSingleQuoted(src);
3327
+ return parseUnquoted(src);
3328
+ }
3329
+ function parseDoubleQuoted(src) {
3330
+ let i = 1;
3331
+ let out = "";
3332
+ while (i < src.length) {
3333
+ const c = src.charAt(i);
3334
+ if (c === "\\" && i + 1 < src.length) {
3335
+ const next = src.charAt(i + 1);
3336
+ switch (next) {
3337
+ case "n":
3338
+ out += "\n";
3339
+ break;
3340
+ case "r":
3341
+ out += "\r";
3342
+ break;
3343
+ case "t":
3344
+ out += " ";
3345
+ break;
3346
+ case "\"":
3347
+ out += "\"";
3348
+ break;
3349
+ case "\\":
3350
+ out += "\\";
3351
+ break;
3352
+ default:
3353
+ out += "\\" + next;
3354
+ break;
3355
+ }
3356
+ i += 2;
3357
+ continue;
3358
+ }
3359
+ if (c === "\"") {
3360
+ const after = src.slice(i + 1).replace(/^\s+/, "");
3361
+ if (after.length === 0 || after.startsWith("#")) return out;
3362
+ return null;
3363
+ }
3364
+ out += c;
3365
+ i++;
3366
+ }
3367
+ return null;
3368
+ }
3369
+ function parseSingleQuoted(src) {
3370
+ const close = src.indexOf("'", 1);
3371
+ if (close === -1) return null;
3372
+ const after = src.slice(close + 1).replace(/^\s+/, "");
3373
+ if (after.length === 0 || after.startsWith("#")) return src.slice(1, close);
3374
+ return null;
3375
+ }
3376
+ function parseUnquoted(src) {
3377
+ let end = src.length;
3378
+ for (let i = 0; i < src.length; i++) if (src.charAt(i) === "#" && i > 0 && /\s/.test(src.charAt(i - 1))) {
3379
+ end = i;
3380
+ break;
3381
+ }
3382
+ return src.slice(0, end).replace(/\s+$/, "");
3383
+ }
3384
+ //#endregion
3104
3385
  //#region src/lib/platform.ts
3105
3386
  function getPlatform() {
3106
3387
  const override = process.env["ROSTER_PLATFORM"];
@@ -3111,6 +3392,376 @@ function isWindows() {
3111
3392
  return getPlatform() === "win32";
3112
3393
  }
3113
3394
  //#endregion
3395
+ //#region src/lib/agent-config-schema.ts
3396
+ const AGENT_RE = /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/;
3397
+ const ENV_VAR_RE = /^[A-Z][A-Z0-9_]*$/;
3398
+ const FORBIDDEN_FS_PREFIXES = [
3399
+ "/Users/",
3400
+ "/home/",
3401
+ "/etc/",
3402
+ "/var/",
3403
+ "/tmp/",
3404
+ "/opt/"
3405
+ ];
3406
+ const workspaceRootedPath = z.string().refine((p) => p.startsWith("/") && !FORBIDDEN_FS_PREFIXES.some((pfx) => p.startsWith(pfx)), { message: "must be a workspace-root-relative path starting with '/' (rejected: literal absolute fs paths /Users/ /home/ /etc/ /var/ /tmp/ /opt/)" });
3407
+ const toolBindingSchema = z.object({
3408
+ env_var: z.string().regex(ENV_VAR_RE, { message: "env_var: must be SCREAMING_SNAKE_CASE" }),
3409
+ required: z.boolean()
3410
+ }).strict();
3411
+ const agentConfigSchema = z.object({
3412
+ agent: z.string().regex(AGENT_RE, { message: "agent: must match '<function>/<agent>' with kebab-case segments" }),
3413
+ plans_dir: z.string().min(1),
3414
+ guideline_refs: z.record(z.string().min(1), workspaceRootedPath).nullish(),
3415
+ tools: z.record(z.string().min(1), toolBindingSchema).nullish()
3416
+ }).strict();
3417
+ function isInsideRoot(root, p) {
3418
+ const rel = relative(root, p);
3419
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
3420
+ }
3421
+ function loadAgentConfig(workspaceRoot, agentPath) {
3422
+ if (!AGENT_RE.test(agentPath)) return {
3423
+ ok: false,
3424
+ errors: [{
3425
+ kind: "invalid-agent-path",
3426
+ message: `agentPath '${agentPath}' must match '<function>/<agent>' with kebab-case segments`
3427
+ }]
3428
+ };
3429
+ let realRoot;
3430
+ try {
3431
+ realRoot = realpathSync(workspaceRoot);
3432
+ } catch (err) {
3433
+ return {
3434
+ ok: false,
3435
+ errors: [{
3436
+ kind: "missing-file",
3437
+ message: `workspaceRoot '${workspaceRoot}' not accessible: ${err.message}`,
3438
+ path: workspaceRoot
3439
+ }]
3440
+ };
3441
+ }
3442
+ const configPath = join(realRoot, agentPath, "config.yaml");
3443
+ let raw;
3444
+ try {
3445
+ raw = readFileSync(configPath, "utf8");
3446
+ } catch (err) {
3447
+ const e = err;
3448
+ if (e.code === "ENOENT") return {
3449
+ ok: false,
3450
+ errors: [{
3451
+ kind: "missing-file",
3452
+ message: `config.yaml not found at ${configPath}`,
3453
+ path: configPath
3454
+ }]
3455
+ };
3456
+ return {
3457
+ ok: false,
3458
+ errors: [{
3459
+ kind: "missing-file",
3460
+ message: `failed to read ${configPath}: ${e.message}`,
3461
+ path: configPath
3462
+ }]
3463
+ };
3464
+ }
3465
+ let doc;
3466
+ try {
3467
+ doc = YAML.parse(raw);
3468
+ } catch (err) {
3469
+ return {
3470
+ ok: false,
3471
+ errors: [{
3472
+ kind: "yaml-parse",
3473
+ message: `YAML parse error in ${configPath}: ${err.message}`,
3474
+ path: configPath
3475
+ }]
3476
+ };
3477
+ }
3478
+ const parsed = agentConfigSchema.safeParse(doc);
3479
+ if (!parsed.success) return {
3480
+ ok: false,
3481
+ errors: parsed.error.issues.map((issue) => ({
3482
+ kind: "schema",
3483
+ message: issue.message,
3484
+ path: issue.path.length > 0 ? issue.path.join(".") : void 0
3485
+ }))
3486
+ };
3487
+ const config = parsed.data;
3488
+ const errors = [];
3489
+ if (config.agent !== agentPath) errors.push({
3490
+ kind: "agent-field-mismatch",
3491
+ message: `agent field '${config.agent}' does not match agentPath argument '${agentPath}'`,
3492
+ path: "agent"
3493
+ });
3494
+ const refs = config.guideline_refs ?? {};
3495
+ for (const [key, ref] of Object.entries(refs)) {
3496
+ const wantsDirectory = ref.endsWith("/");
3497
+ const stripped = ref.replace(/^\/+/, "");
3498
+ const absolute = resolve(realRoot, stripped);
3499
+ if (!isInsideRoot(realRoot, absolute)) {
3500
+ errors.push({
3501
+ kind: "ref-escapes-workspace",
3502
+ message: `guideline_refs.${key}: '${ref}' escapes outside workspace root after resolution`,
3503
+ path: `guideline_refs.${key}`,
3504
+ ref
3505
+ });
3506
+ continue;
3507
+ }
3508
+ let realAbsolute;
3509
+ try {
3510
+ realAbsolute = realpathSync(absolute);
3511
+ } catch (err) {
3512
+ const e = err;
3513
+ if (e.code === "ENOENT") errors.push({
3514
+ kind: "ref-not-found",
3515
+ message: `guideline_refs.${key}: '${ref}' resolves to ${absolute} which does not exist`,
3516
+ path: `guideline_refs.${key}`,
3517
+ ref
3518
+ });
3519
+ else errors.push({
3520
+ kind: "ref-not-found",
3521
+ message: `guideline_refs.${key}: realpath ${absolute} failed: ${e.message}`,
3522
+ path: `guideline_refs.${key}`,
3523
+ ref
3524
+ });
3525
+ continue;
3526
+ }
3527
+ if (!isInsideRoot(realRoot, realAbsolute)) {
3528
+ errors.push({
3529
+ kind: "ref-escapes-workspace",
3530
+ message: `guideline_refs.${key}: '${ref}' resolves via symlink to ${realAbsolute} outside workspace root`,
3531
+ path: `guideline_refs.${key}`,
3532
+ ref
3533
+ });
3534
+ continue;
3535
+ }
3536
+ let stat;
3537
+ try {
3538
+ stat = statSync(realAbsolute);
3539
+ } catch (err) {
3540
+ errors.push({
3541
+ kind: "ref-not-found",
3542
+ message: `guideline_refs.${key}: stat ${realAbsolute} failed: ${err.message}`,
3543
+ path: `guideline_refs.${key}`,
3544
+ ref
3545
+ });
3546
+ continue;
3547
+ }
3548
+ if (wantsDirectory && !stat.isDirectory()) errors.push({
3549
+ kind: "ref-shape-mismatch",
3550
+ message: `guideline_refs.${key}: '${ref}' ends with '/' but ${realAbsolute} is not a directory`,
3551
+ path: `guideline_refs.${key}`,
3552
+ ref
3553
+ });
3554
+ else if (!wantsDirectory && !stat.isFile()) errors.push({
3555
+ kind: "ref-shape-mismatch",
3556
+ message: `guideline_refs.${key}: '${ref}' has no trailing '/' but ${realAbsolute} is not a regular file (add '/' if you meant a directory)`,
3557
+ path: `guideline_refs.${key}`,
3558
+ ref
3559
+ });
3560
+ }
3561
+ if (errors.length > 0) return {
3562
+ ok: false,
3563
+ errors
3564
+ };
3565
+ return {
3566
+ ok: true,
3567
+ config,
3568
+ refsChecked: Object.keys(refs).length
3569
+ };
3570
+ }
3571
+ //#endregion
3572
+ //#region src/lib/env-merge.ts
3573
+ function resolveAgentEnv(workspaceRoot, agentPath) {
3574
+ const workspace = tryReadAndParse(join(workspaceRoot, ".env"));
3575
+ const agent = tryReadAndParse(join(workspaceRoot, agentPath, ".env"));
3576
+ const out = {};
3577
+ for (const [k, v] of workspace) {
3578
+ if (v.length === 0) continue;
3579
+ out[k] = v;
3580
+ }
3581
+ for (const [k, v] of agent) if (v.length === 0) delete out[k];
3582
+ else out[k] = v;
3583
+ return out;
3584
+ }
3585
+ function tryReadAndParse(path) {
3586
+ try {
3587
+ return parseEnvFile(readFileSync(path, "utf8"));
3588
+ } catch {
3589
+ return /* @__PURE__ */ new Map();
3590
+ }
3591
+ }
3592
+ //#endregion
3593
+ //#region src/lib/doctor-agent-env-audit.ts
3594
+ const SKIP_TOP$1 = new Set([
3595
+ "roster",
3596
+ "node_modules",
3597
+ "plans",
3598
+ "spec",
3599
+ "docs",
3600
+ "bin",
3601
+ "lib",
3602
+ "skills",
3603
+ "agents",
3604
+ "templates",
3605
+ "test",
3606
+ "src"
3607
+ ]);
3608
+ function collectAgentEnvFiles(cwd) {
3609
+ const out = [];
3610
+ let topEntries;
3611
+ try {
3612
+ topEntries = readdirSync(cwd);
3613
+ } catch {
3614
+ return [];
3615
+ }
3616
+ for (const top of topEntries) {
3617
+ if (top.startsWith(".")) continue;
3618
+ if (SKIP_TOP$1.has(top)) continue;
3619
+ const topDir = join(cwd, top);
3620
+ let topSt;
3621
+ try {
3622
+ topSt = statSync(topDir);
3623
+ } catch {
3624
+ continue;
3625
+ }
3626
+ if (!topSt.isDirectory()) continue;
3627
+ const topEnv = join(topDir, ".env");
3628
+ try {
3629
+ if (statSync(topEnv).isFile()) out.push(topEnv);
3630
+ } catch {}
3631
+ let agents;
3632
+ try {
3633
+ agents = readdirSync(topDir);
3634
+ } catch {
3635
+ continue;
3636
+ }
3637
+ for (const agent of agents) {
3638
+ if (agent.startsWith(".")) continue;
3639
+ const agentDir = join(topDir, agent);
3640
+ let aSt;
3641
+ try {
3642
+ aSt = statSync(agentDir);
3643
+ } catch {
3644
+ continue;
3645
+ }
3646
+ if (!aSt.isDirectory()) continue;
3647
+ const envPath = join(agentDir, ".env");
3648
+ let eSt;
3649
+ try {
3650
+ eSt = statSync(envPath);
3651
+ } catch {
3652
+ continue;
3653
+ }
3654
+ if (!eSt.isFile()) continue;
3655
+ out.push(envPath);
3656
+ }
3657
+ }
3658
+ return out;
3659
+ }
3660
+ const KEY_RE = /^([A-Za-z_][A-Za-z0-9_]*)\s*=/;
3661
+ function findLastLineForKey(rawContent, key) {
3662
+ let content = rawContent;
3663
+ if (content.charCodeAt(0) === 65279) content = content.slice(1);
3664
+ const lines = content.split(/\r?\n/);
3665
+ let lastMatch = null;
3666
+ for (let i = 0; i < lines.length; i++) {
3667
+ const trimmed = (lines[i] ?? "").replace(/^\s+/, "");
3668
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
3669
+ const m = (trimmed.startsWith("export ") ? trimmed.slice(7).replace(/^\s+/, "") : trimmed).match(KEY_RE);
3670
+ if (m === null) continue;
3671
+ if (m[1] === key) lastMatch = i + 1;
3672
+ }
3673
+ return lastMatch;
3674
+ }
3675
+ function auditAgentEnvRedundancy(cwd) {
3676
+ let workspaceMap;
3677
+ try {
3678
+ workspaceMap = parseEnvFile(readFileSync(join(cwd, ".env"), "utf8"));
3679
+ } catch {
3680
+ return {
3681
+ status: "ok",
3682
+ items: []
3683
+ };
3684
+ }
3685
+ const items = [];
3686
+ for (const envPath of collectAgentEnvFiles(cwd)) {
3687
+ let raw;
3688
+ try {
3689
+ raw = readFileSync(envPath, "utf8");
3690
+ } catch {
3691
+ continue;
3692
+ }
3693
+ const agentMap = parseEnvFile(raw);
3694
+ for (const [key, value] of agentMap) {
3695
+ const wsValue = workspaceMap.get(key);
3696
+ if (wsValue === void 0) continue;
3697
+ if (wsValue !== value) continue;
3698
+ const line = findLastLineForKey(raw, key);
3699
+ if (line === null) continue;
3700
+ items.push({
3701
+ agentEnvPath: relative(cwd, envPath),
3702
+ line,
3703
+ key
3704
+ });
3705
+ }
3706
+ }
3707
+ items.sort((a, b) => {
3708
+ if (a.agentEnvPath !== b.agentEnvPath) return a.agentEnvPath.localeCompare(b.agentEnvPath);
3709
+ return a.line - b.line;
3710
+ });
3711
+ return {
3712
+ status: items.length === 0 ? "ok" : "warn",
3713
+ items
3714
+ };
3715
+ }
3716
+ function removeLineForKey(absPath, oneBasedLine, expectedKey, expectedValue, dryRun) {
3717
+ let raw;
3718
+ try {
3719
+ raw = readFileSync(absPath, "utf8");
3720
+ } catch (err) {
3721
+ return {
3722
+ kind: "error",
3723
+ message: err.message
3724
+ };
3725
+ }
3726
+ const hadTrailingNewline = raw.length > 0 && raw.charCodeAt(raw.length - 1) === 10;
3727
+ const newline = /\r\n/.test(raw) ? "\r\n" : "\n";
3728
+ const lines = raw.split(/\r?\n/);
3729
+ const effectiveLineCount = hadTrailingNewline ? lines.length - 1 : lines.length;
3730
+ if (oneBasedLine < 1 || oneBasedLine > effectiveLineCount) return {
3731
+ kind: "changed",
3732
+ reason: "line out of range"
3733
+ };
3734
+ const targetLine = lines[oneBasedLine - 1] ?? "";
3735
+ const trimmed = targetLine.replace(/^\s+/, "");
3736
+ const m = (trimmed.startsWith("export ") ? trimmed.slice(7).replace(/^\s+/, "") : trimmed).match(KEY_RE);
3737
+ if (m === null || m[1] !== expectedKey) return {
3738
+ kind: "changed",
3739
+ reason: "line no longer declares expected key"
3740
+ };
3741
+ const parsedAtLine = parseEnvFile(targetLine).get(expectedKey);
3742
+ if (parsedAtLine === void 0 || parsedAtLine !== expectedValue) return {
3743
+ kind: "changed",
3744
+ reason: "line value no longer matches workspace"
3745
+ };
3746
+ if (dryRun) return { kind: "would-remove" };
3747
+ const next = lines.slice(0, oneBasedLine - 1).concat(lines.slice(oneBasedLine)).join(newline);
3748
+ let mode = 384;
3749
+ try {
3750
+ mode = statSync(absPath).mode & 511;
3751
+ } catch {}
3752
+ const tmpPath = absPath + ".tmp." + String(process.pid);
3753
+ try {
3754
+ writeFileSync(tmpPath, next, { mode });
3755
+ renameSync(tmpPath, absPath);
3756
+ } catch (err) {
3757
+ return {
3758
+ kind: "error",
3759
+ message: err.message
3760
+ };
3761
+ }
3762
+ return { kind: "removed" };
3763
+ }
3764
+ //#endregion
3114
3765
  //#region src/lib/doctor-secrets-audit.ts
3115
3766
  function formatMode(mode) {
3116
3767
  return "0" + (mode & 511).toString(8).padStart(3, "0");
@@ -3142,100 +3793,142 @@ function auditEnvPermissions(cwd) {
3142
3793
  autoFixable: true
3143
3794
  };
3144
3795
  }
3145
- const ENV_KEY_RE = /^([A-Za-z_][A-Za-z0-9_]*)\s*=/;
3146
- function parseEnvKeys(content) {
3147
- const out = [];
3148
- const seen = /* @__PURE__ */ new Set();
3149
- for (const rawLine of content.split(/\r?\n/)) {
3150
- const line = rawLine.trim();
3151
- if (line.length === 0 || line.startsWith("#")) continue;
3152
- const m = (line.startsWith("export ") ? line.slice(7).trim() : line).match(ENV_KEY_RE);
3153
- if (m && !seen.has(m[1])) {
3154
- seen.add(m[1]);
3155
- out.push(m[1]);
3156
- }
3157
- }
3158
- return out;
3796
+ function classifyAgentEnvMode(modeBits) {
3797
+ if (modeBits === 384) return "ok";
3798
+ if ((modeBits & 18) !== 0) return "fail";
3799
+ return "warn";
3159
3800
  }
3160
- const VAR_REF_RE = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
3161
- function extractVarRefs(content) {
3162
- const out = /* @__PURE__ */ new Map();
3163
- const lines = content.split(/\r?\n/);
3164
- for (let i = 0; i < lines.length; i++) {
3165
- const line = lines[i];
3166
- for (const m of line.matchAll(VAR_REF_RE)) {
3167
- const key = m[1] ?? m[2] ?? "";
3168
- if (key.length === 0) continue;
3169
- const list = out.get(key);
3170
- if (list) list.push(i + 1);
3171
- else out.set(key, [i + 1]);
3172
- }
3173
- }
3174
- return out;
3175
- }
3176
- const SKIP_TOP = new Set([
3177
- "roster",
3178
- "node_modules",
3179
- "plans",
3180
- "spec",
3181
- "docs",
3182
- "bin",
3183
- "lib",
3184
- "skills",
3185
- "agents",
3186
- "templates",
3187
- "test",
3188
- "src"
3189
- ]);
3190
- function collectConfigYamls(cwd) {
3801
+ function listAgentDirs(cwd) {
3191
3802
  const out = [];
3192
3803
  let topEntries;
3193
3804
  try {
3194
3805
  topEntries = readdirSync(cwd);
3195
3806
  } catch {
3196
- return [];
3807
+ return out;
3197
3808
  }
3198
3809
  for (const top of topEntries) {
3199
3810
  if (top.startsWith(".")) continue;
3200
3811
  if (SKIP_TOP.has(top)) continue;
3201
- const fnDir = join(cwd, top);
3202
- let st;
3812
+ const topDir = join(cwd, top);
3813
+ let topStat;
3203
3814
  try {
3204
- st = statSync(fnDir);
3815
+ topStat = statSync(topDir);
3205
3816
  } catch {
3206
3817
  continue;
3207
3818
  }
3208
- if (!st.isDirectory()) continue;
3209
- let agents;
3819
+ if (!topStat.isDirectory()) continue;
3820
+ out.push(topDir);
3821
+ let children;
3210
3822
  try {
3211
- agents = readdirSync(fnDir);
3823
+ children = readdirSync(topDir);
3212
3824
  } catch {
3213
3825
  continue;
3214
3826
  }
3215
- for (const agent of agents) {
3216
- if (agent.startsWith(".")) continue;
3217
- const projectsDir = join(fnDir, agent, "projects");
3218
- let projects;
3827
+ for (const child of children) {
3828
+ if (child.startsWith(".")) continue;
3829
+ const childDir = join(topDir, child);
3830
+ let childStat;
3219
3831
  try {
3220
- projects = readdirSync(projectsDir);
3832
+ childStat = statSync(childDir);
3221
3833
  } catch {
3222
3834
  continue;
3223
3835
  }
3224
- for (const project of projects) {
3225
- if (project.startsWith(".")) continue;
3226
- const configDir = join(projectsDir, project, "config");
3227
- let configFiles;
3228
- try {
3229
- configFiles = readdirSync(configDir);
3230
- } catch {
3231
- continue;
3232
- }
3233
- for (const f of configFiles) if (f.endsWith(".yaml") || f.endsWith(".yml")) out.push(join(configDir, f));
3234
- }
3836
+ if (!childStat.isDirectory()) continue;
3837
+ out.push(childDir);
3838
+ }
3839
+ }
3840
+ return out;
3841
+ }
3842
+ function auditAgentEnvPermissions(cwd) {
3843
+ if (isWindows()) return {
3844
+ status: "skip-platform",
3845
+ reason: "win32-mode-bits-not-portable"
3846
+ };
3847
+ const items = [];
3848
+ let aggregate = "ok";
3849
+ for (const agentDir of listAgentDirs(cwd)) {
3850
+ const envPath = join(agentDir, ".env");
3851
+ let st;
3852
+ try {
3853
+ st = statSync(envPath);
3854
+ } catch {
3855
+ continue;
3856
+ }
3857
+ if (!st.isFile()) continue;
3858
+ const modeBits = st.mode & 511;
3859
+ const mode = formatMode(st.mode);
3860
+ const agentPath = relative(cwd, agentDir);
3861
+ const cls = classifyAgentEnvMode(modeBits);
3862
+ if (cls === "ok") items.push({
3863
+ status: "ok",
3864
+ agentPath,
3865
+ envPath,
3866
+ mode
3867
+ });
3868
+ else if (cls === "warn") {
3869
+ items.push({
3870
+ status: "warn",
3871
+ agentPath,
3872
+ envPath,
3873
+ mode,
3874
+ expected: "0600",
3875
+ autoFixable: true
3876
+ });
3877
+ if (aggregate === "ok") aggregate = "warn";
3878
+ } else {
3879
+ items.push({
3880
+ status: "fail",
3881
+ agentPath,
3882
+ envPath,
3883
+ mode,
3884
+ expected: "0600",
3885
+ autoFixable: true
3886
+ });
3887
+ aggregate = "fail";
3888
+ }
3889
+ }
3890
+ return {
3891
+ status: aggregate,
3892
+ items
3893
+ };
3894
+ }
3895
+ const VAR_REF_RE = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
3896
+ function extractVarRefs(content) {
3897
+ const out = /* @__PURE__ */ new Map();
3898
+ const lines = content.split(/\r?\n/);
3899
+ for (let i = 0; i < lines.length; i++) {
3900
+ const line = lines[i];
3901
+ for (const m of line.matchAll(VAR_REF_RE)) {
3902
+ const key = m[1] ?? m[2] ?? "";
3903
+ if (key.length === 0) continue;
3904
+ const list = out.get(key);
3905
+ if (list) list.push(i + 1);
3906
+ else out.set(key, [i + 1]);
3235
3907
  }
3236
3908
  }
3237
3909
  return out;
3238
3910
  }
3911
+ const SKIP_TOP = new Set([
3912
+ "roster",
3913
+ "node_modules",
3914
+ "plans",
3915
+ "spec",
3916
+ "docs",
3917
+ "bin",
3918
+ "lib",
3919
+ "skills",
3920
+ "agents",
3921
+ "templates",
3922
+ "test",
3923
+ "src",
3924
+ "config",
3925
+ "guidelines",
3926
+ "logs",
3927
+ "scripts"
3928
+ ]);
3929
+ function collectConfigYamls(cwd) {
3930
+ return listV1AgentPaths(cwd).map((rel) => join(cwd, rel, "config.yaml"));
3931
+ }
3239
3932
  const SHELL_VARS = new Set([
3240
3933
  "HOME",
3241
3934
  "PATH",
@@ -3257,7 +3950,8 @@ function auditEnvKeyReferences(cwd) {
3257
3950
  const envPath = join(cwd, ".env");
3258
3951
  let envKeys = [];
3259
3952
  try {
3260
- envKeys = parseEnvKeys(readFileSync(envPath, "utf8"));
3953
+ const raw = readFileSync(envPath, "utf8");
3954
+ envKeys = Array.from(parseEnvFile(raw).entries()).filter(([, v]) => v.length > 0).map(([k]) => k);
3261
3955
  } catch {}
3262
3956
  const envKeySet = new Set(envKeys);
3263
3957
  const yamls = collectConfigYamls(cwd);
@@ -3409,20 +4103,203 @@ function auditPromptLeak(cwd, schedules) {
3409
4103
  items
3410
4104
  };
3411
4105
  }
4106
+ const AGENT_SEGMENT_RE = /^[a-z][a-z0-9-]*$/;
4107
+ function listV1AgentPaths(cwd) {
4108
+ const out = [];
4109
+ let topEntries;
4110
+ try {
4111
+ topEntries = readdirSync(cwd);
4112
+ } catch {
4113
+ return [];
4114
+ }
4115
+ for (const top of topEntries) {
4116
+ if (top.startsWith(".")) continue;
4117
+ if (SKIP_TOP.has(top)) continue;
4118
+ if (!AGENT_SEGMENT_RE.test(top)) continue;
4119
+ const fnDir = join(cwd, top);
4120
+ let st;
4121
+ try {
4122
+ st = statSync(fnDir);
4123
+ } catch {
4124
+ continue;
4125
+ }
4126
+ if (!st.isDirectory()) continue;
4127
+ const topCfg = join(fnDir, "config.yaml");
4128
+ let topIsLeafAgent = false;
4129
+ try {
4130
+ if (statSync(topCfg).isFile()) {
4131
+ out.push(top);
4132
+ topIsLeafAgent = true;
4133
+ }
4134
+ } catch {}
4135
+ if (topIsLeafAgent) continue;
4136
+ let children;
4137
+ try {
4138
+ children = readdirSync(fnDir);
4139
+ } catch {
4140
+ continue;
4141
+ }
4142
+ for (const child of children) {
4143
+ if (child.startsWith(".")) continue;
4144
+ if (!AGENT_SEGMENT_RE.test(child)) continue;
4145
+ const agentDir = join(fnDir, child);
4146
+ let agentSt;
4147
+ try {
4148
+ agentSt = statSync(agentDir);
4149
+ } catch {
4150
+ continue;
4151
+ }
4152
+ if (!agentSt.isDirectory()) continue;
4153
+ const cfg = join(agentDir, "config.yaml");
4154
+ try {
4155
+ if (statSync(cfg).isFile()) out.push(top + "/" + child);
4156
+ } catch {
4157
+ continue;
4158
+ }
4159
+ }
4160
+ }
4161
+ out.sort();
4162
+ return out;
4163
+ }
4164
+ function auditAgentEnvRefs(cwd) {
4165
+ const errors = [];
4166
+ const warns = [];
4167
+ for (const agent of listV1AgentPaths(cwd)) {
4168
+ const loaded = loadAgentConfig(cwd, agent);
4169
+ if (!loaded.ok) continue;
4170
+ const tools = loaded.config.tools ?? {};
4171
+ const resolved = resolveAgentEnv(cwd, agent);
4172
+ for (const [binding, t] of Object.entries(tools)) {
4173
+ if (Object.prototype.hasOwnProperty.call(resolved, t.env_var)) continue;
4174
+ const miss = {
4175
+ agent,
4176
+ binding,
4177
+ key: t.env_var,
4178
+ required: t.required
4179
+ };
4180
+ if (t.required) errors.push(miss);
4181
+ else warns.push(miss);
4182
+ }
4183
+ }
4184
+ return {
4185
+ status: errors.length > 0 ? "fail" : warns.length > 0 ? "warn" : "ok",
4186
+ errors,
4187
+ warns
4188
+ };
4189
+ }
3412
4190
  function runSecretsAudit(opts) {
3413
4191
  const envPermissions = auditEnvPermissions(opts.cwd);
4192
+ const agentEnvPermissions = auditAgentEnvPermissions(opts.cwd);
3414
4193
  const envKeyReferences = auditEnvKeyReferences(opts.cwd);
3415
4194
  const templateSecretLiterals = auditTemplateSecretLiterals(opts.rosterRoot);
3416
4195
  const promptLeak = auditPromptLeak(opts.cwd, opts.schedules);
4196
+ const agentEnvRefs = auditAgentEnvRefs(opts.cwd);
4197
+ const agentEnvRedundancy = auditAgentEnvRedundancy(opts.cwd);
4198
+ const envOk = envPermissions.status === "ok" || envPermissions.status === "absent" || envPermissions.status === "skip-platform";
4199
+ const agentEnvOk = agentEnvPermissions.status !== "fail";
3417
4200
  return {
3418
- ok: (envPermissions.status === "ok" || envPermissions.status === "absent" || envPermissions.status === "skip-platform") && envKeyReferences.status === "ok" && templateSecretLiterals.status === "ok",
4201
+ ok: envOk && agentEnvOk && envKeyReferences.status === "ok" && templateSecretLiterals.status === "ok" && agentEnvRefs.errors.length === 0,
3419
4202
  envPermissions,
4203
+ agentEnvPermissions,
3420
4204
  envKeyReferences,
3421
4205
  templateSecretLiterals,
3422
- promptLeak
4206
+ promptLeak,
4207
+ agentEnvRefs,
4208
+ agentEnvRedundancy
3423
4209
  };
3424
4210
  }
3425
4211
  //#endregion
4212
+ //#region src/lib/agent-env-fix-prompt.ts
4213
+ async function confirmAndDeleteRedundantLines(items, cwd, deps, dryRun) {
4214
+ const out = {
4215
+ deleted: [],
4216
+ failed: [],
4217
+ skipped: [],
4218
+ nonTtySkipped: false
4219
+ };
4220
+ if (items.length === 0) return out;
4221
+ if (!deps.isTTY) {
4222
+ out.nonTtySkipped = true;
4223
+ return out;
4224
+ }
4225
+ let workspaceMap = /* @__PURE__ */ new Map();
4226
+ try {
4227
+ workspaceMap = parseEnvFile(readFileSync(join(cwd, ".env"), "utf8"));
4228
+ } catch {}
4229
+ const rl = createInterface({
4230
+ input: deps.stdin,
4231
+ output: deps.stdout,
4232
+ terminal: false
4233
+ });
4234
+ let stdinClosed = false;
4235
+ rl.once("close", () => {
4236
+ stdinClosed = true;
4237
+ });
4238
+ try {
4239
+ for (const item of items) {
4240
+ const absPath = join(cwd, item.agentEnvPath);
4241
+ const label = `${item.agentEnvPath}:${item.line} ${item.key}`;
4242
+ if (stdinClosed) {
4243
+ out.skipped.push(`${label}: stdin closed`);
4244
+ continue;
4245
+ }
4246
+ deps.stdout.write(`Delete ${label}? [y/N] `);
4247
+ const answer = await readOneLine(rl);
4248
+ if (answer === null) {
4249
+ stdinClosed = true;
4250
+ out.skipped.push(`${label}: stdin closed`);
4251
+ continue;
4252
+ }
4253
+ const normalized = answer.trim().toLowerCase();
4254
+ if (normalized !== "y" && normalized !== "yes") {
4255
+ out.skipped.push(label);
4256
+ continue;
4257
+ }
4258
+ const expectedValue = workspaceMap.get(item.key);
4259
+ if (expectedValue === void 0) {
4260
+ out.failed.push({
4261
+ what: label,
4262
+ error: "workspace .env no longer declares this key"
4263
+ });
4264
+ continue;
4265
+ }
4266
+ const result = removeLineForKey(absPath, item.line, item.key, expectedValue, dryRun);
4267
+ if (result.kind === "removed") out.deleted.push(`${label}: removed`);
4268
+ else if (result.kind === "would-remove") out.deleted.push(`${label}: would remove (dry-run)`);
4269
+ else if (result.kind === "changed") out.failed.push({
4270
+ what: label,
4271
+ error: `file changed (${result.reason})`
4272
+ });
4273
+ else out.failed.push({
4274
+ what: label,
4275
+ error: result.message
4276
+ });
4277
+ }
4278
+ } finally {
4279
+ rl.close();
4280
+ }
4281
+ return out;
4282
+ }
4283
+ function readOneLine(rl) {
4284
+ return new Promise((resolve) => {
4285
+ let settled = false;
4286
+ const onLine = (line) => {
4287
+ if (settled) return;
4288
+ settled = true;
4289
+ rl.off("close", onClose);
4290
+ resolve(line);
4291
+ };
4292
+ const onClose = () => {
4293
+ if (settled) return;
4294
+ settled = true;
4295
+ rl.off("line", onLine);
4296
+ resolve(null);
4297
+ };
4298
+ rl.once("line", onLine);
4299
+ rl.once("close", onClose);
4300
+ });
4301
+ }
4302
+ //#endregion
3426
4303
  //#region src/lib/codex-preflight.ts
3427
4304
  function readAuthJson(codexHome) {
3428
4305
  const authPath = join(codexHome, "auth.json");
@@ -3895,11 +4772,11 @@ function deriveScheduleName(agent, plan, override) {
3895
4772
  if (override !== void 0 && override !== "") return override;
3896
4773
  return `${agent}-${plan}`;
3897
4774
  }
3898
- function buildOrchestratorPrompt(agent, plan, project) {
3899
- return `Use the roster-orchestrator skill to run plan ${plan} for agent ${agent} on project ${project}`;
4775
+ function buildOrchestratorPrompt(agent, plan) {
4776
+ return `Use the roster-orchestrator skill to run plan ${plan} for agent ${agent}`;
3900
4777
  }
3901
4778
  function renderFieldsDoc(args) {
3902
- const prompt = buildOrchestratorPrompt(args.agent, args.plan, args.project);
4779
+ const prompt = buildOrchestratorPrompt(args.agent, args.plan);
3903
4780
  const allowedTools = DEFAULT_ALLOWED_TOOLS.join(", ");
3904
4781
  return [
3905
4782
  `# Claude Desktop Scheduled Task — ${args.name}`,
@@ -3963,7 +4840,6 @@ function installClaudeSchedule(opts) {
3963
4840
  name: resolvedName,
3964
4841
  agent: opts.agent,
3965
4842
  plan: opts.plan,
3966
- project: opts.project,
3967
4843
  cron: opts.cron,
3968
4844
  tool: "claude",
3969
4845
  install_mode: "ui-handoff",
@@ -3994,8 +4870,7 @@ function installClaudeSchedule(opts) {
3994
4870
  cron: validatedEntry.cron,
3995
4871
  workspacePath,
3996
4872
  agent: validatedEntry.agent,
3997
- plan: validatedEntry.plan,
3998
- project: validatedEntry.project
4873
+ plan: validatedEntry.plan
3999
4874
  });
4000
4875
  if (opts.dryRun) return {
4001
4876
  resolvedName,
@@ -4145,7 +5020,7 @@ function auditCronDrift(opts) {
4145
5020
  cron: entry.cron,
4146
5021
  workspacePath,
4147
5022
  codexBinaryPath,
4148
- prompt: buildOrchestratorPrompt(entry.agent, entry.plan, entry.project),
5023
+ prompt: buildOrchestratorPrompt(entry.agent, entry.plan),
4149
5024
  logPath: logPathFor(workspacePath, entry.name),
4150
5025
  exitPath: exitPathFor(workspacePath, entry.name),
4151
5026
  ...entry.capture_events === true ? { eventsPath: eventsPathFor(workspacePath, entry.name) } : {}
@@ -4395,7 +5270,29 @@ function runSchedulingDriftAudit(opts) {
4395
5270
  }
4396
5271
  //#endregion
4397
5272
  //#region src/commands/doctor.ts
4398
- function tildify$4(path) {
5273
+ function listSkillDirNames(target) {
5274
+ try {
5275
+ return readdirSync(target, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
5276
+ } catch {
5277
+ return [];
5278
+ }
5279
+ }
5280
+ function detectShadowCollisions(workspaceRoot) {
5281
+ const collisions = [];
5282
+ for (const userTool of allTools()) {
5283
+ const projectTool = toolForScope(userTool, "project", workspaceRoot);
5284
+ const userNames = new Set(listSkillDirNames(userTool.skillsTarget));
5285
+ const projectNames = listSkillDirNames(projectTool.skillsTarget);
5286
+ for (const name of projectNames) if (userNames.has(name)) collisions.push({
5287
+ tool: userTool.key,
5288
+ skillName: name,
5289
+ userPath: join(userTool.skillsTarget, name),
5290
+ projectPath: join(projectTool.skillsTarget, name)
5291
+ });
5292
+ }
5293
+ return collisions;
5294
+ }
5295
+ function tildify$3(path) {
4399
5296
  const home = homedir();
4400
5297
  return path.startsWith(home) ? "~" + path.slice(home.length) : path;
4401
5298
  }
@@ -4436,7 +5333,7 @@ function workspaceLabel(status) {
4436
5333
  function renderSchedulingSection(report) {
4437
5334
  if (report.files.length === 0) return [];
4438
5335
  const lines = [""];
4439
- lines.push(`Scheduling ${tildify$4(report.cwd)}`);
5336
+ lines.push(`Scheduling ${tildify$3(report.cwd)}`);
4440
5337
  for (const file of report.files) if (file.status === "pass") {
4441
5338
  const entryWord = file.entryCount === 1 ? "entry" : "entries";
4442
5339
  lines.push(` ${chalk.green("✓")} ${file.relativePath} ${chalk.dim("OK")} ${chalk.dim(`(${file.entryCount} ${entryWord})`)}`);
@@ -4449,7 +5346,7 @@ function renderSchedulingSection(report) {
4449
5346
  function renderWorkspaceSection(audit) {
4450
5347
  if (!audit.contextMdExists && audit.items.length === 0) return [];
4451
5348
  const lines = [""];
4452
- lines.push(`Workspace ${tildify$4(audit.cwd)}`);
5349
+ lines.push(`Workspace ${tildify$3(audit.cwd)}`);
4453
5350
  if (audit.contextMdExists) lines.push(` ${chalk.green("✓")} CONTEXT.md ${chalk.dim("present")}`);
4454
5351
  else lines.push(` ${chalk.red("✗")} CONTEXT.md ${chalk.red("MISSING")}`);
4455
5352
  for (const item of audit.items) {
@@ -4476,7 +5373,7 @@ const NO_FIX_REQUESTED = Object.freeze({
4476
5373
  fixed: [],
4477
5374
  failed: []
4478
5375
  });
4479
- function runFixes(cwd, workspace, envPerms, dryRun = false) {
5376
+ function runFixes(cwd, workspace, envPerms, agentEnvPerms, dryRun = false) {
4480
5377
  const fixed = [];
4481
5378
  const failed = [];
4482
5379
  const applied = true;
@@ -4513,12 +5410,142 @@ function runFixes(cwd, workspace, envPerms, dryRun = false) {
4513
5410
  error: err.message
4514
5411
  });
4515
5412
  }
5413
+ if (agentEnvPerms.status === "warn" || agentEnvPerms.status === "fail") for (const item of agentEnvPerms.items) {
5414
+ if (item.status === "ok") continue;
5415
+ const label = `${item.agentPath}/.env`;
5416
+ if (dryRun) {
5417
+ fixed.push(`${label}: would chmod 0600`);
5418
+ continue;
5419
+ }
5420
+ try {
5421
+ chmodSync(item.envPath, 384);
5422
+ fixed.push(`${label}: chmod 0600`);
5423
+ } catch (err) {
5424
+ failed.push({
5425
+ what: label,
5426
+ error: err.message
5427
+ });
5428
+ }
5429
+ }
4516
5430
  return {
4517
5431
  applied,
4518
5432
  fixed,
4519
5433
  failed
4520
5434
  };
4521
5435
  }
5436
+ function uniqueKeysFromRefs(refs) {
5437
+ const byKey = /* @__PURE__ */ new Map();
5438
+ for (const m of [...refs.errors, ...refs.warns]) {
5439
+ const list = byKey.get(m.key);
5440
+ if (list) list.push(m);
5441
+ else byKey.set(m.key, [m]);
5442
+ }
5443
+ return Array.from(byKey.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([key, refs]) => ({
5444
+ key,
5445
+ refs
5446
+ }));
5447
+ }
5448
+ function appendKeysToEnvFile(cwd, keys) {
5449
+ const envPath = join(cwd, ".env");
5450
+ const added = [];
5451
+ const skipped = [];
5452
+ let existing = "";
5453
+ let hadFile = false;
5454
+ try {
5455
+ existing = readFileSync(envPath, "utf8");
5456
+ hadFile = true;
5457
+ } catch {}
5458
+ let existingKeys = /* @__PURE__ */ new Set();
5459
+ try {
5460
+ existingKeys = new Set(parseEnvKeys(existing));
5461
+ } catch {}
5462
+ const newLines = [];
5463
+ for (const key of keys) {
5464
+ if (existingKeys.has(key)) {
5465
+ skipped.push(key);
5466
+ continue;
5467
+ }
5468
+ newLines.push(`${key}=`);
5469
+ added.push(key);
5470
+ }
5471
+ if (newLines.length === 0) return {
5472
+ added,
5473
+ skipped
5474
+ };
5475
+ let body = existing;
5476
+ if (body.length > 0 && !body.endsWith("\n")) body += "\n";
5477
+ body += newLines.join("\n") + "\n";
5478
+ writeFileSync(envPath, body, { encoding: "utf8" });
5479
+ if (!hadFile && !isWindows()) try {
5480
+ chmodSync(envPath, 384);
5481
+ } catch {}
5482
+ return {
5483
+ added,
5484
+ skipped
5485
+ };
5486
+ }
5487
+ async function defaultAgentEnvPrompt(uniqueKeys) {
5488
+ const { checkbox } = await import("@inquirer/prompts");
5489
+ return checkbox({
5490
+ message: "Select env keys to append to /.env (empty value — fill in after):",
5491
+ choices: uniqueKeys.map(({ key, refs }) => {
5492
+ return {
5493
+ value: key,
5494
+ name: `${key} ${refs.some((r) => r.required) ? "(required)" : "(optional)"} ← ${refs.map((r) => r.agent).join(", ")}`
5495
+ };
5496
+ })
5497
+ });
5498
+ }
5499
+ async function applyAgentEnvFix(cwd, refs, opts) {
5500
+ const fixed = [];
5501
+ const failed = [];
5502
+ const uniqueKeys = uniqueKeysFromRefs(refs);
5503
+ if (uniqueKeys.length === 0) return {
5504
+ fixed,
5505
+ failed
5506
+ };
5507
+ if (opts.dryRun) {
5508
+ for (const { key, refs: agents } of uniqueKeys) {
5509
+ const agentList = agents.map((r) => r.agent).join(", ");
5510
+ fixed.push(`/.env: would append ${key}= ${chalk.dim(`(referenced by ${agentList})`)}`);
5511
+ }
5512
+ return {
5513
+ fixed,
5514
+ failed
5515
+ };
5516
+ }
5517
+ let selected;
5518
+ try {
5519
+ selected = await (opts.prompt ?? defaultAgentEnvPrompt)(uniqueKeys);
5520
+ } catch (err) {
5521
+ failed.push({
5522
+ what: "/.env (interactive prompt)",
5523
+ error: err.message
5524
+ });
5525
+ return {
5526
+ fixed,
5527
+ failed
5528
+ };
5529
+ }
5530
+ if (selected.length === 0) return {
5531
+ fixed,
5532
+ failed
5533
+ };
5534
+ try {
5535
+ const { added, skipped } = appendKeysToEnvFile(cwd, selected);
5536
+ for (const k of added) fixed.push(`/.env: appended ${k}=`);
5537
+ for (const k of skipped) fixed.push(`/.env: ${k} already present (skipped)`);
5538
+ } catch (err) {
5539
+ failed.push({
5540
+ what: "/.env",
5541
+ error: err.message
5542
+ });
5543
+ }
5544
+ return {
5545
+ fixed,
5546
+ failed
5547
+ };
5548
+ }
4522
5549
  function renderFixSection(outcome) {
4523
5550
  if (!outcome.applied) return [];
4524
5551
  if (outcome.fixed.length === 0 && outcome.failed.length === 0) return [];
@@ -4590,10 +5617,15 @@ function renderSafetySection(audit) {
4590
5617
  }
4591
5618
  function renderSecretsSection(audit) {
4592
5619
  const env = audit.envPermissions;
5620
+ const agentEnv = audit.agentEnvPermissions;
4593
5621
  const refs = audit.envKeyReferences;
4594
5622
  const templates = audit.templateSecretLiterals;
4595
5623
  const leak = audit.promptLeak;
4596
- if ((env.status === "absent" || env.status === "skip-platform") && refs.status === "ok" && templates.status === "ok" && leak.status === "ok") return [];
5624
+ const agentRefs = audit.agentEnvRefs;
5625
+ const redundancy = audit.agentEnvRedundancy;
5626
+ const noEnv = env.status === "absent" || env.status === "skip-platform";
5627
+ const agentEnvClean = agentEnv.status === "skip-platform" || agentEnv.status === "ok" && agentEnv.items.length === 0;
5628
+ if (noEnv && agentEnvClean && refs.status === "ok" && templates.status === "ok" && leak.status === "ok" && agentRefs.status === "ok" && redundancy.status === "ok") return [];
4597
5629
  const lines = [""];
4598
5630
  lines.push(chalk.bold("Secrets"));
4599
5631
  if (env.status === "ok") lines.push(` ${chalk.green("✓")} .env permissions ${chalk.dim("OK")} ${chalk.dim(`(${env.mode})`)}`);
@@ -4601,6 +5633,28 @@ function renderSecretsSection(audit) {
4601
5633
  lines.push(` ${chalk.red("✗")} .env permissions ${chalk.red("FAIL")} ${chalk.dim(`(got ${env.mode}, expected ${env.expected})`)}`);
4602
5634
  lines.push(` ${chalk.dim("→ Run `roster doctor --fix` to chmod 0600.")}`);
4603
5635
  } else if (env.status === "skip-platform") lines.push(` ${chalk.dim("-")} .env permissions ${chalk.dim("SKIPPED")} ${chalk.dim("(windows mode bits not portable)")}`);
5636
+ if (agentEnv.status === "ok" && agentEnv.items.length > 0) lines.push(` ${chalk.green("✓")} agent .env perms ${chalk.dim("OK")} ${chalk.dim(`(${agentEnv.items.length} agent .env${agentEnv.items.length === 1 ? "" : "s"})`)}`);
5637
+ else if (agentEnv.status === "warn") {
5638
+ const warnCount = agentEnv.items.filter((i) => i.status === "warn").length;
5639
+ lines.push(` ${chalk.yellow("!")} agent .env perms ${chalk.yellow("WARN")} ${chalk.dim(`(${warnCount} agent .env${warnCount === 1 ? "" : "s"} not 0600)`)}`);
5640
+ for (const item of agentEnv.items.slice(0, 10)) {
5641
+ if (item.status === "ok") continue;
5642
+ lines.push(` ${chalk.yellow("-")} ${item.agentPath}/.env ${chalk.dim(`(got ${item.mode}, expected 0600)`)}`);
5643
+ }
5644
+ lines.push(` ${chalk.dim("→ Run `roster doctor --fix` to chmod 0600.")}`);
5645
+ } else if (agentEnv.status === "fail") {
5646
+ const failCount = agentEnv.items.filter((i) => i.status === "fail").length;
5647
+ const warnCount = agentEnv.items.filter((i) => i.status === "warn").length;
5648
+ const detail = failCount === 1 ? "1 world-writable" : `${failCount} world-writable`;
5649
+ const extra = warnCount > 0 ? `, ${warnCount} other not 0600` : "";
5650
+ lines.push(` ${chalk.red("✗")} agent .env perms ${chalk.red("FAIL")} ${chalk.dim(`(${detail}${extra})`)}`);
5651
+ for (const item of agentEnv.items.slice(0, 10)) {
5652
+ if (item.status === "ok") continue;
5653
+ const marker = item.status === "fail" ? chalk.red("-") : chalk.yellow("-");
5654
+ lines.push(` ${marker} ${item.agentPath}/.env ${chalk.dim(`(got ${item.mode}, expected 0600)`)}`);
5655
+ }
5656
+ lines.push(` ${chalk.dim("→ Run `roster doctor --fix` to chmod 0600.")}`);
5657
+ } else if (agentEnv.status === "skip-platform") lines.push(` ${chalk.dim("-")} agent .env perms ${chalk.dim("SKIPPED")} ${chalk.dim("(windows mode bits not portable)")}`);
4604
5658
  if (refs.status === "fail") {
4605
5659
  lines.push(` ${chalk.red("✗")} env-key refs ${chalk.red("FAIL")} ${chalk.dim(`(${refs.missing.length} unreferenced)`)}`);
4606
5660
  for (const m of refs.missing.slice(0, 10)) {
@@ -4616,6 +5670,28 @@ function renderSecretsSection(audit) {
4616
5670
  lines.push(` ${chalk.yellow("!")} prompt-leak ${chalk.yellow("WARN")} ${chalk.dim(`(${leak.items.length} reference${leak.items.length === 1 ? "" : "s"})`)}`);
4617
5671
  for (const item of leak.items.slice(0, 5)) lines.push(` ${chalk.yellow("-")} ${item.schedule}: ${item.reference} ${chalk.dim("in " + item.file + ":" + item.line)}`);
4618
5672
  }
5673
+ if (agentRefs.status === "fail" || agentRefs.status === "warn") {
5674
+ const e = agentRefs.errors.length;
5675
+ const w = agentRefs.warns.length;
5676
+ const counts = [];
5677
+ if (e > 0) counts.push(`${e} error${e === 1 ? "" : "s"}`);
5678
+ if (w > 0) counts.push(`${w} warn${w === 1 ? "" : "s"}`);
5679
+ const tag = agentRefs.status === "fail" ? chalk.red("FAIL") : chalk.yellow("WARN");
5680
+ const symbol = agentRefs.status === "fail" ? chalk.red("✗") : chalk.yellow("!");
5681
+ lines.push(` ${symbol} agent-env-refs ${tag} ${chalk.dim(`(${counts.join(", ")})`)}`);
5682
+ for (const m of agentRefs.errors.slice(0, 10)) lines.push(` ${chalk.red("-")} ${m.agent}: ${m.key} ${chalk.dim(`(tools.${m.binding}, required)`)}`);
5683
+ if (agentRefs.errors.length > 10) lines.push(` ${chalk.dim(`… (${agentRefs.errors.length - 10} more errors)`)}`);
5684
+ for (const m of agentRefs.warns.slice(0, 5)) lines.push(` ${chalk.yellow("-")} ${m.agent}: ${m.key} ${chalk.dim(`(tools.${m.binding}, optional)`)}`);
5685
+ if (agentRefs.warns.length > 5) lines.push(` ${chalk.dim(`… (${agentRefs.warns.length - 5} more warns)`)}`);
5686
+ if (e > 0 || w > 0) lines.push(` ${chalk.dim("→ Run `roster doctor --fix` to append missing keys to /.env.")}`);
5687
+ }
5688
+ if (redundancy.status === "warn") {
5689
+ const n = redundancy.items.length;
5690
+ lines.push(` ${chalk.yellow("!")} agent .env redundancy ${chalk.yellow("WARN")} ${chalk.dim(`(${n} entr${n === 1 ? "y" : "ies"})`)}`);
5691
+ for (const item of redundancy.items.slice(0, 10)) lines.push(` ${chalk.yellow("-")} ${item.agentEnvPath}:${item.line} ${item.key} ${chalk.dim("matches workspace .env")}`);
5692
+ if (n > 10) lines.push(` ${chalk.dim(`… (${n - 10} more)`)}`);
5693
+ lines.push(` ${chalk.dim("→ Run `roster doctor --fix` to prompt removal of redundant lines.")}`);
5694
+ }
4619
5695
  return lines;
4620
5696
  }
4621
5697
  function renderStaleFiresSection(stale) {
@@ -4657,12 +5733,13 @@ function renderSchedulingDriftSection(audit) {
4657
5733
  }
4658
5734
  return lines;
4659
5735
  }
4660
- function renderText$1(results, summary, workarounds) {
5736
+ function renderText$1(results, summary, workarounds, scope) {
4661
5737
  const lines = [""];
4662
5738
  lines.push(chalk.bold("roster doctor"));
5739
+ lines.push(chalk.dim(`Install scope: ${scope} (${scope === "project" ? "workspace-local" : "home directory"})`));
4663
5740
  for (const r of results) {
4664
5741
  lines.push("");
4665
- lines.push(`${chalk.bold(r.toolName)} ${chalk.dim(tildify$4(r.configRoot))}`);
5742
+ lines.push(`${chalk.bold(r.toolName)} ${chalk.dim(tildify$3(r.configRoot))}`);
4666
5743
  for (const item of r.items) {
4667
5744
  const kindCol = chalk.dim(item.kind.padEnd(5));
4668
5745
  const nameCol = item.name.padEnd(22);
@@ -4686,8 +5763,38 @@ function renderText$1(results, summary, workarounds) {
4686
5763
  }
4687
5764
  return lines;
4688
5765
  }
4689
- function executeDoctor(opts) {
4690
- const detected = detectTools();
5766
+ function renderShadowSection(shadows) {
5767
+ if (shadows.length === 0) return [];
5768
+ const lines = [""];
5769
+ lines.push(chalk.bold("Shadow collisions") + chalk.dim(` (${shadows.length})`));
5770
+ lines.push(chalk.dim(" Same skill installed at both scopes. The user-scope copy wins;"));
5771
+ lines.push(chalk.dim(" the workspace skill is silently ignored. Remove one."));
5772
+ for (const s of shadows) {
5773
+ lines.push(` ${chalk.yellow("!")} ${chalk.bold(s.skillName)} ${chalk.dim("(")}${s.tool}${chalk.dim(")")}`);
5774
+ lines.push(` user: ${tildify$3(s.userPath)}`);
5775
+ lines.push(` project: ${s.projectPath}`);
5776
+ }
5777
+ return lines;
5778
+ }
5779
+ function renderInteractiveFixSection(outcome) {
5780
+ if (!(outcome.deleted.length > 0 || outcome.failed.length > 0 || outcome.skipped.length > 0 || outcome.nonTtySkipped)) return [];
5781
+ const lines = [""];
5782
+ lines.push(chalk.bold("agent .env redundancy --fix"));
5783
+ if (outcome.nonTtySkipped) {
5784
+ lines.push(` ${chalk.dim("-")} skipped — re-run \`roster doctor --fix\` in an interactive terminal`);
5785
+ return lines;
5786
+ }
5787
+ for (const f of outcome.deleted) lines.push(` ${chalk.green("✓")} ${f}`);
5788
+ for (const f of outcome.skipped) lines.push(` ${chalk.dim("-")} ${f}: kept`);
5789
+ for (const f of outcome.failed) lines.push(` ${chalk.red("✗")} ${f.what}: ${f.error}`);
5790
+ return lines;
5791
+ }
5792
+ async function executeDoctor(opts) {
5793
+ const workspaceExists = detectWorkspace(opts.cwd);
5794
+ const effectiveScope = opts.scope ?? defaultScopeForContext(workspaceExists);
5795
+ const userScopeTools = detectTools();
5796
+ const detected = effectiveScope === "project" ? allTools().map((t) => toolForScope(t, "project", opts.cwd)).filter((t) => existsSync(t.configRoot)) : userScopeTools;
5797
+ const shadows = workspaceExists ? detectShadowCollisions(opts.cwd) : [];
4691
5798
  const sources = {
4692
5799
  skills: join(ROSTER_ROOT, "skills"),
4693
5800
  agents: join(ROSTER_ROOT, "agents")
@@ -4720,11 +5827,8 @@ function executeDoctor(opts) {
4720
5827
  const workarounds = computeWorkarounds(results);
4721
5828
  let workspaceFinal = workspace;
4722
5829
  const initialEnvPerms = auditEnvPermissions(opts.cwd);
5830
+ const initialAgentEnvPerms = auditAgentEnvPermissions(opts.cwd);
4723
5831
  let fixOutcome = NO_FIX_REQUESTED;
4724
- if (opts.fix) {
4725
- fixOutcome = runFixes(opts.cwd, workspace, initialEnvPerms, opts.dryRun);
4726
- if (!opts.dryRun && fixOutcome.fixed.length > 0) workspaceFinal = auditWorkspace(opts.cwd);
4727
- }
4728
5832
  const schedulesForLeak = listAllScheduleEntries(opts.cwd);
4729
5833
  const codexHomeOverride = process.env["ROSTER_CODEX_HOME"];
4730
5834
  const safety = runSafetyAudit({
@@ -4735,20 +5839,70 @@ function executeDoctor(opts) {
4735
5839
  env: process.env,
4736
5840
  ...codexHomeOverride !== void 0 && codexHomeOverride !== "" ? { codexHome: codexHomeOverride } : {}
4737
5841
  });
4738
- const secrets = runSecretsAudit({
5842
+ const initialSecrets = runSecretsAudit({
4739
5843
  cwd: opts.cwd,
4740
5844
  rosterRoot: ROSTER_ROOT,
4741
5845
  schedules: schedulesForLeak
4742
5846
  });
5847
+ if (opts.fix) {
5848
+ fixOutcome = runFixes(opts.cwd, workspace, initialEnvPerms, initialAgentEnvPerms, opts.dryRun);
5849
+ const refs = initialSecrets.agentEnvRefs;
5850
+ const anyMissing = refs.errors.length > 0 || refs.warns.length > 0;
5851
+ const canInteract = !opts.json && process.stdin.isTTY === true;
5852
+ if (anyMissing) if (opts.dryRun) {
5853
+ const out = await applyAgentEnvFix(opts.cwd, refs, { dryRun: true });
5854
+ fixOutcome.fixed.push(...out.fixed);
5855
+ fixOutcome.failed.push(...out.failed);
5856
+ } else if (canInteract) {
5857
+ const out = await applyAgentEnvFix(opts.cwd, refs, { dryRun: false });
5858
+ fixOutcome.fixed.push(...out.fixed);
5859
+ fixOutcome.failed.push(...out.failed);
5860
+ } else {
5861
+ const reason = opts.json ? "interactive prompt suppressed under --json" : "no TTY (non-interactive shell)";
5862
+ fixOutcome.failed.push({
5863
+ what: "/.env (agent-env-refs)",
5864
+ error: `--fix skipped: ${reason}. Rerun in an interactive shell to append missing keys.`
5865
+ });
5866
+ }
5867
+ if (!opts.dryRun && fixOutcome.fixed.length > 0) workspaceFinal = auditWorkspace(opts.cwd);
5868
+ }
5869
+ const secrets = opts.fix && !opts.dryRun && fixOutcome.fixed.length > 0 ? runSecretsAudit({
5870
+ cwd: opts.cwd,
5871
+ rosterRoot: ROSTER_ROOT,
5872
+ schedules: schedulesForLeak
5873
+ }) : initialSecrets;
4743
5874
  const schedulingDrift = runSchedulingDriftAudit({
4744
5875
  cwd: opts.cwd,
4745
5876
  homeDir: home
4746
5877
  });
4747
5878
  const allOk = results.every((r) => r.ok) && workspaceFinal.ok && scheduling.ok && safety.ok && secrets.ok && schedulingDrift.ok;
5879
+ if (!opts.json && !opts.silent) {
5880
+ for (const line of renderText$1(results, summary, workarounds, effectiveScope)) console.log(line);
5881
+ for (const line of renderShadowSection(shadows)) console.log(line);
5882
+ for (const line of renderWorkspaceSection(workspaceFinal)) console.log(line);
5883
+ for (const line of renderSchedulingSection(scheduling)) console.log(line);
5884
+ for (const line of renderSchedulingDriftSection(schedulingDrift)) console.log(line);
5885
+ for (const line of renderStaleFiresSection(schedulingDrift.staleFires)) console.log(line);
5886
+ for (const line of renderSafetySection(safety)) console.log(line);
5887
+ for (const line of renderSecretsSection(secrets)) console.log(line);
5888
+ for (const line of renderFixSection(fixOutcome)) console.log(line);
5889
+ }
5890
+ let interactiveOutcome = null;
5891
+ if (opts.fix && secrets.agentEnvRedundancy.items.length > 0) {
5892
+ const isTTY = !opts.silent && !opts.json && (process.stdin.isTTY ?? false);
5893
+ interactiveOutcome = await confirmAndDeleteRedundantLines(secrets.agentEnvRedundancy.items, opts.cwd, {
5894
+ isTTY,
5895
+ stdin: process.stdin,
5896
+ stdout: process.stdout
5897
+ }, opts.dryRun);
5898
+ if (!opts.json && !opts.silent) for (const line of renderInteractiveFixSection(interactiveOutcome)) console.log(line);
5899
+ }
4748
5900
  if (opts.json) {
4749
5901
  const payload = {
4750
5902
  ok: allOk,
4751
5903
  rosterVersion: getPackageVersion(),
5904
+ scope: effectiveScope,
5905
+ shadows,
4752
5906
  tools: results,
4753
5907
  summary,
4754
5908
  workspace: workspaceFinal,
@@ -4757,27 +5911,18 @@ function executeDoctor(opts) {
4757
5911
  safety,
4758
5912
  secrets,
4759
5913
  scheduling_drift: schedulingDrift,
4760
- fix: fixOutcome
5914
+ fix: fixOutcome,
5915
+ interactive_fix: interactiveOutcome
4761
5916
  };
4762
5917
  console.log(JSON.stringify(payload, null, 2));
4763
- } else if (!opts.silent) {
4764
- for (const line of renderText$1(results, summary, workarounds)) console.log(line);
4765
- for (const line of renderWorkspaceSection(workspaceFinal)) console.log(line);
4766
- for (const line of renderSchedulingSection(scheduling)) console.log(line);
4767
- for (const line of renderSchedulingDriftSection(schedulingDrift)) console.log(line);
4768
- for (const line of renderStaleFiresSection(schedulingDrift.staleFires)) console.log(line);
4769
- for (const line of renderSafetySection(safety)) console.log(line);
4770
- for (const line of renderSecretsSection(secrets)) console.log(line);
4771
- for (const line of renderFixSection(fixOutcome)) console.log(line);
4772
- if (opts.dryRun) console.log(chalk.dim(opts.fix ? "--dry-run: nothing applied; lines above are what `--fix` would have done." : "--dry-run: read-only audit; pass `--fix` to preview repairs."));
4773
- }
5918
+ } else if (!opts.silent && opts.dryRun) console.log(chalk.dim(opts.fix ? "--dry-run: nothing applied; lines above are what `--fix` would have done." : "--dry-run: read-only audit; pass `--fix` to preview repairs."));
4774
5919
  return allOk ? 0 : 1;
4775
5920
  }
4776
5921
  //#endregion
4777
5922
  //#region src/lib/codex-install.ts
4778
5923
  const KEBAB_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
4779
5924
  function renderCodexHandoffDoc(args) {
4780
- const prompt = buildOrchestratorPrompt(args.agent, args.plan, args.project);
5925
+ const prompt = buildOrchestratorPrompt(args.agent, args.plan);
4781
5926
  return [
4782
5927
  `# Codex App Automation — ${args.name}`,
4783
5928
  "",
@@ -4865,7 +6010,6 @@ function installCodexSchedule(opts) {
4865
6010
  name: resolvedName,
4866
6011
  agent: opts.agent,
4867
6012
  plan: opts.plan,
4868
- project: opts.project,
4869
6013
  cron: opts.cron,
4870
6014
  tool: "codex",
4871
6015
  install_mode: opts.installMode,
@@ -4893,8 +6037,7 @@ function installCodexSchedule(opts) {
4893
6037
  cron: validatedEntry.cron,
4894
6038
  workspacePath,
4895
6039
  agent: validatedEntry.agent,
4896
- plan: validatedEntry.plan,
4897
- project: validatedEntry.project
6040
+ plan: validatedEntry.plan
4898
6041
  }) : null;
4899
6042
  let cronLine = null;
4900
6043
  if (opts.installMode === "via-cron") {
@@ -4903,7 +6046,7 @@ function installCodexSchedule(opts) {
4903
6046
  cron: validatedEntry.cron,
4904
6047
  workspacePath,
4905
6048
  codexBinaryPath,
4906
- prompt: buildOrchestratorPrompt(validatedEntry.agent, validatedEntry.plan, validatedEntry.project),
6049
+ prompt: buildOrchestratorPrompt(validatedEntry.agent, validatedEntry.plan),
4907
6050
  logPath,
4908
6051
  exitPath,
4909
6052
  ...validatedEntry.capture_events === true ? { eventsPath: eventsPathFor(workspacePath, resolvedName) } : {}
@@ -5037,7 +6180,7 @@ function buildListReport(cwd, now = /* @__PURE__ */ new Date()) {
5037
6180
  warnings
5038
6181
  };
5039
6182
  }
5040
- function tildify$3(path) {
6183
+ function tildify$2(path) {
5041
6184
  return path;
5042
6185
  }
5043
6186
  function fmtTs$1(d) {
@@ -5054,7 +6197,7 @@ function lastStatusCell(row) {
5054
6197
  function renderListText(report) {
5055
6198
  const lines = [""];
5056
6199
  lines.push(chalk.bold("roster schedule list"));
5057
- lines.push(chalk.dim(`cwd: ${tildify$3(report.cwd)}`));
6200
+ lines.push(chalk.dim(`cwd: ${tildify$2(report.cwd)}`));
5058
6201
  if (report.rows.length === 0) {
5059
6202
  lines.push("");
5060
6203
  lines.push(chalk.dim("(no schedules registered)"));
@@ -5716,7 +6859,7 @@ async function executeRun(opts) {
5716
6859
  name: opts.name,
5717
6860
  functionName: opts.functionName
5718
6861
  });
5719
- const prompt = buildOrchestratorPrompt(resolved.entry.agent, resolved.entry.plan, resolved.entry.project);
6862
+ const prompt = buildOrchestratorPrompt(resolved.entry.agent, resolved.entry.plan);
5720
6863
  if (resolved.entry.tool === "claude") {
5721
6864
  for (const line of renderClaudeHandoff(resolved.workspacePath, prompt, resolved.entry.name, opts.silent, opts.dryRun)) console.log(line);
5722
6865
  return {
@@ -5796,7 +6939,7 @@ async function executeRun(opts) {
5796
6939
  //#endregion
5797
6940
  //#region src/commands/schedule.ts
5798
6941
  const READONLY_DRYRUN_LINE = chalk.dim("--dry-run: read-only command; nothing would be written.");
5799
- function tildify$2(path) {
6942
+ function tildify$1(path) {
5800
6943
  const home = homedir();
5801
6944
  return path.startsWith(home) ? "~" + path.slice(home.length) : path;
5802
6945
  }
@@ -5806,7 +6949,7 @@ function countTotalErrors(report) {
5806
6949
  function renderText(report) {
5807
6950
  const lines = [""];
5808
6951
  lines.push(chalk.bold("roster schedule validate"));
5809
- lines.push(chalk.dim(`cwd: ${tildify$2(report.cwd)}`));
6952
+ lines.push(chalk.dim(`cwd: ${tildify$1(report.cwd)}`));
5810
6953
  if (report.files.length === 0) {
5811
6954
  lines.push("");
5812
6955
  lines.push(chalk.dim("No roster/<function>/schedules.yaml files found."));
@@ -5893,7 +7036,6 @@ function executeScheduleInstall(opts) {
5893
7036
  functionName: opts.functionName,
5894
7037
  agent: opts.agent,
5895
7038
  plan: opts.plan,
5896
- project: opts.project,
5897
7039
  cron: opts.cron,
5898
7040
  name: opts.name,
5899
7041
  dryRun: opts.dryRun
@@ -5917,7 +7059,6 @@ function executeScheduleInstall(opts) {
5917
7059
  functionName: opts.functionName,
5918
7060
  agent: opts.agent,
5919
7061
  plan: opts.plan,
5920
- project: opts.project,
5921
7062
  cron: opts.cron,
5922
7063
  name: opts.name,
5923
7064
  installMode,
@@ -6479,14 +7620,14 @@ function installHook(host) {
6479
7620
  }
6480
7621
  //#endregion
6481
7622
  //#region src/commands/hooks.ts
6482
- function tildify$1(path) {
7623
+ function tildify(path) {
6483
7624
  const home = homedir();
6484
7625
  return path.startsWith(home) ? "~" + path.slice(home.length) : path;
6485
7626
  }
6486
7627
  function summarize(result) {
6487
7628
  const hostLabel = result.host === "claude" ? "Claude Code" : "Codex CLI";
6488
- if (result.status === "installed") return `${chalk.green("✓")} ${chalk.bold(hostLabel)} — SessionStart hook → ${tildify$1(result.configFile)}`;
6489
- if (result.status === "already-present") return `${chalk.dim("·")} ${chalk.bold(hostLabel)} — SessionStart hook already installed (${tildify$1(result.configFile)})`;
7629
+ if (result.status === "installed") return `${chalk.green("✓")} ${chalk.bold(hostLabel)} — SessionStart hook → ${tildify(result.configFile)}`;
7630
+ if (result.status === "already-present") return `${chalk.dim("·")} ${chalk.bold(hostLabel)} — SessionStart hook already installed (${tildify(result.configFile)})`;
6490
7631
  return `${chalk.yellow("⚠")} ${chalk.bold(hostLabel)} — skipped: ${result.reason ?? "host not detected"}`;
6491
7632
  }
6492
7633
  function hostsForTarget(target) {
@@ -7650,9 +8791,12 @@ const SUBCOMMANDS = new Set([
7650
8791
  "migrate",
7651
8792
  "pending"
7652
8793
  ]);
7653
- function tildify(path) {
8794
+ function displayPath(path, cwd) {
7654
8795
  const home = homedir();
7655
- return path.startsWith(home) ? "~" + path.slice(home.length) : path;
8796
+ if (path.startsWith(home)) return "~" + path.slice(home.length);
8797
+ const rel = relative(cwd, path);
8798
+ if (!rel.startsWith("..") && !rel.startsWith("/")) return "./" + rel;
8799
+ return path;
7656
8800
  }
7657
8801
  function printBanner(version) {
7658
8802
  console.log();
@@ -7685,15 +8829,16 @@ function printHelp(version) {
7685
8829
  ` -v, --version ${chalk.dim("Print version and exit")}`,
7686
8830
  ` --silent ${chalk.dim("Suppress non-error output (install)")}`,
7687
8831
  ` --verbose ${chalk.dim("Log each file path written (install)")}`,
7688
- ` --all ${chalk.dim("Install to every detected tool (install)")}`,
7689
- ` --tool <name> ${chalk.dim("Install to a single tool: claude | codex | gemini (install)")}`,
8832
+ ` --all ${chalk.dim("Install to every detected tool (alias of --tool all) (install)")}`,
8833
+ ` --tool <name[,name...]> ${chalk.dim("Install to one or more tools: claude | codex | gemini (install)")}`,
8834
+ ` --scope <project|user> ${chalk.dim("Install at workspace-local or home-dir scope (install)")}`,
8835
+ ` --yes, -y ${chalk.dim("Skip prompts; use safe defaults (install)")}`,
7690
8836
  ` --tool <name> ${chalk.dim("Required scheduler tool: claude | codex (schedule install)")}`,
7691
8837
  ` --migrate ${chalk.dim("Upgrade pre-CONTEXT.md workspace, preserving CLAUDE.md content (init)")}`,
7692
8838
  ` --json ${chalk.dim("Emit machine-readable JSON (doctor, schedule validate)")}`,
7693
8839
  ` --fix ${chalk.dim("Auto-fix broken symlinks + .env permissions (doctor)")}`,
7694
8840
  ` --cwd <dir> ${chalk.dim("Run schedule validate against a different cwd")}`,
7695
8841
  ` --dest <dir> ${chalk.dim("Destination workspace for migrate (default: cwd)")}`,
7696
- ` --project <name> ${chalk.dim("Project slug for schedule install (default: _demo)")}`,
7697
8842
  ` --dry-run ${chalk.dim("Print plan without writes (schedule *, doctor, migrate)")}`,
7698
8843
  ` --force-resync ${chalk.dim("Re-copy source files that changed since last migration (migrate)")}`,
7699
8844
  ` --debug ${chalk.dim("Print full stack trace on error (global)")}`,
@@ -7723,23 +8868,28 @@ function toolHints(tools) {
7723
8868
  installLink: t.installLink
7724
8869
  }));
7725
8870
  }
7726
- function summarizeInstall(tool, result) {
7727
- const skillsLine = `${result.skillsCount} skills → ${tildify(result.skillsTarget)}`;
7728
- const agentsLine = result.agentsTarget ? `${result.agentsCount} agents → ${tildify(result.agentsTarget)}` : `${result.agentsCount} agents → (n/a)`;
8871
+ function summarizeInstall(tool, result, cwd) {
8872
+ const skillsLine = `${result.skillsCount} skills → ${displayPath(result.skillsTarget, cwd)}`;
8873
+ const agentsLine = result.agentsTarget ? `${result.agentsCount} agents → ${displayPath(result.agentsTarget, cwd)}` : `${result.agentsCount} agents → (n/a)`;
7729
8874
  return `${chalk.green("✓")} ${chalk.bold(tool.name)} — ${skillsLine}, ${agentsLine}`;
7730
8875
  }
7731
- async function promptForTools(detected) {
7732
- if (detected.length === 1) return detected;
8876
+ async function promptForTools(detected, undetected) {
8877
+ if (detected.length === 1 && undetected.length === 0) return detected;
7733
8878
  const { checkbox, confirm } = await import("@inquirer/prompts");
8879
+ const choices = [...detected.map((t) => ({
8880
+ name: t.name,
8881
+ value: t.key,
8882
+ checked: true
8883
+ })), ...undetected.map((t) => ({
8884
+ name: t.name,
8885
+ value: t.key,
8886
+ disabled: "(not detected)"
8887
+ }))];
7734
8888
  let selectedKeys;
7735
8889
  try {
7736
8890
  selectedKeys = await checkbox({
7737
8891
  message: "Install roster into which AI tools?",
7738
- choices: detected.map((t) => ({
7739
- name: t.name,
7740
- value: t.key,
7741
- checked: true
7742
- }))
8892
+ choices
7743
8893
  });
7744
8894
  } catch {
7745
8895
  return null;
@@ -7755,10 +8905,31 @@ async function promptForTools(detected) {
7755
8905
  return null;
7756
8906
  }
7757
8907
  if (exitAnyway) return null;
7758
- return promptForTools(detected);
8908
+ return promptForTools(detected, undetected);
7759
8909
  }
7760
8910
  return detected.filter((t) => selectedKeys.includes(t.key));
7761
8911
  }
8912
+ async function promptForScope(workspaceExists, cwd) {
8913
+ const { select } = await import("@inquirer/prompts");
8914
+ const projectHint = workspaceExists ? `workspace-local — skills land in ${displayPath(join(cwd, ".<tool>"), cwd)}/skills/` : "workspace-local — REQUIRES roster init (config/project.yaml not found here)";
8915
+ try {
8916
+ return await select({
8917
+ message: "Install at which scope?",
8918
+ choices: [{
8919
+ name: "project",
8920
+ value: "project",
8921
+ description: projectHint
8922
+ }, {
8923
+ name: "user",
8924
+ value: "user",
8925
+ description: "home directory — skills land in ~/.<tool>/, visible to every Claude Code project on this machine"
8926
+ }],
8927
+ default: workspaceExists ? "project" : "user"
8928
+ });
8929
+ } catch {
8930
+ return null;
8931
+ }
8932
+ }
7762
8933
  async function runInstall(args) {
7763
8934
  const parsed = parseInstallArgs(args);
7764
8935
  if (parsed.kind === "err") throw new RosterError({
@@ -7767,39 +8938,58 @@ async function runInstall(args) {
7767
8938
  remedy: ` Run ${chalk.bold("roster --help")} for usage.`,
7768
8939
  exitCode: 1
7769
8940
  });
7770
- const { silent, verbose, target } = parsed;
8941
+ const { silent, verbose, yes, scope: requestedScope, target } = parsed;
7771
8942
  const version = getPackageVersion();
7772
8943
  if (!silent) printBanner(version);
8944
+ const cwd = process.cwd();
8945
+ const isTTY = process.stdin.isTTY === true;
8946
+ const workspaceExists = detectWorkspace(cwd);
8947
+ const nonInteractive = yes || !isTTY;
7773
8948
  const detected = detectTools();
7774
- if (detected.length === 0) throw noToolsError(toolHints(allTools()));
7775
8949
  let targetTools;
7776
- if (target.mode === "all") targetTools = detected;
7777
- else if (target.mode === "tool") {
7778
- const match = detected.find((t) => t.key === target.key);
7779
- if (!match) throw new RosterError({
7780
- header: `${chalk.red.bold("roster:")} ${target.key} not detected on this machine`,
7781
- body: ` --tool ${target.key} was requested, but no ${target.key} install was found.`,
7782
- remedy: ` Install it first, or omit ${chalk.bold("--tool")} to install to all detected tools.`,
7783
- exitCode: 3
7784
- });
7785
- targetTools = [match];
7786
- } else targetTools = await promptForTools(detected);
7787
- if (targetTools === null) throw userCancelledInstall();
8950
+ if (target.mode === "all") {
8951
+ if (detected.length === 0) throw noToolsError(toolHints(allTools()));
8952
+ targetTools = detected;
8953
+ } else if (target.mode === "tools") {
8954
+ const detectedKeys = detected.map((t) => t.key);
8955
+ if (target.keys.filter((k) => !detectedKeys.includes(k)).length > 0) throw toolsNotDetectedError(target.keys, detectedKeys);
8956
+ targetTools = detected.filter((t) => target.keys.includes(t.key));
8957
+ } else {
8958
+ if (detected.length === 0) throw noToolsError(toolHints(allTools()));
8959
+ if (nonInteractive) targetTools = detected;
8960
+ else {
8961
+ const picked = await promptForTools(detected, allTools().filter((t) => !detected.some((d) => d.key === t.key)));
8962
+ if (picked === null) throw userCancelledInstall();
8963
+ targetTools = picked;
8964
+ }
8965
+ }
8966
+ let scope;
8967
+ if (requestedScope !== null) scope = requestedScope;
8968
+ else if (nonInteractive) scope = defaultScopeForContext(workspaceExists);
8969
+ else {
8970
+ const picked = await promptForScope(workspaceExists, cwd);
8971
+ if (picked === null) throw userCancelledInstall();
8972
+ scope = picked;
8973
+ }
8974
+ if (scope === "project" && !workspaceExists) throw workspaceRequiredError(cwd);
7788
8975
  const skillsSrc = join(ROSTER_ROOT, "skills");
7789
8976
  const agentsSrc = join(ROSTER_ROOT, "agents");
7790
- const confirmFn = target.mode !== "interactive" ? async () => false : void 0;
8977
+ const confirmFn = nonInteractive ? async () => false : void 0;
7791
8978
  for (const tool of targetTools) {
7792
- const result = await installToTool(tool, {
8979
+ const scopedTool = scope === "project" ? toolForScope(tool, "project", cwd) : tool;
8980
+ const result = await installToTool(scopedTool, {
7793
8981
  skills: skillsSrc,
7794
8982
  agents: agentsSrc,
7795
8983
  silent: !verbose,
8984
+ scope,
7796
8985
  ...confirmFn ? { confirm: confirmFn } : {}
7797
8986
  });
7798
- if (!silent) console.log(summarizeInstall(tool, result));
8987
+ if (!silent) console.log(summarizeInstall(scopedTool, result, cwd));
7799
8988
  }
7800
8989
  if (!silent) {
7801
8990
  console.log();
7802
- console.log(`${chalk.dim("Next: ")}${chalk.bold("roster init")}${chalk.dim(" to scaffold a workspace.")}`);
8991
+ if (scope === "project") console.log(`${chalk.dim("Next: ")}${chalk.bold("open Claude Code (or your AI tool) in this directory")}${chalk.dim(" skills are workspace-local.")}`);
8992
+ else console.log(`${chalk.dim("Next: ")}${chalk.bold("roster init")}${chalk.dim(" to scaffold a workspace, then re-run install at project scope.")}`);
7803
8993
  }
7804
8994
  return 0;
7805
8995
  }
@@ -7839,7 +9029,6 @@ async function runSchedule(args) {
7839
9029
  functionName: parsed.functionName,
7840
9030
  agent: parsed.agent,
7841
9031
  plan: parsed.plan,
7842
- project: parsed.project,
7843
9032
  cron: parsed.cron,
7844
9033
  tool: parsed.tool,
7845
9034
  via: parsed.via,
@@ -7961,7 +9150,7 @@ async function runHooks(args) {
7961
9150
  exitCode: 1
7962
9151
  });
7963
9152
  }
7964
- function runDoctor(args) {
9153
+ async function runDoctor(args) {
7965
9154
  const parsed = parseDoctorArgs(args);
7966
9155
  if (parsed.kind === "err") throw new RosterError({
7967
9156
  header: `${chalk.red.bold("roster:")} ${parsed.message}`,
@@ -7969,12 +9158,13 @@ function runDoctor(args) {
7969
9158
  remedy: ` Run ${chalk.bold("roster --help")} for usage.`,
7970
9159
  exitCode: 1
7971
9160
  });
7972
- const code = executeDoctor({
9161
+ const code = await executeDoctor({
7973
9162
  json: parsed.json,
7974
9163
  silent: parsed.silent,
7975
9164
  fix: parsed.fix,
7976
9165
  dryRun: parsed.dryRun,
7977
- cwd: process.cwd()
9166
+ cwd: process.cwd(),
9167
+ scope: parsed.scope
7978
9168
  });
7979
9169
  if (code === 3 && !parsed.json) throw noToolsError(toolHints(allTools()));
7980
9170
  return code;
@@ -8000,7 +9190,7 @@ async function main() {
8000
9190
  if (isSubcommand(first)) {
8001
9191
  if (first === "install") return runInstall(rest);
8002
9192
  if (first === "init") return await runInit(rest);
8003
- if (first === "doctor") return runDoctor(rest);
9193
+ if (first === "doctor") return await runDoctor(rest);
8004
9194
  if (first === "schedule") return await runSchedule(rest);
8005
9195
  if (first === "review") return await runReview(rest);
8006
9196
  if (first === "hooks") return await runHooks(rest);