@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.
- package/README.md +79 -219
- package/agents/lesson-drafter.md +3 -8
- package/agents/pattern-detector.md +0 -1
- package/bin/roster.js +1407 -217
- package/package.json +2 -3
- package/skills/chief-of-staff/SKILL.md +62 -78
- package/skills/dreamer/SKILL.md +8 -7
- package/skills/roster-orchestrator/SKILL.md +53 -25
- package/templates/CLAUDE.project.template.md +1 -1
- package/templates/CONTEXT.template.md +2 -2
- package/templates/gitignore-defaults.txt +2 -0
- package/templates/scaffold/chief-of-staff/README.md +16 -24
- package/templates/scaffold/chief-of-staff/agent.md +22 -32
- package/templates/scaffold/chief-of-staff/plans/audit-agent.yaml +4 -4
- package/templates/scaffold/chief-of-staff/plans/audit-repo.yaml +5 -4
- package/templates/scaffold/chief-of-staff/plans/create-agent.yaml +5 -34
- package/templates/scaffold/config/project.yaml.template +10 -0
- package/templates/scaffold/conventions.md +159 -171
- package/templates/scaffold/dreamer/README.md +2 -2
- package/templates/scaffold/dreamer/agent.md +0 -1
- package/templates/scaffold/dreamer/plans/nightly-reflection.yaml +23 -37
- package/templates/scaffold/dreamer/subagents/lesson-drafter.md +2 -7
- package/templates/scaffold/{projects/_demo/guidelines → guidelines}/asset-links.md +4 -0
- package/templates/scaffold/{projects/_demo/guidelines → guidelines}/brand-book.md +4 -0
- package/templates/scaffold/{projects/_demo/guidelines → guidelines}/messaging.md +4 -0
- package/templates/scaffold/{projects/_demo/guidelines → guidelines}/voice.md +4 -0
- package/templates/scaffold/scripts/audit-agent.sh +74 -47
- package/templates/scaffold/scripts/audit-repo.sh +27 -49
- package/templates/scaffold/scripts/create-function.sh +1 -1
- package/templates/scaffold/scripts/lib/README.md +1 -1
- package/templates/scaffold/scripts/lib/bindings-prompt.sh +43 -124
- package/templates/scaffold/scripts/new-agent.sh +99 -91
- package/templates/scaffold/scripts/rename-agent.sh +91 -0
- package/templates/scaffold/scripts/save-state.sh +32 -0
- package/agents/critic.md +0 -74
- package/agents/enricher.md +0 -56
- package/agents/promotion-arbiter.md +0 -71
- package/agents/prospector.md +0 -51
- package/agents/writer.md +0 -58
- package/skills/sdr/SKILL.md +0 -147
- package/templates/scaffold/chief-of-staff/plans/add-agent-to-project.yaml +0 -45
- package/templates/scaffold/chief-of-staff/plans/archive-project.yaml +0 -51
- package/templates/scaffold/chief-of-staff/plans/audit-project.yaml +0 -34
- package/templates/scaffold/chief-of-staff/plans/create-project.yaml +0 -65
- package/templates/scaffold/chief-of-staff/plans/remove-agent-from-project.yaml +0 -50
- package/templates/scaffold/chief-of-staff/plans/rename-project.yaml +0 -62
- package/templates/scaffold/chief-of-staff/plans/unarchive-project.yaml +0 -41
- package/templates/scaffold/dreamer/subagents/promotion-arbiter.md +0 -64
- package/templates/scaffold/gtm/sdr/.claude/settings.json +0 -3
- package/templates/scaffold/gtm/sdr/.mcp.json +0 -21
- package/templates/scaffold/gtm/sdr/README.md +0 -41
- package/templates/scaffold/gtm/sdr/agent.md +0 -136
- package/templates/scaffold/gtm/sdr/plans/cold-outreach.yaml +0 -92
- package/templates/scaffold/gtm/sdr/projects/_demo/asset-references.md +0 -7
- package/templates/scaffold/gtm/sdr/projects/_demo/config/default.yaml +0 -69
- package/templates/scaffold/gtm/sdr/projects/_demo/log/feedback/.gitkeep +0 -0
- package/templates/scaffold/gtm/sdr/projects/_demo/log/runs/.gitkeep +0 -0
- package/templates/scaffold/gtm/sdr/projects/_demo/playbook/.gitkeep +0 -0
- package/templates/scaffold/gtm/sdr/subagents/critic.md +0 -67
- package/templates/scaffold/gtm/sdr/subagents/enricher.md +0 -49
- package/templates/scaffold/gtm/sdr/subagents/prospector.md +0 -44
- package/templates/scaffold/gtm/sdr/subagents/writer.md +0 -51
- package/templates/scaffold/projects/_demo/CLAUDE.md +0 -35
- package/templates/scaffold/projects/_demo/README.md +0 -16
- package/templates/scaffold/projects/_demo/assets/.gitkeep +0 -0
- package/templates/scaffold/projects/_demo/config/default.yaml +0 -28
- package/templates/scaffold/projects/_demo/state.md +0 -11
- package/templates/scaffold/scripts/archive-project.sh +0 -98
- package/templates/scaffold/scripts/audit-project.sh +0 -361
- package/templates/scaffold/scripts/new-agent-instance.sh +0 -114
- package/templates/scaffold/scripts/new-project.sh +0 -125
- package/templates/scaffold/scripts/remove-agent-from-project.sh +0 -67
- package/templates/scaffold/scripts/rename-project.sh +0 -118
- package/templates/scaffold/scripts/unarchive-project.sh +0 -115
- /package/templates/scaffold/gtm/{sdr/playbook/.gitkeep → .gitkeep} +0 -0
- /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
|
|
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
|
-
|
|
691
|
+
const parsed = parseToolValue(toolValue);
|
|
692
|
+
if (!parsed.ok) return {
|
|
598
693
|
kind: "err",
|
|
599
|
-
message:
|
|
694
|
+
message: parsed.message
|
|
600
695
|
};
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
verbose,
|
|
605
|
-
target: {
|
|
606
|
-
mode: "tool",
|
|
607
|
-
key: toolValue
|
|
608
|
-
}
|
|
696
|
+
target = {
|
|
697
|
+
mode: "tools",
|
|
698
|
+
keys: parsed.keys
|
|
609
699
|
};
|
|
610
|
-
}
|
|
611
|
-
|
|
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
|
-
|
|
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
|
-
|
|
619
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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, {
|
|
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
|
|
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
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
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
|
-
|
|
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
|
|
3202
|
-
let
|
|
3812
|
+
const topDir = join(cwd, top);
|
|
3813
|
+
let topStat;
|
|
3203
3814
|
try {
|
|
3204
|
-
|
|
3815
|
+
topStat = statSync(topDir);
|
|
3205
3816
|
} catch {
|
|
3206
3817
|
continue;
|
|
3207
3818
|
}
|
|
3208
|
-
if (!
|
|
3209
|
-
|
|
3819
|
+
if (!topStat.isDirectory()) continue;
|
|
3820
|
+
out.push(topDir);
|
|
3821
|
+
let children;
|
|
3210
3822
|
try {
|
|
3211
|
-
|
|
3823
|
+
children = readdirSync(topDir);
|
|
3212
3824
|
} catch {
|
|
3213
3825
|
continue;
|
|
3214
3826
|
}
|
|
3215
|
-
for (const
|
|
3216
|
-
if (
|
|
3217
|
-
const
|
|
3218
|
-
let
|
|
3827
|
+
for (const child of children) {
|
|
3828
|
+
if (child.startsWith(".")) continue;
|
|
3829
|
+
const childDir = join(topDir, child);
|
|
3830
|
+
let childStat;
|
|
3219
3831
|
try {
|
|
3220
|
-
|
|
3832
|
+
childStat = statSync(childDir);
|
|
3221
3833
|
} catch {
|
|
3222
3834
|
continue;
|
|
3223
3835
|
}
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
3899
|
-
return `Use the roster-orchestrator skill to run plan ${plan} for agent ${agent}
|
|
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
|
|
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
|
|
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
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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$
|
|
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
|
|
4690
|
-
|
|
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
|
|
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
|
|
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
|
|
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$
|
|
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$
|
|
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
|
|
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$
|
|
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$
|
|
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
|
|
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
|
|
6489
|
-
if (result.status === "already-present") return `${chalk.dim("·")} ${chalk.bold(hostLabel)} — SessionStart hook already installed (${tildify
|
|
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
|
|
8794
|
+
function displayPath(path, cwd) {
|
|
7654
8795
|
const home = homedir();
|
|
7655
|
-
|
|
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>
|
|
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 → ${
|
|
7728
|
-
const agentsLine = result.agentsTarget ? `${result.agentsCount} agents → ${
|
|
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
|
|
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")
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
targetTools =
|
|
7786
|
-
|
|
7787
|
-
|
|
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 =
|
|
8977
|
+
const confirmFn = nonInteractive ? async () => false : void 0;
|
|
7791
8978
|
for (const tool of targetTools) {
|
|
7792
|
-
const
|
|
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(
|
|
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("
|
|
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);
|