facult 2.1.2 → 2.3.0
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 +59 -0
- package/bin/fclt.cjs +55 -3
- package/package.json +1 -1
- package/src/ai.ts +13 -2
- package/src/index.ts +1 -1
- package/src/remote.ts +942 -4
package/README.md
CHANGED
|
@@ -683,6 +683,7 @@ fclt templates init mcp <name>
|
|
|
683
683
|
fclt templates init snippet <marker>
|
|
684
684
|
fclt templates init agents
|
|
685
685
|
fclt templates init claude
|
|
686
|
+
fclt templates init automation <template-id> --scope global|project|wide [--name <name>] [--project-root <path>] [--cwds <path1,path2>] [--rrule <RRULE>] [--status PAUSED|ACTIVE]
|
|
686
687
|
|
|
687
688
|
fclt snippets list
|
|
688
689
|
fclt snippets show <marker>
|
|
@@ -691,6 +692,64 @@ fclt snippets edit <marker>
|
|
|
691
692
|
fclt snippets sync [--dry-run] [file...]
|
|
692
693
|
```
|
|
693
694
|
|
|
695
|
+
### Codex automations
|
|
696
|
+
|
|
697
|
+
`templates init automation` can scaffold three Codex automation forms:
|
|
698
|
+
|
|
699
|
+
- `--scope project` (single repo): set `--project-root` (or infer from current working directory)
|
|
700
|
+
- `--scope wide|global` (multiple repos): set `--cwds` explicitly; if omitted, created automation has no `cwds` by default.
|
|
701
|
+
- If you run it interactively without `--scope`, `fclt` prompts for scope and, where possible, known workspaces (git worktrees, configured scan roots, and existing Codex automation paths).
|
|
702
|
+
- Built-in automation templates are opinionated: they reference the global Codex operating model, point at relevant Codex skills, and tell Codex when to use focused subagents for bounded review work.
|
|
703
|
+
|
|
704
|
+
Recommended topology:
|
|
705
|
+
|
|
706
|
+
- Use `learning-review --scope project` for repo-local writeback and evolution. This keeps review state, verification, and follow-up scoped to the repo that actually produced the evidence.
|
|
707
|
+
- Use `evolution-review` on a slower cadence, usually weekly, to triage open proposals and proposal-worthy clusters and suggest the next operator action (`draft`, `review`, `accept`, `reject`, `promote`, or `apply`).
|
|
708
|
+
- Use a separate wide/global automation only for cross-repo or shared-surface review, such as global doctrine, shared skills, or repeated tool/agent patterns across repos.
|
|
709
|
+
- If you do use a wide learning review, keep the `cwds` list intentionally small and related. The prompt is designed to partition by cwd first, not to blur unrelated repos together.
|
|
710
|
+
- A practical default is daily `learning-review` plus weekly `evolution-review`. The first finds and records durable signal; the second keeps proposal review from stalling.
|
|
711
|
+
|
|
712
|
+
Files are written to:
|
|
713
|
+
|
|
714
|
+
- `~/.codex/automations/<name>/automation.toml`
|
|
715
|
+
- `~/.codex/automations/<name>/memory.md`
|
|
716
|
+
|
|
717
|
+
Example project automation:
|
|
718
|
+
|
|
719
|
+
```bash
|
|
720
|
+
fclt templates init automation tool-call-audit \
|
|
721
|
+
--scope project \
|
|
722
|
+
--project-root /path/to/repo \
|
|
723
|
+
--name project-tool-audit \
|
|
724
|
+
--status ACTIVE
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
Example global automation:
|
|
728
|
+
|
|
729
|
+
```bash
|
|
730
|
+
fclt templates init automation learning-review \
|
|
731
|
+
--scope wide \
|
|
732
|
+
--cwds /path/to/repo-a,/path/to/repo-b \
|
|
733
|
+
--status PAUSED
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
Example weekly evolution automation:
|
|
737
|
+
|
|
738
|
+
```bash
|
|
739
|
+
fclt templates init automation evolution-review \
|
|
740
|
+
--scope wide \
|
|
741
|
+
--cwds /path/to/repo-a,/path/to/repo-b \
|
|
742
|
+
--name weekly-evolution-review \
|
|
743
|
+
--status PAUSED
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Interactive prompt example:
|
|
747
|
+
|
|
748
|
+
```bash
|
|
749
|
+
fclt templates init automation learning-review
|
|
750
|
+
# prompts for scope, then lets you select known workspaces or add custom paths.
|
|
751
|
+
```
|
|
752
|
+
|
|
694
753
|
For full flags and exact usage:
|
|
695
754
|
```bash
|
|
696
755
|
fclt --help
|
package/bin/fclt.cjs
CHANGED
|
@@ -38,15 +38,16 @@ async function main() {
|
|
|
38
38
|
);
|
|
39
39
|
const binaryName = resolved.platform === "windows" ? "fclt.exe" : "fclt";
|
|
40
40
|
const binaryPath = path.join(installDir, binaryName);
|
|
41
|
+
const sourceEntry = path.join(__dirname, "..", "src", "index.ts");
|
|
41
42
|
|
|
42
43
|
if (!(await fileExists(binaryPath))) {
|
|
43
44
|
const tag = `v${version}`;
|
|
44
45
|
const assetName = `${PACKAGE_NAME}-${version}-${resolved.platform}-${resolved.arch}${resolved.ext}`;
|
|
45
46
|
const url = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${tag}/${assetName}`;
|
|
46
|
-
|
|
47
|
-
await fsp.mkdir(installDir, { recursive: true });
|
|
48
47
|
const tmpPath = `${binaryPath}.tmp-${Date.now()}`;
|
|
48
|
+
|
|
49
49
|
try {
|
|
50
|
+
await fsp.mkdir(installDir, { recursive: true });
|
|
50
51
|
await downloadWithRetry(url, tmpPath, {
|
|
51
52
|
attempts: DOWNLOAD_RETRIES,
|
|
52
53
|
delayMs: DOWNLOAD_RETRY_DELAY_MS,
|
|
@@ -57,6 +58,14 @@ async function main() {
|
|
|
57
58
|
await fsp.rename(tmpPath, binaryPath);
|
|
58
59
|
} catch (error) {
|
|
59
60
|
await safeUnlink(tmpPath);
|
|
61
|
+
if (await canUseSourceFallback(sourceEntry)) {
|
|
62
|
+
return runSourceFallback({
|
|
63
|
+
sourceEntry,
|
|
64
|
+
version,
|
|
65
|
+
packageManager: detectPackageManager(),
|
|
66
|
+
reason: error,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
60
69
|
const message =
|
|
61
70
|
error instanceof Error ? error.message : String(error ?? "");
|
|
62
71
|
console.error(
|
|
@@ -75,7 +84,7 @@ async function main() {
|
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
const packageManager = detectPackageManager();
|
|
78
|
-
await
|
|
87
|
+
await bestEffortWriteInstallState({
|
|
79
88
|
method: "npm-binary-cache",
|
|
80
89
|
version,
|
|
81
90
|
binaryPath,
|
|
@@ -100,6 +109,41 @@ async function main() {
|
|
|
100
109
|
process.exit(1);
|
|
101
110
|
}
|
|
102
111
|
|
|
112
|
+
async function canUseSourceFallback(sourceEntry) {
|
|
113
|
+
if (!(await fileExists(sourceEntry))) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const result = spawnSync("bun", ["--version"], {
|
|
117
|
+
stdio: "ignore",
|
|
118
|
+
env: process.env,
|
|
119
|
+
});
|
|
120
|
+
return result.status === 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function runSourceFallback({ sourceEntry, version, packageManager, reason }) {
|
|
124
|
+
const message =
|
|
125
|
+
reason instanceof Error ? reason.message : String(reason ?? "");
|
|
126
|
+
console.error(
|
|
127
|
+
`fclt: cached runtime unavailable, falling back to Bun source entry (${message})`
|
|
128
|
+
);
|
|
129
|
+
const args = process.argv.slice(2);
|
|
130
|
+
const result = spawnSync("bun", [sourceEntry, ...args], {
|
|
131
|
+
stdio: "inherit",
|
|
132
|
+
env: {
|
|
133
|
+
...process.env,
|
|
134
|
+
FACULT_INSTALL_METHOD: "npm-source-fallback",
|
|
135
|
+
FACULT_NPM_PACKAGE_VERSION: version,
|
|
136
|
+
FACULT_SOURCE_ENTRY: sourceEntry,
|
|
137
|
+
FACULT_INSTALL_PM: packageManager,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (typeof result.status === "number") {
|
|
142
|
+
process.exit(result.status);
|
|
143
|
+
}
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
103
147
|
function resolveTarget() {
|
|
104
148
|
const platform = process.platform;
|
|
105
149
|
const arch = process.arch;
|
|
@@ -257,6 +301,14 @@ async function writeInstallState(state) {
|
|
|
257
301
|
await fsp.rename(`${installStatePath}.tmp`, installStatePath);
|
|
258
302
|
}
|
|
259
303
|
|
|
304
|
+
async function bestEffortWriteInstallState(state) {
|
|
305
|
+
try {
|
|
306
|
+
await writeInstallState(state);
|
|
307
|
+
} catch {
|
|
308
|
+
// Install state is useful metadata, but it should not block normal CLI usage.
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
260
312
|
main().catch((error) => {
|
|
261
313
|
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
262
314
|
console.error(message);
|
package/package.json
CHANGED
package/src/ai.ts
CHANGED
|
@@ -171,6 +171,7 @@ interface AddWritebackArgs {
|
|
|
171
171
|
summary: string;
|
|
172
172
|
asset?: string;
|
|
173
173
|
evidence?: WritebackEvidence[];
|
|
174
|
+
allowEmptyEvidence?: boolean;
|
|
174
175
|
confidence?: ConfidenceLevel;
|
|
175
176
|
source?: string;
|
|
176
177
|
suggestedDestination?: string;
|
|
@@ -348,6 +349,12 @@ export async function addWriteback(
|
|
|
348
349
|
args: AddWritebackArgs
|
|
349
350
|
): Promise<AiWritebackRecord> {
|
|
350
351
|
const homeDir = args.homeDir ?? process.env.HOME ?? "";
|
|
352
|
+
const evidence = args.evidence ?? [];
|
|
353
|
+
if (evidence.length === 0 && !args.allowEmptyEvidence) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
"writeback add requires at least one evidence item; pass --evidence <type:ref> or use --allow-empty-evidence for scratch/demo notes"
|
|
356
|
+
);
|
|
357
|
+
}
|
|
351
358
|
const scopeContext = resolveScopeContext(args.rootDir, homeDir);
|
|
352
359
|
const latest = await latestWritebackMap({
|
|
353
360
|
homeDir,
|
|
@@ -368,7 +375,7 @@ export async function addWriteback(
|
|
|
368
375
|
projectRoot: scopeContext.projectRoot,
|
|
369
376
|
kind: args.kind.trim(),
|
|
370
377
|
summary: args.summary.trim(),
|
|
371
|
-
evidence
|
|
378
|
+
evidence,
|
|
372
379
|
confidence: args.confidence ?? "medium",
|
|
373
380
|
source: args.source ?? "facult:manual",
|
|
374
381
|
assetRef: asset.assetRef,
|
|
@@ -669,6 +676,9 @@ export async function proposeEvolution(args: {
|
|
|
669
676
|
if (entry.status === "dismissed" || entry.status === "superseded") {
|
|
670
677
|
return false;
|
|
671
678
|
}
|
|
679
|
+
if (entry.evidence.length === 0) {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
672
682
|
if (filterAsset) {
|
|
673
683
|
return (
|
|
674
684
|
entry.assetId === filterAsset.assetId ||
|
|
@@ -1405,7 +1415,7 @@ function writebackHelp(): string {
|
|
|
1405
1415
|
return `fclt ai writeback
|
|
1406
1416
|
|
|
1407
1417
|
Usage:
|
|
1408
|
-
fclt ai writeback add --kind <kind> --summary <text> [--asset <selector>] [--tag <tag>] [--evidence <type:ref>]
|
|
1418
|
+
fclt ai writeback add --kind <kind> --summary <text> [--asset <selector>] [--tag <tag>] [--evidence <type:ref>] [--allow-empty-evidence]
|
|
1409
1419
|
fclt ai writeback list [--json]
|
|
1410
1420
|
fclt ai writeback show <id> [--json]
|
|
1411
1421
|
fclt ai writeback group --by <asset|kind|domain> [--json]
|
|
@@ -1521,6 +1531,7 @@ async function writebackCommand(argv: string[]) {
|
|
|
1521
1531
|
kind,
|
|
1522
1532
|
summary,
|
|
1523
1533
|
asset: parseStringFlag(parsed.argv, "--asset"),
|
|
1534
|
+
allowEmptyEvidence: parsed.argv.includes("--allow-empty-evidence"),
|
|
1524
1535
|
confidence:
|
|
1525
1536
|
(parseStringFlag(parsed.argv, "--confidence") as
|
|
1526
1537
|
| ConfidenceLevel
|
package/src/index.ts
CHANGED
|
@@ -163,7 +163,7 @@ Commands:
|
|
|
163
163
|
self-update Update fclt itself based on install method
|
|
164
164
|
verify-source Verify source trust and manifest integrity/signature status
|
|
165
165
|
sources Manage source trust policy for remote indices
|
|
166
|
-
templates Scaffold DX-first templates (skills/instructions/MCP/snippets)
|
|
166
|
+
templates Scaffold DX-first templates (skills/instructions/MCP/snippets/automations)
|
|
167
167
|
snippets Sync reusable snippet blocks into config files
|
|
168
168
|
|
|
169
169
|
Options:
|
package/src/remote.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { mkdir, readdir, readFile, rm } from "node:fs/promises";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
basename,
|
|
6
|
+
dirname,
|
|
7
|
+
isAbsolute,
|
|
8
|
+
join,
|
|
9
|
+
relative,
|
|
10
|
+
resolve,
|
|
11
|
+
} from "node:path";
|
|
4
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
5
14
|
import { buildIndex } from "./index-builder";
|
|
6
|
-
import { facultRootDir } from "./paths";
|
|
15
|
+
import { facultRootDir, readFacultConfig } from "./paths";
|
|
7
16
|
import {
|
|
8
17
|
assertManifestIntegrity,
|
|
9
18
|
assertManifestSignature,
|
|
@@ -40,6 +49,23 @@ const REMOTE_STATE_VERSION = 1;
|
|
|
40
49
|
const VERSION_TOKEN_RE = /[A-Za-z]+|[0-9]+/g;
|
|
41
50
|
const QUERY_SPLIT_RE = /\s+/;
|
|
42
51
|
const MD_EXT_RE = /\.md$/i;
|
|
52
|
+
const PROMPT_PATH_SPLIT_RE = /[,\n]/;
|
|
53
|
+
const GIT_WORKTREE_LINE_RE = /\r?\n/;
|
|
54
|
+
|
|
55
|
+
type BuiltinAutomationTemplateScope = "global" | "project" | "wide";
|
|
56
|
+
|
|
57
|
+
interface BuiltinAutomationTemplate {
|
|
58
|
+
id: string;
|
|
59
|
+
title: string;
|
|
60
|
+
description: string;
|
|
61
|
+
prompt: string;
|
|
62
|
+
memory: string;
|
|
63
|
+
defaultRRule: string;
|
|
64
|
+
defaultStatus: "PAUSED" | "ACTIVE";
|
|
65
|
+
defaultModel: string;
|
|
66
|
+
defaultReasoningEffort: "low" | "medium" | "high";
|
|
67
|
+
scope: BuiltinAutomationTemplateScope;
|
|
68
|
+
}
|
|
43
69
|
|
|
44
70
|
interface InstalledRemoteItem {
|
|
45
71
|
ref: string;
|
|
@@ -54,6 +80,12 @@ interface InstalledRemoteItem {
|
|
|
54
80
|
installedAt: string;
|
|
55
81
|
}
|
|
56
82
|
|
|
83
|
+
interface AutomationCwdCandidate {
|
|
84
|
+
value: string;
|
|
85
|
+
label: string;
|
|
86
|
+
hint?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
57
89
|
interface InstalledRemoteState {
|
|
58
90
|
version: number;
|
|
59
91
|
updatedAt: string;
|
|
@@ -290,6 +322,192 @@ Ship reliable changes quickly while keeping behavior predictable.
|
|
|
290
322
|
],
|
|
291
323
|
};
|
|
292
324
|
|
|
325
|
+
const BUILTIN_AUTOMATION_TEMPLATES: BuiltinAutomationTemplate[] = [
|
|
326
|
+
{
|
|
327
|
+
id: "learning-review",
|
|
328
|
+
title: "Learning Review Loop",
|
|
329
|
+
description:
|
|
330
|
+
"Daily/weekly Codex session review that converts repeated signals into fclt writebacks and evolution candidates.",
|
|
331
|
+
defaultRRule: "RRULE:FREQ=DAILY;BYHOUR=19;BYMINUTE=0",
|
|
332
|
+
defaultStatus: "PAUSED",
|
|
333
|
+
defaultModel: "gpt-5.4",
|
|
334
|
+
defaultReasoningEffort: "high",
|
|
335
|
+
scope: "wide",
|
|
336
|
+
memory: `# Learning Review Loop
|
|
337
|
+
|
|
338
|
+
Use this memory for pattern continuity:
|
|
339
|
+
|
|
340
|
+
- Primary goal: convert repeated, evidence-backed session signal into durable writeback or evolution, not chat-only summary.
|
|
341
|
+
- For wide reviews, partition evidence by cwd first; do not let one repo's evidence stand in for another.
|
|
342
|
+
- Grounding: prefer evidence from session messages, tool calls, shell commands, diffs, tests, commits, and touched files.
|
|
343
|
+
- Threshold: only encode signal when you can name what was learned, why it matters, and the most plausible destination.
|
|
344
|
+
- Scope: default to project writeback unless the signal clearly belongs in global doctrine or a shared capability.
|
|
345
|
+
- Promote to global only when the same signal appears across multiple repos or clearly targets shared doctrine, shared agents, or shared skills.
|
|
346
|
+
- Verification: distinguish one-off friction from a repeated pattern before escalating it.
|
|
347
|
+
- If available, use [$feedback-loop-setup]({{feedbackLoopSkill}}) when the review needs stronger feedback loops or verification framing.
|
|
348
|
+
- If available, use [$capability-evolution]({{capabilityEvolutionSkill}}) when repeated signal should become a concrete proposal.
|
|
349
|
+
- If available, delegate bounded review slices to \`learning-extractor\`, \`writeback-curator\`, \`scope-promoter\`, \`evolution-planner\`, or \`verification-auditor\` when that materially improves the review.
|
|
350
|
+
`,
|
|
351
|
+
prompt: `Goal: review recent Codex work in the configured CWDs and convert durable, evidence-backed signal into writebacks or reviewable evolution proposals.
|
|
352
|
+
|
|
353
|
+
Before producing output:
|
|
354
|
+
- Treat [AGENTS.md]({{codexAgents}}) as the rendered operating-model baseline for this Codex environment.
|
|
355
|
+
- Use [LEARNING_AND_WRITEBACK.md]({{aiLearningAndWriteback}}) and [EVOLUTION.md]({{aiEvolution}}) as the durable doctrine for writeback and capability change decisions.
|
|
356
|
+
- Use [FEEDBACK_LOOPS.md]({{aiFeedbackLoops}}) and [VERIFICATION.md]({{aiVerification}}) when you need stronger loop design or more defensible proof.
|
|
357
|
+
- If available, use [$feedback-loop-setup]({{feedbackLoopSkill}}) when you need stronger feedback loops, success criteria, or verification framing.
|
|
358
|
+
- If available, use [$capability-evolution]({{capabilityEvolutionSkill}}) when repeated signal appears strong enough to become a durable capability proposal.
|
|
359
|
+
- If it will materially improve quality, explicitly ask Codex to spawn narrow subagents such as \`learning-extractor\`, \`writeback-curator\`, \`scope-promoter\`, \`evolution-planner\`, or \`verification-auditor\`. Only use them for bounded, non-overlapping review slices.
|
|
360
|
+
|
|
361
|
+
Grounding rules:
|
|
362
|
+
- Work only from evidence in Codex sessions and nearby repo artifacts for the configured CWDs.
|
|
363
|
+
- Partition the review by cwd first. Name which configured cwds had real evidence this run and which did not.
|
|
364
|
+
- Prefer evidence from session messages, tool calls, shell commands, diffs, tests, commits, and touched files.
|
|
365
|
+
- Do not speculate about intent or propose changes that are not anchored in evidence.
|
|
366
|
+
- Distinguish one-off friction from repeated signal. Escalate only when the signal is durable enough to matter.
|
|
367
|
+
|
|
368
|
+
Decision rules:
|
|
369
|
+
- Use \`fclt ai writeback add\` when the signal, target asset, and scope are clear.
|
|
370
|
+
- Use \`fclt ai evolve\` only when repeated signal is strong enough to justify a reviewable capability change.
|
|
371
|
+
- Prefer project scope unless the learning clearly belongs in shared global doctrine, shared agents, shared skills, or other cross-project capability.
|
|
372
|
+
- For wide automations, require repeated evidence across more than one cwd before recommending a global/shared capability change unless the target is obviously global.
|
|
373
|
+
- Skip weak, speculative, or purely anecdotal observations.
|
|
374
|
+
|
|
375
|
+
Verification:
|
|
376
|
+
- Verify every claim against at least one concrete artifact.
|
|
377
|
+
- Call out residual uncertainty instead of overstating confidence.
|
|
378
|
+
- Separate missing context, weak verification, failed execution, and reusable pattern; do not collapse them together.
|
|
379
|
+
|
|
380
|
+
Output:
|
|
381
|
+
- Coverage: which cwds had concrete evidence, and which were effectively idle for this run.
|
|
382
|
+
- Recorded writebacks: what you recorded, why, and the target asset or command used.
|
|
383
|
+
- Evolution candidates: only the strongest repeated signals, with rationale and likely scope.
|
|
384
|
+
- Watch list: promising signals not yet strong enough to encode.
|
|
385
|
+
- Gaps in current operating model or verification harness: only if evidence supports them.
|
|
386
|
+
|
|
387
|
+
Keep the result concise, high-signal, and operational. If nothing crosses the threshold, say what you reviewed and why no writeback or evolution was justified.`,
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
id: "evolution-review",
|
|
391
|
+
title: "Evolution Review Loop",
|
|
392
|
+
description:
|
|
393
|
+
"Weekly Codex review of open evolution proposals and strong writeback clusters, with suggested next actions for review, acceptance, rejection, promotion, or apply.",
|
|
394
|
+
defaultRRule: "RRULE:FREQ=WEEKLY;BYHOUR=16;BYMINUTE=0;BYDAY=FR",
|
|
395
|
+
defaultStatus: "PAUSED",
|
|
396
|
+
defaultModel: "gpt-5.4",
|
|
397
|
+
defaultReasoningEffort: "high",
|
|
398
|
+
scope: "wide",
|
|
399
|
+
memory: `# Evolution Review Loop
|
|
400
|
+
|
|
401
|
+
Use this memory for continuity:
|
|
402
|
+
|
|
403
|
+
- Primary goal: keep proposal review moving so durable changes do not stall after writeback.
|
|
404
|
+
- Review continuity matters: track which proposals were already seen, what changed since the last review, and which action was previously recommended.
|
|
405
|
+
- Prefer reviewing existing proposal and writeback state over rediscovering the entire history from scratch.
|
|
406
|
+
- Scope: default to project evolution unless the proposal clearly belongs in shared doctrine, shared agents, shared skills, or another cross-project capability.
|
|
407
|
+
- For wide reviews, partition by cwd first and only recommend shared/global promotion when evidence truly spans multiple repos or the target asset is obviously shared.
|
|
408
|
+
- Recommend actions, do not silently apply high-risk changes.
|
|
409
|
+
- If available, use [$capability-evolution]({{capabilityEvolutionSkill}}) when proposal shaping or promotion decisions need stronger structure.
|
|
410
|
+
- If available, use [$feedback-loop-setup]({{feedbackLoopSkill}}) when proposal validity depends on weak or stale verification.
|
|
411
|
+
- If available, delegate bounded slices to \`evolution-planner\`, \`scope-promoter\`, \`writeback-curator\`, or \`verification-auditor\` when that materially improves rigor.
|
|
412
|
+
`,
|
|
413
|
+
prompt: `Goal: review current evolution state in the configured CWDs, keep proposal continuity intact, and suggest the highest-signal next actions for draft, review, accept, reject, promote, supersede, or apply.
|
|
414
|
+
|
|
415
|
+
Before producing output:
|
|
416
|
+
- Treat [AGENTS.md]({{codexAgents}}) as the rendered operating-model baseline for this Codex environment.
|
|
417
|
+
- Use [EVOLUTION.md]({{aiEvolution}}), [LEARNING_AND_WRITEBACK.md]({{aiLearningAndWriteback}}), and [VERIFICATION.md]({{aiVerification}}) as the doctrine for proposal quality, thresholding, and proof.
|
|
418
|
+
- Use [FEEDBACK_LOOPS.md]({{aiFeedbackLoops}}) when a proposal depends on a weak or gameable verification loop.
|
|
419
|
+
- If available, use [$capability-evolution]({{capabilityEvolutionSkill}}) when you need stronger proposal-shaping or promotion judgment.
|
|
420
|
+
- If available, use [$feedback-loop-setup]({{feedbackLoopSkill}}) when proposal validity depends on missing or stale verification.
|
|
421
|
+
- If it will materially improve quality, explicitly ask Codex to spawn narrow subagents such as \`evolution-planner\`, \`scope-promoter\`, \`writeback-curator\`, or \`verification-auditor\`. Only use them for bounded, non-overlapping review slices.
|
|
422
|
+
|
|
423
|
+
Grounding rules:
|
|
424
|
+
- Work from concrete proposal and writeback artifacts first, then confirm with nearby repo evidence when needed.
|
|
425
|
+
- Preserve continuity: compare this run against the automation memory and note what is actually new, unchanged, strengthened, weakened, accepted, rejected, or stale.
|
|
426
|
+
- Partition the review by cwd first. Name which configured cwds had real proposal or writeback state this run and which did not.
|
|
427
|
+
- Do not speculate about intent or recommend advancing a proposal without citing the evidence that still supports it.
|
|
428
|
+
- Distinguish proposal quality problems from execution gaps, stale evidence, missing verification, and simple lack of reviewer attention.
|
|
429
|
+
|
|
430
|
+
Decision rules:
|
|
431
|
+
- Prefer suggesting the next operator action over narrating the whole proposal history.
|
|
432
|
+
- Recommend \`draft\` when a proposal exists but is under-specified.
|
|
433
|
+
- Recommend \`review\` or \`accept\` only when the rationale, scope, and evidence are strong enough.
|
|
434
|
+
- Recommend \`apply\` only for already-accepted proposals whose evidence still looks valid and whose risk is appropriate.
|
|
435
|
+
- Recommend \`reject\` or \`supersede\` when the proposal is stale, contradicted, duplicated, or too weak.
|
|
436
|
+
- Recommend \`promote --to global\` only when a project-scoped proposal now clearly belongs in shared doctrine, shared agents, shared skills, or another cross-project surface.
|
|
437
|
+
- For wide automations, require repeated evidence across more than one cwd before recommending shared/global promotion unless the target is obviously global.
|
|
438
|
+
|
|
439
|
+
Verification:
|
|
440
|
+
- Verify every recommendation against at least one concrete artifact.
|
|
441
|
+
- Call out residual uncertainty instead of overstating confidence.
|
|
442
|
+
- If a proposal should move forward but the proof is weak, say exactly what verification is missing.
|
|
443
|
+
|
|
444
|
+
Output:
|
|
445
|
+
- Coverage: which cwds had concrete proposal/writeback evidence, and which were effectively idle for this run.
|
|
446
|
+
- Proposal queue: the strongest active proposals or proposal-worthy clusters, with what changed since the last review.
|
|
447
|
+
- Recommended actions: for each important item, the next operator action and why.
|
|
448
|
+
- Hold or reject: proposals that should stay parked, be rejected, or be superseded.
|
|
449
|
+
- Verification gaps: only the missing proof that materially blocks a recommendation.
|
|
450
|
+
|
|
451
|
+
Keep the result concise, continuity-aware, and operational. If nothing is ready to move, say what you reviewed and why no proposal should advance this run.`,
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
id: "tool-call-audit",
|
|
455
|
+
title: "Tool Call Audit",
|
|
456
|
+
description:
|
|
457
|
+
"Checks whether repeated Codex tool usage looks repetitive or missing guardrails and proposes operating-model adjustments.",
|
|
458
|
+
defaultRRule: "RRULE:FREQ=WEEKLY;BYHOUR=10;BYMINUTE=0;BYDAY=MO,WE,FR",
|
|
459
|
+
defaultStatus: "PAUSED",
|
|
460
|
+
defaultModel: "gpt-5.4",
|
|
461
|
+
defaultReasoningEffort: "high",
|
|
462
|
+
scope: "wide",
|
|
463
|
+
memory: `# Tool Call Audit
|
|
464
|
+
|
|
465
|
+
Use this memory for continuity:
|
|
466
|
+
|
|
467
|
+
- Focus on repeated tool failures, retries, shallow-success loops, and missing operating-model guardrails.
|
|
468
|
+
- Distinguish whether the root issue is instruction quality, missing verification, missing skill usage, missing subagent delegation, or a real tool limitation.
|
|
469
|
+
- Prefer reusable operating-model changes over one-off commentary.
|
|
470
|
+
- If available, use [$feedback-loop-setup]({{feedbackLoopSkill}}) when the audit reveals weak or gameable verification loops.
|
|
471
|
+
- If available, use [$capability-evolution]({{capabilityEvolutionSkill}}) when the same pattern should become a lasting capability change.
|
|
472
|
+
- If available, delegate bounded slices to \`verification-auditor\`, \`writeback-curator\`, or \`evolution-planner\` when that improves rigor.
|
|
473
|
+
`,
|
|
474
|
+
prompt: `Goal: audit recent Codex tool and agent usage in the configured CWDs, find repeated high-cost patterns, and turn strong evidence into operating-model improvements.
|
|
475
|
+
|
|
476
|
+
Before producing output:
|
|
477
|
+
- Treat [AGENTS.md]({{codexAgents}}) as the rendered operating-model baseline for this Codex environment.
|
|
478
|
+
- Use [LEARNING_AND_WRITEBACK.md]({{aiLearningAndWriteback}}), [EVOLUTION.md]({{aiEvolution}}), and [VERIFICATION.md]({{aiVerification}}) when deciding whether a repeated operational pattern deserves durable change.
|
|
479
|
+
- Use [FEEDBACK_LOOPS.md]({{aiFeedbackLoops}}) when the audit exposes weak, stale, or gameable loops.
|
|
480
|
+
- If available, use [$feedback-loop-setup]({{feedbackLoopSkill}}) when the audit exposes weak, stale, or gameable verification loops.
|
|
481
|
+
- If available, use [$capability-evolution]({{capabilityEvolutionSkill}}) when repeated operational pain should become a durable capability proposal.
|
|
482
|
+
- If it will materially improve rigor, explicitly ask Codex to spawn focused subagents such as \`verification-auditor\`, \`writeback-curator\`, \`scope-promoter\`, or \`evolution-planner\`.
|
|
483
|
+
|
|
484
|
+
Grounding rules:
|
|
485
|
+
- Anchor findings in concrete evidence from session messages, tool calls, shell commands, diffs, tests, commits, and touched files.
|
|
486
|
+
- Focus on repeated misses, repeated retries, expensive dead ends, missing skill use, missing delegation, or weak proof of correctness.
|
|
487
|
+
- Do not report style-only observations unless they hide a real operational problem.
|
|
488
|
+
|
|
489
|
+
For each candidate pattern, determine:
|
|
490
|
+
- what tool, agent, or command pattern recurred,
|
|
491
|
+
- what the actual failure mode or inefficiency was,
|
|
492
|
+
- what evidence supports the pattern,
|
|
493
|
+
- whether the better fix is instruction, skill usage, subagent usage, verification, or a capability change.
|
|
494
|
+
|
|
495
|
+
Decision rules:
|
|
496
|
+
- Use \`fclt ai writeback add\` when the signal and target destination are clear.
|
|
497
|
+
- Use \`fclt ai evolve\` only when the pattern is repeated enough to justify a durable capability change.
|
|
498
|
+
- Prefer project scope unless the problem clearly generalizes across projects or global doctrine.
|
|
499
|
+
- Skip isolated incidents that do not justify durable change.
|
|
500
|
+
|
|
501
|
+
Output:
|
|
502
|
+
- Recorded writebacks.
|
|
503
|
+
- Evolution candidates.
|
|
504
|
+
- Watch list.
|
|
505
|
+
- Operational gaps: the most important missing skill, missing instruction, weak loop, or missing guardrail revealed by the audit.
|
|
506
|
+
|
|
507
|
+
Keep the output concise, evidence-backed, and biased toward durable improvement rather than narration.`,
|
|
508
|
+
},
|
|
509
|
+
];
|
|
510
|
+
|
|
293
511
|
function isSafePathString(p: string): boolean {
|
|
294
512
|
return !p.includes("\0");
|
|
295
513
|
}
|
|
@@ -321,6 +539,640 @@ function renderTemplate(text: string, values: Record<string, string>): string {
|
|
|
321
539
|
return out;
|
|
322
540
|
}
|
|
323
541
|
|
|
542
|
+
function automationTemplateValues(homeDir: string): Record<string, string> {
|
|
543
|
+
const codexRoot = join(homeDir, ".codex");
|
|
544
|
+
const aiRoot = join(homeDir, ".ai");
|
|
545
|
+
return {
|
|
546
|
+
codexAgents: join(codexRoot, "AGENTS.md"),
|
|
547
|
+
aiLearningAndWriteback: join(
|
|
548
|
+
aiRoot,
|
|
549
|
+
"instructions",
|
|
550
|
+
"LEARNING_AND_WRITEBACK.md"
|
|
551
|
+
),
|
|
552
|
+
aiEvolution: join(aiRoot, "instructions", "EVOLUTION.md"),
|
|
553
|
+
aiFeedbackLoops: join(aiRoot, "instructions", "FEEDBACK_LOOPS.md"),
|
|
554
|
+
aiVerification: join(aiRoot, "instructions", "VERIFICATION.md"),
|
|
555
|
+
feedbackLoopSkill: join(
|
|
556
|
+
codexRoot,
|
|
557
|
+
"skills",
|
|
558
|
+
"feedback-loop-setup",
|
|
559
|
+
"SKILL.md"
|
|
560
|
+
),
|
|
561
|
+
capabilityEvolutionSkill: join(
|
|
562
|
+
codexRoot,
|
|
563
|
+
"skills",
|
|
564
|
+
"capability-evolution",
|
|
565
|
+
"SKILL.md"
|
|
566
|
+
),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function quoteTomlString(value: string): string {
|
|
571
|
+
return JSON.stringify(value);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function quoteTomlStringArray(values: string[]): string {
|
|
575
|
+
return `[${values.map(quoteTomlString).join(", ")}]`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function normalizeCwdList(raw: string | null | undefined): string[] {
|
|
579
|
+
if (!raw) {
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
return raw
|
|
583
|
+
.split(",")
|
|
584
|
+
.map((entry) => entry.trim())
|
|
585
|
+
.filter((entry) => entry.length > 0);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function isInteractiveOutputRequested(args: string[]): boolean {
|
|
589
|
+
return (
|
|
590
|
+
!args.includes("--json") &&
|
|
591
|
+
process.stdin.isTTY === true &&
|
|
592
|
+
process.stdout.isTTY === true
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function parseAutomationScope(
|
|
597
|
+
raw: string | null
|
|
598
|
+
): BuiltinAutomationTemplateScope | null {
|
|
599
|
+
if (!raw) {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
const normalized = raw.trim().toLowerCase();
|
|
603
|
+
if (
|
|
604
|
+
normalized === "global" ||
|
|
605
|
+
normalized === "project" ||
|
|
606
|
+
normalized === "wide"
|
|
607
|
+
) {
|
|
608
|
+
return normalized;
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function expandPathForUserHome(p: string, home: string): string {
|
|
614
|
+
if (p === "~") {
|
|
615
|
+
return home;
|
|
616
|
+
}
|
|
617
|
+
if (p.startsWith("~/")) {
|
|
618
|
+
return join(home, p.slice(2));
|
|
619
|
+
}
|
|
620
|
+
return p;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function normalizeCwdInput(
|
|
624
|
+
raw: string,
|
|
625
|
+
cwd: string,
|
|
626
|
+
homeDir: string
|
|
627
|
+
): string[] {
|
|
628
|
+
return normalizeCwdList(raw)
|
|
629
|
+
.map((entry) => {
|
|
630
|
+
const expanded = expandPathForUserHome(entry, homeDir);
|
|
631
|
+
return resolve(cwd, expanded);
|
|
632
|
+
})
|
|
633
|
+
.filter((entry) => isSafePathString(entry));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function normalizePromptPath(
|
|
637
|
+
raw: string,
|
|
638
|
+
cwd: string,
|
|
639
|
+
homeDir: string
|
|
640
|
+
): string | null {
|
|
641
|
+
const trimmed = raw.trim();
|
|
642
|
+
if (!trimmed) {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
return resolve(cwd, expandPathForUserHome(trimmed, homeDir));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function normalizePromptPathList(
|
|
649
|
+
raw: string,
|
|
650
|
+
cwd: string,
|
|
651
|
+
homeDir: string
|
|
652
|
+
): string[] {
|
|
653
|
+
return raw
|
|
654
|
+
.split(PROMPT_PATH_SPLIT_RE)
|
|
655
|
+
.map((entry) => normalizePromptPath(entry, cwd, homeDir))
|
|
656
|
+
.filter((value): value is string => Boolean(value));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function runGitCommand(
|
|
660
|
+
cwd: string,
|
|
661
|
+
args: string[]
|
|
662
|
+
): { stdout: string; status: number } | null {
|
|
663
|
+
try {
|
|
664
|
+
const result = spawnSync("git", args, {
|
|
665
|
+
cwd,
|
|
666
|
+
encoding: "utf8",
|
|
667
|
+
maxBuffer: 1_000_000,
|
|
668
|
+
});
|
|
669
|
+
if (!result || result.status === null || result.status === undefined) {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
674
|
+
status: result.status,
|
|
675
|
+
};
|
|
676
|
+
} catch {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function findGitRootFromPath(cwd: string): string | null {
|
|
682
|
+
const result = runGitCommand(cwd, ["rev-parse", "--show-toplevel"]);
|
|
683
|
+
if (!result || result.status !== 0) {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
const root = result.stdout.trim();
|
|
687
|
+
return root || null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function parseGitWorktreeList(raw: string): string[] {
|
|
691
|
+
const lines = raw.split(GIT_WORKTREE_LINE_RE);
|
|
692
|
+
const out: string[] = [];
|
|
693
|
+
for (const line of lines) {
|
|
694
|
+
if (!line.startsWith("worktree ")) {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
const value = line.slice("worktree ".length).trim();
|
|
698
|
+
if (value) {
|
|
699
|
+
out.push(value);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return out;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async function addGitWorkspaceCandidatesFromDirectory(
|
|
706
|
+
root: string,
|
|
707
|
+
out: Map<string, AutomationCwdCandidate>,
|
|
708
|
+
homeDir: string
|
|
709
|
+
) {
|
|
710
|
+
const gitRootPath = resolve(root);
|
|
711
|
+
const direct = normalizePromptPath(root, gitRootPath, homeDir);
|
|
712
|
+
if (direct) {
|
|
713
|
+
const gitFile = join(gitRootPath, ".git");
|
|
714
|
+
try {
|
|
715
|
+
await Bun.file(gitFile).stat();
|
|
716
|
+
out.set(gitRootPath, {
|
|
717
|
+
value: gitRootPath,
|
|
718
|
+
label: `${basename(gitRootPath)} (root)`,
|
|
719
|
+
hint: `Git root: ${gitRootPath}`,
|
|
720
|
+
});
|
|
721
|
+
} catch {
|
|
722
|
+
// Not a git root at this path.
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const dirEntries = await readdir(root, { withFileTypes: true }).catch(
|
|
727
|
+
() => []
|
|
728
|
+
);
|
|
729
|
+
for (const entry of dirEntries) {
|
|
730
|
+
if (!entry.isDirectory()) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const candidate = join(root, entry.name);
|
|
734
|
+
const gitDir = join(candidate, ".git");
|
|
735
|
+
try {
|
|
736
|
+
await Bun.file(gitDir).stat();
|
|
737
|
+
const abs = resolve(candidate);
|
|
738
|
+
const existing = out.get(abs);
|
|
739
|
+
if (!existing) {
|
|
740
|
+
out.set(abs, {
|
|
741
|
+
value: abs,
|
|
742
|
+
label: `${entry.name} (candidate)`,
|
|
743
|
+
hint: `Git workspace: ${abs}`,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
} catch {
|
|
747
|
+
// Not a git directory.
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async function collectKnownAutomationCwdCandidates(
|
|
753
|
+
homeDir: string,
|
|
754
|
+
cwd: string
|
|
755
|
+
): Promise<AutomationCwdCandidate[]> {
|
|
756
|
+
const discovered = new Map<string, AutomationCwdCandidate>();
|
|
757
|
+
const cwdResolved = resolve(cwd);
|
|
758
|
+
|
|
759
|
+
const gitRoot = findGitRootFromPath(cwdResolved);
|
|
760
|
+
if (gitRoot) {
|
|
761
|
+
const worktreeResult = runGitCommand(gitRoot, [
|
|
762
|
+
"worktree",
|
|
763
|
+
"list",
|
|
764
|
+
"--porcelain",
|
|
765
|
+
]);
|
|
766
|
+
if (worktreeResult && worktreeResult.status === 0) {
|
|
767
|
+
for (const pathValue of parseGitWorktreeList(worktreeResult.stdout)) {
|
|
768
|
+
const abs = resolve(pathValue);
|
|
769
|
+
if (!discovered.has(abs)) {
|
|
770
|
+
discovered.set(abs, {
|
|
771
|
+
value: abs,
|
|
772
|
+
label: `${basename(abs)} (git worktree)`,
|
|
773
|
+
hint: abs,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (discovered.size === 0) {
|
|
780
|
+
const abs = resolve(gitRoot);
|
|
781
|
+
discovered.set(abs, {
|
|
782
|
+
value: abs,
|
|
783
|
+
label: `${basename(abs)} (project root)`,
|
|
784
|
+
hint: abs,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const cfg = readFacultConfig(homeDir);
|
|
790
|
+
for (const rawPath of cfg?.scanFrom ?? []) {
|
|
791
|
+
const scanRoot = expandPathForUserHome(rawPath, homeDir);
|
|
792
|
+
await addGitWorkspaceCandidatesFromDirectory(scanRoot, discovered, homeDir);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const automationRoot = join(homeDir, ".codex", "automations");
|
|
796
|
+
const entries = await readdir(automationRoot, { withFileTypes: true }).catch(
|
|
797
|
+
() => []
|
|
798
|
+
);
|
|
799
|
+
for (const entry of entries) {
|
|
800
|
+
if (!entry.isDirectory()) {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
const tomlPath = join(automationRoot, entry.name, "automation.toml");
|
|
804
|
+
try {
|
|
805
|
+
const rawToml = await Bun.file(tomlPath).text();
|
|
806
|
+
const parsed = Bun.TOML.parse(rawToml) as Record<string, unknown>;
|
|
807
|
+
const cwds = parsed.cwds;
|
|
808
|
+
if (!Array.isArray(cwds)) {
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
for (const rawValue of cwds) {
|
|
812
|
+
if (typeof rawValue !== "string") {
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const normalized = normalizePromptPath(rawValue, cwdResolved, homeDir);
|
|
816
|
+
if (!normalized) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
const label = basename(normalized);
|
|
820
|
+
if (!discovered.has(normalized)) {
|
|
821
|
+
discovered.set(normalized, {
|
|
822
|
+
value: normalized,
|
|
823
|
+
label: `${label} (from Codex automation)`,
|
|
824
|
+
hint: normalized,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
} catch {
|
|
829
|
+
// Ignore malformed or missing automation files.
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return Array.from(discovered.values()).sort((a, b) =>
|
|
834
|
+
a.label.localeCompare(b.label, "en-US")
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function resolveAutomationScopeInputs(opts: {
|
|
839
|
+
template: BuiltinAutomationTemplate;
|
|
840
|
+
requestedScope: string | null;
|
|
841
|
+
requestedProjectRoot: string | null;
|
|
842
|
+
requestedCwdsRaw: string | null;
|
|
843
|
+
requestedCwdsArray?: string[];
|
|
844
|
+
homeDir: string;
|
|
845
|
+
cwd: string;
|
|
846
|
+
interactive: boolean;
|
|
847
|
+
}): Promise<{
|
|
848
|
+
scope: string | null;
|
|
849
|
+
projectRoot: string | null;
|
|
850
|
+
cwds: string[] | null;
|
|
851
|
+
}> {
|
|
852
|
+
const parsedRequestedScope = parseAutomationScope(opts.requestedScope);
|
|
853
|
+
if (opts.requestedScope && !parsedRequestedScope) {
|
|
854
|
+
throw new Error(`Unsupported automation scope: ${opts.requestedScope}`);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const requestedCwds = opts.requestedCwdsArray?.length
|
|
858
|
+
? opts.requestedCwdsArray
|
|
859
|
+
: normalizeCwdInput(opts.requestedCwdsRaw ?? "", opts.cwd, opts.homeDir);
|
|
860
|
+
const requestedProjectRoot = opts.requestedProjectRoot
|
|
861
|
+
? normalizePromptPath(opts.requestedProjectRoot, opts.cwd, opts.homeDir)
|
|
862
|
+
: null;
|
|
863
|
+
|
|
864
|
+
if (
|
|
865
|
+
!opts.interactive ||
|
|
866
|
+
(parsedRequestedScope &&
|
|
867
|
+
(parsedRequestedScope === "global" || parsedRequestedScope === "wide") &&
|
|
868
|
+
requestedCwds.length > 0) ||
|
|
869
|
+
(parsedRequestedScope === "project" && requestedProjectRoot)
|
|
870
|
+
) {
|
|
871
|
+
return {
|
|
872
|
+
scope: parsedRequestedScope,
|
|
873
|
+
projectRoot:
|
|
874
|
+
parsedRequestedScope === "project" ? requestedProjectRoot : null,
|
|
875
|
+
cwds:
|
|
876
|
+
parsedRequestedScope === "global" || parsedRequestedScope === "wide"
|
|
877
|
+
? requestedCwds
|
|
878
|
+
: [],
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const candidates = await collectKnownAutomationCwdCandidates(
|
|
883
|
+
opts.homeDir,
|
|
884
|
+
opts.cwd
|
|
885
|
+
);
|
|
886
|
+
const scopeDefault: BuiltinAutomationTemplateScope =
|
|
887
|
+
parsedRequestedScope ?? opts.template.scope;
|
|
888
|
+
|
|
889
|
+
let scope: BuiltinAutomationTemplateScope = scopeDefault;
|
|
890
|
+
if (parsedRequestedScope) {
|
|
891
|
+
scope = parsedRequestedScope;
|
|
892
|
+
} else {
|
|
893
|
+
const chosen = await select({
|
|
894
|
+
message: "Choose automation scope",
|
|
895
|
+
options: [
|
|
896
|
+
{ value: "project", label: "project", hint: "Track one project root" },
|
|
897
|
+
{
|
|
898
|
+
value: "wide",
|
|
899
|
+
label: "wide",
|
|
900
|
+
hint: "Track many explicit project roots",
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
value: "global",
|
|
904
|
+
label: "global",
|
|
905
|
+
hint: "Create a global/default scaffold",
|
|
906
|
+
},
|
|
907
|
+
],
|
|
908
|
+
initialValue: scopeDefault,
|
|
909
|
+
});
|
|
910
|
+
if (isCancel(chosen)) {
|
|
911
|
+
process.exit(1);
|
|
912
|
+
}
|
|
913
|
+
scope = chosen;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (scope === "project" && !requestedProjectRoot) {
|
|
917
|
+
if (!candidates.length) {
|
|
918
|
+
const txt = await text({
|
|
919
|
+
message: "Project root path",
|
|
920
|
+
placeholder: opts.cwd,
|
|
921
|
+
});
|
|
922
|
+
if (isCancel(txt) || !txt || typeof txt !== "string") {
|
|
923
|
+
process.exit(1);
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
scope,
|
|
927
|
+
projectRoot: normalizePromptPath(txt, opts.cwd, opts.homeDir),
|
|
928
|
+
cwds: [],
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const choices = [
|
|
933
|
+
...candidates.map((c) => ({
|
|
934
|
+
value: c.value,
|
|
935
|
+
label: c.label,
|
|
936
|
+
hint: c.hint,
|
|
937
|
+
})),
|
|
938
|
+
{
|
|
939
|
+
value: "__custom__",
|
|
940
|
+
label: "Custom project path",
|
|
941
|
+
hint: "Enter a different absolute or relative path",
|
|
942
|
+
},
|
|
943
|
+
];
|
|
944
|
+
const chosen = await select({
|
|
945
|
+
message: "Select project scope root",
|
|
946
|
+
options: choices,
|
|
947
|
+
initialValue: candidates[0]?.value ?? "__custom__",
|
|
948
|
+
});
|
|
949
|
+
if (isCancel(chosen)) {
|
|
950
|
+
process.exit(1);
|
|
951
|
+
}
|
|
952
|
+
if (chosen === "__custom__") {
|
|
953
|
+
const txt = await text({
|
|
954
|
+
message: "Project root path",
|
|
955
|
+
placeholder: opts.cwd,
|
|
956
|
+
});
|
|
957
|
+
if (isCancel(txt) || !txt || typeof txt !== "string") {
|
|
958
|
+
process.exit(1);
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
scope,
|
|
962
|
+
projectRoot: normalizePromptPath(txt, opts.cwd, opts.homeDir),
|
|
963
|
+
cwds: [],
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
return { scope, projectRoot: chosen, cwds: [] };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (scope === "global" || scope === "wide") {
|
|
970
|
+
if (requestedCwds.length > 0) {
|
|
971
|
+
return { scope, projectRoot: null, cwds: requestedCwds };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (!candidates.length) {
|
|
975
|
+
const txt = await text({
|
|
976
|
+
message: "Workspace paths (comma-separated or leave blank for none)",
|
|
977
|
+
placeholder: "",
|
|
978
|
+
});
|
|
979
|
+
if (isCancel(txt) || typeof txt !== "string") {
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
const parsed = normalizePromptPathList(txt, opts.cwd, opts.homeDir);
|
|
983
|
+
return { scope, projectRoot: null, cwds: parsed };
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const chosen = await multiselect({
|
|
987
|
+
message: "Select workspaces",
|
|
988
|
+
options: [
|
|
989
|
+
...candidates.map((c) => ({
|
|
990
|
+
value: c.value,
|
|
991
|
+
label: c.label,
|
|
992
|
+
hint: c.hint,
|
|
993
|
+
})),
|
|
994
|
+
{
|
|
995
|
+
value: "__manual__",
|
|
996
|
+
label: "Add custom paths",
|
|
997
|
+
hint: "Comma-separated absolute or relative paths",
|
|
998
|
+
},
|
|
999
|
+
],
|
|
1000
|
+
required: false,
|
|
1001
|
+
});
|
|
1002
|
+
if (isCancel(chosen) || !Array.isArray(chosen)) {
|
|
1003
|
+
process.exit(1);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const base = chosen.filter((value) => value !== "__manual__");
|
|
1007
|
+
if (!chosen.includes("__manual__")) {
|
|
1008
|
+
return { scope, projectRoot: null, cwds: base };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const manual = await text({
|
|
1012
|
+
message: "Additional workspace paths (comma-separated)",
|
|
1013
|
+
placeholder: "",
|
|
1014
|
+
});
|
|
1015
|
+
if (isCancel(manual) || typeof manual !== "string") {
|
|
1016
|
+
process.exit(1);
|
|
1017
|
+
}
|
|
1018
|
+
const manualList = normalizePromptPathList(manual, opts.cwd, opts.homeDir);
|
|
1019
|
+
return {
|
|
1020
|
+
scope,
|
|
1021
|
+
projectRoot: null,
|
|
1022
|
+
cwds: uniqueSorted([...base, ...manualList]),
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return {
|
|
1027
|
+
scope,
|
|
1028
|
+
projectRoot: null,
|
|
1029
|
+
cwds: requestedCwds,
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function sanitizeAutomationName(value: string): string {
|
|
1034
|
+
const safe = value
|
|
1035
|
+
.trim()
|
|
1036
|
+
.toLowerCase()
|
|
1037
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
1038
|
+
.replace(/-+/g, "-")
|
|
1039
|
+
.replace(/^-+|-+$/g, "");
|
|
1040
|
+
if (!safe) {
|
|
1041
|
+
throw new Error("Invalid automation name");
|
|
1042
|
+
}
|
|
1043
|
+
return safe;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function pickScopeTemplateCwds(opts: {
|
|
1047
|
+
template: BuiltinAutomationTemplate;
|
|
1048
|
+
requestedScope: string | null;
|
|
1049
|
+
providedCwds: string[];
|
|
1050
|
+
projectRoot: string | null;
|
|
1051
|
+
cwd: string;
|
|
1052
|
+
}): string[] {
|
|
1053
|
+
const requested = (opts.requestedScope ?? "global").trim().toLowerCase();
|
|
1054
|
+
if (!["global", "project", "wide"].includes(requested)) {
|
|
1055
|
+
throw new Error(`Unsupported automation scope: ${opts.requestedScope}`);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (requested === "project") {
|
|
1059
|
+
if (opts.projectRoot) {
|
|
1060
|
+
return [resolve(opts.projectRoot)];
|
|
1061
|
+
}
|
|
1062
|
+
return [resolve(opts.cwd)];
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (opts.providedCwds.length) {
|
|
1066
|
+
return opts.providedCwds.map((pathValue) => resolve(pathValue));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (opts.template.scope === "project") {
|
|
1070
|
+
return [resolve(opts.cwd)];
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return [];
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async function scaffoldCodexAutomationTemplate(args: {
|
|
1077
|
+
homeDir?: string;
|
|
1078
|
+
cwd?: string;
|
|
1079
|
+
templateId: string;
|
|
1080
|
+
force?: boolean;
|
|
1081
|
+
dryRun?: boolean;
|
|
1082
|
+
name?: string;
|
|
1083
|
+
scope?: string | null;
|
|
1084
|
+
projectRoot?: string | null;
|
|
1085
|
+
cwds?: string[] | null;
|
|
1086
|
+
cwdsRaw?: string | null;
|
|
1087
|
+
rrule?: string | null;
|
|
1088
|
+
status?: string | null;
|
|
1089
|
+
}): Promise<{
|
|
1090
|
+
installedAs: string;
|
|
1091
|
+
path: string;
|
|
1092
|
+
dryRun: boolean;
|
|
1093
|
+
changedPaths: string[];
|
|
1094
|
+
}> {
|
|
1095
|
+
const home = args.homeDir ?? homedir();
|
|
1096
|
+
const cwd = resolve(args.cwd ?? process.cwd());
|
|
1097
|
+
const template = BUILTIN_AUTOMATION_TEMPLATES.find(
|
|
1098
|
+
(candidate) => candidate.id === args.templateId
|
|
1099
|
+
);
|
|
1100
|
+
if (!template) {
|
|
1101
|
+
throw new Error(`Unknown automation template: ${args.templateId}`);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const safeName = sanitizeAutomationName(args.name ?? template.id);
|
|
1105
|
+
const requestedCwds = Array.isArray(args.cwds)
|
|
1106
|
+
? args.cwds
|
|
1107
|
+
: normalizeCwdList(args.cwdsRaw ?? "");
|
|
1108
|
+
const cwds = pickScopeTemplateCwds({
|
|
1109
|
+
template,
|
|
1110
|
+
requestedScope: args.scope ?? null,
|
|
1111
|
+
providedCwds: requestedCwds,
|
|
1112
|
+
projectRoot: args.projectRoot ?? null,
|
|
1113
|
+
cwd,
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
const scopeStatus =
|
|
1117
|
+
args.status === "active" || args.status === "ACTIVE"
|
|
1118
|
+
? "ACTIVE"
|
|
1119
|
+
: args.status === "paused" || args.status === "PAUSED"
|
|
1120
|
+
? "PAUSED"
|
|
1121
|
+
: template.defaultStatus;
|
|
1122
|
+
const rrule = args.rrule?.trim() || template.defaultRRule;
|
|
1123
|
+
const model = template.defaultModel;
|
|
1124
|
+
const reasoningEffort = template.defaultReasoningEffort;
|
|
1125
|
+
const templateValues = automationTemplateValues(home);
|
|
1126
|
+
const renderedPrompt = renderTemplate(template.prompt.trim(), templateValues);
|
|
1127
|
+
const renderedMemory = renderTemplate(template.memory.trim(), templateValues);
|
|
1128
|
+
|
|
1129
|
+
const timestamp = String(Date.now());
|
|
1130
|
+
const automationPath = join(home, ".codex", "automations", safeName);
|
|
1131
|
+
const automationTomlPath = join(automationPath, "automation.toml");
|
|
1132
|
+
const memoryPath = join(automationPath, "memory.md");
|
|
1133
|
+
|
|
1134
|
+
const automationToml = `version = 1
|
|
1135
|
+
id = ${quoteTomlString(safeName)}
|
|
1136
|
+
name = ${quoteTomlString(template.title)}
|
|
1137
|
+
prompt = ${quoteTomlString(renderedPrompt)}
|
|
1138
|
+
status = ${quoteTomlString(scopeStatus)}
|
|
1139
|
+
rrule = ${quoteTomlString(rrule)}
|
|
1140
|
+
model = ${quoteTomlString(model)}
|
|
1141
|
+
reasoning_effort = ${quoteTomlString(reasoningEffort)}
|
|
1142
|
+
cwds = ${quoteTomlStringArray(cwds)}
|
|
1143
|
+
created_at = ${timestamp}
|
|
1144
|
+
updated_at = ${timestamp}
|
|
1145
|
+
`;
|
|
1146
|
+
|
|
1147
|
+
const memory = `${renderedMemory}\n`;
|
|
1148
|
+
const changedPaths: string[] = [];
|
|
1149
|
+
|
|
1150
|
+
const automationTomlExists = await fileExists(automationTomlPath);
|
|
1151
|
+
if (!automationTomlExists || args.force) {
|
|
1152
|
+
changedPaths.push(automationTomlPath);
|
|
1153
|
+
if (!args.dryRun) {
|
|
1154
|
+
await mkdir(automationPath, { recursive: true });
|
|
1155
|
+
await Bun.write(automationTomlPath, `${automationToml}\n`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const memoryExists = await fileExists(memoryPath);
|
|
1160
|
+
if (!memoryExists || args.force) {
|
|
1161
|
+
changedPaths.push(memoryPath);
|
|
1162
|
+
if (!args.dryRun) {
|
|
1163
|
+
await mkdir(automationPath, { recursive: true });
|
|
1164
|
+
await Bun.write(memoryPath, memory);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
return {
|
|
1169
|
+
installedAs: safeName,
|
|
1170
|
+
path: automationPath,
|
|
1171
|
+
dryRun: Boolean(args.dryRun),
|
|
1172
|
+
changedPaths: uniqueSorted(changedPaths),
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
324
1176
|
function builtinPackRoot(packName: string): string {
|
|
325
1177
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
326
1178
|
return join(here, "..", "assets", "packs", packName);
|
|
@@ -1685,9 +2537,19 @@ Usage:
|
|
|
1685
2537
|
fclt templates init agents [--force] [--dry-run]
|
|
1686
2538
|
fclt templates init claude [--force] [--dry-run]
|
|
1687
2539
|
fclt templates init project-ai [--force] [--dry-run]
|
|
2540
|
+
fclt templates init automation <template-id> [--scope global|project|wide] [--name <name>] [--project-root <path>] [--cwds <path1,path2>] [--rrule <RRULE>]
|
|
2541
|
+
--status [PAUSED|ACTIVE] [--force] [--dry-run]
|
|
1688
2542
|
|
|
1689
|
-
Notes:
|
|
2543
|
+
Notes:
|
|
1690
2544
|
- Templates are powered by the builtin remote index (${BUILTIN_INDEX_NAME}).
|
|
2545
|
+
- Automation templates scaffold Codex automation files under ~/.codex/automations/.
|
|
2546
|
+
- scope=global|wide creates a wide automation. Provide --cwds for explicit repo roots.
|
|
2547
|
+
Without --cwds, wide/global automation has no cwds by default.
|
|
2548
|
+
- scope=project creates an automation scoped to one project root.
|
|
2549
|
+
- If --scope (or --project-root / --cwds) is omitted and the command runs in an
|
|
2550
|
+
interactive terminal, you will be prompted to choose scope and candidate paths.
|
|
2551
|
+
Without explicit answers, project scope falls back to current git project and
|
|
2552
|
+
wide/global scope can be left empty.
|
|
1691
2553
|
`);
|
|
1692
2554
|
}
|
|
1693
2555
|
|
|
@@ -1984,6 +2846,13 @@ export async function templatesCommand(
|
|
|
1984
2846
|
"Seed a repo-local .ai with the built-in Facult operating-model pack.",
|
|
1985
2847
|
version: "1.0.0",
|
|
1986
2848
|
},
|
|
2849
|
+
...BUILTIN_AUTOMATION_TEMPLATES.map((item) => ({
|
|
2850
|
+
id: item.id,
|
|
2851
|
+
type: "automation",
|
|
2852
|
+
title: item.title,
|
|
2853
|
+
description: item.description,
|
|
2854
|
+
version: "wide",
|
|
2855
|
+
})),
|
|
1987
2856
|
];
|
|
1988
2857
|
if (json) {
|
|
1989
2858
|
console.log(JSON.stringify(rows, null, 2));
|
|
@@ -2003,7 +2872,7 @@ export async function templatesCommand(
|
|
|
2003
2872
|
const [kind, ...args] = rest;
|
|
2004
2873
|
if (!kind) {
|
|
2005
2874
|
console.error(
|
|
2006
|
-
"templates init requires a kind (skill|mcp|snippet|agents|claude|project-ai)"
|
|
2875
|
+
"templates init requires a kind (skill|mcp|snippet|agents|claude|project-ai|automation)"
|
|
2007
2876
|
);
|
|
2008
2877
|
process.exitCode = 2;
|
|
2009
2878
|
return;
|
|
@@ -2070,6 +2939,75 @@ export async function templatesCommand(
|
|
|
2070
2939
|
} else if (kind === "claude") {
|
|
2071
2940
|
ref = `${BUILTIN_INDEX_NAME}:claude-md-template`;
|
|
2072
2941
|
as = positional[0];
|
|
2942
|
+
} else if (kind === "automation") {
|
|
2943
|
+
const templateId = positional[0];
|
|
2944
|
+
if (!templateId) {
|
|
2945
|
+
console.error(
|
|
2946
|
+
"templates init automation requires a <template-id> (learning-review|evolution-review|tool-call-audit)"
|
|
2947
|
+
);
|
|
2948
|
+
process.exitCode = 2;
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
const template = BUILTIN_AUTOMATION_TEMPLATES.find(
|
|
2952
|
+
(candidate) => candidate.id === templateId
|
|
2953
|
+
);
|
|
2954
|
+
if (!template) {
|
|
2955
|
+
console.error(`Unknown automation template: ${templateId}`);
|
|
2956
|
+
process.exitCode = 1;
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
const status =
|
|
2960
|
+
parseLongFlag(args, "--status") ??
|
|
2961
|
+
parseLongFlag(args, "--automation-status");
|
|
2962
|
+
const scope = parseLongFlag(args, "--scope");
|
|
2963
|
+
const projectRoot = parseLongFlag(args, "--project-root");
|
|
2964
|
+
const cwdsRaw = parseLongFlag(args, "--cwds");
|
|
2965
|
+
const rrule = parseLongFlag(args, "--rrule");
|
|
2966
|
+
const name = parseLongFlag(args, "--name");
|
|
2967
|
+
const cwd = resolve(ctx.cwd ?? process.cwd());
|
|
2968
|
+
const home = ctx.homeDir ?? homedir();
|
|
2969
|
+
const normalizedCwds = normalizeCwdInput(cwdsRaw ?? "", cwd, home);
|
|
2970
|
+
const resolved = await resolveAutomationScopeInputs({
|
|
2971
|
+
template,
|
|
2972
|
+
requestedScope: scope,
|
|
2973
|
+
requestedProjectRoot: projectRoot,
|
|
2974
|
+
requestedCwdsRaw: cwdsRaw,
|
|
2975
|
+
requestedCwdsArray: normalizedCwds,
|
|
2976
|
+
homeDir: home,
|
|
2977
|
+
cwd,
|
|
2978
|
+
interactive: isInteractiveOutputRequested(args),
|
|
2979
|
+
});
|
|
2980
|
+
try {
|
|
2981
|
+
const result = await scaffoldCodexAutomationTemplate({
|
|
2982
|
+
homeDir: ctx.homeDir,
|
|
2983
|
+
cwd,
|
|
2984
|
+
templateId,
|
|
2985
|
+
force,
|
|
2986
|
+
dryRun,
|
|
2987
|
+
name: name ?? undefined,
|
|
2988
|
+
scope: resolved.scope,
|
|
2989
|
+
projectRoot: resolved.projectRoot,
|
|
2990
|
+
cwds: resolved.cwds,
|
|
2991
|
+
rrule,
|
|
2992
|
+
status,
|
|
2993
|
+
});
|
|
2994
|
+
if (json) {
|
|
2995
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
const action = dryRun ? "Would scaffold" : "Scaffolded";
|
|
2999
|
+
console.log(
|
|
3000
|
+
`${action} automation template as ${result.installedAs} (${result.path})`
|
|
3001
|
+
);
|
|
3002
|
+
for (const path of result.changedPaths) {
|
|
3003
|
+
console.log(` - ${path}`);
|
|
3004
|
+
}
|
|
3005
|
+
return;
|
|
3006
|
+
} catch (err) {
|
|
3007
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
3008
|
+
process.exitCode = 1;
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
2073
3011
|
} else {
|
|
2074
3012
|
console.error(`Unknown template kind: ${kind}`);
|
|
2075
3013
|
process.exitCode = 2;
|