facult 2.4.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/adapters/factory.ts +228 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/types.ts +22 -0
- package/src/cli-context.ts +7 -6
- package/src/index-builder.ts +10 -1
- package/src/manage.ts +95 -7
- package/src/remote.ts +3 -1
- package/src/scan.ts +26 -1
package/README.md
CHANGED
|
@@ -282,6 +282,8 @@ bunx fclt index
|
|
|
282
282
|
|
|
283
283
|
This seeds `<repo>/.ai` from the built-in Facult operating-model pack and writes a merged project index/graph under `<repo>/.ai/.facult/ai/`.
|
|
284
284
|
|
|
285
|
+
Wide learning-review automations should use this same bootstrap when they hit a local writable repo with durable project-local signal but no repo-local `.ai` yet.
|
|
286
|
+
|
|
285
287
|
### 4. Inspect what you have
|
|
286
288
|
|
|
287
289
|
```bash
|
package/package.json
CHANGED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { basename, extname } from "node:path";
|
|
2
|
+
import { renderCanonicalText } from "../agents";
|
|
3
|
+
import { generateMcpConfig, parseMcpConfig } from "./mcp";
|
|
4
|
+
import { parseSkillsDir } from "./skills";
|
|
5
|
+
import type {
|
|
6
|
+
CanonicalMcpConfig,
|
|
7
|
+
CanonicalMcpServer,
|
|
8
|
+
ParsedManagedAgentFile,
|
|
9
|
+
RenderManagedAgentOptions,
|
|
10
|
+
ToolAdapter,
|
|
11
|
+
} from "./types";
|
|
12
|
+
import { detectExplicitVersion } from "./version";
|
|
13
|
+
|
|
14
|
+
const FRONTMATTER_LINE_SPLIT_REGEX = /\r?\n/;
|
|
15
|
+
const FACTORY_AGENT_FRONTMATTER_REGEX =
|
|
16
|
+
/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
17
|
+
const LEADING_WHITESPACE_REGEX = /^\s+/;
|
|
18
|
+
const TRAILING_WHITESPACE_REGEX = /\s+$/;
|
|
19
|
+
|
|
20
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
21
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function escapeTomlMultiline(value: string): string {
|
|
25
|
+
return value.replace(/"""/g, '\\"""');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function escapeYamlString(value: string): string {
|
|
29
|
+
return JSON.stringify(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function stringifyFrontmatter(values: Record<string, string>): string {
|
|
33
|
+
return Object.entries(values)
|
|
34
|
+
.map(([key, value]) => `${key}: ${escapeYamlString(value)}`)
|
|
35
|
+
.join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseFrontmatterScalar(value: string): string {
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
if (!trimmed) {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
if (
|
|
44
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
45
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
46
|
+
) {
|
|
47
|
+
const quote = trimmed[0];
|
|
48
|
+
const inner = trimmed.slice(1, -1);
|
|
49
|
+
if (quote === '"') {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(trimmed);
|
|
52
|
+
} catch {
|
|
53
|
+
return inner;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return inner;
|
|
57
|
+
}
|
|
58
|
+
return trimmed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseFrontmatter(text: string): Record<string, string> {
|
|
62
|
+
const out: Record<string, string> = {};
|
|
63
|
+
for (const line of text.split(FRONTMATTER_LINE_SPLIT_REGEX)) {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const separator = trimmed.indexOf(":");
|
|
69
|
+
if (separator === -1) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const key = trimmed.slice(0, separator).trim();
|
|
73
|
+
const value = parseFrontmatterScalar(trimmed.slice(separator + 1));
|
|
74
|
+
if (key) {
|
|
75
|
+
out[key] = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeFactoryServer(
|
|
82
|
+
server: CanonicalMcpServer
|
|
83
|
+
): CanonicalMcpServer {
|
|
84
|
+
if (!isPlainObject(server.vendorExtensions)) {
|
|
85
|
+
return server;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { type, ...vendorExtensions } = server.vendorExtensions;
|
|
89
|
+
return {
|
|
90
|
+
...server,
|
|
91
|
+
transport:
|
|
92
|
+
typeof type === "string" && !server.transport ? type : server.transport,
|
|
93
|
+
vendorExtensions:
|
|
94
|
+
Object.keys(vendorExtensions).length > 0 ? vendorExtensions : undefined,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseFactoryMcp(config: unknown): CanonicalMcpConfig {
|
|
99
|
+
const parsed = parseMcpConfig(config);
|
|
100
|
+
for (const [name, server] of Object.entries(parsed.servers)) {
|
|
101
|
+
parsed.servers[name] = normalizeFactoryServer({ ...server });
|
|
102
|
+
}
|
|
103
|
+
return parsed;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function generateFactoryMcp(
|
|
107
|
+
canonical: CanonicalMcpConfig
|
|
108
|
+
): Record<string, unknown> {
|
|
109
|
+
const generated = generateMcpConfig(canonical, "mcpServers");
|
|
110
|
+
const servers = generated.mcpServers;
|
|
111
|
+
if (!isPlainObject(servers)) {
|
|
112
|
+
return generated;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const [name, value] of Object.entries(servers)) {
|
|
116
|
+
if (!isPlainObject(value)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const { transport, ...server } = value as Record<string, unknown>;
|
|
120
|
+
const inferredType =
|
|
121
|
+
(typeof transport === "string" ? transport : undefined) ??
|
|
122
|
+
(typeof server.url === "string"
|
|
123
|
+
? "http"
|
|
124
|
+
: typeof server.command === "string"
|
|
125
|
+
? "stdio"
|
|
126
|
+
: undefined);
|
|
127
|
+
if (inferredType && typeof server.type !== "string") {
|
|
128
|
+
server.type = inferredType;
|
|
129
|
+
}
|
|
130
|
+
if (typeof server.disabled !== "boolean") {
|
|
131
|
+
server.disabled = false;
|
|
132
|
+
}
|
|
133
|
+
servers[name] = server;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return generated;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function renderFactoryAgent(
|
|
140
|
+
options: RenderManagedAgentOptions
|
|
141
|
+
): Promise<string> {
|
|
142
|
+
const parsed = Bun.TOML.parse(options.raw) as Record<string, unknown>;
|
|
143
|
+
const name =
|
|
144
|
+
typeof parsed.name === "string"
|
|
145
|
+
? parsed.name
|
|
146
|
+
: basename(options.targetPath, extname(options.targetPath));
|
|
147
|
+
const description =
|
|
148
|
+
typeof parsed.description === "string" ? parsed.description : undefined;
|
|
149
|
+
const instructions =
|
|
150
|
+
typeof parsed.developer_instructions === "string"
|
|
151
|
+
? parsed.developer_instructions
|
|
152
|
+
: "";
|
|
153
|
+
const renderedInstructions = await renderCanonicalText(instructions, {
|
|
154
|
+
homeDir: options.homeDir,
|
|
155
|
+
rootDir: options.rootDir,
|
|
156
|
+
projectRoot: options.projectRoot,
|
|
157
|
+
targetTool: options.tool,
|
|
158
|
+
targetPath: options.targetPath,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const frontmatter = stringifyFrontmatter({
|
|
162
|
+
name,
|
|
163
|
+
...(description ? { description } : {}),
|
|
164
|
+
model: "inherit",
|
|
165
|
+
});
|
|
166
|
+
const body = renderedInstructions.trim();
|
|
167
|
+
|
|
168
|
+
return body
|
|
169
|
+
? `---\n${frontmatter}\n---\n\n${body}\n`
|
|
170
|
+
: `---\n${frontmatter}\n---\n`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function parseFactoryManagedAgentFile(
|
|
174
|
+
path: string
|
|
175
|
+
): Promise<ParsedManagedAgentFile | null> {
|
|
176
|
+
const file = Bun.file(path);
|
|
177
|
+
if (!(await file.exists())) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const raw = await file.text();
|
|
182
|
+
const match = raw.match(FACTORY_AGENT_FRONTMATTER_REGEX);
|
|
183
|
+
if (!match) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const [, frontmatterRaw, bodyRaw] = match;
|
|
188
|
+
const frontmatter = parseFrontmatter(frontmatterRaw ?? "");
|
|
189
|
+
const name = frontmatter.name || basename(path, extname(path));
|
|
190
|
+
const description = frontmatter.description || undefined;
|
|
191
|
+
const body = (bodyRaw ?? "")
|
|
192
|
+
.replace(LEADING_WHITESPACE_REGEX, "")
|
|
193
|
+
.replace(TRAILING_WHITESPACE_REGEX, "");
|
|
194
|
+
const lines = [`name = ${JSON.stringify(name)}`];
|
|
195
|
+
if (description) {
|
|
196
|
+
lines.push(`description = ${JSON.stringify(description)}`);
|
|
197
|
+
}
|
|
198
|
+
lines.push("", 'developer_instructions = """');
|
|
199
|
+
if (body) {
|
|
200
|
+
lines.push(escapeTomlMultiline(body));
|
|
201
|
+
}
|
|
202
|
+
lines.push('"""', "");
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
name,
|
|
206
|
+
raw: lines.join("\n"),
|
|
207
|
+
sourcePath: path,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export const factoryAdapter: ToolAdapter = {
|
|
212
|
+
id: "factory",
|
|
213
|
+
name: "Factory",
|
|
214
|
+
versions: ["v1"],
|
|
215
|
+
detectVersion: detectExplicitVersion,
|
|
216
|
+
getDefaultPaths: () => ({
|
|
217
|
+
mcp: "~/.factory/mcp.json",
|
|
218
|
+
skills: ["~/.factory/skills", ".factory/skills"],
|
|
219
|
+
agents: ["~/.factory/droids", ".factory/droids"],
|
|
220
|
+
}),
|
|
221
|
+
parseMcp: (config) => parseFactoryMcp(config),
|
|
222
|
+
generateMcp: (canonical) => generateFactoryMcp(canonical),
|
|
223
|
+
parseSkills: async (skillsDir) => await parseSkillsDir(skillsDir),
|
|
224
|
+
agentFileExtension: ".md",
|
|
225
|
+
renderAgent: async (options) => await renderFactoryAgent(options),
|
|
226
|
+
parseManagedAgentFile: async (path) =>
|
|
227
|
+
await parseFactoryManagedAgentFile(path),
|
|
228
|
+
};
|
package/src/adapters/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { claudeDesktopAdapter } from "./claude-desktop";
|
|
|
3
3
|
import { clawdbotAdapter } from "./clawdbot";
|
|
4
4
|
import { codexAdapter } from "./codex";
|
|
5
5
|
import { cursorAdapter } from "./cursor";
|
|
6
|
+
import { factoryAdapter } from "./factory";
|
|
6
7
|
import { referenceAdapter } from "./reference";
|
|
7
8
|
import type { ResolveVersionOptions, ToolAdapter } from "./types";
|
|
8
9
|
|
|
@@ -64,6 +65,7 @@ export async function resolveAdapterVersion(
|
|
|
64
65
|
registerAdapter(referenceAdapter);
|
|
65
66
|
registerAdapter(cursorAdapter);
|
|
66
67
|
registerAdapter(codexAdapter);
|
|
68
|
+
registerAdapter(factoryAdapter);
|
|
67
69
|
registerAdapter(claudeCliAdapter);
|
|
68
70
|
registerAdapter(claudeDesktopAdapter);
|
|
69
71
|
registerAdapter(clawdbotAdapter);
|
package/src/adapters/types.ts
CHANGED
|
@@ -18,6 +18,21 @@ export interface CanonicalSkill {
|
|
|
18
18
|
path?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export interface RenderManagedAgentOptions {
|
|
22
|
+
raw: string;
|
|
23
|
+
rootDir: string;
|
|
24
|
+
tool: string;
|
|
25
|
+
targetPath: string;
|
|
26
|
+
homeDir?: string;
|
|
27
|
+
projectRoot?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ParsedManagedAgentFile {
|
|
31
|
+
name: string;
|
|
32
|
+
raw: string;
|
|
33
|
+
sourcePath: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
export interface AdapterDefaultPaths {
|
|
22
37
|
mcp?: string;
|
|
23
38
|
skills?: string | string[];
|
|
@@ -34,6 +49,13 @@ export interface ToolAdapter {
|
|
|
34
49
|
parseSkills?: (skillsDir: string) => Promise<CanonicalSkill[]>;
|
|
35
50
|
generateMcp?: (canonical: CanonicalMcpConfig, version?: string) => unknown;
|
|
36
51
|
generateSkillsDir?: (skills: CanonicalSkill[]) => Promise<void>;
|
|
52
|
+
agentFileExtension?: string;
|
|
53
|
+
renderAgent?: (
|
|
54
|
+
options: RenderManagedAgentOptions
|
|
55
|
+
) => Promise<string> | string;
|
|
56
|
+
parseManagedAgentFile?: (
|
|
57
|
+
path: string
|
|
58
|
+
) => Promise<ParsedManagedAgentFile | null>;
|
|
37
59
|
getDefaultPaths?: () => AdapterDefaultPaths;
|
|
38
60
|
}
|
|
39
61
|
|
package/src/cli-context.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface ParsedCliContext {
|
|
|
16
16
|
sourceKind?: AssetSourceKind;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function missingProjectAiRootMessage(pathValue?: string): string {
|
|
20
|
+
const suffix = pathValue ? `: ${pathValue}` : "";
|
|
21
|
+
return `No project-local .ai root found${suffix}. Run "fclt templates init project-ai" in the repo first, or pass --root <repo>/.ai.`;
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
function expandHomePath(pathValue: string, home: string): string {
|
|
20
25
|
if (pathValue === "~") {
|
|
21
26
|
return home;
|
|
@@ -173,9 +178,7 @@ export function resolveCliContextRoot(args?: {
|
|
|
173
178
|
if (args?.rootArg) {
|
|
174
179
|
const rootDir = coerceCanonicalRoot(args.rootArg, homeDir);
|
|
175
180
|
if (scope === "project" && !projectRootFromAiRoot(rootDir, homeDir)) {
|
|
176
|
-
throw new Error(
|
|
177
|
-
`Project scope requires a repo-local .ai root: ${rootDir}`
|
|
178
|
-
);
|
|
181
|
+
throw new Error(missingProjectAiRootMessage(rootDir));
|
|
179
182
|
}
|
|
180
183
|
return rootDir;
|
|
181
184
|
}
|
|
@@ -187,9 +190,7 @@ export function resolveCliContextRoot(args?: {
|
|
|
187
190
|
if (scope === "project") {
|
|
188
191
|
const projectRoot = findNearestProjectAiRoot(cwd);
|
|
189
192
|
if (!projectRoot) {
|
|
190
|
-
throw new Error(
|
|
191
|
-
"No project-local .ai root found from the current directory"
|
|
192
|
-
);
|
|
193
|
+
throw new Error(missingProjectAiRootMessage(cwd));
|
|
193
194
|
}
|
|
194
195
|
return projectRoot;
|
|
195
196
|
}
|
package/src/index-builder.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readdir } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, join, relative } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { getAdapter } from "./adapters";
|
|
4
5
|
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
5
6
|
import {
|
|
6
7
|
type AssetScope,
|
|
@@ -31,6 +32,10 @@ interface AssetEntryBase {
|
|
|
31
32
|
shadow?: boolean;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function managedAgentFileExtension(tool: string): string {
|
|
36
|
+
return getAdapter(tool)?.agentFileExtension ?? ".toml";
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
export interface SkillEntry {
|
|
35
40
|
name: string;
|
|
36
41
|
path: string;
|
|
@@ -1213,6 +1218,7 @@ function registerManagedRenderedTargets(args: {
|
|
|
1213
1218
|
const nodes = args.graph.nodes;
|
|
1214
1219
|
for (const toolState of toolStates) {
|
|
1215
1220
|
if (toolState.agentsDir) {
|
|
1221
|
+
const extension = managedAgentFileExtension(toolState.tool);
|
|
1216
1222
|
for (const entry of Object.values(args.index.agents)) {
|
|
1217
1223
|
const sourceNodeId = sourceNodeIdForEntry({
|
|
1218
1224
|
kind: "agent",
|
|
@@ -1221,7 +1227,10 @@ function registerManagedRenderedTargets(args: {
|
|
|
1221
1227
|
if (!nodes[sourceNodeId]) {
|
|
1222
1228
|
continue;
|
|
1223
1229
|
}
|
|
1224
|
-
const targetPath = join(
|
|
1230
|
+
const targetPath = join(
|
|
1231
|
+
toolState.agentsDir,
|
|
1232
|
+
`${entry.name}${extension}`
|
|
1233
|
+
);
|
|
1225
1234
|
registerRenderedTargetNode({
|
|
1226
1235
|
graph: args.graph,
|
|
1227
1236
|
currentScope: args.currentScope,
|
package/src/manage.ts
CHANGED
|
@@ -203,6 +203,19 @@ function defaultToolPaths(
|
|
|
203
203
|
skillsDir: toolBase(".antigravity", "skills"),
|
|
204
204
|
mcpConfig: toolBase(".antigravity", "mcp.json"),
|
|
205
205
|
},
|
|
206
|
+
factory: {
|
|
207
|
+
tool: "factory",
|
|
208
|
+
skillsDir: projectRoot
|
|
209
|
+
? join(projectRoot, ".factory", "skills")
|
|
210
|
+
: homePath(home, ".factory", "skills"),
|
|
211
|
+
mcpConfig: projectRoot
|
|
212
|
+
? join(projectRoot, ".factory", "mcp.json")
|
|
213
|
+
: homePath(home, ".factory", "mcp.json"),
|
|
214
|
+
agentsDir: projectRoot
|
|
215
|
+
? join(projectRoot, ".factory", "droids")
|
|
216
|
+
: homePath(home, ".factory", "droids"),
|
|
217
|
+
toolHome: projectRoot ? undefined : homePath(home, ".factory"),
|
|
218
|
+
},
|
|
206
219
|
};
|
|
207
220
|
|
|
208
221
|
const adapterDefaults = (tool: string): ToolPaths | null => {
|
|
@@ -436,6 +449,69 @@ async function loadCanonicalAgents(
|
|
|
436
449
|
return await loadAgentsFromRoot(homePath(rootDir, "agents"));
|
|
437
450
|
}
|
|
438
451
|
|
|
452
|
+
function managedAgentFileExtension(tool: string): string {
|
|
453
|
+
return getAdapter(tool)?.agentFileExtension ?? ".toml";
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function renderManagedAgentFile(args: {
|
|
457
|
+
agent: { name: string; sourcePath: string; raw: string };
|
|
458
|
+
homeDir: string;
|
|
459
|
+
rootDir: string;
|
|
460
|
+
tool: string;
|
|
461
|
+
targetPath: string;
|
|
462
|
+
}): Promise<string> {
|
|
463
|
+
const adapter = getAdapter(args.tool);
|
|
464
|
+
if (adapter?.renderAgent) {
|
|
465
|
+
return await adapter.renderAgent({
|
|
466
|
+
raw: args.agent.raw,
|
|
467
|
+
homeDir: args.homeDir,
|
|
468
|
+
rootDir: args.rootDir,
|
|
469
|
+
projectRoot:
|
|
470
|
+
projectRootFromAiRoot(args.rootDir, args.homeDir) ?? undefined,
|
|
471
|
+
tool: args.tool,
|
|
472
|
+
targetPath: args.targetPath,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return await renderCanonicalText(args.agent.raw, {
|
|
477
|
+
homeDir: args.homeDir,
|
|
478
|
+
rootDir: args.rootDir,
|
|
479
|
+
projectRoot: projectRootFromAiRoot(args.rootDir, args.homeDir) ?? undefined,
|
|
480
|
+
targetTool: args.tool,
|
|
481
|
+
targetPath: args.targetPath,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function loadManagedAgentsFromTool(args: {
|
|
486
|
+
tool: string;
|
|
487
|
+
agentsDir: string;
|
|
488
|
+
}): Promise<{ name: string; sourcePath: string; raw: string }[]> {
|
|
489
|
+
const adapter = getAdapter(args.tool);
|
|
490
|
+
if (!adapter?.parseManagedAgentFile) {
|
|
491
|
+
return await loadAgentsFromRoot(args.agentsDir);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const extension = managedAgentFileExtension(args.tool);
|
|
495
|
+
const entries = await readdir(args.agentsDir, { withFileTypes: true }).catch(
|
|
496
|
+
() => [] as import("node:fs").Dirent[]
|
|
497
|
+
);
|
|
498
|
+
const out: { name: string; sourcePath: string; raw: string }[] = [];
|
|
499
|
+
|
|
500
|
+
for (const entry of entries) {
|
|
501
|
+
if (!(entry.isFile() && entry.name.endsWith(extension))) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const sourcePath = join(args.agentsDir, entry.name);
|
|
505
|
+
const parsed = await adapter.parseManagedAgentFile(sourcePath);
|
|
506
|
+
if (!parsed) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
out.push(parsed);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
513
|
+
}
|
|
514
|
+
|
|
439
515
|
interface AutomationEntry {
|
|
440
516
|
name: string;
|
|
441
517
|
sourceDir: string;
|
|
@@ -637,14 +713,15 @@ async function planAgentFileChanges({
|
|
|
637
713
|
const contents = new Map<string, string>();
|
|
638
714
|
const sources = new Map<string, string>();
|
|
639
715
|
const desiredPaths = new Set<string>();
|
|
716
|
+
const extension = managedAgentFileExtension(tool);
|
|
640
717
|
|
|
641
718
|
for (const agent of agents) {
|
|
642
|
-
const target = homePath(agentsDir, `${agent.name}
|
|
643
|
-
const rendered = await
|
|
719
|
+
const target = homePath(agentsDir, `${agent.name}${extension}`);
|
|
720
|
+
const rendered = await renderManagedAgentFile({
|
|
721
|
+
agent,
|
|
644
722
|
homeDir,
|
|
645
723
|
rootDir,
|
|
646
|
-
|
|
647
|
-
targetTool: tool,
|
|
724
|
+
tool,
|
|
648
725
|
targetPath: target,
|
|
649
726
|
});
|
|
650
727
|
desiredPaths.add(target);
|
|
@@ -659,7 +736,7 @@ async function planAgentFileChanges({
|
|
|
659
736
|
const remove = new Set<string>();
|
|
660
737
|
|
|
661
738
|
for (const entry of existing) {
|
|
662
|
-
if (!(entry.isFile() && entry.name.endsWith(
|
|
739
|
+
if (!(entry.isFile() && entry.name.endsWith(extension))) {
|
|
663
740
|
continue;
|
|
664
741
|
}
|
|
665
742
|
const p = homePath(agentsDir, entry.name);
|
|
@@ -1214,11 +1291,15 @@ function logManagedImportPlan(tool: string, plan: ExistingManagedImportPlan) {
|
|
|
1214
1291
|
}
|
|
1215
1292
|
|
|
1216
1293
|
async function planExistingToolAgentAdoption(args: {
|
|
1294
|
+
tool: string;
|
|
1217
1295
|
rootDir: string;
|
|
1218
1296
|
agentsDir: string;
|
|
1219
1297
|
}): Promise<ExistingManagedImportPlan> {
|
|
1220
1298
|
const plan = emptyManagedImportPlan();
|
|
1221
|
-
const agents = await
|
|
1299
|
+
const agents = await loadManagedAgentsFromTool({
|
|
1300
|
+
tool: args.tool,
|
|
1301
|
+
agentsDir: args.agentsDir,
|
|
1302
|
+
});
|
|
1222
1303
|
for (const agent of agents) {
|
|
1223
1304
|
const canonicalPath = join(
|
|
1224
1305
|
args.rootDir,
|
|
@@ -1245,12 +1326,16 @@ async function planExistingToolAgentAdoption(args: {
|
|
|
1245
1326
|
}
|
|
1246
1327
|
|
|
1247
1328
|
async function adoptExistingToolAgents(args: {
|
|
1329
|
+
tool: string;
|
|
1248
1330
|
rootDir: string;
|
|
1249
1331
|
agentsDir: string;
|
|
1250
1332
|
conflictMode: "keep-canonical" | "keep-existing";
|
|
1251
1333
|
}): Promise<ExistingManagedItem[]> {
|
|
1252
1334
|
const adopted: ExistingManagedItem[] = [];
|
|
1253
|
-
const agents = await
|
|
1335
|
+
const agents = await loadManagedAgentsFromTool({
|
|
1336
|
+
tool: args.tool,
|
|
1337
|
+
agentsDir: args.agentsDir,
|
|
1338
|
+
});
|
|
1254
1339
|
for (const agent of agents) {
|
|
1255
1340
|
const canonicalPath = join(
|
|
1256
1341
|
args.rootDir,
|
|
@@ -2019,6 +2104,7 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2019
2104
|
asManagedSkillPlan(existingSkillPlan),
|
|
2020
2105
|
toolPaths.agentsDir
|
|
2021
2106
|
? await planExistingToolAgentAdoption({
|
|
2107
|
+
tool,
|
|
2022
2108
|
rootDir,
|
|
2023
2109
|
agentsDir: toolPaths.agentsDir,
|
|
2024
2110
|
})
|
|
@@ -2142,6 +2228,7 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2142
2228
|
}
|
|
2143
2229
|
if (toolPaths.agentsDir && opts.adoptExisting) {
|
|
2144
2230
|
const result = await adoptExistingToolAgents({
|
|
2231
|
+
tool,
|
|
2145
2232
|
rootDir,
|
|
2146
2233
|
agentsDir: toolPaths.agentsDir,
|
|
2147
2234
|
conflictMode: importConflictMode,
|
|
@@ -3006,6 +3093,7 @@ async function repairManagedCanonicalContent(args: {
|
|
|
3006
3093
|
|
|
3007
3094
|
if (args.entry.agentsBackup) {
|
|
3008
3095
|
const items = await adoptExistingToolAgents({
|
|
3096
|
+
tool: args.entry.tool,
|
|
3009
3097
|
rootDir: args.rootDir,
|
|
3010
3098
|
agentsDir: args.entry.agentsBackup,
|
|
3011
3099
|
conflictMode: "keep-canonical",
|
package/src/remote.ts
CHANGED
|
@@ -341,7 +341,7 @@ Use this memory for pattern continuity:
|
|
|
341
341
|
- For wide reviews, partition evidence by cwd first; do not let one repo's evidence stand in for another.
|
|
342
342
|
- Grounding: prefer evidence from session messages, tool calls, shell commands, diffs, tests, commits, and touched files.
|
|
343
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
|
|
344
|
+
- Scope: default to project writeback only when the repo has a project-local \`.ai\` root. If a local writable repo is missing one, bootstrap baseline project AI state with \`fclt templates init project-ai\` before retrying project-scoped writeback. If bootstrap fails or the repo is not writable, treat that as the blocker instead of silently falling back to global runtime state.
|
|
345
345
|
- Promote to global only when the same signal appears across multiple repos or clearly targets shared doctrine, shared agents, or shared skills.
|
|
346
346
|
- Verification: distinguish one-off friction from a repeated pattern before escalating it.
|
|
347
347
|
- If available, use [$feedback-loop-setup]({{feedbackLoopSkill}}) when the review needs stronger feedback loops or verification framing.
|
|
@@ -367,6 +367,8 @@ Grounding rules:
|
|
|
367
367
|
|
|
368
368
|
Decision rules:
|
|
369
369
|
- Use \`fclt ai writeback add\` when the signal, target asset, and scope are clear.
|
|
370
|
+
- Before attempting project-scoped writeback, verify the cwd has a repo-local \`.ai\` root. If it does not and the cwd is a local writable repo, run \`fclt templates init project-ai\` from that repo root, then continue. If bootstrap fails or the repo is not writable, report the writeback as blocked by missing project AI state rather than falling back to merged/global runtime state.
|
|
371
|
+
- Before passing \`--asset\`, verify the target resolves in the Facult graph. If the destination is a raw file path or otherwise not graph-backed, report that as a missing-asset blocker instead of retrying blind.
|
|
370
372
|
- Use \`fclt ai evolve\` only when repeated signal is strong enough to justify a reviewable capability change.
|
|
371
373
|
- Prefer project scope unless the learning clearly belongs in shared global doctrine, shared agents, shared skills, or other cross-project capability.
|
|
372
374
|
- For wide automations, require repeated evidence across more than one cwd before recommending a global/shared capability change unless the target is obviously global.
|
package/src/scan.ts
CHANGED
|
@@ -601,6 +601,19 @@ function defaultSourceSpecs(
|
|
|
601
601
|
"~/.codex/mcp.json",
|
|
602
602
|
],
|
|
603
603
|
},
|
|
604
|
+
{
|
|
605
|
+
id: "factory",
|
|
606
|
+
name: "Factory",
|
|
607
|
+
candidates: ["~/.factory", "~/.factory/mcp.json"],
|
|
608
|
+
skillDirs: ["~/.factory/skills"],
|
|
609
|
+
configFiles: ["~/.factory/mcp.json"],
|
|
610
|
+
assets: [
|
|
611
|
+
{
|
|
612
|
+
kind: "agents-instructions",
|
|
613
|
+
patterns: ["~/.factory/AGENTS.md"],
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
},
|
|
604
617
|
{
|
|
605
618
|
id: "claude",
|
|
606
619
|
name: "Claude (CLI)",
|
|
@@ -784,6 +797,13 @@ function defaultSourceSpecs(
|
|
|
784
797
|
},
|
|
785
798
|
],
|
|
786
799
|
},
|
|
800
|
+
{
|
|
801
|
+
id: "factory-project",
|
|
802
|
+
name: "Factory (project)",
|
|
803
|
+
candidates: [join(cwd, ".factory")],
|
|
804
|
+
skillDirs: [join(cwd, ".factory", "skills")],
|
|
805
|
+
configFiles: [join(cwd, ".factory", "mcp.json")],
|
|
806
|
+
},
|
|
787
807
|
];
|
|
788
808
|
|
|
789
809
|
if (includeGitHooks) {
|
|
@@ -1288,7 +1308,12 @@ async function buildFromRootResult(args: {
|
|
|
1288
1308
|
}
|
|
1289
1309
|
continue;
|
|
1290
1310
|
}
|
|
1291
|
-
if (
|
|
1311
|
+
if (
|
|
1312
|
+
name === ".codex" ||
|
|
1313
|
+
name === ".agents" ||
|
|
1314
|
+
name === ".clawdbot" ||
|
|
1315
|
+
name === ".factory"
|
|
1316
|
+
) {
|
|
1292
1317
|
await scanToolDotDir(child);
|
|
1293
1318
|
continue;
|
|
1294
1319
|
}
|