facult 2.1.1 → 2.2.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 +72 -0
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/remote.ts +873 -4
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
</a>
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img alt="fclt demo" src="./Ghostty.gif">
|
|
23
|
+
</p>
|
|
24
|
+
|
|
21
25
|
`fclt` is a CLI for building and evolving AI faculties across tools, users, and projects.
|
|
22
26
|
|
|
23
27
|
Most AI tooling manages files. `fclt` manages faculties: the instructions, snippets, templates, skills, agents, rules, and learning loops that should compound, improve, and survive the next session.
|
|
@@ -88,6 +92,12 @@ That means:
|
|
|
88
92
|
- builtin agents sync into tool agent directories when the tool supports agents
|
|
89
93
|
- if you do not author your own `AGENTS.global.md`, `fclt` renders a builtin global baseline doc into tool-native global docs
|
|
90
94
|
|
|
95
|
+
The activation point is managed mode:
|
|
96
|
+
- until you run `fclt manage <tool>`, the builtin operating-model layer is just packaged capability
|
|
97
|
+
- once a tool is managed, the default operating-model layer becomes live for that tool automatically
|
|
98
|
+
- for Codex, Claude, and Cursor, that means the core global doc surface plus the bundled writeback/evolution agents and skills are what agents actually see on disk
|
|
99
|
+
- this is why the normal setup step is to manage the tools you care about first, then sync
|
|
100
|
+
|
|
91
101
|
This is intentionally virtual at the canonical level:
|
|
92
102
|
- builtin defaults remain part of the packaged tool
|
|
93
103
|
- your personal `~/.ai` stays clean unless you explicitly vendor or override something
|
|
@@ -95,6 +105,12 @@ This is intentionally virtual at the canonical level:
|
|
|
95
105
|
|
|
96
106
|
In practice, this means the system is meant to learn by default. The CLI is there when you want to operate it directly, but the default skills, agents, and global docs are supposed to make writeback and evolution available without ceremony.
|
|
97
107
|
|
|
108
|
+
More concretely:
|
|
109
|
+
- the normal path is not a human manually typing `fclt ai ...` after every task
|
|
110
|
+
- the bundled operating-model layer is meant to instruct synced agents and skills to notice reusable signal, preserve it, and push it toward writeback/evolution
|
|
111
|
+
- the CLI remains the explicit operator surface for inspection, review, cleanup, and controlled apply
|
|
112
|
+
- the generated state under `.ai/.facult/` gives those agents a durable thread of what was learned, when it was learned, what asset it pointed at, and what proposals or reviews happened afterward
|
|
113
|
+
|
|
98
114
|
If you want to disable the builtin default layer for a specific global or project canonical root:
|
|
99
115
|
|
|
100
116
|
```toml
|
|
@@ -157,6 +173,12 @@ This makes it possible to answer:
|
|
|
157
173
|
Writeback is the act of recording that signal in a structured way.
|
|
158
174
|
Evolution is the act of grouping that signal into reviewable proposals and applying it back into canonical assets.
|
|
159
175
|
|
|
176
|
+
The intended workflow is agent-driven by default:
|
|
177
|
+
- synced global docs, agents, and skills should push your tooling toward creating writebacks when something important was learned
|
|
178
|
+
- specialist agents such as `writeback-curator`, `evolution-planner`, and `scope-promoter` are there to help turn that signal into cleaner proposals and scope decisions
|
|
179
|
+
- the CLI is what you use when you want to inspect, override, review, reject, apply, or otherwise operate the system directly
|
|
180
|
+
- the point is not a new UI. The point is that the operating layer itself can accumulate memory and context across tasks, sessions, and tools
|
|
181
|
+
|
|
160
182
|
This matters because otherwise the same problems repeat in chat without ever improving the actual operating layer. With `fclt`, you can:
|
|
161
183
|
- record a weak verification pattern
|
|
162
184
|
- group repeated writebacks around an instruction or agent
|
|
@@ -297,6 +319,8 @@ fclt sync
|
|
|
297
319
|
```
|
|
298
320
|
|
|
299
321
|
At this point, your selected skills are actively synced to all managed tools.
|
|
322
|
+
This is also the point where the default operating-model layer becomes active for those tools. If you manage Codex or Claude, the bundled learning/writeback/evolution guidance is no longer just discoverable in `fclt`; it is rendered into the managed global doc surface and synced alongside the bundled agents and skills.
|
|
323
|
+
|
|
300
324
|
If you run these commands from inside a repo that has `<repo>/.ai`, `facult` targets the project-local canonical store and repo-local tool outputs by default.
|
|
301
325
|
On first entry to managed mode, use `--dry-run` first if the live tool already has local content. `facult` will show what it would adopt into the active canonical store across skills, agents, docs, rules, config, and MCP, plus any conflicts. Then rerun with `--adopt-existing`; if names or files collide, add `--existing-conflicts keep-canonical` or `--existing-conflicts keep-existing`.
|
|
302
326
|
For builtin-backed rendered defaults, `facult` now tracks the last managed render hash. If a user edits the generated target locally, normal sync warns and preserves that local edit instead of silently overwriting it. To replace the local edit with the latest packaged builtin default, rerun sync with `--builtin-conflicts overwrite`.
|
|
@@ -522,6 +546,7 @@ Runtime state stays generated and local inside the active canonical root:
|
|
|
522
546
|
That split is intentional:
|
|
523
547
|
- canonical source remains in `~/.ai` or `<repo>/.ai`
|
|
524
548
|
- writeback queues, journals, proposal records, trust state, autosync state, and other Facult-owned runtime/config state stay inside `.ai/.facult/` rather than inside the tool homes
|
|
549
|
+
- those records create a historical thread agents can inspect over time: what changed, what triggered it, which asset it pointed at, what proposal was drafted, how it was reviewed, and whether it was applied or rejected
|
|
525
550
|
|
|
526
551
|
Use writeback when:
|
|
527
552
|
- a task exposed a weak or misleading verification loop
|
|
@@ -533,6 +558,12 @@ Do not think of writeback as “taking notes.” Think of it as preserving signa
|
|
|
533
558
|
|
|
534
559
|
For many users, the normal entrypoint is not the CLI directly. The builtin operating-model layer is designed so synced agents, skills, and global docs can push the system toward writeback and evolution by default, while the `fclt ai ...` commands remain the explicit operator surface when you want direct control.
|
|
535
560
|
|
|
561
|
+
In other words:
|
|
562
|
+
- agents should be the ones noticing friction and capturing it
|
|
563
|
+
- skills should be the ones teaching when writeback or evolution is warranted
|
|
564
|
+
- proposal history should give future agents enough context to understand why a rule, instruction, or prompt changed
|
|
565
|
+
- you drop to the CLI when you want to inspect the thread, steer it, or make the final call
|
|
566
|
+
|
|
536
567
|
Current apply semantics are intentionally policy-bound:
|
|
537
568
|
- targets are resolved through the generated graph when possible and fall back to canonical ref resolution for missing assets
|
|
538
569
|
- apply is limited to markdown canonical assets
|
|
@@ -652,6 +683,7 @@ fclt templates init mcp <name>
|
|
|
652
683
|
fclt templates init snippet <marker>
|
|
653
684
|
fclt templates init agents
|
|
654
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]
|
|
655
687
|
|
|
656
688
|
fclt snippets list
|
|
657
689
|
fclt snippets show <marker>
|
|
@@ -660,6 +692,46 @@ fclt snippets edit <marker>
|
|
|
660
692
|
fclt snippets sync [--dry-run] [file...]
|
|
661
693
|
```
|
|
662
694
|
|
|
695
|
+
### Codex automations
|
|
696
|
+
|
|
697
|
+
`templates init automation` can scaffold two 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
|
+
Files are written to:
|
|
705
|
+
|
|
706
|
+
- `~/.codex/automations/<name>/automation.toml`
|
|
707
|
+
- `~/.codex/automations/<name>/memory.md`
|
|
708
|
+
|
|
709
|
+
Example project automation:
|
|
710
|
+
|
|
711
|
+
```bash
|
|
712
|
+
fclt templates init automation tool-call-audit \
|
|
713
|
+
--scope project \
|
|
714
|
+
--project-root /path/to/repo \
|
|
715
|
+
--name project-tool-audit \
|
|
716
|
+
--status ACTIVE
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
Example global automation:
|
|
720
|
+
|
|
721
|
+
```bash
|
|
722
|
+
fclt templates init automation learning-review \
|
|
723
|
+
--scope wide \
|
|
724
|
+
--cwds /path/to/repo-a,/path/to/repo-b \
|
|
725
|
+
--status PAUSED
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Interactive prompt example:
|
|
729
|
+
|
|
730
|
+
```bash
|
|
731
|
+
fclt templates init automation learning-review
|
|
732
|
+
# prompts for scope, then lets you select known workspaces or add custom paths.
|
|
733
|
+
```
|
|
734
|
+
|
|
663
735
|
For full flags and exact usage:
|
|
664
736
|
```bash
|
|
665
737
|
fclt --help
|
package/package.json
CHANGED
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,123 @@ 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
|
+
- Grounding: prefer evidence from session messages, tool calls, shell commands, diffs, tests, commits, and touched files.
|
|
342
|
+
- Threshold: only encode signal when you can name what was learned, why it matters, and the most plausible destination.
|
|
343
|
+
- Scope: default to project writeback unless the signal clearly belongs in global doctrine or a shared capability.
|
|
344
|
+
- Verification: distinguish one-off friction from a repeated pattern before escalating it.
|
|
345
|
+
- If available, use [$feedback-loop-setup]({{feedbackLoopSkill}}) when the review needs stronger feedback loops or verification framing.
|
|
346
|
+
- If available, use [$capability-evolution]({{capabilityEvolutionSkill}}) when repeated signal should become a concrete proposal.
|
|
347
|
+
- If available, delegate bounded review slices to \`learning-extractor\`, \`writeback-curator\`, \`scope-promoter\`, \`evolution-planner\`, or \`verification-auditor\` when that materially improves the review.
|
|
348
|
+
`,
|
|
349
|
+
prompt: `Goal: review recent Codex work in the configured CWDs and convert durable, evidence-backed signal into writebacks or reviewable evolution proposals.
|
|
350
|
+
|
|
351
|
+
Before producing output:
|
|
352
|
+
- Treat [AGENTS.md]({{codexAgents}}) as the rendered operating-model baseline for this Codex environment.
|
|
353
|
+
- Use [LEARNING_AND_WRITEBACK.md]({{aiLearningAndWriteback}}) and [EVOLUTION.md]({{aiEvolution}}) as the durable doctrine for writeback and capability change decisions.
|
|
354
|
+
- Use [FEEDBACK_LOOPS.md]({{aiFeedbackLoops}}) and [VERIFICATION.md]({{aiVerification}}) when you need stronger loop design or more defensible proof.
|
|
355
|
+
- If available, use [$feedback-loop-setup]({{feedbackLoopSkill}}) when you need stronger feedback loops, success criteria, or verification framing.
|
|
356
|
+
- If available, use [$capability-evolution]({{capabilityEvolutionSkill}}) when repeated signal appears strong enough to become a durable capability proposal.
|
|
357
|
+
- 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.
|
|
358
|
+
|
|
359
|
+
Grounding rules:
|
|
360
|
+
- Work only from evidence in Codex sessions and nearby repo artifacts for the configured CWDs.
|
|
361
|
+
- Prefer evidence from session messages, tool calls, shell commands, diffs, tests, commits, and touched files.
|
|
362
|
+
- Do not speculate about intent or propose changes that are not anchored in evidence.
|
|
363
|
+
- Distinguish one-off friction from repeated signal. Escalate only when the signal is durable enough to matter.
|
|
364
|
+
|
|
365
|
+
Decision rules:
|
|
366
|
+
- Use \`fclt ai writeback add\` when the signal, target asset, and scope are clear.
|
|
367
|
+
- Use \`fclt ai evolve\` only when repeated signal is strong enough to justify a reviewable capability change.
|
|
368
|
+
- Prefer project scope unless the learning clearly belongs in shared global doctrine, shared agents, shared skills, or other cross-project capability.
|
|
369
|
+
- Skip weak, speculative, or purely anecdotal observations.
|
|
370
|
+
|
|
371
|
+
Verification:
|
|
372
|
+
- Verify every claim against at least one concrete artifact.
|
|
373
|
+
- Call out residual uncertainty instead of overstating confidence.
|
|
374
|
+
- Separate missing context, weak verification, failed execution, and reusable pattern; do not collapse them together.
|
|
375
|
+
|
|
376
|
+
Output:
|
|
377
|
+
- Recorded writebacks: what you recorded, why, and the target asset or command used.
|
|
378
|
+
- Evolution candidates: only the strongest repeated signals, with rationale and likely scope.
|
|
379
|
+
- Watch list: promising signals not yet strong enough to encode.
|
|
380
|
+
- Gaps in current operating model or verification harness: only if evidence supports them.
|
|
381
|
+
|
|
382
|
+
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.`,
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
id: "tool-call-audit",
|
|
386
|
+
title: "Tool Call Audit",
|
|
387
|
+
description:
|
|
388
|
+
"Checks whether repeated Codex tool usage looks repetitive or missing guardrails and proposes operating-model adjustments.",
|
|
389
|
+
defaultRRule: "RRULE:FREQ=WEEKLY;BYHOUR=10;BYMINUTE=0;BYDAY=MO,WE,FR",
|
|
390
|
+
defaultStatus: "PAUSED",
|
|
391
|
+
defaultModel: "gpt-5.4",
|
|
392
|
+
defaultReasoningEffort: "high",
|
|
393
|
+
scope: "wide",
|
|
394
|
+
memory: `# Tool Call Audit
|
|
395
|
+
|
|
396
|
+
Use this memory for continuity:
|
|
397
|
+
|
|
398
|
+
- Focus on repeated tool failures, retries, shallow-success loops, and missing operating-model guardrails.
|
|
399
|
+
- Distinguish whether the root issue is instruction quality, missing verification, missing skill usage, missing subagent delegation, or a real tool limitation.
|
|
400
|
+
- Prefer reusable operating-model changes over one-off commentary.
|
|
401
|
+
- If available, use [$feedback-loop-setup]({{feedbackLoopSkill}}) when the audit reveals weak or gameable verification loops.
|
|
402
|
+
- If available, use [$capability-evolution]({{capabilityEvolutionSkill}}) when the same pattern should become a lasting capability change.
|
|
403
|
+
- If available, delegate bounded slices to \`verification-auditor\`, \`writeback-curator\`, or \`evolution-planner\` when that improves rigor.
|
|
404
|
+
`,
|
|
405
|
+
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.
|
|
406
|
+
|
|
407
|
+
Before producing output:
|
|
408
|
+
- Treat [AGENTS.md]({{codexAgents}}) as the rendered operating-model baseline for this Codex environment.
|
|
409
|
+
- Use [LEARNING_AND_WRITEBACK.md]({{aiLearningAndWriteback}}), [EVOLUTION.md]({{aiEvolution}}), and [VERIFICATION.md]({{aiVerification}}) when deciding whether a repeated operational pattern deserves durable change.
|
|
410
|
+
- Use [FEEDBACK_LOOPS.md]({{aiFeedbackLoops}}) when the audit exposes weak, stale, or gameable loops.
|
|
411
|
+
- If available, use [$feedback-loop-setup]({{feedbackLoopSkill}}) when the audit exposes weak, stale, or gameable verification loops.
|
|
412
|
+
- If available, use [$capability-evolution]({{capabilityEvolutionSkill}}) when repeated operational pain should become a durable capability proposal.
|
|
413
|
+
- If it will materially improve rigor, explicitly ask Codex to spawn focused subagents such as \`verification-auditor\`, \`writeback-curator\`, \`scope-promoter\`, or \`evolution-planner\`.
|
|
414
|
+
|
|
415
|
+
Grounding rules:
|
|
416
|
+
- Anchor findings in concrete evidence from session messages, tool calls, shell commands, diffs, tests, commits, and touched files.
|
|
417
|
+
- Focus on repeated misses, repeated retries, expensive dead ends, missing skill use, missing delegation, or weak proof of correctness.
|
|
418
|
+
- Do not report style-only observations unless they hide a real operational problem.
|
|
419
|
+
|
|
420
|
+
For each candidate pattern, determine:
|
|
421
|
+
- what tool, agent, or command pattern recurred,
|
|
422
|
+
- what the actual failure mode or inefficiency was,
|
|
423
|
+
- what evidence supports the pattern,
|
|
424
|
+
- whether the better fix is instruction, skill usage, subagent usage, verification, or a capability change.
|
|
425
|
+
|
|
426
|
+
Decision rules:
|
|
427
|
+
- Use \`fclt ai writeback add\` when the signal and target destination are clear.
|
|
428
|
+
- Use \`fclt ai evolve\` only when the pattern is repeated enough to justify a durable capability change.
|
|
429
|
+
- Prefer project scope unless the problem clearly generalizes across projects or global doctrine.
|
|
430
|
+
- Skip isolated incidents that do not justify durable change.
|
|
431
|
+
|
|
432
|
+
Output:
|
|
433
|
+
- Recorded writebacks.
|
|
434
|
+
- Evolution candidates.
|
|
435
|
+
- Watch list.
|
|
436
|
+
- Operational gaps: the most important missing skill, missing instruction, weak loop, or missing guardrail revealed by the audit.
|
|
437
|
+
|
|
438
|
+
Keep the output concise, evidence-backed, and biased toward durable improvement rather than narration.`,
|
|
439
|
+
},
|
|
440
|
+
];
|
|
441
|
+
|
|
293
442
|
function isSafePathString(p: string): boolean {
|
|
294
443
|
return !p.includes("\0");
|
|
295
444
|
}
|
|
@@ -321,6 +470,640 @@ function renderTemplate(text: string, values: Record<string, string>): string {
|
|
|
321
470
|
return out;
|
|
322
471
|
}
|
|
323
472
|
|
|
473
|
+
function automationTemplateValues(homeDir: string): Record<string, string> {
|
|
474
|
+
const codexRoot = join(homeDir, ".codex");
|
|
475
|
+
const aiRoot = join(homeDir, ".ai");
|
|
476
|
+
return {
|
|
477
|
+
codexAgents: join(codexRoot, "AGENTS.md"),
|
|
478
|
+
aiLearningAndWriteback: join(
|
|
479
|
+
aiRoot,
|
|
480
|
+
"instructions",
|
|
481
|
+
"LEARNING_AND_WRITEBACK.md"
|
|
482
|
+
),
|
|
483
|
+
aiEvolution: join(aiRoot, "instructions", "EVOLUTION.md"),
|
|
484
|
+
aiFeedbackLoops: join(aiRoot, "instructions", "FEEDBACK_LOOPS.md"),
|
|
485
|
+
aiVerification: join(aiRoot, "instructions", "VERIFICATION.md"),
|
|
486
|
+
feedbackLoopSkill: join(
|
|
487
|
+
codexRoot,
|
|
488
|
+
"skills",
|
|
489
|
+
"feedback-loop-setup",
|
|
490
|
+
"SKILL.md"
|
|
491
|
+
),
|
|
492
|
+
capabilityEvolutionSkill: join(
|
|
493
|
+
codexRoot,
|
|
494
|
+
"skills",
|
|
495
|
+
"capability-evolution",
|
|
496
|
+
"SKILL.md"
|
|
497
|
+
),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function quoteTomlString(value: string): string {
|
|
502
|
+
return JSON.stringify(value);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function quoteTomlStringArray(values: string[]): string {
|
|
506
|
+
return `[${values.map(quoteTomlString).join(", ")}]`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function normalizeCwdList(raw: string | null | undefined): string[] {
|
|
510
|
+
if (!raw) {
|
|
511
|
+
return [];
|
|
512
|
+
}
|
|
513
|
+
return raw
|
|
514
|
+
.split(",")
|
|
515
|
+
.map((entry) => entry.trim())
|
|
516
|
+
.filter((entry) => entry.length > 0);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function isInteractiveOutputRequested(args: string[]): boolean {
|
|
520
|
+
return (
|
|
521
|
+
!args.includes("--json") &&
|
|
522
|
+
process.stdin.isTTY === true &&
|
|
523
|
+
process.stdout.isTTY === true
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function parseAutomationScope(
|
|
528
|
+
raw: string | null
|
|
529
|
+
): BuiltinAutomationTemplateScope | null {
|
|
530
|
+
if (!raw) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
const normalized = raw.trim().toLowerCase();
|
|
534
|
+
if (
|
|
535
|
+
normalized === "global" ||
|
|
536
|
+
normalized === "project" ||
|
|
537
|
+
normalized === "wide"
|
|
538
|
+
) {
|
|
539
|
+
return normalized;
|
|
540
|
+
}
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function expandPathForUserHome(p: string, home: string): string {
|
|
545
|
+
if (p === "~") {
|
|
546
|
+
return home;
|
|
547
|
+
}
|
|
548
|
+
if (p.startsWith("~/")) {
|
|
549
|
+
return join(home, p.slice(2));
|
|
550
|
+
}
|
|
551
|
+
return p;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function normalizeCwdInput(
|
|
555
|
+
raw: string,
|
|
556
|
+
cwd: string,
|
|
557
|
+
homeDir: string
|
|
558
|
+
): string[] {
|
|
559
|
+
return normalizeCwdList(raw)
|
|
560
|
+
.map((entry) => {
|
|
561
|
+
const expanded = expandPathForUserHome(entry, homeDir);
|
|
562
|
+
return resolve(cwd, expanded);
|
|
563
|
+
})
|
|
564
|
+
.filter((entry) => isSafePathString(entry));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function normalizePromptPath(
|
|
568
|
+
raw: string,
|
|
569
|
+
cwd: string,
|
|
570
|
+
homeDir: string
|
|
571
|
+
): string | null {
|
|
572
|
+
const trimmed = raw.trim();
|
|
573
|
+
if (!trimmed) {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
return resolve(cwd, expandPathForUserHome(trimmed, homeDir));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function normalizePromptPathList(
|
|
580
|
+
raw: string,
|
|
581
|
+
cwd: string,
|
|
582
|
+
homeDir: string
|
|
583
|
+
): string[] {
|
|
584
|
+
return raw
|
|
585
|
+
.split(PROMPT_PATH_SPLIT_RE)
|
|
586
|
+
.map((entry) => normalizePromptPath(entry, cwd, homeDir))
|
|
587
|
+
.filter((value): value is string => Boolean(value));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function runGitCommand(
|
|
591
|
+
cwd: string,
|
|
592
|
+
args: string[]
|
|
593
|
+
): { stdout: string; status: number } | null {
|
|
594
|
+
try {
|
|
595
|
+
const result = spawnSync("git", args, {
|
|
596
|
+
cwd,
|
|
597
|
+
encoding: "utf8",
|
|
598
|
+
maxBuffer: 1_000_000,
|
|
599
|
+
});
|
|
600
|
+
if (!result || result.status === null || result.status === undefined) {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
605
|
+
status: result.status,
|
|
606
|
+
};
|
|
607
|
+
} catch {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function findGitRootFromPath(cwd: string): string | null {
|
|
613
|
+
const result = runGitCommand(cwd, ["rev-parse", "--show-toplevel"]);
|
|
614
|
+
if (!result || result.status !== 0) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
const root = result.stdout.trim();
|
|
618
|
+
return root || null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function parseGitWorktreeList(raw: string): string[] {
|
|
622
|
+
const lines = raw.split(GIT_WORKTREE_LINE_RE);
|
|
623
|
+
const out: string[] = [];
|
|
624
|
+
for (const line of lines) {
|
|
625
|
+
if (!line.startsWith("worktree ")) {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const value = line.slice("worktree ".length).trim();
|
|
629
|
+
if (value) {
|
|
630
|
+
out.push(value);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return out;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function addGitWorkspaceCandidatesFromDirectory(
|
|
637
|
+
root: string,
|
|
638
|
+
out: Map<string, AutomationCwdCandidate>,
|
|
639
|
+
homeDir: string
|
|
640
|
+
) {
|
|
641
|
+
const gitRootPath = resolve(root);
|
|
642
|
+
const direct = normalizePromptPath(root, gitRootPath, homeDir);
|
|
643
|
+
if (direct) {
|
|
644
|
+
const gitFile = join(gitRootPath, ".git");
|
|
645
|
+
try {
|
|
646
|
+
await Bun.file(gitFile).stat();
|
|
647
|
+
out.set(gitRootPath, {
|
|
648
|
+
value: gitRootPath,
|
|
649
|
+
label: `${basename(gitRootPath)} (root)`,
|
|
650
|
+
hint: `Git root: ${gitRootPath}`,
|
|
651
|
+
});
|
|
652
|
+
} catch {
|
|
653
|
+
// Not a git root at this path.
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const dirEntries = await readdir(root, { withFileTypes: true }).catch(
|
|
658
|
+
() => []
|
|
659
|
+
);
|
|
660
|
+
for (const entry of dirEntries) {
|
|
661
|
+
if (!entry.isDirectory()) {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const candidate = join(root, entry.name);
|
|
665
|
+
const gitDir = join(candidate, ".git");
|
|
666
|
+
try {
|
|
667
|
+
await Bun.file(gitDir).stat();
|
|
668
|
+
const abs = resolve(candidate);
|
|
669
|
+
const existing = out.get(abs);
|
|
670
|
+
if (!existing) {
|
|
671
|
+
out.set(abs, {
|
|
672
|
+
value: abs,
|
|
673
|
+
label: `${entry.name} (candidate)`,
|
|
674
|
+
hint: `Git workspace: ${abs}`,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
} catch {
|
|
678
|
+
// Not a git directory.
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async function collectKnownAutomationCwdCandidates(
|
|
684
|
+
homeDir: string,
|
|
685
|
+
cwd: string
|
|
686
|
+
): Promise<AutomationCwdCandidate[]> {
|
|
687
|
+
const discovered = new Map<string, AutomationCwdCandidate>();
|
|
688
|
+
const cwdResolved = resolve(cwd);
|
|
689
|
+
|
|
690
|
+
const gitRoot = findGitRootFromPath(cwdResolved);
|
|
691
|
+
if (gitRoot) {
|
|
692
|
+
const worktreeResult = runGitCommand(gitRoot, [
|
|
693
|
+
"worktree",
|
|
694
|
+
"list",
|
|
695
|
+
"--porcelain",
|
|
696
|
+
]);
|
|
697
|
+
if (worktreeResult && worktreeResult.status === 0) {
|
|
698
|
+
for (const pathValue of parseGitWorktreeList(worktreeResult.stdout)) {
|
|
699
|
+
const abs = resolve(pathValue);
|
|
700
|
+
if (!discovered.has(abs)) {
|
|
701
|
+
discovered.set(abs, {
|
|
702
|
+
value: abs,
|
|
703
|
+
label: `${basename(abs)} (git worktree)`,
|
|
704
|
+
hint: abs,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (discovered.size === 0) {
|
|
711
|
+
const abs = resolve(gitRoot);
|
|
712
|
+
discovered.set(abs, {
|
|
713
|
+
value: abs,
|
|
714
|
+
label: `${basename(abs)} (project root)`,
|
|
715
|
+
hint: abs,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const cfg = readFacultConfig(homeDir);
|
|
721
|
+
for (const rawPath of cfg?.scanFrom ?? []) {
|
|
722
|
+
const scanRoot = expandPathForUserHome(rawPath, homeDir);
|
|
723
|
+
await addGitWorkspaceCandidatesFromDirectory(scanRoot, discovered, homeDir);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const automationRoot = join(homeDir, ".codex", "automations");
|
|
727
|
+
const entries = await readdir(automationRoot, { withFileTypes: true }).catch(
|
|
728
|
+
() => []
|
|
729
|
+
);
|
|
730
|
+
for (const entry of entries) {
|
|
731
|
+
if (!entry.isDirectory()) {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const tomlPath = join(automationRoot, entry.name, "automation.toml");
|
|
735
|
+
try {
|
|
736
|
+
const rawToml = await Bun.file(tomlPath).text();
|
|
737
|
+
const parsed = Bun.TOML.parse(rawToml) as Record<string, unknown>;
|
|
738
|
+
const cwds = parsed.cwds;
|
|
739
|
+
if (!Array.isArray(cwds)) {
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
for (const rawValue of cwds) {
|
|
743
|
+
if (typeof rawValue !== "string") {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
const normalized = normalizePromptPath(rawValue, cwdResolved, homeDir);
|
|
747
|
+
if (!normalized) {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const label = basename(normalized);
|
|
751
|
+
if (!discovered.has(normalized)) {
|
|
752
|
+
discovered.set(normalized, {
|
|
753
|
+
value: normalized,
|
|
754
|
+
label: `${label} (from Codex automation)`,
|
|
755
|
+
hint: normalized,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
} catch {
|
|
760
|
+
// Ignore malformed or missing automation files.
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return Array.from(discovered.values()).sort((a, b) =>
|
|
765
|
+
a.label.localeCompare(b.label, "en-US")
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function resolveAutomationScopeInputs(opts: {
|
|
770
|
+
template: BuiltinAutomationTemplate;
|
|
771
|
+
requestedScope: string | null;
|
|
772
|
+
requestedProjectRoot: string | null;
|
|
773
|
+
requestedCwdsRaw: string | null;
|
|
774
|
+
requestedCwdsArray?: string[];
|
|
775
|
+
homeDir: string;
|
|
776
|
+
cwd: string;
|
|
777
|
+
interactive: boolean;
|
|
778
|
+
}): Promise<{
|
|
779
|
+
scope: string | null;
|
|
780
|
+
projectRoot: string | null;
|
|
781
|
+
cwds: string[] | null;
|
|
782
|
+
}> {
|
|
783
|
+
const parsedRequestedScope = parseAutomationScope(opts.requestedScope);
|
|
784
|
+
if (opts.requestedScope && !parsedRequestedScope) {
|
|
785
|
+
throw new Error(`Unsupported automation scope: ${opts.requestedScope}`);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const requestedCwds = opts.requestedCwdsArray?.length
|
|
789
|
+
? opts.requestedCwdsArray
|
|
790
|
+
: normalizeCwdInput(opts.requestedCwdsRaw ?? "", opts.cwd, opts.homeDir);
|
|
791
|
+
const requestedProjectRoot = opts.requestedProjectRoot
|
|
792
|
+
? normalizePromptPath(opts.requestedProjectRoot, opts.cwd, opts.homeDir)
|
|
793
|
+
: null;
|
|
794
|
+
|
|
795
|
+
if (
|
|
796
|
+
!opts.interactive ||
|
|
797
|
+
(parsedRequestedScope &&
|
|
798
|
+
(parsedRequestedScope === "global" || parsedRequestedScope === "wide") &&
|
|
799
|
+
requestedCwds.length > 0) ||
|
|
800
|
+
(parsedRequestedScope === "project" && requestedProjectRoot)
|
|
801
|
+
) {
|
|
802
|
+
return {
|
|
803
|
+
scope: parsedRequestedScope,
|
|
804
|
+
projectRoot:
|
|
805
|
+
parsedRequestedScope === "project" ? requestedProjectRoot : null,
|
|
806
|
+
cwds:
|
|
807
|
+
parsedRequestedScope === "global" || parsedRequestedScope === "wide"
|
|
808
|
+
? requestedCwds
|
|
809
|
+
: [],
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const candidates = await collectKnownAutomationCwdCandidates(
|
|
814
|
+
opts.homeDir,
|
|
815
|
+
opts.cwd
|
|
816
|
+
);
|
|
817
|
+
const scopeDefault: BuiltinAutomationTemplateScope =
|
|
818
|
+
parsedRequestedScope ?? opts.template.scope;
|
|
819
|
+
|
|
820
|
+
let scope: BuiltinAutomationTemplateScope = scopeDefault;
|
|
821
|
+
if (parsedRequestedScope) {
|
|
822
|
+
scope = parsedRequestedScope;
|
|
823
|
+
} else {
|
|
824
|
+
const chosen = await select({
|
|
825
|
+
message: "Choose automation scope",
|
|
826
|
+
options: [
|
|
827
|
+
{ value: "project", label: "project", hint: "Track one project root" },
|
|
828
|
+
{
|
|
829
|
+
value: "wide",
|
|
830
|
+
label: "wide",
|
|
831
|
+
hint: "Track many explicit project roots",
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
value: "global",
|
|
835
|
+
label: "global",
|
|
836
|
+
hint: "Create a global/default scaffold",
|
|
837
|
+
},
|
|
838
|
+
],
|
|
839
|
+
initialValue: scopeDefault,
|
|
840
|
+
});
|
|
841
|
+
if (isCancel(chosen)) {
|
|
842
|
+
process.exit(1);
|
|
843
|
+
}
|
|
844
|
+
scope = chosen;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (scope === "project" && !requestedProjectRoot) {
|
|
848
|
+
if (!candidates.length) {
|
|
849
|
+
const txt = await text({
|
|
850
|
+
message: "Project root path",
|
|
851
|
+
placeholder: opts.cwd,
|
|
852
|
+
});
|
|
853
|
+
if (isCancel(txt) || !txt || typeof txt !== "string") {
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
857
|
+
scope,
|
|
858
|
+
projectRoot: normalizePromptPath(txt, opts.cwd, opts.homeDir),
|
|
859
|
+
cwds: [],
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const choices = [
|
|
864
|
+
...candidates.map((c) => ({
|
|
865
|
+
value: c.value,
|
|
866
|
+
label: c.label,
|
|
867
|
+
hint: c.hint,
|
|
868
|
+
})),
|
|
869
|
+
{
|
|
870
|
+
value: "__custom__",
|
|
871
|
+
label: "Custom project path",
|
|
872
|
+
hint: "Enter a different absolute or relative path",
|
|
873
|
+
},
|
|
874
|
+
];
|
|
875
|
+
const chosen = await select({
|
|
876
|
+
message: "Select project scope root",
|
|
877
|
+
options: choices,
|
|
878
|
+
initialValue: candidates[0]?.value ?? "__custom__",
|
|
879
|
+
});
|
|
880
|
+
if (isCancel(chosen)) {
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
if (chosen === "__custom__") {
|
|
884
|
+
const txt = await text({
|
|
885
|
+
message: "Project root path",
|
|
886
|
+
placeholder: opts.cwd,
|
|
887
|
+
});
|
|
888
|
+
if (isCancel(txt) || !txt || typeof txt !== "string") {
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
scope,
|
|
893
|
+
projectRoot: normalizePromptPath(txt, opts.cwd, opts.homeDir),
|
|
894
|
+
cwds: [],
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
return { scope, projectRoot: chosen, cwds: [] };
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (scope === "global" || scope === "wide") {
|
|
901
|
+
if (requestedCwds.length > 0) {
|
|
902
|
+
return { scope, projectRoot: null, cwds: requestedCwds };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (!candidates.length) {
|
|
906
|
+
const txt = await text({
|
|
907
|
+
message: "Workspace paths (comma-separated or leave blank for none)",
|
|
908
|
+
placeholder: "",
|
|
909
|
+
});
|
|
910
|
+
if (isCancel(txt) || typeof txt !== "string") {
|
|
911
|
+
process.exit(1);
|
|
912
|
+
}
|
|
913
|
+
const parsed = normalizePromptPathList(txt, opts.cwd, opts.homeDir);
|
|
914
|
+
return { scope, projectRoot: null, cwds: parsed };
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const chosen = await multiselect({
|
|
918
|
+
message: "Select workspaces",
|
|
919
|
+
options: [
|
|
920
|
+
...candidates.map((c) => ({
|
|
921
|
+
value: c.value,
|
|
922
|
+
label: c.label,
|
|
923
|
+
hint: c.hint,
|
|
924
|
+
})),
|
|
925
|
+
{
|
|
926
|
+
value: "__manual__",
|
|
927
|
+
label: "Add custom paths",
|
|
928
|
+
hint: "Comma-separated absolute or relative paths",
|
|
929
|
+
},
|
|
930
|
+
],
|
|
931
|
+
required: false,
|
|
932
|
+
});
|
|
933
|
+
if (isCancel(chosen) || !Array.isArray(chosen)) {
|
|
934
|
+
process.exit(1);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const base = chosen.filter((value) => value !== "__manual__");
|
|
938
|
+
if (!chosen.includes("__manual__")) {
|
|
939
|
+
return { scope, projectRoot: null, cwds: base };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const manual = await text({
|
|
943
|
+
message: "Additional workspace paths (comma-separated)",
|
|
944
|
+
placeholder: "",
|
|
945
|
+
});
|
|
946
|
+
if (isCancel(manual) || typeof manual !== "string") {
|
|
947
|
+
process.exit(1);
|
|
948
|
+
}
|
|
949
|
+
const manualList = normalizePromptPathList(manual, opts.cwd, opts.homeDir);
|
|
950
|
+
return {
|
|
951
|
+
scope,
|
|
952
|
+
projectRoot: null,
|
|
953
|
+
cwds: uniqueSorted([...base, ...manualList]),
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return {
|
|
958
|
+
scope,
|
|
959
|
+
projectRoot: null,
|
|
960
|
+
cwds: requestedCwds,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function sanitizeAutomationName(value: string): string {
|
|
965
|
+
const safe = value
|
|
966
|
+
.trim()
|
|
967
|
+
.toLowerCase()
|
|
968
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
969
|
+
.replace(/-+/g, "-")
|
|
970
|
+
.replace(/^-+|-+$/g, "");
|
|
971
|
+
if (!safe) {
|
|
972
|
+
throw new Error("Invalid automation name");
|
|
973
|
+
}
|
|
974
|
+
return safe;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function pickScopeTemplateCwds(opts: {
|
|
978
|
+
template: BuiltinAutomationTemplate;
|
|
979
|
+
requestedScope: string | null;
|
|
980
|
+
providedCwds: string[];
|
|
981
|
+
projectRoot: string | null;
|
|
982
|
+
cwd: string;
|
|
983
|
+
}): string[] {
|
|
984
|
+
const requested = (opts.requestedScope ?? "global").trim().toLowerCase();
|
|
985
|
+
if (!["global", "project", "wide"].includes(requested)) {
|
|
986
|
+
throw new Error(`Unsupported automation scope: ${opts.requestedScope}`);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (requested === "project") {
|
|
990
|
+
if (opts.projectRoot) {
|
|
991
|
+
return [resolve(opts.projectRoot)];
|
|
992
|
+
}
|
|
993
|
+
return [resolve(opts.cwd)];
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (opts.providedCwds.length) {
|
|
997
|
+
return opts.providedCwds.map((pathValue) => resolve(pathValue));
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (opts.template.scope === "project") {
|
|
1001
|
+
return [resolve(opts.cwd)];
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
async function scaffoldCodexAutomationTemplate(args: {
|
|
1008
|
+
homeDir?: string;
|
|
1009
|
+
cwd?: string;
|
|
1010
|
+
templateId: string;
|
|
1011
|
+
force?: boolean;
|
|
1012
|
+
dryRun?: boolean;
|
|
1013
|
+
name?: string;
|
|
1014
|
+
scope?: string | null;
|
|
1015
|
+
projectRoot?: string | null;
|
|
1016
|
+
cwds?: string[] | null;
|
|
1017
|
+
cwdsRaw?: string | null;
|
|
1018
|
+
rrule?: string | null;
|
|
1019
|
+
status?: string | null;
|
|
1020
|
+
}): Promise<{
|
|
1021
|
+
installedAs: string;
|
|
1022
|
+
path: string;
|
|
1023
|
+
dryRun: boolean;
|
|
1024
|
+
changedPaths: string[];
|
|
1025
|
+
}> {
|
|
1026
|
+
const home = args.homeDir ?? homedir();
|
|
1027
|
+
const cwd = resolve(args.cwd ?? process.cwd());
|
|
1028
|
+
const template = BUILTIN_AUTOMATION_TEMPLATES.find(
|
|
1029
|
+
(candidate) => candidate.id === args.templateId
|
|
1030
|
+
);
|
|
1031
|
+
if (!template) {
|
|
1032
|
+
throw new Error(`Unknown automation template: ${args.templateId}`);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const safeName = sanitizeAutomationName(args.name ?? template.id);
|
|
1036
|
+
const requestedCwds = Array.isArray(args.cwds)
|
|
1037
|
+
? args.cwds
|
|
1038
|
+
: normalizeCwdList(args.cwdsRaw ?? "");
|
|
1039
|
+
const cwds = pickScopeTemplateCwds({
|
|
1040
|
+
template,
|
|
1041
|
+
requestedScope: args.scope ?? null,
|
|
1042
|
+
providedCwds: requestedCwds,
|
|
1043
|
+
projectRoot: args.projectRoot ?? null,
|
|
1044
|
+
cwd,
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
const scopeStatus =
|
|
1048
|
+
args.status === "active" || args.status === "ACTIVE"
|
|
1049
|
+
? "ACTIVE"
|
|
1050
|
+
: args.status === "paused" || args.status === "PAUSED"
|
|
1051
|
+
? "PAUSED"
|
|
1052
|
+
: template.defaultStatus;
|
|
1053
|
+
const rrule = args.rrule?.trim() || template.defaultRRule;
|
|
1054
|
+
const model = template.defaultModel;
|
|
1055
|
+
const reasoningEffort = template.defaultReasoningEffort;
|
|
1056
|
+
const templateValues = automationTemplateValues(home);
|
|
1057
|
+
const renderedPrompt = renderTemplate(template.prompt.trim(), templateValues);
|
|
1058
|
+
const renderedMemory = renderTemplate(template.memory.trim(), templateValues);
|
|
1059
|
+
|
|
1060
|
+
const timestamp = String(Date.now());
|
|
1061
|
+
const automationPath = join(home, ".codex", "automations", safeName);
|
|
1062
|
+
const automationTomlPath = join(automationPath, "automation.toml");
|
|
1063
|
+
const memoryPath = join(automationPath, "memory.md");
|
|
1064
|
+
|
|
1065
|
+
const automationToml = `version = 1
|
|
1066
|
+
id = ${quoteTomlString(safeName)}
|
|
1067
|
+
name = ${quoteTomlString(template.title)}
|
|
1068
|
+
prompt = ${quoteTomlString(renderedPrompt)}
|
|
1069
|
+
status = ${quoteTomlString(scopeStatus)}
|
|
1070
|
+
rrule = ${quoteTomlString(rrule)}
|
|
1071
|
+
model = ${quoteTomlString(model)}
|
|
1072
|
+
reasoning_effort = ${quoteTomlString(reasoningEffort)}
|
|
1073
|
+
cwds = ${quoteTomlStringArray(cwds)}
|
|
1074
|
+
created_at = ${timestamp}
|
|
1075
|
+
updated_at = ${timestamp}
|
|
1076
|
+
`;
|
|
1077
|
+
|
|
1078
|
+
const memory = `${renderedMemory}\n`;
|
|
1079
|
+
const changedPaths: string[] = [];
|
|
1080
|
+
|
|
1081
|
+
const automationTomlExists = await fileExists(automationTomlPath);
|
|
1082
|
+
if (!automationTomlExists || args.force) {
|
|
1083
|
+
changedPaths.push(automationTomlPath);
|
|
1084
|
+
if (!args.dryRun) {
|
|
1085
|
+
await mkdir(automationPath, { recursive: true });
|
|
1086
|
+
await Bun.write(automationTomlPath, `${automationToml}\n`);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const memoryExists = await fileExists(memoryPath);
|
|
1091
|
+
if (!memoryExists || args.force) {
|
|
1092
|
+
changedPaths.push(memoryPath);
|
|
1093
|
+
if (!args.dryRun) {
|
|
1094
|
+
await mkdir(automationPath, { recursive: true });
|
|
1095
|
+
await Bun.write(memoryPath, memory);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
return {
|
|
1100
|
+
installedAs: safeName,
|
|
1101
|
+
path: automationPath,
|
|
1102
|
+
dryRun: Boolean(args.dryRun),
|
|
1103
|
+
changedPaths: uniqueSorted(changedPaths),
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
324
1107
|
function builtinPackRoot(packName: string): string {
|
|
325
1108
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
326
1109
|
return join(here, "..", "assets", "packs", packName);
|
|
@@ -1685,9 +2468,19 @@ Usage:
|
|
|
1685
2468
|
fclt templates init agents [--force] [--dry-run]
|
|
1686
2469
|
fclt templates init claude [--force] [--dry-run]
|
|
1687
2470
|
fclt templates init project-ai [--force] [--dry-run]
|
|
2471
|
+
fclt templates init automation <template-id> [--scope global|project|wide] [--name <name>] [--project-root <path>] [--cwds <path1,path2>] [--rrule <RRULE>]
|
|
2472
|
+
--status [PAUSED|ACTIVE] [--force] [--dry-run]
|
|
1688
2473
|
|
|
1689
|
-
Notes:
|
|
2474
|
+
Notes:
|
|
1690
2475
|
- Templates are powered by the builtin remote index (${BUILTIN_INDEX_NAME}).
|
|
2476
|
+
- Automation templates scaffold Codex automation files under ~/.codex/automations/.
|
|
2477
|
+
- scope=global|wide creates a wide automation. Provide --cwds for explicit repo roots.
|
|
2478
|
+
Without --cwds, wide/global automation has no cwds by default.
|
|
2479
|
+
- scope=project creates an automation scoped to one project root.
|
|
2480
|
+
- If --scope (or --project-root / --cwds) is omitted and the command runs in an
|
|
2481
|
+
interactive terminal, you will be prompted to choose scope and candidate paths.
|
|
2482
|
+
Without explicit answers, project scope falls back to current git project and
|
|
2483
|
+
wide/global scope can be left empty.
|
|
1691
2484
|
`);
|
|
1692
2485
|
}
|
|
1693
2486
|
|
|
@@ -1984,6 +2777,13 @@ export async function templatesCommand(
|
|
|
1984
2777
|
"Seed a repo-local .ai with the built-in Facult operating-model pack.",
|
|
1985
2778
|
version: "1.0.0",
|
|
1986
2779
|
},
|
|
2780
|
+
...BUILTIN_AUTOMATION_TEMPLATES.map((item) => ({
|
|
2781
|
+
id: item.id,
|
|
2782
|
+
type: "automation",
|
|
2783
|
+
title: item.title,
|
|
2784
|
+
description: item.description,
|
|
2785
|
+
version: "wide",
|
|
2786
|
+
})),
|
|
1987
2787
|
];
|
|
1988
2788
|
if (json) {
|
|
1989
2789
|
console.log(JSON.stringify(rows, null, 2));
|
|
@@ -2003,7 +2803,7 @@ export async function templatesCommand(
|
|
|
2003
2803
|
const [kind, ...args] = rest;
|
|
2004
2804
|
if (!kind) {
|
|
2005
2805
|
console.error(
|
|
2006
|
-
"templates init requires a kind (skill|mcp|snippet|agents|claude|project-ai)"
|
|
2806
|
+
"templates init requires a kind (skill|mcp|snippet|agents|claude|project-ai|automation)"
|
|
2007
2807
|
);
|
|
2008
2808
|
process.exitCode = 2;
|
|
2009
2809
|
return;
|
|
@@ -2070,6 +2870,75 @@ export async function templatesCommand(
|
|
|
2070
2870
|
} else if (kind === "claude") {
|
|
2071
2871
|
ref = `${BUILTIN_INDEX_NAME}:claude-md-template`;
|
|
2072
2872
|
as = positional[0];
|
|
2873
|
+
} else if (kind === "automation") {
|
|
2874
|
+
const templateId = positional[0];
|
|
2875
|
+
if (!templateId) {
|
|
2876
|
+
console.error(
|
|
2877
|
+
"templates init automation requires a <template-id> (learning-review|tool-call-audit)"
|
|
2878
|
+
);
|
|
2879
|
+
process.exitCode = 2;
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
const template = BUILTIN_AUTOMATION_TEMPLATES.find(
|
|
2883
|
+
(candidate) => candidate.id === templateId
|
|
2884
|
+
);
|
|
2885
|
+
if (!template) {
|
|
2886
|
+
console.error(`Unknown automation template: ${templateId}`);
|
|
2887
|
+
process.exitCode = 1;
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
const status =
|
|
2891
|
+
parseLongFlag(args, "--status") ??
|
|
2892
|
+
parseLongFlag(args, "--automation-status");
|
|
2893
|
+
const scope = parseLongFlag(args, "--scope");
|
|
2894
|
+
const projectRoot = parseLongFlag(args, "--project-root");
|
|
2895
|
+
const cwdsRaw = parseLongFlag(args, "--cwds");
|
|
2896
|
+
const rrule = parseLongFlag(args, "--rrule");
|
|
2897
|
+
const name = parseLongFlag(args, "--name");
|
|
2898
|
+
const cwd = resolve(ctx.cwd ?? process.cwd());
|
|
2899
|
+
const home = ctx.homeDir ?? homedir();
|
|
2900
|
+
const normalizedCwds = normalizeCwdInput(cwdsRaw ?? "", cwd, home);
|
|
2901
|
+
const resolved = await resolveAutomationScopeInputs({
|
|
2902
|
+
template,
|
|
2903
|
+
requestedScope: scope,
|
|
2904
|
+
requestedProjectRoot: projectRoot,
|
|
2905
|
+
requestedCwdsRaw: cwdsRaw,
|
|
2906
|
+
requestedCwdsArray: normalizedCwds,
|
|
2907
|
+
homeDir: home,
|
|
2908
|
+
cwd,
|
|
2909
|
+
interactive: isInteractiveOutputRequested(args),
|
|
2910
|
+
});
|
|
2911
|
+
try {
|
|
2912
|
+
const result = await scaffoldCodexAutomationTemplate({
|
|
2913
|
+
homeDir: ctx.homeDir,
|
|
2914
|
+
cwd,
|
|
2915
|
+
templateId,
|
|
2916
|
+
force,
|
|
2917
|
+
dryRun,
|
|
2918
|
+
name: name ?? undefined,
|
|
2919
|
+
scope: resolved.scope,
|
|
2920
|
+
projectRoot: resolved.projectRoot,
|
|
2921
|
+
cwds: resolved.cwds,
|
|
2922
|
+
rrule,
|
|
2923
|
+
status,
|
|
2924
|
+
});
|
|
2925
|
+
if (json) {
|
|
2926
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
const action = dryRun ? "Would scaffold" : "Scaffolded";
|
|
2930
|
+
console.log(
|
|
2931
|
+
`${action} automation template as ${result.installedAs} (${result.path})`
|
|
2932
|
+
);
|
|
2933
|
+
for (const path of result.changedPaths) {
|
|
2934
|
+
console.log(` - ${path}`);
|
|
2935
|
+
}
|
|
2936
|
+
return;
|
|
2937
|
+
} catch (err) {
|
|
2938
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
2939
|
+
process.exitCode = 1;
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2073
2942
|
} else {
|
|
2074
2943
|
console.error(`Unknown template kind: ${kind}`);
|
|
2075
2944
|
process.exitCode = 2;
|