@supporterino/ai-factory 0.2.0 → 0.3.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/CHANGELOG.md +16 -0
- package/README.md +47 -28
- package/dist/cli.js +1825 -359
- package/dist/package.json +3 -0
- package/package.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -14682,7 +14682,7 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
14682
14682
|
});
|
|
14683
14683
|
|
|
14684
14684
|
// src/cli/main.ts
|
|
14685
|
-
import { resolve as
|
|
14685
|
+
import { resolve as resolve6 } from "node:path";
|
|
14686
14686
|
import { createInterface } from "node:readline/promises";
|
|
14687
14687
|
|
|
14688
14688
|
// src/cli/exit-codes.ts
|
|
@@ -14743,7 +14743,7 @@ class TransientError extends Error {
|
|
|
14743
14743
|
}
|
|
14744
14744
|
|
|
14745
14745
|
// src/version.ts
|
|
14746
|
-
var FACTORY_VERSION = "0.
|
|
14746
|
+
var FACTORY_VERSION = "0.3.1";
|
|
14747
14747
|
var PACKAGE_NAME = "@supporterino/ai-factory";
|
|
14748
14748
|
var runVersion = (version) => `npx ${PACKAGE_NAME}@${version}`;
|
|
14749
14749
|
|
|
@@ -22304,13 +22304,49 @@ var PONYTAIL = {
|
|
|
22304
22304
|
};
|
|
22305
22305
|
|
|
22306
22306
|
// src/config/schema.ts
|
|
22307
|
-
var CONFIG_SCHEMA_VERSION =
|
|
22307
|
+
var CONFIG_SCHEMA_VERSION = 3;
|
|
22308
22308
|
var CONFIG_FILE = "factory.config.jsonc";
|
|
22309
|
-
var LANGUAGES = [
|
|
22309
|
+
var LANGUAGES = [
|
|
22310
|
+
"typescript",
|
|
22311
|
+
"react",
|
|
22312
|
+
"python",
|
|
22313
|
+
"go",
|
|
22314
|
+
"rust",
|
|
22315
|
+
"helm",
|
|
22316
|
+
"tauri",
|
|
22317
|
+
"swift"
|
|
22318
|
+
];
|
|
22310
22319
|
var Language = _enum(LANGUAGES);
|
|
22320
|
+
var ROLES = [
|
|
22321
|
+
"orchestrator",
|
|
22322
|
+
"architect",
|
|
22323
|
+
"reviewer",
|
|
22324
|
+
"contractReviewer",
|
|
22325
|
+
"implementer",
|
|
22326
|
+
"tester",
|
|
22327
|
+
"explorer"
|
|
22328
|
+
];
|
|
22329
|
+
var AGENT_PHASES = [
|
|
22330
|
+
"orchestrator",
|
|
22331
|
+
"architect",
|
|
22332
|
+
"explorer",
|
|
22333
|
+
"reviewer",
|
|
22334
|
+
"contract-reviewer",
|
|
22335
|
+
"tester",
|
|
22336
|
+
"implementer"
|
|
22337
|
+
];
|
|
22311
22338
|
var GATE_TYPES = ["format", "lint", "types", "test", "testAffected"];
|
|
22312
22339
|
var GateType = _enum(GATE_TYPES);
|
|
22313
|
-
var CONTRACT_KINDS = [
|
|
22340
|
+
var CONTRACT_KINDS = [
|
|
22341
|
+
"openapi",
|
|
22342
|
+
"proto",
|
|
22343
|
+
"json-schema",
|
|
22344
|
+
"helm-values",
|
|
22345
|
+
"generated",
|
|
22346
|
+
"crd",
|
|
22347
|
+
"ipc",
|
|
22348
|
+
"custom"
|
|
22349
|
+
];
|
|
22314
22350
|
var ContractKind = _enum(CONTRACT_KINDS);
|
|
22315
22351
|
var RepoName = string2().regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters, digits and dashes, starting alphanumeric");
|
|
22316
22352
|
var RelPath = string2().min(1).refine((p) => p === "." || !p.startsWith("/") && !p.includes("\\") && !p.split("/").includes(".."), "must be '.' or a relative POSIX path inside the workspace");
|
|
@@ -22330,6 +22366,7 @@ var RepoConfig = strictObject({
|
|
|
22330
22366
|
var repoLanguages = (repo) => [
|
|
22331
22367
|
...new Set([...repo.languages, ...(repo.subPaths ?? []).flatMap((s) => s.languages)])
|
|
22332
22368
|
];
|
|
22369
|
+
var ModelByRole = partialRecord(_enum(ROLES), ModelId);
|
|
22333
22370
|
var ModelsConfig = strictObject({
|
|
22334
22371
|
orchestrator: ModelId,
|
|
22335
22372
|
architect: ModelId,
|
|
@@ -22337,7 +22374,54 @@ var ModelsConfig = strictObject({
|
|
|
22337
22374
|
contractReviewer: ModelId.optional(),
|
|
22338
22375
|
implementer: ModelId,
|
|
22339
22376
|
tester: ModelId,
|
|
22340
|
-
explorer: ModelId
|
|
22377
|
+
explorer: ModelId,
|
|
22378
|
+
roles: ModelByRole.default({}),
|
|
22379
|
+
repos: record(string2(), ModelByRole).default({}),
|
|
22380
|
+
agents: record(string2(), ModelId).default({}),
|
|
22381
|
+
fallback: array(ModelId).default([])
|
|
22382
|
+
});
|
|
22383
|
+
var InstructionsConfig = strictObject({
|
|
22384
|
+
workspace: array(RelPath).default([]),
|
|
22385
|
+
repos: record(string2(), array(RelPath)).default({})
|
|
22386
|
+
});
|
|
22387
|
+
var SkillsConfig = strictObject({
|
|
22388
|
+
sources: array(RelPath).default([".opencode/skills"]),
|
|
22389
|
+
roles: partialRecord(_enum(ROLES), array(string2().min(1))).default({}),
|
|
22390
|
+
repos: record(string2(), array(string2().min(1))).default({})
|
|
22391
|
+
});
|
|
22392
|
+
var McpConfig = strictObject({
|
|
22393
|
+
roles: partialRecord(_enum(ROLES), array(string2().min(1))).default({})
|
|
22394
|
+
});
|
|
22395
|
+
var CustomAgent = strictObject({
|
|
22396
|
+
id: string2().regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters, digits and dashes, starting alphanumeric"),
|
|
22397
|
+
phase: _enum(AGENT_PHASES),
|
|
22398
|
+
description: string2().min(1).default("custom ai-factory agent"),
|
|
22399
|
+
model: ModelId.optional(),
|
|
22400
|
+
mode: _enum(["primary", "subagent", "all"]).default("subagent"),
|
|
22401
|
+
prompt: string2().min(1).optional(),
|
|
22402
|
+
promptFile: RelPath.optional(),
|
|
22403
|
+
skills: array(string2().min(1)).default([]),
|
|
22404
|
+
edits: array(string2().min(1)).default([]),
|
|
22405
|
+
shell: array(string2().min(1)).default([]),
|
|
22406
|
+
web: boolean2().default(false),
|
|
22407
|
+
mcp: array(string2().min(1)).default([])
|
|
22408
|
+
});
|
|
22409
|
+
var RoutingRule = strictObject({
|
|
22410
|
+
paths: array(string2().min(1)).min(1),
|
|
22411
|
+
agent: string2().min(1)
|
|
22412
|
+
});
|
|
22413
|
+
var ExtraGate = strictObject({
|
|
22414
|
+
name: string2().regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters, digits and dashes, starting alphanumeric"),
|
|
22415
|
+
type: _enum(GATE_TYPES).default("test"),
|
|
22416
|
+
phases: array(_enum(["task", "full"])).default(["task", "full"]),
|
|
22417
|
+
when: strictObject({ changed: array(string2().min(1)).default([]) }).default({ changed: [] }),
|
|
22418
|
+
cwd: RelPath.default("."),
|
|
22419
|
+
command: string2().min(1),
|
|
22420
|
+
env: record(string2(), string2()).default({}),
|
|
22421
|
+
setup: string2().min(1).optional(),
|
|
22422
|
+
outputs: array(RelPath).default([]),
|
|
22423
|
+
checkOnly: boolean2().default(true),
|
|
22424
|
+
timeoutMs: int().min(1).optional()
|
|
22341
22425
|
});
|
|
22342
22426
|
var GateCommands = partialRecord(GateType, string2().min(1));
|
|
22343
22427
|
var GatesConfig = strictObject({
|
|
@@ -22349,13 +22433,16 @@ var GatesConfig = strictObject({
|
|
|
22349
22433
|
test: "test",
|
|
22350
22434
|
testAffected: "test:affected"
|
|
22351
22435
|
}),
|
|
22352
|
-
overrides: record(string2(), GateCommands).default({})
|
|
22436
|
+
overrides: record(string2(), GateCommands).default({}),
|
|
22437
|
+
extra: array(ExtraGate).default([])
|
|
22353
22438
|
});
|
|
22354
22439
|
var ContractConfig = strictObject({
|
|
22355
22440
|
kind: ContractKind,
|
|
22356
22441
|
provider: RepoName,
|
|
22357
22442
|
path: RelPath,
|
|
22358
22443
|
consumers: array(RepoName),
|
|
22444
|
+
command: string2().min(1).optional(),
|
|
22445
|
+
outputs: array(RelPath).optional(),
|
|
22359
22446
|
allowBreaking: boolean2().optional(),
|
|
22360
22447
|
validate: record(RepoName, string2().min(1)).optional()
|
|
22361
22448
|
});
|
|
@@ -22400,12 +22487,32 @@ var SecurityConfig = strictObject({
|
|
|
22400
22487
|
var GraphConfig = strictObject({
|
|
22401
22488
|
exclude: array(string2().min(1)).default([...DEFAULT_GRAPH_EXCLUDE])
|
|
22402
22489
|
});
|
|
22490
|
+
var CommitsConfig = strictObject({
|
|
22491
|
+
style: _enum(["conventional", "gitmoji"]).default("conventional"),
|
|
22492
|
+
scopes: boolean2().default(false)
|
|
22493
|
+
});
|
|
22494
|
+
var QuickConfig = strictObject({
|
|
22495
|
+
enabled: boolean2().default(true),
|
|
22496
|
+
branchPrefix: string2().min(1).default("task/")
|
|
22497
|
+
});
|
|
22498
|
+
var WorkflowConfig = strictObject({
|
|
22499
|
+
refine: boolean2().default(true),
|
|
22500
|
+
quick: QuickConfig.prefault({})
|
|
22501
|
+
});
|
|
22502
|
+
var ReleaseConfig = strictObject({
|
|
22503
|
+
detect: boolean2().default(true)
|
|
22504
|
+
});
|
|
22403
22505
|
var FactoryConfigBase = strictObject({
|
|
22404
22506
|
$schema: string2().optional(),
|
|
22405
|
-
schemaVersion: literal(CONFIG_SCHEMA_VERSION),
|
|
22507
|
+
schemaVersion: union([literal(2), literal(CONFIG_SCHEMA_VERSION)]),
|
|
22406
22508
|
layout: _enum(["single", "submodules"]),
|
|
22407
22509
|
repos: array(RepoConfig).min(1),
|
|
22408
22510
|
models: ModelsConfig,
|
|
22511
|
+
instructions: InstructionsConfig.prefault({}),
|
|
22512
|
+
skills: SkillsConfig.prefault({}),
|
|
22513
|
+
mcp: McpConfig.prefault({}),
|
|
22514
|
+
agents: array(CustomAgent).default([]),
|
|
22515
|
+
routing: array(RoutingRule).default([]),
|
|
22409
22516
|
gates: GatesConfig.prefault({}),
|
|
22410
22517
|
contracts: array(ContractConfig).default([]),
|
|
22411
22518
|
parallel: ParallelConfig.prefault({}),
|
|
@@ -22414,6 +22521,9 @@ var FactoryConfigBase = strictObject({
|
|
|
22414
22521
|
logs: LogsConfig.prefault({}),
|
|
22415
22522
|
graph: GraphConfig.prefault({}),
|
|
22416
22523
|
security: SecurityConfig.prefault({}),
|
|
22524
|
+
commits: CommitsConfig.prefault({}),
|
|
22525
|
+
workflow: WorkflowConfig.prefault({}),
|
|
22526
|
+
release: ReleaseConfig.prefault({}),
|
|
22417
22527
|
unmanaged: array(RelPath).default([])
|
|
22418
22528
|
});
|
|
22419
22529
|
var FactoryConfig = FactoryConfigBase.superRefine((cfg, ctx) => {
|
|
@@ -22465,17 +22575,126 @@ var FactoryConfig = FactoryConfigBase.superRefine((cfg, ctx) => {
|
|
|
22465
22575
|
issue(["contracts", i, "consumers", j], "a provider cannot consume its own contract");
|
|
22466
22576
|
}
|
|
22467
22577
|
});
|
|
22578
|
+
if (["generated", "crd", "ipc", "custom"].includes(contract.kind) && contract.command === undefined) {
|
|
22579
|
+
issue(["contracts", i, "command"], `${contract.kind} contracts need a command`);
|
|
22580
|
+
}
|
|
22468
22581
|
});
|
|
22469
22582
|
for (const repoName of Object.keys(cfg.gates.overrides)) {
|
|
22470
22583
|
if (!codeRepos.has(repoName)) {
|
|
22471
22584
|
issue(["gates", "overrides", repoName], `unknown code repo "${repoName}"`);
|
|
22472
22585
|
}
|
|
22473
22586
|
}
|
|
22587
|
+
for (const key of ["instructions", "skills"]) {
|
|
22588
|
+
for (const repoName of Object.keys(cfg[key].repos)) {
|
|
22589
|
+
if (!codeRepos.has(repoName)) {
|
|
22590
|
+
issue([key, "repos", repoName], `unknown code repo "${repoName}"`);
|
|
22591
|
+
}
|
|
22592
|
+
}
|
|
22593
|
+
}
|
|
22594
|
+
for (const repoName of Object.keys(cfg.models.repos)) {
|
|
22595
|
+
if (!codeRepos.has(repoName)) {
|
|
22596
|
+
issue(["models", "repos", repoName], `unknown code repo "${repoName}"`);
|
|
22597
|
+
}
|
|
22598
|
+
}
|
|
22599
|
+
const agentIds = new Set;
|
|
22600
|
+
for (const [i, agent] of cfg.agents.entries()) {
|
|
22601
|
+
if (agentIds.has(agent.id))
|
|
22602
|
+
issue(["agents", i, "id"], `duplicate agent id "${agent.id}"`);
|
|
22603
|
+
if (agent.prompt === undefined && agent.promptFile === undefined) {
|
|
22604
|
+
issue(["agents", i], "a custom agent needs `prompt` or `promptFile`");
|
|
22605
|
+
}
|
|
22606
|
+
if (agent.prompt !== undefined && agent.promptFile !== undefined) {
|
|
22607
|
+
issue(["agents", i], "use either `prompt` or `promptFile`, not both");
|
|
22608
|
+
}
|
|
22609
|
+
agentIds.add(agent.id);
|
|
22610
|
+
}
|
|
22611
|
+
const builtin = new Set([
|
|
22612
|
+
"orchestrator",
|
|
22613
|
+
"architect",
|
|
22614
|
+
"explorer",
|
|
22615
|
+
"reviewer",
|
|
22616
|
+
"contract-reviewer",
|
|
22617
|
+
"tester",
|
|
22618
|
+
...cfg.repos.filter((r) => r.role === "code").map((r) => `implementer-${r.name}`)
|
|
22619
|
+
]);
|
|
22620
|
+
for (const [i, agent] of cfg.agents.entries()) {
|
|
22621
|
+
if (builtin.has(agent.id)) {
|
|
22622
|
+
issue(["agents", i, "id"], `"${agent.id}" is a built-in agent id; choose another id`);
|
|
22623
|
+
}
|
|
22624
|
+
}
|
|
22625
|
+
for (const [i, rule] of cfg.routing.entries()) {
|
|
22626
|
+
if (!agentIds.has(rule.agent) && !builtin.has(rule.agent)) {
|
|
22627
|
+
issue(["routing", i, "agent"], `unknown agent "${rule.agent}"`);
|
|
22628
|
+
}
|
|
22629
|
+
}
|
|
22630
|
+
for (const agentId of Object.keys(cfg.models.agents)) {
|
|
22631
|
+
if (!agentIds.has(agentId)) {
|
|
22632
|
+
issue(["models", "agents", agentId], `unknown custom agent "${agentId}"`);
|
|
22633
|
+
}
|
|
22634
|
+
}
|
|
22474
22635
|
});
|
|
22475
|
-
function modelFor(config, role) {
|
|
22476
|
-
|
|
22477
|
-
|
|
22478
|
-
|
|
22636
|
+
function modelFor(config, role, options = {}) {
|
|
22637
|
+
const flat = role === "contractReviewer" ? config.models.contractReviewer ?? config.models.reviewer : config.models[role];
|
|
22638
|
+
if (options.agent !== undefined) {
|
|
22639
|
+
const agent = config.models.agents[options.agent];
|
|
22640
|
+
if (agent !== undefined)
|
|
22641
|
+
return agent;
|
|
22642
|
+
}
|
|
22643
|
+
if (options.repo !== undefined) {
|
|
22644
|
+
const repo = config.models.repos[options.repo]?.[role];
|
|
22645
|
+
if (repo !== undefined)
|
|
22646
|
+
return repo;
|
|
22647
|
+
}
|
|
22648
|
+
return config.models.roles[role] ?? flat;
|
|
22649
|
+
}
|
|
22650
|
+
function modelEntries(config) {
|
|
22651
|
+
const out = [];
|
|
22652
|
+
for (const role of ROLES) {
|
|
22653
|
+
if (role === "contractReviewer")
|
|
22654
|
+
continue;
|
|
22655
|
+
const model = config.models[role];
|
|
22656
|
+
if (typeof model === "string")
|
|
22657
|
+
out.push([role, model]);
|
|
22658
|
+
}
|
|
22659
|
+
if (config.models.contractReviewer !== undefined)
|
|
22660
|
+
out.push(["contractReviewer", config.models.contractReviewer]);
|
|
22661
|
+
for (const [role, model] of Object.entries(config.models.roles)) {
|
|
22662
|
+
if (model !== undefined)
|
|
22663
|
+
out.push([`${role} (role)`, model]);
|
|
22664
|
+
}
|
|
22665
|
+
for (const [repo, byRole] of Object.entries(config.models.repos)) {
|
|
22666
|
+
for (const [role, model] of Object.entries(byRole)) {
|
|
22667
|
+
if (model !== undefined)
|
|
22668
|
+
out.push([`${role} (${repo})`, model]);
|
|
22669
|
+
}
|
|
22670
|
+
}
|
|
22671
|
+
for (const [agent, model] of Object.entries(config.models.agents)) {
|
|
22672
|
+
out.push([`agent ${agent}`, model]);
|
|
22673
|
+
}
|
|
22674
|
+
return out;
|
|
22675
|
+
}
|
|
22676
|
+
function allModels(config) {
|
|
22677
|
+
const out = new Set;
|
|
22678
|
+
for (const role of ROLES) {
|
|
22679
|
+
const flat = config.models[role];
|
|
22680
|
+
if (typeof flat === "string")
|
|
22681
|
+
out.add(flat);
|
|
22682
|
+
}
|
|
22683
|
+
if (config.models.contractReviewer !== undefined)
|
|
22684
|
+
out.add(config.models.contractReviewer);
|
|
22685
|
+
for (const model of Object.values(config.models.roles))
|
|
22686
|
+
if (model !== undefined)
|
|
22687
|
+
out.add(model);
|
|
22688
|
+
for (const byRole of Object.values(config.models.repos)) {
|
|
22689
|
+
for (const model of Object.values(byRole))
|
|
22690
|
+
if (model !== undefined)
|
|
22691
|
+
out.add(model);
|
|
22692
|
+
}
|
|
22693
|
+
for (const model of Object.values(config.models.agents))
|
|
22694
|
+
out.add(model);
|
|
22695
|
+
for (const model of config.models.fallback)
|
|
22696
|
+
out.add(model);
|
|
22697
|
+
return [...out].sort();
|
|
22479
22698
|
}
|
|
22480
22699
|
|
|
22481
22700
|
// src/config/load.ts
|
|
@@ -22515,9 +22734,12 @@ function parseConfigText(text, file) {
|
|
|
22515
22734
|
throw new ConfigError(`${file} is not valid JSONC`, errors);
|
|
22516
22735
|
}
|
|
22517
22736
|
const version = value?.schemaVersion;
|
|
22518
|
-
if (typeof version === "number" && version <
|
|
22737
|
+
if (typeof version === "number" && version < 2) {
|
|
22519
22738
|
throw new ConfigError(`${file} uses config schemaVersion ${version}; this factory needs ${CONFIG_SCHEMA_VERSION}`, ["run `ai-factory update` to migrate it"]);
|
|
22520
22739
|
}
|
|
22740
|
+
if (typeof version === "number" && version > CONFIG_SCHEMA_VERSION) {
|
|
22741
|
+
throw new ConfigError(`${file} uses config schemaVersion ${version}, newer than this factory (${CONFIG_SCHEMA_VERSION})`, ["install a newer ai-factory"]);
|
|
22742
|
+
}
|
|
22521
22743
|
const parsed = FactoryConfig.safeParse(value);
|
|
22522
22744
|
if (!parsed.success) {
|
|
22523
22745
|
throw new ConfigError(`${file} is invalid`, formatZodIssues(parsed.error));
|
|
@@ -23208,7 +23430,7 @@ function benchCommand(cli) {
|
|
|
23208
23430
|
|
|
23209
23431
|
// src/cli/commands/detect.ts
|
|
23210
23432
|
import { existsSync as existsSync13, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "node:fs";
|
|
23211
|
-
import { join as
|
|
23433
|
+
import { join as join17 } from "node:path";
|
|
23212
23434
|
|
|
23213
23435
|
// src/detect/contracts.ts
|
|
23214
23436
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
@@ -23360,6 +23582,57 @@ function findProvided(repo) {
|
|
|
23360
23582
|
const chartDirs = new Set(candidates.filter((f) => baseOf(f.path) === "Chart.yaml").map((f) => dirOf(f.path)));
|
|
23361
23583
|
const protoDirs = [];
|
|
23362
23584
|
const protoPackages = new Set;
|
|
23585
|
+
const pkgText = read(repo.root, "package.json");
|
|
23586
|
+
let scripts = {};
|
|
23587
|
+
if (pkgText !== undefined) {
|
|
23588
|
+
try {
|
|
23589
|
+
scripts = JSON.parse(pkgText).scripts ?? {};
|
|
23590
|
+
} catch {
|
|
23591
|
+
scripts = {};
|
|
23592
|
+
}
|
|
23593
|
+
}
|
|
23594
|
+
const runner = /bun\.lockb?/.test(repo.files.map((f) => f.path).join(`
|
|
23595
|
+
`)) ? "bun run" : "npm run";
|
|
23596
|
+
for (const binding of ["src/bindings.ts", "bindings.ts"]) {
|
|
23597
|
+
if (candidates.some((f) => f.path === binding) && typeof scripts["gen:bindings"] === "string") {
|
|
23598
|
+
found.push({
|
|
23599
|
+
kind: "ipc",
|
|
23600
|
+
path: binding,
|
|
23601
|
+
needles: [binding],
|
|
23602
|
+
command: `${runner} gen:bindings`,
|
|
23603
|
+
outputs: [binding]
|
|
23604
|
+
});
|
|
23605
|
+
}
|
|
23606
|
+
}
|
|
23607
|
+
if (candidates.some((f) => f.path === "src/routeTree.gen.ts") && typeof scripts.build === "string") {
|
|
23608
|
+
found.push({
|
|
23609
|
+
kind: "generated",
|
|
23610
|
+
path: "src/routeTree.gen.ts",
|
|
23611
|
+
needles: ["src/routeTree.gen.ts"],
|
|
23612
|
+
command: `${runner} build`,
|
|
23613
|
+
outputs: ["src/routeTree.gen.ts"]
|
|
23614
|
+
});
|
|
23615
|
+
}
|
|
23616
|
+
for (const dir of ["operator/config/crd/bases", "config/crd/bases"]) {
|
|
23617
|
+
if (!candidates.some((f) => f.path.startsWith(`${dir}/`)))
|
|
23618
|
+
continue;
|
|
23619
|
+
const taskfile = read(repo.root, "Taskfile.yml") ?? read(repo.root, "Taskfile.yaml");
|
|
23620
|
+
if (taskfile !== undefined && /^ {2}manifests:/m.test(taskfile)) {
|
|
23621
|
+
const root = dir.includes("/config/crd/bases") ? dir.slice(0, dir.indexOf("/config/crd/bases")) : ".";
|
|
23622
|
+
const outputs = [
|
|
23623
|
+
dir,
|
|
23624
|
+
...["config/rbac", "api/v1alpha1/zz_generated.deepcopy.go"].map((path) => root === "." ? path : `${root}/${path}`)
|
|
23625
|
+
].filter((path) => candidates.some((f) => f.path === path || f.path.startsWith(`${path}/`)));
|
|
23626
|
+
found.push({
|
|
23627
|
+
kind: "crd",
|
|
23628
|
+
path: dir,
|
|
23629
|
+
needles: [],
|
|
23630
|
+
command: "task manifests generate",
|
|
23631
|
+
outputs
|
|
23632
|
+
});
|
|
23633
|
+
break;
|
|
23634
|
+
}
|
|
23635
|
+
}
|
|
23363
23636
|
for (const file of candidates) {
|
|
23364
23637
|
const name = baseOf(file.path);
|
|
23365
23638
|
if (name === "values.schema.json" && chartDirs.has(dirOf(file.path))) {
|
|
@@ -23413,7 +23686,10 @@ function detectContracts(repos) {
|
|
|
23413
23686
|
kind: found.kind,
|
|
23414
23687
|
provider: provider.name,
|
|
23415
23688
|
path: found.path,
|
|
23416
|
-
consumers: consumers.sort()
|
|
23689
|
+
consumers: consumers.sort(),
|
|
23690
|
+
...found.command === undefined ? {} : { command: found.command },
|
|
23691
|
+
...found.outputs === undefined ? {} : { outputs: found.outputs },
|
|
23692
|
+
...found.validate === undefined ? {} : { validate: found.validate }
|
|
23417
23693
|
});
|
|
23418
23694
|
}
|
|
23419
23695
|
}
|
|
@@ -23456,7 +23732,7 @@ function applyChanges(text, config, changes) {
|
|
|
23456
23732
|
}
|
|
23457
23733
|
|
|
23458
23734
|
// src/detect/workspace.ts
|
|
23459
|
-
import { existsSync as existsSync6 } from "node:fs";
|
|
23735
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
|
|
23460
23736
|
import { basename, join as join9 } from "node:path";
|
|
23461
23737
|
|
|
23462
23738
|
// src/detect/languages.ts
|
|
@@ -23605,6 +23881,62 @@ async function defaultBranchOf(dir) {
|
|
|
23605
23881
|
function isInitialized(dir) {
|
|
23606
23882
|
return existsSync6(join9(dir, ".git"));
|
|
23607
23883
|
}
|
|
23884
|
+
function readScripts(dir) {
|
|
23885
|
+
try {
|
|
23886
|
+
const pkg = JSON.parse(readFileSync7(join9(dir, "package.json"), "utf8"));
|
|
23887
|
+
return Object.fromEntries(Object.entries(pkg.scripts ?? {}).filter((entry) => typeof entry[1] === "string"));
|
|
23888
|
+
} catch {
|
|
23889
|
+
return {};
|
|
23890
|
+
}
|
|
23891
|
+
}
|
|
23892
|
+
function packageRunner(dir) {
|
|
23893
|
+
if (existsSync6(join9(dir, "bun.lock")) || existsSync6(join9(dir, "bun.lockb")))
|
|
23894
|
+
return "bun run";
|
|
23895
|
+
if (existsSync6(join9(dir, "pnpm-lock.yaml")))
|
|
23896
|
+
return "pnpm run";
|
|
23897
|
+
if (existsSync6(join9(dir, "yarn.lock")))
|
|
23898
|
+
return "yarn";
|
|
23899
|
+
return "npm run";
|
|
23900
|
+
}
|
|
23901
|
+
function e2eScript(dir) {
|
|
23902
|
+
const scripts = readScripts(dir);
|
|
23903
|
+
const name = scripts["test:e2e"] !== undefined ? "test:e2e" : scripts.e2e !== undefined ? "e2e" : undefined;
|
|
23904
|
+
return name === undefined ? undefined : `${packageRunner(dir)} ${name}`;
|
|
23905
|
+
}
|
|
23906
|
+
function taskNames(dir) {
|
|
23907
|
+
for (const file of ["Taskfile.yml", "Taskfile.yaml"]) {
|
|
23908
|
+
const path = join9(dir, file);
|
|
23909
|
+
if (!existsSync6(path))
|
|
23910
|
+
continue;
|
|
23911
|
+
try {
|
|
23912
|
+
const doc = $parse(readFileSync7(path, "utf8"), { logLevel: "silent" });
|
|
23913
|
+
return Object.keys(doc?.tasks ?? {}).sort();
|
|
23914
|
+
} catch {
|
|
23915
|
+
return [];
|
|
23916
|
+
}
|
|
23917
|
+
}
|
|
23918
|
+
return [];
|
|
23919
|
+
}
|
|
23920
|
+
function readInstructions(dir) {
|
|
23921
|
+
for (const file of [
|
|
23922
|
+
"opencode.json",
|
|
23923
|
+
"opencode.jsonc",
|
|
23924
|
+
".opencode/opencode.json",
|
|
23925
|
+
".opencode/opencode.jsonc"
|
|
23926
|
+
]) {
|
|
23927
|
+
if (!existsSync6(join9(dir, file)))
|
|
23928
|
+
continue;
|
|
23929
|
+
try {
|
|
23930
|
+
const text = readFileSync7(join9(dir, file), "utf8");
|
|
23931
|
+
const value = parseJsonc(text).value;
|
|
23932
|
+
const list = Array.isArray(value?.instructions) ? value.instructions : [];
|
|
23933
|
+
return list.filter((v) => typeof v === "string" && !v.startsWith("/") && !v.startsWith("~") && !v.includes("://") && !v.split("/").includes(".."));
|
|
23934
|
+
} catch {
|
|
23935
|
+
return [];
|
|
23936
|
+
}
|
|
23937
|
+
}
|
|
23938
|
+
return [];
|
|
23939
|
+
}
|
|
23608
23940
|
function withOptional(base, subPaths, defaultBranch) {
|
|
23609
23941
|
return {
|
|
23610
23942
|
...base,
|
|
@@ -23617,6 +23949,12 @@ async function detectWorkspace(root) {
|
|
|
23617
23949
|
const repos = [];
|
|
23618
23950
|
const scanned = [];
|
|
23619
23951
|
const taken = new Set;
|
|
23952
|
+
const instructions = {
|
|
23953
|
+
workspace: readInstructions(root),
|
|
23954
|
+
repos: {}
|
|
23955
|
+
};
|
|
23956
|
+
const e2e = [];
|
|
23957
|
+
const tasks = [];
|
|
23620
23958
|
const submodules = existsSync6(join9(root, ".gitmodules")) ? await readSubmodules(root) : [];
|
|
23621
23959
|
const layout = submodules.length > 0 ? "submodules" : "single";
|
|
23622
23960
|
if (layout === "single") {
|
|
@@ -23625,6 +23963,10 @@ async function detectWorkspace(root) {
|
|
|
23625
23963
|
const name = uniqueName(repoNameFrom(root), taken);
|
|
23626
23964
|
repos.push(withOptional({ name, path: ".", role: "code", languages, initialized: true }, subPaths, await defaultBranchOf(root)));
|
|
23627
23965
|
scanned.push({ name, root, files });
|
|
23966
|
+
const singleE2e = e2eScript(root);
|
|
23967
|
+
if (singleE2e !== undefined)
|
|
23968
|
+
e2e.push({ repo: name, command: singleE2e });
|
|
23969
|
+
tasks.push({ repo: name, names: taskNames(root) });
|
|
23628
23970
|
if (languages.length === 0)
|
|
23629
23971
|
notes.push("no language markers found in the repository");
|
|
23630
23972
|
} else {
|
|
@@ -23643,10 +23985,25 @@ async function detectWorkspace(root) {
|
|
|
23643
23985
|
if (languages.length === 0)
|
|
23644
23986
|
notes.push(`no language markers found in ${sub.path}`);
|
|
23645
23987
|
repos.push(withOptional({ name, path: sub.path, role: "code", languages, initialized: true }, subPaths, await defaultBranchOf(dir)));
|
|
23988
|
+
const inherited = readInstructions(dir);
|
|
23989
|
+
if (inherited.length > 0)
|
|
23990
|
+
instructions.repos[name] = inherited;
|
|
23991
|
+
const repoE2e = e2eScript(dir);
|
|
23992
|
+
if (repoE2e !== undefined)
|
|
23993
|
+
e2e.push({ repo: name, command: repoE2e });
|
|
23994
|
+
tasks.push({ repo: name, names: taskNames(dir) });
|
|
23646
23995
|
scanned.push({ name, root: dir, files });
|
|
23647
23996
|
}
|
|
23648
23997
|
}
|
|
23649
|
-
return {
|
|
23998
|
+
return {
|
|
23999
|
+
layout,
|
|
24000
|
+
repos,
|
|
24001
|
+
contracts: detectContracts(scanned),
|
|
24002
|
+
instructions,
|
|
24003
|
+
e2e,
|
|
24004
|
+
tasks,
|
|
24005
|
+
notes
|
|
24006
|
+
};
|
|
23650
24007
|
}
|
|
23651
24008
|
function toRepoConfig(repo) {
|
|
23652
24009
|
const { initialized: _initialized, ...rest } = repo;
|
|
@@ -23750,7 +24107,7 @@ function formatChange(change) {
|
|
|
23750
24107
|
}
|
|
23751
24108
|
|
|
23752
24109
|
// src/lock/lock.ts
|
|
23753
|
-
import { existsSync as existsSync7, readFileSync as
|
|
24110
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "node:fs";
|
|
23754
24111
|
import { join as join10 } from "node:path";
|
|
23755
24112
|
|
|
23756
24113
|
// src/tools.ts
|
|
@@ -23888,7 +24245,7 @@ function readLock(root) {
|
|
|
23888
24245
|
return;
|
|
23889
24246
|
let value;
|
|
23890
24247
|
try {
|
|
23891
|
-
value = JSON.parse(
|
|
24248
|
+
value = JSON.parse(readFileSync8(file, "utf8"));
|
|
23892
24249
|
} catch (err) {
|
|
23893
24250
|
throw new ConfigError(`${LOCK_FILE} is not valid JSON`, [String(err)]);
|
|
23894
24251
|
}
|
|
@@ -23910,7 +24267,7 @@ function ensureNotNewer(lock, allow = false) {
|
|
|
23910
24267
|
|
|
23911
24268
|
// src/workspace/sync.ts
|
|
23912
24269
|
import { existsSync as existsSync12, writeFileSync as writeFileSync6 } from "node:fs";
|
|
23913
|
-
import { join as
|
|
24270
|
+
import { join as join16 } from "node:path";
|
|
23914
24271
|
|
|
23915
24272
|
// templates/addons/ponytail/.opencode/command/ponytail-review.md
|
|
23916
24273
|
var ponytail_review_default = `---
|
|
@@ -24258,11 +24615,24 @@ var TYPE_BY_VERB = [
|
|
|
24258
24615
|
[/^(bump|upgrade|pin)\b/i, "chore"],
|
|
24259
24616
|
[/^(speed up|optimi[sz]e|cache)\b/i, "perf"]
|
|
24260
24617
|
];
|
|
24261
|
-
|
|
24618
|
+
var GITMOJI = {
|
|
24619
|
+
feat: "✨",
|
|
24620
|
+
fix: "\uD83D\uDC1B",
|
|
24621
|
+
test: "✅",
|
|
24622
|
+
refactor: "♻️",
|
|
24623
|
+
docs: "\uD83D\uDCDD",
|
|
24624
|
+
chore: "\uD83D\uDD27",
|
|
24625
|
+
perf: "⚡️",
|
|
24626
|
+
ci: "\uD83D\uDC77",
|
|
24627
|
+
build: "\uD83D\uDD27"
|
|
24628
|
+
};
|
|
24629
|
+
function conventionalSubject(title, type, options = {}) {
|
|
24262
24630
|
const clean = title.trim().replace(/\s+/g, " ").replace(/\.$/, "");
|
|
24263
24631
|
const kind = type ?? TYPE_BY_VERB.find(([re]) => re.test(clean))?.[1] ?? "feat";
|
|
24264
24632
|
const lowered = clean.charAt(0).toLowerCase() + clean.slice(1);
|
|
24265
|
-
|
|
24633
|
+
const scope = options.scope === undefined ? "" : `(${options.scope})`;
|
|
24634
|
+
const emoji = options.style === "gitmoji" ? `${GITMOJI[kind] ?? "\uD83D\uDD27"} ` : "";
|
|
24635
|
+
return `${kind}${scope}: ${emoji}${lowered}`;
|
|
24266
24636
|
}
|
|
24267
24637
|
function message(subject, trailers) {
|
|
24268
24638
|
const block = Object.entries(trailers).map(([k, v]) => `${k}: ${v}`);
|
|
@@ -24292,12 +24662,15 @@ async function commitPaths(dir, paths, subject, trailers = {}) {
|
|
|
24292
24662
|
});
|
|
24293
24663
|
return gitOk(["rev-parse", "HEAD"], { cwd: dir });
|
|
24294
24664
|
}
|
|
24295
|
-
function commitTask(worktree, id, task, title, type) {
|
|
24296
|
-
return commitAll(worktree, conventionalSubject(title, type), {
|
|
24665
|
+
function commitTask(worktree, id, task, title, type, style = {}) {
|
|
24666
|
+
return commitAll(worktree, conventionalSubject(title, type, style), {
|
|
24667
|
+
"Change-Id": id,
|
|
24668
|
+
Task: task
|
|
24669
|
+
});
|
|
24297
24670
|
}
|
|
24298
24671
|
|
|
24299
24672
|
// src/git/hooks.ts
|
|
24300
|
-
import { chmodSync, existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as
|
|
24673
|
+
import { chmodSync, existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync9, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
24301
24674
|
import { join as join12, relative as relative2 } from "node:path";
|
|
24302
24675
|
var HOOK_MARKER = "# ai-factory-hook v1";
|
|
24303
24676
|
var CHAINED_HOOK = "pre-push.ai-factory-chained";
|
|
@@ -24341,7 +24714,7 @@ async function installPrePush(repoDir) {
|
|
|
24341
24714
|
const path = join12(hooks, "pre-push");
|
|
24342
24715
|
let status = "installed";
|
|
24343
24716
|
if (existsSync9(path)) {
|
|
24344
|
-
const current =
|
|
24717
|
+
const current = readFileSync9(path, "utf8");
|
|
24345
24718
|
if (current === PRE_PUSH_HOOK)
|
|
24346
24719
|
return { status: "unchanged", path };
|
|
24347
24720
|
if (current.includes(HOOK_MARKER)) {
|
|
@@ -24370,7 +24743,7 @@ async function prePushState(repoDir) {
|
|
|
24370
24743
|
const path = join12(hooks, "pre-push");
|
|
24371
24744
|
if (!existsSync9(path))
|
|
24372
24745
|
return "missing";
|
|
24373
|
-
const text =
|
|
24746
|
+
const text = readFileSync9(path, "utf8");
|
|
24374
24747
|
if (text === PRE_PUSH_HOOK)
|
|
24375
24748
|
return "installed";
|
|
24376
24749
|
return text.includes("# ai-factory-hook") ? "outdated" : "missing";
|
|
@@ -24392,13 +24765,17 @@ import {
|
|
|
24392
24765
|
existsSync as existsSync10,
|
|
24393
24766
|
mkdirSync as mkdirSync5,
|
|
24394
24767
|
mkdtempSync as mkdtempSync2,
|
|
24395
|
-
readFileSync as
|
|
24768
|
+
readFileSync as readFileSync11,
|
|
24396
24769
|
renameSync as renameSync4,
|
|
24397
24770
|
rmSync as rmSync3,
|
|
24398
24771
|
writeFileSync as writeFileSync5
|
|
24399
24772
|
} from "node:fs";
|
|
24400
24773
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
24401
|
-
import { dirname as dirname4, join as
|
|
24774
|
+
import { dirname as dirname4, join as join14 } from "node:path";
|
|
24775
|
+
|
|
24776
|
+
// src/render/workspace.ts
|
|
24777
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
24778
|
+
import { join as join13 } from "node:path";
|
|
24402
24779
|
|
|
24403
24780
|
// templates/agents/architect.md
|
|
24404
24781
|
var architect_default = "# Role: architect\n\nYou write the OpenSpec change for an approved intake: proposal, delta specs with scenarios, design and tasks. You do not write code.\n\n## Inputs\n\nThe task message contains the change id, the request summary, explorer summaries per repo, the workspace repos and the contracts. Start from those; query the graph for anything they do not cover.\n\n## Where you may write\n\nOnly `openspec/changes/<change-id>/` and `.factory/context/<change-id>/`. Everything else is read-only for you.\n\n## Proposal (`proposal.md`)\n\n`## Why` (the problem, 2–5 sentences), `## What Changes` (bullets), `## Capabilities` (`### New Capabilities` / `### Modified Capabilities`), `## Impact` (repos, APIs, contracts, data).\n\n## Specs (`specs/<capability>/spec.md`)\n\n- Use `## ADDED|MODIFIED|REMOVED Requirements`, `### Requirement: <name>` with SHALL/MUST, and at least one `#### Scenario: <name>` per requirement with `- **WHEN** …` / `- **THEN** …` lines.\n- Scenarios are concrete and testable: inputs, observable outputs, error cases. Every scenario will get a test.\n\n## Design (`design.md`)\n\nContext, decisions with the alternatives you rejected, contract changes, risks. Keep it short.\n\n## Tasks (`tasks.md`)\n\nGroup by repo. Every task is one checkbox line followed by three metadata lines, exactly:\n```\n## 1. backend\n- [ ] 1.1 Add health handler\n - repo: backend\n - deps: none\n - touches: internal/http/**, cmd/server/main.go\n```\n\n- Each task names exactly one repo from the workspace list.\n- `deps` lists task ids it needs (`1.1, 2.3`) or `none`. No cycles.\n- `touches` lists the file globs (relative to that repo) the task may change. Tasks in the same repo with overlapping globs run one after another, so keep globs tight.\n- Contract changes (API schemas, events, Helm values) are their own tasks, first, and consumer tasks depend on them so they can run in parallel.\n- Size tasks so one agent can finish one in a single focused session. Tests for a task's behaviour belong to that task.\n- Never put `[ ]` anywhere except on the task line itself.\n\n## Context packs (after approval only)\n\nWhen asked to write context packs, write one file per task to `.factory/context/<change-id>/<task-id>.md`, under the token cap in the task message, containing: the task, the spec scenarios it covers, the contract excerpts it needs, the target symbols with file paths (line ranges are hints only), and the gate commands.\n\nUse `task: \"design\"` or `task: \"context-packs\"` in your handoff and list every file you wrote.\n";
|
|
@@ -27252,6 +27629,31 @@ export {
|
|
|
27252
27629
|
};
|
|
27253
27630
|
`;
|
|
27254
27631
|
|
|
27632
|
+
// src/config/instructions.ts
|
|
27633
|
+
import { relative as relative3 } from "node:path";
|
|
27634
|
+
var repoOf = (config, name) => config.repos.find((r) => r.name === name)?.path ?? name;
|
|
27635
|
+
function instructionPaths(config, repo) {
|
|
27636
|
+
const repoPaths = repo === undefined ? [] : (config.instructions.repos[repo] ?? []).map((path) => {
|
|
27637
|
+
const base = repoOf(config, repo);
|
|
27638
|
+
return base === "." ? path : `${base}/${path}`;
|
|
27639
|
+
});
|
|
27640
|
+
return [...new Set([...config.instructions.workspace, ...repoPaths])];
|
|
27641
|
+
}
|
|
27642
|
+
function sessionInstructionPaths(config, repoName, repoPath) {
|
|
27643
|
+
return instructionPaths(config, repoName).map((path) => {
|
|
27644
|
+
const rel = relative3(repoPath, path);
|
|
27645
|
+
return rel === "" ? path : rel;
|
|
27646
|
+
});
|
|
27647
|
+
}
|
|
27648
|
+
function allInstructionPaths(config) {
|
|
27649
|
+
return [
|
|
27650
|
+
...new Set([
|
|
27651
|
+
...config.instructions.workspace,
|
|
27652
|
+
...Object.keys(config.instructions.repos).flatMap((repo) => instructionPaths(config, repo))
|
|
27653
|
+
])
|
|
27654
|
+
];
|
|
27655
|
+
}
|
|
27656
|
+
|
|
27255
27657
|
// src/handoff/schema.ts
|
|
27256
27658
|
var Finding = strictObject({
|
|
27257
27659
|
severity: _enum(["must", "should", "nice"]),
|
|
@@ -27374,7 +27776,7 @@ var READ_TOOLS = [
|
|
|
27374
27776
|
rule("glob", "*", "allow"),
|
|
27375
27777
|
rule("grep", "*", "allow")
|
|
27376
27778
|
];
|
|
27377
|
-
function agentRules(role, testGlobs = []) {
|
|
27779
|
+
function agentRules(role, testGlobs = [], options = {}) {
|
|
27378
27780
|
const rules = ACTIONS.map((a) => rule(a, "*", "deny"));
|
|
27379
27781
|
switch (role) {
|
|
27380
27782
|
case "orchestrator":
|
|
@@ -27384,6 +27786,7 @@ function agentRules(role, testGlobs = []) {
|
|
|
27384
27786
|
break;
|
|
27385
27787
|
case "architect":
|
|
27386
27788
|
rules.push(...READ_TOOLS, ...allowAll("shell", GRAPH_QUERY_COMMANDS), rule("edit", "openspec/changes/*", "allow"));
|
|
27789
|
+
rules.push(...optionalTools(options));
|
|
27387
27790
|
return [...rules, ...alwaysDeny([".factory/context/*"])];
|
|
27388
27791
|
case "reviewer":
|
|
27389
27792
|
case "contract-reviewer":
|
|
@@ -27396,8 +27799,32 @@ function agentRules(role, testGlobs = []) {
|
|
|
27396
27799
|
rules.push(...READ_TOOLS, rule("shell", "*", "allow"), rule("edit", "*", "allow"));
|
|
27397
27800
|
break;
|
|
27398
27801
|
}
|
|
27802
|
+
rules.push(...optionalTools(options));
|
|
27399
27803
|
return [...rules, ...alwaysDeny()];
|
|
27400
27804
|
}
|
|
27805
|
+
function customAgentRules(phase, options) {
|
|
27806
|
+
const rules = ACTIONS.map((a) => rule(a, "*", "deny"));
|
|
27807
|
+
rules.push(...READ_TOOLS);
|
|
27808
|
+
if (phase === "reviewer" || phase === "contract-reviewer") {
|
|
27809
|
+
rules.push(...allowAll("shell", [...GRAPH_QUERY_COMMANDS, ...READ_ONLY_GIT]));
|
|
27810
|
+
} else {
|
|
27811
|
+
rules.push(rule("shell", "*", "allow"));
|
|
27812
|
+
}
|
|
27813
|
+
rules.push(...allowAll("shell", options.shell ?? []));
|
|
27814
|
+
rules.push(...allowAll("edit", options.edits ?? []));
|
|
27815
|
+
rules.push(...optionalTools(options));
|
|
27816
|
+
return [...rules, ...alwaysDeny()];
|
|
27817
|
+
}
|
|
27818
|
+
function optionalTools(options) {
|
|
27819
|
+
const out = [];
|
|
27820
|
+
if (options.skills === true)
|
|
27821
|
+
out.push(rule("skill", "*", "allow"));
|
|
27822
|
+
if (options.web === true)
|
|
27823
|
+
out.push(rule("webfetch", "*", "allow"), rule("websearch", "*", "allow"));
|
|
27824
|
+
for (const action of options.mcp ?? [])
|
|
27825
|
+
out.push(rule(action, "*", "allow"));
|
|
27826
|
+
return out;
|
|
27827
|
+
}
|
|
27401
27828
|
function alwaysDeny(reallow = []) {
|
|
27402
27829
|
return [
|
|
27403
27830
|
...denyAll("edit", PROTECTED_PATHS),
|
|
@@ -28105,6 +28532,84 @@ var rust_default = `# Rust standard
|
|
|
28105
28532
|
- One behaviour per \`#[test]\`; every spec scenario maps to at least one test named after it (\`fn rejects_expired_token()\`). <!-- factory -->
|
|
28106
28533
|
`;
|
|
28107
28534
|
|
|
28535
|
+
// templates/standards/swift.md
|
|
28536
|
+
var swift_default = `# Swift standard
|
|
28537
|
+
|
|
28538
|
+
<!-- Sources:
|
|
28539
|
+
[api] Swift API Design Guidelines — https://www.swift.org/documentation/api-design-guidelines/
|
|
28540
|
+
[style] Swift.org: Swift Style Guide — https://www.swift.org/documentation/api-design-guidelines/#fundamentals
|
|
28541
|
+
[format] SwiftFormat rules — https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md
|
|
28542
|
+
[lint] SwiftLint rules — https://realm.github.io/SwiftLint/rule-directory.html
|
|
28543
|
+
[testing] Swift Testing — https://developer.apple.com/documentation/testing
|
|
28544
|
+
[xcode] Xcode Build Settings Reference — https://developer.apple.com/documentation/xcode/build-settings-reference
|
|
28545
|
+
-->
|
|
28546
|
+
|
|
28547
|
+
## Summary
|
|
28548
|
+
|
|
28549
|
+
- Keep the package/plugin layout and deployment target the repository already declares.
|
|
28550
|
+
- \`swiftformat --lint\` and \`swiftlint\` are gates; never suppress a rule without a reason.
|
|
28551
|
+
- Names read as prose at the call site; types are nouns, methods are verbs, booleans are assertions.
|
|
28552
|
+
- Prefer values and \`let\`; use \`var\` only when mutation is part of the design.
|
|
28553
|
+
- Handle errors with \`throws\`/\`Result\`; do not trap or force-unwrap outside tests and proven invariants.
|
|
28554
|
+
- Public APIs have documentation comments and explicit access control; implementation stays internal.
|
|
28555
|
+
- Test behaviour with Swift Testing (or the repo's XCTest suite); one test per spec scenario.
|
|
28556
|
+
|
|
28557
|
+
## Layout and access
|
|
28558
|
+
|
|
28559
|
+
<!-- [api] Fundamentals; [xcode] Packaging -->
|
|
28560
|
+
- Keep the repository's existing package and plugin directories; don't introduce a second source root. <!-- factory -->
|
|
28561
|
+
- One primary type per file, file named after the type; extensions that add protocol conformance live beside it.
|
|
28562
|
+
- Default to \`internal\`. Add \`public\` only for the module's real surface; mark implementation details \`private\` or \`fileprivate\`.
|
|
28563
|
+
- Keep platform code behind \`#if os(iOS)\` / \`#if os(macOS)\` and shared code platform-neutral. <!-- factory -->
|
|
28564
|
+
|
|
28565
|
+
## Naming and API shape
|
|
28566
|
+
|
|
28567
|
+
<!-- [api] Naming; [style] Naming -->
|
|
28568
|
+
- Types and protocols are \`UpperCamelCase\`; functions, properties and cases are \`lowerCamelCase\`.
|
|
28569
|
+
- Methods read as a sentence at the call site: \`move(from:to:)\`, not \`moveFromTo(_:_:)\`.
|
|
28570
|
+
- Booleans assert a fact: \`isEnabled\`, \`hasPermission\`, \`canRetry\`.
|
|
28571
|
+
- Omit needless words; the argument label carries the role (\`add(_ element:)\`, \`remove(at:)\`).
|
|
28572
|
+
- Acronyms keep a single case (\`URLSession\`, \`userID\`); don't abbreviate domain words.
|
|
28573
|
+
|
|
28574
|
+
## Values, optionals and errors
|
|
28575
|
+
|
|
28576
|
+
<!-- [api] Fundamentals; [testing] Error Handling -->
|
|
28577
|
+
- Prefer \`let\`; use \`var\` only where mutation is required and scoped.
|
|
28578
|
+
- Avoid force unwrap (\`!\`), \`try!\` and \`as!\` outside tests; use \`guard let\`, \`if let\`, or throw.
|
|
28579
|
+
- Use \`throws\` for recoverable failure and typed errors for cases callers branch on; never discard an error.
|
|
28580
|
+
- \`fatalError\` only for a broken invariant at startup, with a message naming the invariant. <!-- factory -->
|
|
28581
|
+
|
|
28582
|
+
## Concurrency
|
|
28583
|
+
|
|
28584
|
+
<!-- [api] Concurrency; [lint] Concurrency rules -->
|
|
28585
|
+
- Keep UI work on the main actor; mark views and view models \`@MainActor\` where the framework expects it.
|
|
28586
|
+
- Use structured concurrency (\`async let\`, task groups); do not detach a task without an owner and cancellation path.
|
|
28587
|
+
- Shared mutable state is an actor or guarded by a lock; document the isolation boundary.
|
|
28588
|
+
- Never block the main actor with file, network or database I/O. <!-- factory -->
|
|
28589
|
+
|
|
28590
|
+
## Tauri plugins and iOS/macOS glue
|
|
28591
|
+
|
|
28592
|
+
<!-- [xcode] Frameworks; [api] Documentation -->
|
|
28593
|
+
- A Tauri plugin exposes a typed Rust API; Swift stays behind that boundary and never becomes the public contract.
|
|
28594
|
+
- Request permission strings and entitlements from the security owner; do not edit capability files here. <!-- factory -->
|
|
28595
|
+
- Verify the host build and the iOS target compile; report which SDK/target you used. <!-- factory -->
|
|
28596
|
+
|
|
28597
|
+
## Testing
|
|
28598
|
+
|
|
28599
|
+
<!-- [testing] Test declarations; [lint] Test rules -->
|
|
28600
|
+
- Use Swift Testing (\`@Test\`, \`#expect\`) in new code; keep the repo's XCTest suite where it already exists.
|
|
28601
|
+
- Test behaviour through the public API; avoid snapshot-only tests and sleeps.
|
|
28602
|
+
- Name tests after the scenario they cover; one scenario can have several focused tests.
|
|
28603
|
+
- Keep tests deterministic: inject clocks, file systems and network clients at the boundary. <!-- factory -->
|
|
28604
|
+
|
|
28605
|
+
## Formatting and verification
|
|
28606
|
+
|
|
28607
|
+
<!-- [format] Rules; [lint] Configuration -->
|
|
28608
|
+
- \`swiftformat --lint\` must pass; do not reformat code outside the task.
|
|
28609
|
+
- \`swiftlint lint\` must pass with the repository's config; fix the cause instead of adding \`// swiftlint:disable\`.
|
|
28610
|
+
- \`swift build\` (and the iOS target build where relevant) must pass before the task is done. <!-- factory -->
|
|
28611
|
+
`;
|
|
28612
|
+
|
|
28108
28613
|
// templates/standards/tauri.md
|
|
28109
28614
|
var tauri_default = `# Tauri v2 standard
|
|
28110
28615
|
|
|
@@ -28527,6 +29032,24 @@ var PROFILES = {
|
|
|
28527
29032
|
},
|
|
28528
29033
|
install: [{ lockfile: "go.sum", command: "go mod download" }]
|
|
28529
29034
|
},
|
|
29035
|
+
swift: {
|
|
29036
|
+
id: "swift",
|
|
29037
|
+
title: "Swift",
|
|
29038
|
+
prose: swift_default,
|
|
29039
|
+
testGlobs: ["*Tests.swift", "*/Tests/*", "Tests/*"],
|
|
29040
|
+
markers: ["Package.swift"],
|
|
29041
|
+
targets: "dir",
|
|
29042
|
+
extensions: [".swift"],
|
|
29043
|
+
binDirs: [],
|
|
29044
|
+
gates: {
|
|
29045
|
+
format: [{ command: "swiftformat --lint .", scoped: "swiftformat --lint {files}" }],
|
|
29046
|
+
lint: [{ command: "swiftlint lint", scoped: "swiftlint lint --path {files}" }],
|
|
29047
|
+
types: [{ command: "swift build" }],
|
|
29048
|
+
test: [{ command: "swift test" }],
|
|
29049
|
+
testAffected: [{ command: "swift test" }]
|
|
29050
|
+
},
|
|
29051
|
+
install: [{ lockfile: "Package.resolved", command: "swift package resolve" }]
|
|
29052
|
+
},
|
|
28530
29053
|
rust: {
|
|
28531
29054
|
id: "rust",
|
|
28532
29055
|
title: "Rust",
|
|
@@ -28699,9 +29222,9 @@ var AGENTS = [
|
|
|
28699
29222
|
function frontmatter(spec) {
|
|
28700
29223
|
const data = {
|
|
28701
29224
|
description: spec.description,
|
|
28702
|
-
mode: "primary",
|
|
29225
|
+
mode: spec.mode ?? "primary",
|
|
28703
29226
|
model: spec.model,
|
|
28704
|
-
permissions: agentRules(spec.role, spec.testGlobs)
|
|
29227
|
+
permissions: spec.permissions ?? agentRules(spec.role, spec.testGlobs)
|
|
28705
29228
|
};
|
|
28706
29229
|
const doc = new $Document(data);
|
|
28707
29230
|
const rules = doc.get("permissions", true);
|
|
@@ -28721,10 +29244,55 @@ ${parts.sections.map((s) => s.trim()).join(`
|
|
|
28721
29244
|
`)}
|
|
28722
29245
|
`;
|
|
28723
29246
|
}
|
|
29247
|
+
var ROLE_FOR_PHASE = {
|
|
29248
|
+
orchestrator: "orchestrator",
|
|
29249
|
+
architect: "architect",
|
|
29250
|
+
explorer: "explorer",
|
|
29251
|
+
reviewer: "reviewer",
|
|
29252
|
+
"contract-reviewer": "contractReviewer",
|
|
29253
|
+
tester: "tester",
|
|
29254
|
+
implementer: "implementer"
|
|
29255
|
+
};
|
|
29256
|
+
function skillNames(config, role, repo) {
|
|
29257
|
+
return [
|
|
29258
|
+
...new Set([
|
|
29259
|
+
...config.skills.roles[role] ?? [],
|
|
29260
|
+
...repo === undefined ? [] : config.skills.repos[repo] ?? []
|
|
29261
|
+
])
|
|
29262
|
+
];
|
|
29263
|
+
}
|
|
29264
|
+
function skillsSection(names) {
|
|
29265
|
+
if (names.length === 0)
|
|
29266
|
+
return;
|
|
29267
|
+
return `## Skills
|
|
29268
|
+
|
|
29269
|
+
Load these skills first: ${names.map((n) => `\`${n}\``).join(", ")}. Use the skill tool before you start; follow the loaded skill over your own habits.`;
|
|
29270
|
+
}
|
|
29271
|
+
function instructionsSection(config, repo) {
|
|
29272
|
+
const paths = instructionPaths(config, repo);
|
|
29273
|
+
if (paths.length === 0)
|
|
29274
|
+
return;
|
|
29275
|
+
return `## Project instructions
|
|
29276
|
+
|
|
29277
|
+
Read these files before you change anything; they are the project's own bar and take precedence over your general knowledge. Paths are workspace-relative; from a worktree or repo checkout, open \`$AI_FACTORY_WORKSPACE/<path>\`:
|
|
29278
|
+
${paths.map((p) => `- \`${p}\``).join(`
|
|
29279
|
+
`)}`;
|
|
29280
|
+
}
|
|
29281
|
+
function sharedSections(role) {
|
|
29282
|
+
return role === "orchestrator" ? [untrusted_default, handoff_default] : [untrusted_default, graph_default, handoff_default];
|
|
29283
|
+
}
|
|
28724
29284
|
function renderAgent(config, spec) {
|
|
28725
|
-
const
|
|
29285
|
+
const skillList = skillNames(config, spec.modelRole);
|
|
29286
|
+
const web = spec.role === "reviewer" || spec.role === "contract-reviewer";
|
|
29287
|
+
const options = {
|
|
29288
|
+
...skillList.length > 0 ? { skills: true } : {},
|
|
29289
|
+
...web ? { web: true } : {},
|
|
29290
|
+
...config.mcp.roles[spec.modelRole] === undefined ? {} : { mcp: config.mcp.roles[spec.modelRole] }
|
|
29291
|
+
};
|
|
28726
29292
|
const section = spec.role === "reviewer" ? ponytailSection(config, "reviewer") : undefined;
|
|
28727
29293
|
const addon = section === undefined ? [] : [section];
|
|
29294
|
+
const skills = skillsSection(skillList);
|
|
29295
|
+
const instructions = instructionsSection(config);
|
|
28728
29296
|
return {
|
|
28729
29297
|
path: `.opencode/agents/${spec.id}.md`,
|
|
28730
29298
|
template: `agents/${spec.id}`,
|
|
@@ -28734,9 +29302,16 @@ function renderAgent(config, spec) {
|
|
|
28734
29302
|
description: spec.description,
|
|
28735
29303
|
model: modelFor(config, spec.modelRole),
|
|
28736
29304
|
role: spec.role,
|
|
28737
|
-
...spec.role === "tester" ? { testGlobs: workspaceTestGlobs(config) } : {}
|
|
29305
|
+
...spec.role === "tester" ? { testGlobs: workspaceTestGlobs(config) } : {},
|
|
29306
|
+
permissions: agentRules(spec.role, spec.role === "tester" ? workspaceTestGlobs(config) : [], options)
|
|
28738
29307
|
}),
|
|
28739
|
-
sections: [
|
|
29308
|
+
sections: [
|
|
29309
|
+
spec.body,
|
|
29310
|
+
...instructions === undefined ? [] : [instructions],
|
|
29311
|
+
...skills === undefined ? [] : [skills],
|
|
29312
|
+
...addon,
|
|
29313
|
+
...sharedSections(spec.role)
|
|
29314
|
+
]
|
|
28740
29315
|
})
|
|
28741
29316
|
};
|
|
28742
29317
|
}
|
|
@@ -28752,6 +29327,8 @@ function renderImplementer(config, repo) {
|
|
|
28752
29327
|
const standards = profilesFor(languages).map((p) => renderStandard(p, { comments: false }));
|
|
28753
29328
|
const langs = languages.length > 0 ? languages.join(", ") : "none detected";
|
|
28754
29329
|
const addon = ponytailSection(config, "implementer");
|
|
29330
|
+
const skills = skillsSection(skillNames(config, "implementer", repo.name));
|
|
29331
|
+
const instructions = instructionsSection(config, repo.name);
|
|
28755
29332
|
return {
|
|
28756
29333
|
path: `.opencode/agents/implementer-${repo.name}.md`,
|
|
28757
29334
|
template: "agents/implementer",
|
|
@@ -28759,11 +29336,19 @@ function renderImplementer(config, repo) {
|
|
|
28759
29336
|
content: agentFile({
|
|
28760
29337
|
head: frontmatter({
|
|
28761
29338
|
description: `ai-factory implementer for ${repo.name} (${repo.path}; ${langs}).`,
|
|
28762
|
-
model: modelFor(config, "implementer"),
|
|
28763
|
-
role: "implementer"
|
|
29339
|
+
model: modelFor(config, "implementer", { repo: repo.name }),
|
|
29340
|
+
role: "implementer",
|
|
29341
|
+
...skills === undefined && config.mcp.roles.implementer === undefined ? {} : {
|
|
29342
|
+
permissions: agentRules("implementer", [], {
|
|
29343
|
+
skills: skills !== undefined,
|
|
29344
|
+
...config.mcp.roles.implementer === undefined ? {} : { mcp: config.mcp.roles.implementer }
|
|
29345
|
+
})
|
|
29346
|
+
}
|
|
28764
29347
|
}),
|
|
28765
29348
|
sections: [
|
|
28766
29349
|
implementer_default.replaceAll("{{repo}}", repo.name),
|
|
29350
|
+
...instructions === undefined ? [] : [instructions],
|
|
29351
|
+
...skills === undefined ? [] : [skills],
|
|
28767
29352
|
...standards,
|
|
28768
29353
|
...addon === undefined ? [] : [addon],
|
|
28769
29354
|
untrusted_default,
|
|
@@ -28773,6 +29358,41 @@ function renderImplementer(config, repo) {
|
|
|
28773
29358
|
})
|
|
28774
29359
|
};
|
|
28775
29360
|
}
|
|
29361
|
+
function renderCustomAgent(config, agent, root) {
|
|
29362
|
+
const role = ROLE_FOR_PHASE[agent.phase] ?? "implementer";
|
|
29363
|
+
const agentRole = agent.phase;
|
|
29364
|
+
const body = agent.prompt ?? (agent.promptFile !== undefined && root !== undefined ? readFileSync10(join13(root, agent.promptFile), "utf8") : `# Role: ${agent.id}
|
|
29365
|
+
|
|
29366
|
+
${agent.description}`);
|
|
29367
|
+
const skills = skillsSection(agent.skills);
|
|
29368
|
+
const instructions = instructionsSection(config);
|
|
29369
|
+
return {
|
|
29370
|
+
path: `.opencode/agents/${agent.id}.md`,
|
|
29371
|
+
template: `agents/custom/${agent.id}`,
|
|
29372
|
+
mode: "file",
|
|
29373
|
+
content: agentFile({
|
|
29374
|
+
head: frontmatter({
|
|
29375
|
+
description: agent.description,
|
|
29376
|
+
model: agent.model ?? modelFor(config, role, { agent: agent.id }),
|
|
29377
|
+
role: agentRole,
|
|
29378
|
+
mode: agent.mode,
|
|
29379
|
+
permissions: customAgentRules(agentRole, {
|
|
29380
|
+
edits: agent.edits,
|
|
29381
|
+
shell: agent.shell,
|
|
29382
|
+
skills: agent.skills.length > 0,
|
|
29383
|
+
web: agent.web,
|
|
29384
|
+
mcp: agent.mcp
|
|
29385
|
+
})
|
|
29386
|
+
}),
|
|
29387
|
+
sections: [
|
|
29388
|
+
body,
|
|
29389
|
+
...instructions === undefined ? [] : [instructions],
|
|
29390
|
+
...skills === undefined ? [] : [skills],
|
|
29391
|
+
...sharedSections(agentRole)
|
|
29392
|
+
]
|
|
29393
|
+
})
|
|
29394
|
+
};
|
|
29395
|
+
}
|
|
28776
29396
|
function renderAgentsMd(config) {
|
|
28777
29397
|
const markers = BLOCK_MARKERS["AGENTS.md"];
|
|
28778
29398
|
if (markers === undefined)
|
|
@@ -28811,8 +29431,13 @@ ${config.graph.exclude.join(`
|
|
|
28811
29431
|
`)}
|
|
28812
29432
|
`;
|
|
28813
29433
|
}
|
|
28814
|
-
function renderOpencodeConfig() {
|
|
28815
|
-
const
|
|
29434
|
+
function renderOpencodeConfig(factory) {
|
|
29435
|
+
const instructions = allInstructionPaths(factory);
|
|
29436
|
+
const config = {
|
|
29437
|
+
$schema: "https://opencode.ai/config.json",
|
|
29438
|
+
update: "disable",
|
|
29439
|
+
...instructions.length > 0 ? { instructions } : {}
|
|
29440
|
+
};
|
|
28816
29441
|
return `// ${MANAGED_HEADER}
|
|
28817
29442
|
// Agents and their permissions are in .opencode/agents/.
|
|
28818
29443
|
${JSON.stringify(config, null, 2)}
|
|
@@ -28881,7 +29506,7 @@ function ponytailFiles(config) {
|
|
|
28881
29506
|
}
|
|
28882
29507
|
];
|
|
28883
29508
|
}
|
|
28884
|
-
function renderWorkspace(config) {
|
|
29509
|
+
function renderWorkspace(config, options = {}) {
|
|
28885
29510
|
const files = [
|
|
28886
29511
|
{ path: "AGENTS.md", template: "agents-md", mode: "block", content: renderAgentsMd(config) },
|
|
28887
29512
|
{ path: ".gitignore", template: "gitignore", mode: "block", content: renderGitignore() },
|
|
@@ -28895,7 +29520,7 @@ function renderWorkspace(config) {
|
|
|
28895
29520
|
path: ".opencode/opencode.jsonc",
|
|
28896
29521
|
template: "opencode-config",
|
|
28897
29522
|
mode: "file",
|
|
28898
|
-
content: renderOpencodeConfig()
|
|
29523
|
+
content: renderOpencodeConfig(config)
|
|
28899
29524
|
},
|
|
28900
29525
|
{
|
|
28901
29526
|
path: ".opencode/plugins/factory.js",
|
|
@@ -28929,7 +29554,8 @@ function renderWorkspace(config) {
|
|
|
28929
29554
|
})),
|
|
28930
29555
|
...ponytailFiles(config),
|
|
28931
29556
|
...AGENTS.map((spec) => renderAgent(config, spec)),
|
|
28932
|
-
...config.repos.filter((r) => r.role === "code").map((r) => renderImplementer(config, r))
|
|
29557
|
+
...config.repos.filter((r) => r.role === "code").map((r) => renderImplementer(config, r)),
|
|
29558
|
+
...config.agents.map((agent) => renderCustomAgent(config, agent, options.root))
|
|
28933
29559
|
];
|
|
28934
29560
|
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
28935
29561
|
}
|
|
@@ -28954,10 +29580,10 @@ function findBlock(text, markers) {
|
|
|
28954
29580
|
return { start, end: endMarker + markers.end.length };
|
|
28955
29581
|
}
|
|
28956
29582
|
function readOptional(abs) {
|
|
28957
|
-
return existsSync10(abs) ?
|
|
29583
|
+
return existsSync10(abs) ? readFileSync11(abs, "utf8") : undefined;
|
|
28958
29584
|
}
|
|
28959
29585
|
function currentOwned(root, path, mode) {
|
|
28960
|
-
const text = readOptional(
|
|
29586
|
+
const text = readOptional(join14(root, path));
|
|
28961
29587
|
if (text === undefined || mode === "file")
|
|
28962
29588
|
return text;
|
|
28963
29589
|
const block = findBlock(text, markersFor(path));
|
|
@@ -29025,12 +29651,12 @@ ${block}
|
|
|
29025
29651
|
writeAtomic(abs, next);
|
|
29026
29652
|
}
|
|
29027
29653
|
function removeOwned(root, path, mode) {
|
|
29028
|
-
const abs =
|
|
29654
|
+
const abs = join14(root, path);
|
|
29029
29655
|
if (mode === "file") {
|
|
29030
29656
|
rmSync3(abs, { force: true });
|
|
29031
29657
|
return;
|
|
29032
29658
|
}
|
|
29033
|
-
const text =
|
|
29659
|
+
const text = readFileSync11(abs, "utf8");
|
|
29034
29660
|
const range = findBlock(text, markersFor(path));
|
|
29035
29661
|
if (range === undefined)
|
|
29036
29662
|
return;
|
|
@@ -29047,9 +29673,9 @@ async function blobText(root, blob) {
|
|
|
29047
29673
|
return res.code === 0 ? res.stdout : undefined;
|
|
29048
29674
|
}
|
|
29049
29675
|
async function mergeThree(root, ours, base, theirs) {
|
|
29050
|
-
const dir = mkdtempSync2(
|
|
29676
|
+
const dir = mkdtempSync2(join14(tmpdir2(), "ai-factory-merge-"));
|
|
29051
29677
|
try {
|
|
29052
|
-
const files = { ours:
|
|
29678
|
+
const files = { ours: join14(dir, "ours"), base: join14(dir, "base"), theirs: join14(dir, "theirs") };
|
|
29053
29679
|
writeFileSync5(files.ours, ours);
|
|
29054
29680
|
writeFileSync5(files.base, base);
|
|
29055
29681
|
writeFileSync5(files.theirs, theirs);
|
|
@@ -29096,7 +29722,7 @@ async function executeSync(root, plan, previous, options = {}) {
|
|
|
29096
29722
|
if (file === undefined)
|
|
29097
29723
|
throw new Error(`no render for ${change.path}`);
|
|
29098
29724
|
if (change.action !== "unchanged") {
|
|
29099
|
-
const abs =
|
|
29725
|
+
const abs = join14(root, file.path);
|
|
29100
29726
|
if (file.mode === "block")
|
|
29101
29727
|
writeBlock(abs, file.path, file.content);
|
|
29102
29728
|
else
|
|
@@ -29114,7 +29740,7 @@ async function executeSync(root, plan, previous, options = {}) {
|
|
|
29114
29740
|
break;
|
|
29115
29741
|
}
|
|
29116
29742
|
const result = await mergeThree(root, ours, base, file.content);
|
|
29117
|
-
const abs =
|
|
29743
|
+
const abs = join14(root, file.path);
|
|
29118
29744
|
if (file.mode === "block")
|
|
29119
29745
|
writeBlock(abs, file.path, result.text.replace(/\n$/, ""));
|
|
29120
29746
|
else
|
|
@@ -29146,12 +29772,12 @@ import {
|
|
|
29146
29772
|
mkdirSync as mkdirSync6,
|
|
29147
29773
|
mkdtempSync as mkdtempSync3,
|
|
29148
29774
|
readdirSync as readdirSync5,
|
|
29149
|
-
readFileSync as
|
|
29775
|
+
readFileSync as readFileSync12,
|
|
29150
29776
|
rmSync as rmSync4,
|
|
29151
29777
|
statSync as statSync4
|
|
29152
29778
|
} from "node:fs";
|
|
29153
29779
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
29154
|
-
import { join as
|
|
29780
|
+
import { join as join15, relative as relative4, sep } from "node:path";
|
|
29155
29781
|
|
|
29156
29782
|
// src/tasks/glob.ts
|
|
29157
29783
|
var cache = new Map;
|
|
@@ -29441,14 +30067,14 @@ function normalizeCheckboxes(text) {
|
|
|
29441
30067
|
|
|
29442
30068
|
// src/openspec/cli.ts
|
|
29443
30069
|
function env(root) {
|
|
29444
|
-
const xdg =
|
|
30070
|
+
const xdg = join15(root, ".factory", "cache", "openspec-xdg");
|
|
29445
30071
|
mkdirSync6(xdg, { recursive: true });
|
|
29446
30072
|
return { ...toolEnv("openspec"), XDG_CONFIG_HOME: xdg };
|
|
29447
30073
|
}
|
|
29448
30074
|
async function openspec(root, cwd, args) {
|
|
29449
30075
|
return exec([toolBin("openspec"), ...args], { cwd, env: env(root), timeoutMs: 120000 });
|
|
29450
30076
|
}
|
|
29451
|
-
var changePath = (dir, id) =>
|
|
30077
|
+
var changePath = (dir, id) => join15(dir, "openspec", "changes", id);
|
|
29452
30078
|
async function newChange(root, dir, id) {
|
|
29453
30079
|
if (existsSync11(changePath(dir, id)))
|
|
29454
30080
|
return;
|
|
@@ -29470,10 +30096,10 @@ async function validateChange(root, dir, id) {
|
|
|
29470
30096
|
return item.valid ? [] : item.issues.map((i) => `openspec ${i.level.toLowerCase()}: ${i.path ? `${i.path}: ` : ""}${i.message}`);
|
|
29471
30097
|
}
|
|
29472
30098
|
function checkProposal(dir, id) {
|
|
29473
|
-
const file =
|
|
30099
|
+
const file = join15(changePath(dir, id), "proposal.md");
|
|
29474
30100
|
if (!existsSync11(file))
|
|
29475
30101
|
return ["proposal.md is missing"];
|
|
29476
|
-
const text =
|
|
30102
|
+
const text = readFileSync12(file, "utf8");
|
|
29477
30103
|
const issues = [];
|
|
29478
30104
|
for (const heading of ["## Why", "## What Changes"]) {
|
|
29479
30105
|
if (!text.split(`
|
|
@@ -29487,10 +30113,10 @@ function checkProposal(dir, id) {
|
|
|
29487
30113
|
}
|
|
29488
30114
|
async function checkDesign(root, dir, id, codeRepos, contracts = []) {
|
|
29489
30115
|
const issues = [...checkProposal(dir, id), ...await validateChange(root, dir, id)];
|
|
29490
|
-
const tasksFile =
|
|
30116
|
+
const tasksFile = join15(changePath(dir, id), "tasks.md");
|
|
29491
30117
|
if (!existsSync11(tasksFile))
|
|
29492
30118
|
return { issues: [...issues, "tasks.md is missing"], tasks: [] };
|
|
29493
|
-
const parsed = parseTasks(
|
|
30119
|
+
const parsed = parseTasks(readFileSync12(tasksFile, "utf8"));
|
|
29494
30120
|
issues.push(...parsed.issues.map((i) => `tasks.md:${i.line}: ${i.message}`));
|
|
29495
30121
|
if (parsed.tasks.length === 0)
|
|
29496
30122
|
issues.push("tasks.md has no tasks");
|
|
@@ -29508,7 +30134,7 @@ async function archiveChange(root, dir, id) {
|
|
|
29508
30134
|
}
|
|
29509
30135
|
var OPENSPEC_TOOLS_TEMPLATE = "openspec-tools";
|
|
29510
30136
|
async function renderOpenspecTools(root) {
|
|
29511
|
-
const scratch = mkdtempSync3(
|
|
30137
|
+
const scratch = mkdtempSync3(join15(tmpdir3(), "ai-factory-openspec-"));
|
|
29512
30138
|
try {
|
|
29513
30139
|
const res = await exec([
|
|
29514
30140
|
toolBin("openspec"),
|
|
@@ -29522,21 +30148,21 @@ async function renderOpenspecTools(root) {
|
|
|
29522
30148
|
], { cwd: scratch, env: env(root), timeoutMs: 120000 });
|
|
29523
30149
|
if (res.code !== 0)
|
|
29524
30150
|
throw new ExecError("openspec init failed", res);
|
|
29525
|
-
const base =
|
|
30151
|
+
const base = join15(scratch, ".opencode");
|
|
29526
30152
|
if (!existsSync11(base))
|
|
29527
30153
|
return [];
|
|
29528
30154
|
const files = [];
|
|
29529
30155
|
const walk = (dir) => {
|
|
29530
30156
|
for (const name of readdirSync5(dir).sort()) {
|
|
29531
|
-
const abs =
|
|
30157
|
+
const abs = join15(dir, name);
|
|
29532
30158
|
if (statSync4(abs).isDirectory())
|
|
29533
30159
|
walk(abs);
|
|
29534
30160
|
else
|
|
29535
30161
|
files.push({
|
|
29536
|
-
path:
|
|
30162
|
+
path: relative4(scratch, abs).split(sep).join("/"),
|
|
29537
30163
|
template: OPENSPEC_TOOLS_TEMPLATE,
|
|
29538
30164
|
mode: "file",
|
|
29539
|
-
content:
|
|
30165
|
+
content: readFileSync12(abs, "utf8")
|
|
29540
30166
|
});
|
|
29541
30167
|
}
|
|
29542
30168
|
};
|
|
@@ -29567,7 +30193,7 @@ function carriedTools(previous, options) {
|
|
|
29567
30193
|
function planWorkspace(root, config, options = {}) {
|
|
29568
30194
|
const previous = readLock(root)?.managed ?? {};
|
|
29569
30195
|
const tools = options.tools ?? [];
|
|
29570
|
-
const plan = planSync(root, [...renderWorkspace(config), ...tools], previous, {
|
|
30196
|
+
const plan = planSync(root, [...renderWorkspace(config, { root }), ...tools], previous, {
|
|
29571
30197
|
unmanaged: config.unmanaged,
|
|
29572
30198
|
...options,
|
|
29573
30199
|
carry: Object.keys(carriedTools(previous, options)),
|
|
@@ -29608,10 +30234,10 @@ async function syncWorkspace(root, config, options = {}) {
|
|
|
29608
30234
|
files: [...PONYTAIL.files]
|
|
29609
30235
|
}
|
|
29610
30236
|
} : {});
|
|
29611
|
-
writeFileSync6(
|
|
30237
|
+
writeFileSync6(join16(root, LOCK_FILE), serializeLock(lock));
|
|
29612
30238
|
const hooks = [];
|
|
29613
30239
|
for (const repo of config.repos) {
|
|
29614
|
-
hooks.push({ repo: repo.name, result: await installPrePush(
|
|
30240
|
+
hooks.push({ repo: repo.name, result: await installPrePush(join16(root, repo.path)) });
|
|
29615
30241
|
}
|
|
29616
30242
|
return { plan, lock, hooks, merged, conflicts };
|
|
29617
30243
|
}
|
|
@@ -29623,7 +30249,7 @@ async function dirtyInstallPaths(root, ignore = []) {
|
|
|
29623
30249
|
`).filter((l) => l.trim() !== "").map((l) => l.slice(3)).filter((p) => !ignore.includes(p));
|
|
29624
30250
|
}
|
|
29625
30251
|
function commitInstall(root, message) {
|
|
29626
|
-
return commitPaths(root, INSTALL_PATHS.filter((p) => existsSync12(
|
|
30252
|
+
return commitPaths(root, INSTALL_PATHS.filter((p) => existsSync12(join16(root, p))), message);
|
|
29627
30253
|
}
|
|
29628
30254
|
|
|
29629
30255
|
// src/cli/commands/detect.ts
|
|
@@ -29658,7 +30284,7 @@ var detectCommand = {
|
|
|
29658
30284
|
"run ai-factory from the workspace (AI repo or single repo)"
|
|
29659
30285
|
]);
|
|
29660
30286
|
}
|
|
29661
|
-
const hasConfig = existsSync13(
|
|
30287
|
+
const hasConfig = existsSync13(join17(root, CONFIG_FILE));
|
|
29662
30288
|
const loaded = hasConfig ? loadConfig(root) : undefined;
|
|
29663
30289
|
if (values.apply && loaded === undefined) {
|
|
29664
30290
|
throw new PreconditionError(`no ${CONFIG_FILE} to apply to`, ["run `ai-factory init` first"]);
|
|
@@ -29734,21 +30360,21 @@ var detectCommand = {
|
|
|
29734
30360
|
};
|
|
29735
30361
|
|
|
29736
30362
|
// src/doctor/doctor.ts
|
|
29737
|
-
import { existsSync as existsSync20, lstatSync as lstatSync2, mkdtempSync as mkdtempSync4, readdirSync as readdirSync7, readFileSync as
|
|
30363
|
+
import { existsSync as existsSync20, lstatSync as lstatSync2, mkdtempSync as mkdtempSync4, readdirSync as readdirSync7, readFileSync as readFileSync18, rmSync as rmSync8 } from "node:fs";
|
|
29738
30364
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
29739
|
-
import { delimiter, extname, join as
|
|
30365
|
+
import { delimiter, extname, join as join27 } from "node:path";
|
|
29740
30366
|
|
|
29741
30367
|
// src/gates/resolve.ts
|
|
29742
|
-
import { existsSync as existsSync16, readFileSync as
|
|
29743
|
-
import { join as
|
|
30368
|
+
import { existsSync as existsSync16, readFileSync as readFileSync15 } from "node:fs";
|
|
30369
|
+
import { join as join20 } from "node:path";
|
|
29744
30370
|
|
|
29745
30371
|
// src/gates/affected.ts
|
|
29746
|
-
import { existsSync as existsSync15, readFileSync as
|
|
29747
|
-
import { join as
|
|
30372
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14 } from "node:fs";
|
|
30373
|
+
import { join as join19, relative as relative5 } from "node:path";
|
|
29748
30374
|
|
|
29749
30375
|
// src/gates/targets.ts
|
|
29750
|
-
import { existsSync as existsSync14, readFileSync as
|
|
29751
|
-
import { join as
|
|
30376
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "node:fs";
|
|
30377
|
+
import { join as join18 } from "node:path";
|
|
29752
30378
|
var WALK2 = { maxDepth: 8, maxFiles: 50000 };
|
|
29753
30379
|
function worktreeFiles(worktree) {
|
|
29754
30380
|
return listFiles(worktree, WALK2).map((f) => f.path);
|
|
@@ -29789,7 +30415,7 @@ function installDirs(profile, files) {
|
|
|
29789
30415
|
}
|
|
29790
30416
|
function packageDependencies(abs) {
|
|
29791
30417
|
try {
|
|
29792
|
-
const pkg = JSON.parse(
|
|
30418
|
+
const pkg = JSON.parse(readFileSync13(join18(abs, "package.json"), "utf8"));
|
|
29793
30419
|
const names = new Set;
|
|
29794
30420
|
for (const key of [
|
|
29795
30421
|
"dependencies",
|
|
@@ -29810,9 +30436,9 @@ function packageDependencies(abs) {
|
|
|
29810
30436
|
function requirementHolds(req, worktree, dir) {
|
|
29811
30437
|
if (req === undefined)
|
|
29812
30438
|
return true;
|
|
29813
|
-
const dirs = dir === "." ? [worktree] : [
|
|
30439
|
+
const dirs = dir === "." ? [worktree] : [join18(worktree, dir), worktree];
|
|
29814
30440
|
if ("file" in req)
|
|
29815
|
-
return dirs.some((d) => req.file.some((f) => existsSync14(
|
|
30441
|
+
return dirs.some((d) => req.file.some((f) => existsSync14(join18(d, f))));
|
|
29816
30442
|
if ("dependency" in req) {
|
|
29817
30443
|
return dirs.some((d) => {
|
|
29818
30444
|
const deps = packageDependencies(d);
|
|
@@ -29821,7 +30447,7 @@ function requirementHolds(req, worktree, dir) {
|
|
|
29821
30447
|
}
|
|
29822
30448
|
return dirs.some((d) => {
|
|
29823
30449
|
try {
|
|
29824
|
-
return
|
|
30450
|
+
return readFileSync13(join18(d, req.fileContains.file), "utf8").includes(req.fileContains.text);
|
|
29825
30451
|
} catch {
|
|
29826
30452
|
return false;
|
|
29827
30453
|
}
|
|
@@ -29852,12 +30478,12 @@ var stem = (path) => {
|
|
|
29852
30478
|
return dot === -1 ? base : base.slice(0, dot);
|
|
29853
30479
|
};
|
|
29854
30480
|
function targetChanges(target, changed) {
|
|
29855
|
-
const roots = target.charts.length > 0 ? target.charts.map((c) =>
|
|
30481
|
+
const roots = target.charts.length > 0 ? target.charts.map((c) => join19(target.dir, c)) : [target.dir];
|
|
29856
30482
|
return changed.filter((f) => roots.some((r) => within(f, r)) && (target.charts.length > 0 || hasExt(target.profile, f)));
|
|
29857
30483
|
}
|
|
29858
30484
|
function crateName(cargoToml) {
|
|
29859
30485
|
try {
|
|
29860
|
-
const text =
|
|
30486
|
+
const text = readFileSync14(cargoToml, "utf8");
|
|
29861
30487
|
const pkg = text.split(/^\[/m).find((section) => section.startsWith("package]"));
|
|
29862
30488
|
return pkg?.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1];
|
|
29863
30489
|
} catch {
|
|
@@ -29872,9 +30498,9 @@ function ancestorDirs(path) {
|
|
|
29872
30498
|
return [...dirs, "."];
|
|
29873
30499
|
}
|
|
29874
30500
|
function placeholderValues(worktree, target, changed, allFiles, base, mode, related = []) {
|
|
29875
|
-
const rel = (p) => target.dir === "." ? p :
|
|
30501
|
+
const rel = (p) => target.dir === "." ? p : relative5(target.dir, p);
|
|
29876
30502
|
const mine = targetChanges(target, changed);
|
|
29877
|
-
const existing = mine.filter((f) => existsSync15(
|
|
30503
|
+
const existing = mine.filter((f) => existsSync15(join19(worktree, f)));
|
|
29878
30504
|
const tests = new Set(existing.filter((f) => isTestFile(target.profile, f)));
|
|
29879
30505
|
const stems = new Set(existing.filter((f) => !isTestFile(target.profile, f)).map(stem));
|
|
29880
30506
|
const dependents = new Set(related);
|
|
@@ -29887,11 +30513,11 @@ function placeholderValues(worktree, target, changed, allFiles, base, mode, rela
|
|
|
29887
30513
|
const packages = [...new Set(existing.filter((f) => f.endsWith(".go")).map((f) => rel(f)))].map((f) => f.includes("/") ? `./${f.slice(0, f.lastIndexOf("/"))}` : ".").filter((p, i, all) => all.indexOf(p) === i);
|
|
29888
30514
|
const crates = new Set;
|
|
29889
30515
|
for (const f of existing.filter((x) => x.endsWith(".rs"))) {
|
|
29890
|
-
const name = ancestorDirs(f).map((dir) => crateName(
|
|
30516
|
+
const name = ancestorDirs(f).map((dir) => crateName(join19(worktree, dir, "Cargo.toml"))).find((n) => n !== undefined);
|
|
29891
30517
|
if (name)
|
|
29892
30518
|
crates.add(name);
|
|
29893
30519
|
}
|
|
29894
|
-
const charts = mode === "full" ? [...target.charts] : target.charts.filter((c) => mine.some((f) => within(f,
|
|
30520
|
+
const charts = mode === "full" ? [...target.charts] : target.charts.filter((c) => mine.some((f) => within(f, join19(target.dir, c))));
|
|
29895
30521
|
return {
|
|
29896
30522
|
files: existing.map(rel),
|
|
29897
30523
|
testFiles: [...tests].sort().map(rel),
|
|
@@ -29923,22 +30549,22 @@ var GATES_BY_MODE = {
|
|
|
29923
30549
|
var SKIP = "skip";
|
|
29924
30550
|
function readText2(path) {
|
|
29925
30551
|
try {
|
|
29926
|
-
return
|
|
30552
|
+
return readFileSync15(path, "utf8");
|
|
29927
30553
|
} catch {
|
|
29928
30554
|
return;
|
|
29929
30555
|
}
|
|
29930
30556
|
}
|
|
29931
30557
|
function scriptRunner(worktree) {
|
|
29932
|
-
if (existsSync16(
|
|
30558
|
+
if (existsSync16(join20(worktree, "bun.lock")) || existsSync16(join20(worktree, "bun.lockb")))
|
|
29933
30559
|
return "bun run";
|
|
29934
|
-
if (existsSync16(
|
|
30560
|
+
if (existsSync16(join20(worktree, "pnpm-lock.yaml")))
|
|
29935
30561
|
return "pnpm run";
|
|
29936
|
-
if (existsSync16(
|
|
30562
|
+
if (existsSync16(join20(worktree, "yarn.lock")))
|
|
29937
30563
|
return "yarn run";
|
|
29938
30564
|
return "npm run --silent";
|
|
29939
30565
|
}
|
|
29940
30566
|
function findScript(worktree, name) {
|
|
29941
|
-
const pkg = readText2(
|
|
30567
|
+
const pkg = readText2(join20(worktree, "package.json"));
|
|
29942
30568
|
if (pkg !== undefined) {
|
|
29943
30569
|
try {
|
|
29944
30570
|
const scripts = JSON.parse(pkg).scripts;
|
|
@@ -29947,14 +30573,14 @@ function findScript(worktree, name) {
|
|
|
29947
30573
|
} catch {}
|
|
29948
30574
|
}
|
|
29949
30575
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
29950
|
-
const make = readText2(
|
|
30576
|
+
const make = readText2(join20(worktree, "Makefile"));
|
|
29951
30577
|
if (make !== undefined && new RegExp(`^${escaped}\\s*:(?!=)`, "m").test(make))
|
|
29952
30578
|
return "Makefile";
|
|
29953
|
-
const just = readText2(
|
|
30579
|
+
const just = readText2(join20(worktree, "justfile")) ?? readText2(join20(worktree, "Justfile"));
|
|
29954
30580
|
if (just !== undefined && new RegExp(`^@?${escaped}(\\s[^:]*)?:(?!=)`, "m").test(just))
|
|
29955
30581
|
return "justfile";
|
|
29956
30582
|
for (const file of ["Taskfile.yml", "Taskfile.yaml"]) {
|
|
29957
|
-
const task = readText2(
|
|
30583
|
+
const task = readText2(join20(worktree, file));
|
|
29958
30584
|
if (task === undefined)
|
|
29959
30585
|
continue;
|
|
29960
30586
|
try {
|
|
@@ -30004,7 +30630,7 @@ function profileGate(input, type, target) {
|
|
|
30004
30630
|
cwd: target.dir,
|
|
30005
30631
|
source: "profile",
|
|
30006
30632
|
profile: target.profile.id,
|
|
30007
|
-
binDirs: target.profile.binDirs.map((b) => target.dir === "." ? b :
|
|
30633
|
+
binDirs: target.profile.binDirs.map((b) => target.dir === "." ? b : join20(target.dir, b))
|
|
30008
30634
|
};
|
|
30009
30635
|
}
|
|
30010
30636
|
var isGap = (x) => ("reason" in x);
|
|
@@ -30035,7 +30661,7 @@ function resolveGates(input) {
|
|
|
30035
30661
|
const scriptName = input.config.gates.scripts[type];
|
|
30036
30662
|
const host = scriptName === undefined ? undefined : findScript(input.worktree, scriptName);
|
|
30037
30663
|
if (host !== undefined && scriptName !== undefined) {
|
|
30038
|
-
const files = type === "testAffected" ? input.changed.filter((f) => existsSync16(
|
|
30664
|
+
const files = type === "testAffected" ? input.changed.filter((f) => existsSync16(join20(input.worktree, f))) : [];
|
|
30039
30665
|
if (type === "testAffected" && files.length === 0) {
|
|
30040
30666
|
plan.skipped.push({ type, cwd: ".", reason: "skip: nothing affected" });
|
|
30041
30667
|
} else {
|
|
@@ -30069,13 +30695,50 @@ function resolveGates(input) {
|
|
|
30069
30695
|
plan.missing.push(result);
|
|
30070
30696
|
}
|
|
30071
30697
|
}
|
|
30698
|
+
for (const extra of input.config.gates.extra) {
|
|
30699
|
+
if (!extra.phases.includes(input.mode))
|
|
30700
|
+
continue;
|
|
30701
|
+
const wanted = extra.when.changed;
|
|
30702
|
+
if (wanted.length > 0 && input.mode === "task" && !input.changed.some((file) => wanted.some((glob) => opencodeGlobRegExp(glob).test(file)))) {
|
|
30703
|
+
plan.skipped.push({
|
|
30704
|
+
type: extra.type,
|
|
30705
|
+
cwd: extra.cwd,
|
|
30706
|
+
reason: `skip: ${extra.name} does not match the changed paths`
|
|
30707
|
+
});
|
|
30708
|
+
continue;
|
|
30709
|
+
}
|
|
30710
|
+
const values = repoPlaceholders(input, targets);
|
|
30711
|
+
const command = fillPlaceholders(extra.command, values);
|
|
30712
|
+
if (command === undefined) {
|
|
30713
|
+
plan.skipped.push({
|
|
30714
|
+
type: extra.type,
|
|
30715
|
+
cwd: extra.cwd,
|
|
30716
|
+
reason: `skip: nothing affected for ${extra.name}`
|
|
30717
|
+
});
|
|
30718
|
+
continue;
|
|
30719
|
+
}
|
|
30720
|
+
const setup = extra.setup === undefined ? undefined : fillPlaceholders(extra.setup, values);
|
|
30721
|
+
plan.gates.push({
|
|
30722
|
+
type: extra.type,
|
|
30723
|
+
name: extra.name,
|
|
30724
|
+
command,
|
|
30725
|
+
cwd: extra.cwd,
|
|
30726
|
+
source: "extra",
|
|
30727
|
+
binDirs: rootBins,
|
|
30728
|
+
...Object.keys(extra.env).length > 0 ? { env: extra.env } : {},
|
|
30729
|
+
...setup === undefined ? {} : { setup },
|
|
30730
|
+
...extra.outputs.length > 0 ? { outputs: extra.outputs } : {},
|
|
30731
|
+
checkOnly: extra.checkOnly,
|
|
30732
|
+
...extra.timeoutMs === undefined ? {} : { timeoutMs: extra.timeoutMs }
|
|
30733
|
+
});
|
|
30734
|
+
}
|
|
30072
30735
|
return plan;
|
|
30073
30736
|
}
|
|
30074
30737
|
function repoPlaceholders(input, targets) {
|
|
30075
|
-
const perTarget = targets.map((t) => placeholderValues(input.worktree, { profile: t.profile, dir: ".", charts: t.charts.map((c) =>
|
|
30738
|
+
const perTarget = targets.map((t) => placeholderValues(input.worktree, { profile: t.profile, dir: ".", charts: t.charts.map((c) => join20(t.dir, c)) }, input.changed, input.files, input.base, input.mode));
|
|
30076
30739
|
const union = (key) => [...new Set(perTarget.flatMap((v) => v[key]))].sort();
|
|
30077
30740
|
return {
|
|
30078
|
-
files: input.changed.filter((f) => existsSync16(
|
|
30741
|
+
files: input.changed.filter((f) => existsSync16(join20(input.worktree, f))),
|
|
30079
30742
|
testFiles: union("testFiles"),
|
|
30080
30743
|
packages: union("packages"),
|
|
30081
30744
|
crates: union("crates"),
|
|
@@ -30139,8 +30802,8 @@ async function resolveBranchRef(dir, branch, options = {}) {
|
|
|
30139
30802
|
}
|
|
30140
30803
|
|
|
30141
30804
|
// src/graph/graph.ts
|
|
30142
|
-
import { existsSync as existsSync18, readFileSync as
|
|
30143
|
-
import { join as
|
|
30805
|
+
import { existsSync as existsSync18, readFileSync as readFileSync16, rmSync as rmSync6, statSync as statSync6 } from "node:fs";
|
|
30806
|
+
import { join as join22 } from "node:path";
|
|
30144
30807
|
|
|
30145
30808
|
// src/logs/log.ts
|
|
30146
30809
|
import {
|
|
@@ -30152,7 +30815,7 @@ import {
|
|
|
30152
30815
|
rmSync as rmSync5,
|
|
30153
30816
|
statSync as statSync5
|
|
30154
30817
|
} from "node:fs";
|
|
30155
|
-
import { join as
|
|
30818
|
+
import { join as join21 } from "node:path";
|
|
30156
30819
|
|
|
30157
30820
|
// src/logs/redact.ts
|
|
30158
30821
|
var MARK = (kind) => `[REDACTED:${kind}]`;
|
|
@@ -30210,7 +30873,7 @@ function redactValue(value, env, heuristic) {
|
|
|
30210
30873
|
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactValue(v, env, heuristic)]));
|
|
30211
30874
|
return value;
|
|
30212
30875
|
}
|
|
30213
|
-
var logDir = (root, changeId) =>
|
|
30876
|
+
var logDir = (root, changeId) => join21(root, ".factory", "logs", changeId);
|
|
30214
30877
|
function appendLog(root, changeId, entry, options = {}) {
|
|
30215
30878
|
const dir = logDir(root, changeId);
|
|
30216
30879
|
mkdirSync7(dir, { recursive: true });
|
|
@@ -30220,21 +30883,21 @@ function appendLog(root, changeId, entry, options = {}) {
|
|
|
30220
30883
|
at: at.toISOString(),
|
|
30221
30884
|
...redactValue(entry, env, options.redact ?? true)
|
|
30222
30885
|
});
|
|
30223
|
-
appendFileSync(
|
|
30886
|
+
appendFileSync(join21(dir, `${at.toISOString().slice(0, 10)}.jsonl`), `${line}
|
|
30224
30887
|
`);
|
|
30225
30888
|
}
|
|
30226
30889
|
function pruneLogs(root, retentionDays, now = Date.now()) {
|
|
30227
|
-
const base =
|
|
30890
|
+
const base = join21(root, ".factory", "logs");
|
|
30228
30891
|
if (!existsSync17(base))
|
|
30229
30892
|
return [];
|
|
30230
30893
|
const cutoff = now - retentionDays * 24 * 60 * 60 * 1000;
|
|
30231
30894
|
const removed = [];
|
|
30232
30895
|
for (const change of readdirSync6(base)) {
|
|
30233
|
-
const dir =
|
|
30896
|
+
const dir = join21(base, change);
|
|
30234
30897
|
if (!statSync5(dir).isDirectory())
|
|
30235
30898
|
continue;
|
|
30236
30899
|
for (const file of readdirSync6(dir)) {
|
|
30237
|
-
const abs =
|
|
30900
|
+
const abs = join21(dir, file);
|
|
30238
30901
|
if (statSync5(abs).mtimeMs < cutoff) {
|
|
30239
30902
|
rmSync5(abs, { force: true });
|
|
30240
30903
|
removed.push(abs);
|
|
@@ -30247,12 +30910,12 @@ function pruneLogs(root, retentionDays, now = Date.now()) {
|
|
|
30247
30910
|
}
|
|
30248
30911
|
|
|
30249
30912
|
// src/graph/graph.ts
|
|
30250
|
-
var graphDir = (root) =>
|
|
30251
|
-
var graphFile = (root) =>
|
|
30913
|
+
var graphDir = (root) => join22(root, "graphify-out");
|
|
30914
|
+
var graphFile = (root) => join22(graphDir(root), "graph.json");
|
|
30252
30915
|
var metaFile = (root) => factoryDir(root, "graph-meta.json");
|
|
30253
30916
|
function readGraph(root) {
|
|
30254
30917
|
try {
|
|
30255
|
-
return JSON.parse(
|
|
30918
|
+
return JSON.parse(readFileSync16(graphFile(root), "utf8"));
|
|
30256
30919
|
} catch {
|
|
30257
30920
|
return;
|
|
30258
30921
|
}
|
|
@@ -30353,12 +31016,12 @@ function dependentFiles(root, files) {
|
|
|
30353
31016
|
}
|
|
30354
31017
|
|
|
30355
31018
|
// src/policy/sandbox.ts
|
|
30356
|
-
import { existsSync as existsSync19, readFileSync as
|
|
30357
|
-
import { isAbsolute, join as
|
|
31019
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17 } from "node:fs";
|
|
31020
|
+
import { isAbsolute, join as join24, resolve as resolve3 } from "node:path";
|
|
30358
31021
|
|
|
30359
31022
|
// src/policy/realpath.ts
|
|
30360
31023
|
import { lstatSync, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
|
|
30361
|
-
import { basename as basename2, dirname as dirname5, join as
|
|
31024
|
+
import { basename as basename2, dirname as dirname5, join as join23, resolve as resolve2 } from "node:path";
|
|
30362
31025
|
function realPathOf(abs, depth = 0) {
|
|
30363
31026
|
if (depth > 40)
|
|
30364
31027
|
return abs;
|
|
@@ -30368,11 +31031,11 @@ function realPathOf(abs, depth = 0) {
|
|
|
30368
31031
|
const st = lstatSync(current, { throwIfNoEntry: false });
|
|
30369
31032
|
if (st !== undefined) {
|
|
30370
31033
|
try {
|
|
30371
|
-
return
|
|
31034
|
+
return join23(realpathSync2.native(current), ...missing.reverse());
|
|
30372
31035
|
} catch {
|
|
30373
31036
|
if (st.isSymbolicLink()) {
|
|
30374
31037
|
const target = resolve2(dirname5(current), readlinkSync(current));
|
|
30375
|
-
return realPathOf(
|
|
31038
|
+
return realPathOf(join23(target, ...missing.reverse()), depth + 1);
|
|
30376
31039
|
}
|
|
30377
31040
|
return abs;
|
|
30378
31041
|
}
|
|
@@ -30474,8 +31137,8 @@ function seatbeltProfile(policy) {
|
|
|
30474
31137
|
if ((policy.writableInside ?? []).length > 0)
|
|
30475
31138
|
lines.push(`(allow file-write* ${(policy.writableInside ?? []).map(subpath).join(" ")})`);
|
|
30476
31139
|
if (home !== undefined) {
|
|
30477
|
-
lines.push(`(deny file-write* ${HOME_WRITE_DENY.map((p) => subpath(
|
|
30478
|
-
lines.push(`(deny file-read* ${HOME_READ_DENY.map((p) => subpath(
|
|
31140
|
+
lines.push(`(deny file-write* ${HOME_WRITE_DENY.map((p) => subpath(join24(home, p))).join(" ")})`);
|
|
31141
|
+
lines.push(`(deny file-read* ${HOME_READ_DENY.map((p) => subpath(join24(home, p))).join(" ")})`);
|
|
30479
31142
|
}
|
|
30480
31143
|
lines.push(`(deny file-write* ${SYSTEM_WRITE_DENY.map(subpath).join(" ")})`);
|
|
30481
31144
|
if (policy.denySecretReads === true)
|
|
@@ -30489,22 +31152,22 @@ function sandboxArgv(profile, argv) {
|
|
|
30489
31152
|
return [SANDBOX_EXEC, "-p", profile, ...argv];
|
|
30490
31153
|
}
|
|
30491
31154
|
function gitWritableDirs(worktree) {
|
|
30492
|
-
const dotGit =
|
|
31155
|
+
const dotGit = join24(worktree, ".git");
|
|
30493
31156
|
let gitDir = dotGit;
|
|
30494
31157
|
try {
|
|
30495
|
-
const text =
|
|
31158
|
+
const text = readFileSync17(dotGit, "utf8");
|
|
30496
31159
|
const m = text.match(/^gitdir:\s*(.+)$/m);
|
|
30497
31160
|
if (m?.[1])
|
|
30498
31161
|
gitDir = isAbsolute(m[1].trim()) ? m[1].trim() : resolve3(worktree, m[1].trim());
|
|
30499
31162
|
} catch {
|
|
30500
|
-
return [
|
|
31163
|
+
return [join24(dotGit, "index"), join24(dotGit, "index.lock"), join24(dotGit, "objects")];
|
|
30501
31164
|
}
|
|
30502
31165
|
let common = gitDir;
|
|
30503
31166
|
try {
|
|
30504
|
-
const rel =
|
|
31167
|
+
const rel = readFileSync17(join24(gitDir, "commondir"), "utf8").trim();
|
|
30505
31168
|
common = isAbsolute(rel) ? rel : resolve3(gitDir, rel);
|
|
30506
31169
|
} catch {}
|
|
30507
|
-
return [gitDir,
|
|
31170
|
+
return [gitDir, join24(common, "objects")];
|
|
30508
31171
|
}
|
|
30509
31172
|
function protectedInWorktree(worktree) {
|
|
30510
31173
|
return [
|
|
@@ -30516,7 +31179,7 @@ function protectedInWorktree(worktree) {
|
|
|
30516
31179
|
"factory.lock.json",
|
|
30517
31180
|
"openspec/specs",
|
|
30518
31181
|
".gitmodules"
|
|
30519
|
-
].map((p) =>
|
|
31182
|
+
].map((p) => join24(worktree, p));
|
|
30520
31183
|
}
|
|
30521
31184
|
function checkoutPolicy(workspace, dir, extra = [], home = process.env.HOME) {
|
|
30522
31185
|
return {
|
|
@@ -30907,11 +31570,11 @@ import { spawn as spawn2 } from "node:child_process";
|
|
|
30907
31570
|
import { randomBytes } from "node:crypto";
|
|
30908
31571
|
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "node:fs";
|
|
30909
31572
|
import { createServer } from "node:net";
|
|
30910
|
-
import { join as
|
|
31573
|
+
import { join as join26 } from "node:path";
|
|
30911
31574
|
|
|
30912
31575
|
// src/policy/session-files.ts
|
|
30913
|
-
import { join as
|
|
30914
|
-
var sessionContextFile = (workspace, sessionId) =>
|
|
31576
|
+
import { join as join25 } from "node:path";
|
|
31577
|
+
var sessionContextFile = (workspace, sessionId) => join25(workspace, ".factory", "sessions", `${sessionId}.json`);
|
|
30915
31578
|
|
|
30916
31579
|
// src/runtime/credentials.ts
|
|
30917
31580
|
var PROVIDER_ENV = {
|
|
@@ -30984,8 +31647,7 @@ var PROXY_ENV = [
|
|
|
30984
31647
|
"no_proxy"
|
|
30985
31648
|
];
|
|
30986
31649
|
function providersOf(config) {
|
|
30987
|
-
|
|
30988
|
-
return [...new Set(models.map((m) => m.split("/")[0] ?? ""))].filter(Boolean);
|
|
31650
|
+
return [...new Set(allModels(config).map((m) => m.split("/")[0] ?? ""))].filter(Boolean);
|
|
30989
31651
|
}
|
|
30990
31652
|
function serverEnv(root, config, password, base = process.env) {
|
|
30991
31653
|
const env = {};
|
|
@@ -31003,11 +31665,11 @@ function serverEnv(root, config, password, base = process.env) {
|
|
|
31003
31665
|
...env,
|
|
31004
31666
|
...agentGitEnv({}),
|
|
31005
31667
|
OPENCODE_PASSWORD: password,
|
|
31006
|
-
OPENCODE_DB:
|
|
31668
|
+
OPENCODE_DB: join26(root, ".factory", "opencode", "opencode.db"),
|
|
31007
31669
|
AI_FACTORY_WORKSPACE: root,
|
|
31008
31670
|
AI_FACTORY_SCRUB: scrubList().join(","),
|
|
31009
31671
|
AI_FACTORY_SANDBOX: sandboxEnabled(config.security.sandbox) ? "seatbelt" : "off",
|
|
31010
|
-
AI_FACTORY_GRAPH:
|
|
31672
|
+
AI_FACTORY_GRAPH: join26(root, "graphify-out", "graph.json"),
|
|
31011
31673
|
NO_COLOR: "1"
|
|
31012
31674
|
};
|
|
31013
31675
|
}
|
|
@@ -31045,6 +31707,19 @@ var minus = (a, b) => ({
|
|
|
31045
31707
|
cacheWrite: a.cacheWrite - b.cacheWrite,
|
|
31046
31708
|
cost: a.cost - b.cost
|
|
31047
31709
|
});
|
|
31710
|
+
var WAIT_POLL_MS = 60000;
|
|
31711
|
+
function isTimeout(err) {
|
|
31712
|
+
const seen = new Set;
|
|
31713
|
+
let current = err;
|
|
31714
|
+
while (current !== null && typeof current === "object" && !seen.has(current)) {
|
|
31715
|
+
seen.add(current);
|
|
31716
|
+
const { name, cause } = current;
|
|
31717
|
+
if (name === "TimeoutError" || name === "HeadersTimeoutError" || name === "BodyTimeoutError")
|
|
31718
|
+
return true;
|
|
31719
|
+
current = cause;
|
|
31720
|
+
}
|
|
31721
|
+
return false;
|
|
31722
|
+
}
|
|
31048
31723
|
|
|
31049
31724
|
class OpencodeRuntime {
|
|
31050
31725
|
#opts;
|
|
@@ -31071,7 +31746,10 @@ class OpencodeRuntime {
|
|
|
31071
31746
|
} catch (err) {
|
|
31072
31747
|
if (signal?.aborted)
|
|
31073
31748
|
throw err;
|
|
31074
|
-
|
|
31749
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
31750
|
+
if (isTimeout(err))
|
|
31751
|
+
throw new TransientError(`opencode client timed out during ${method} ${path}${this.#alive() ? "" : " (the server exited)"}: ${detail}`);
|
|
31752
|
+
throw new TransientError(`opencode server unreachable${this.#alive() ? "" : " (it exited)"}: ${detail}`);
|
|
31075
31753
|
}
|
|
31076
31754
|
const text = await res.text();
|
|
31077
31755
|
let json = text;
|
|
@@ -31097,7 +31775,7 @@ class OpencodeRuntime {
|
|
|
31097
31775
|
}
|
|
31098
31776
|
const password = randomBytes(18).toString("base64url");
|
|
31099
31777
|
const port = await freePort();
|
|
31100
|
-
mkdirSync8(
|
|
31778
|
+
mkdirSync8(join26(this.#opts.root, ".factory", "opencode"), { recursive: true });
|
|
31101
31779
|
this.#base = `http://127.0.0.1:${port}`;
|
|
31102
31780
|
this.#auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
|
|
31103
31781
|
this.#proc = spawn2(toolBin("opencode"), ["serve", "--hostname", "127.0.0.1", "--port", String(port)], {
|
|
@@ -31206,7 +31884,7 @@ class OpencodeRuntime {
|
|
|
31206
31884
|
throw new Error(`opencode session create failed (${created.status}): ${JSON.stringify(created.data)}`);
|
|
31207
31885
|
}
|
|
31208
31886
|
const file = sessionContextFile(this.#opts.root, id);
|
|
31209
|
-
mkdirSync8(
|
|
31887
|
+
mkdirSync8(join26(this.#opts.root, ".factory", "sessions"), { recursive: true });
|
|
31210
31888
|
writeFileSync8(file, JSON.stringify({ sessionId: id, ...spec.context }, null, 2));
|
|
31211
31889
|
return {
|
|
31212
31890
|
id,
|
|
@@ -31225,8 +31903,15 @@ class OpencodeRuntime {
|
|
|
31225
31903
|
return cut === -1 ? messages : messages.slice(0, cut);
|
|
31226
31904
|
}
|
|
31227
31905
|
async#awaitTurn(id, marker, stop) {
|
|
31906
|
+
const pollMs = this.#opts.waitPollMs ?? WAIT_POLL_MS;
|
|
31228
31907
|
for (;; ) {
|
|
31229
|
-
|
|
31908
|
+
const poll = AbortSignal.any([stop, AbortSignal.timeout(pollMs)]);
|
|
31909
|
+
try {
|
|
31910
|
+
await this.#api("POST", `/api/experimental/session/${id}/wait`, undefined, poll);
|
|
31911
|
+
} catch (err) {
|
|
31912
|
+
if (stop.aborted || !poll.aborted)
|
|
31913
|
+
throw err;
|
|
31914
|
+
}
|
|
31230
31915
|
const all = await this.#recent(id);
|
|
31231
31916
|
const fresh = this.#since(all, marker);
|
|
31232
31917
|
const markerSeen = marker === undefined || all.some((m) => m.id === marker);
|
|
@@ -31254,7 +31939,9 @@ class OpencodeRuntime {
|
|
|
31254
31939
|
throw err;
|
|
31255
31940
|
interrupted = true;
|
|
31256
31941
|
await this.#api("POST", `/api/session/${id}/interrupt`);
|
|
31257
|
-
|
|
31942
|
+
try {
|
|
31943
|
+
await this.#api("POST", `/api/experimental/session/${id}/wait`, undefined, AbortSignal.timeout(this.#opts.waitPollMs ?? WAIT_POLL_MS));
|
|
31944
|
+
} catch {}
|
|
31258
31945
|
messages = this.#since(await this.#recent(id), marker);
|
|
31259
31946
|
}
|
|
31260
31947
|
const after = await this.#session(id);
|
|
@@ -31425,8 +32112,8 @@ function gateExecutables(command) {
|
|
|
31425
32112
|
}
|
|
31426
32113
|
function onPath(name, dirs) {
|
|
31427
32114
|
if (name.includes("/"))
|
|
31428
|
-
return dirs.some((d) => existsSync20(
|
|
31429
|
-
return dirs.some((d) => d !== "" && existsSync20(
|
|
32115
|
+
return dirs.some((d) => existsSync20(join27(d, name)));
|
|
32116
|
+
return dirs.some((d) => d !== "" && existsSync20(join27(d, name)));
|
|
31430
32117
|
}
|
|
31431
32118
|
function dirSize(path) {
|
|
31432
32119
|
let total = 0;
|
|
@@ -31440,7 +32127,7 @@ function dirSize(path) {
|
|
|
31440
32127
|
continue;
|
|
31441
32128
|
}
|
|
31442
32129
|
for (const name of entries) {
|
|
31443
|
-
const child =
|
|
32130
|
+
const child = join27(dir, name);
|
|
31444
32131
|
const st = lstatSync2(child, { throwIfNoEntry: false });
|
|
31445
32132
|
if (st === undefined || st.isSymbolicLink())
|
|
31446
32133
|
continue;
|
|
@@ -31558,12 +32245,12 @@ async function checkRepos(c, root, config, uninitialised) {
|
|
|
31558
32245
|
}
|
|
31559
32246
|
const checkouts = [{ name: "AI repo", dir: root }];
|
|
31560
32247
|
for (const repo of config.repos) {
|
|
31561
|
-
const dir =
|
|
32248
|
+
const dir = join27(root, repo.path);
|
|
31562
32249
|
if (repo.path === ".") {
|
|
31563
32250
|
ready.push(repo);
|
|
31564
32251
|
continue;
|
|
31565
32252
|
}
|
|
31566
|
-
if (!existsSync20(
|
|
32253
|
+
if (!existsSync20(join27(dir, ".git"))) {
|
|
31567
32254
|
if (!uninitialised.includes(repo.path))
|
|
31568
32255
|
c.add("warn", repo.name, `${repo.path} is not a git checkout`);
|
|
31569
32256
|
continue;
|
|
@@ -31584,12 +32271,36 @@ async function checkRepos(c, root, config, uninitialised) {
|
|
|
31584
32271
|
}
|
|
31585
32272
|
return ready;
|
|
31586
32273
|
}
|
|
32274
|
+
function packageScript(dir, name) {
|
|
32275
|
+
try {
|
|
32276
|
+
const pkg = JSON.parse(readFileSync18(join27(dir, "package.json"), "utf8"));
|
|
32277
|
+
const value = pkg.scripts?.[name];
|
|
32278
|
+
return typeof value === "string" ? value : undefined;
|
|
32279
|
+
} catch {
|
|
32280
|
+
return;
|
|
32281
|
+
}
|
|
32282
|
+
}
|
|
32283
|
+
var WRITE_FLAG = /(^|\s)(--write|--fix|--apply|-w)(\s|$)/;
|
|
32284
|
+
function checkWriteModeScripts(c, root, config, repos) {
|
|
32285
|
+
c.in("gates");
|
|
32286
|
+
for (const repo of repos.filter((r) => r.role === "code")) {
|
|
32287
|
+
const dir = join27(root, repo.path);
|
|
32288
|
+
for (const [type, name] of Object.entries(config.gates.scripts)) {
|
|
32289
|
+
if (typeof name !== "string")
|
|
32290
|
+
continue;
|
|
32291
|
+
const script = packageScript(dir, name);
|
|
32292
|
+
if (script !== undefined && WRITE_FLAG.test(script)) {
|
|
32293
|
+
c.add("warn", `${repo.name} ${type} script`, `\`${name}\` writes files: ${script}`, `point gates.scripts.${type} at a check-only variant, or set gates.overrides.${repo.name}.${type}`);
|
|
32294
|
+
}
|
|
32295
|
+
}
|
|
32296
|
+
}
|
|
32297
|
+
}
|
|
31587
32298
|
function checkGates(c, root, config, repos) {
|
|
31588
32299
|
c.in("gates");
|
|
31589
32300
|
const rows = [];
|
|
31590
32301
|
const pathDirs = (process.env.PATH ?? "").split(delimiter);
|
|
31591
32302
|
for (const repo of repos.filter((r) => r.role === "code")) {
|
|
31592
|
-
const dir =
|
|
32303
|
+
const dir = join27(root, repo.path);
|
|
31593
32304
|
const plan = resolveGates({
|
|
31594
32305
|
config,
|
|
31595
32306
|
repo,
|
|
@@ -31604,11 +32315,12 @@ function checkGates(c, root, config, repos) {
|
|
|
31604
32315
|
rows.push({
|
|
31605
32316
|
repo: repo.name,
|
|
31606
32317
|
type: g.type,
|
|
32318
|
+
...g.name === undefined ? {} : { name: g.name },
|
|
31607
32319
|
cwd: g.cwd,
|
|
31608
32320
|
command: g.command,
|
|
31609
32321
|
source: g.source
|
|
31610
32322
|
});
|
|
31611
|
-
const dirs = [...g.binDirs.map((b) =>
|
|
32323
|
+
const dirs = [...g.binDirs.map((b) => join27(dir, g.cwd, b)), join27(dir, g.cwd), ...pathDirs];
|
|
31612
32324
|
for (const exe of gateExecutables(g.command))
|
|
31613
32325
|
if (!onPath(exe, dirs))
|
|
31614
32326
|
missingTools.add(exe);
|
|
@@ -31636,10 +32348,10 @@ function looseTsconfig(dir, gates) {
|
|
|
31636
32348
|
const gate = gates.find((g) => g.type === "types");
|
|
31637
32349
|
if (gate === undefined || /--strict\b/.test(gate.command))
|
|
31638
32350
|
return;
|
|
31639
|
-
const rel =
|
|
32351
|
+
const rel = join27(gate.cwd, "tsconfig.json");
|
|
31640
32352
|
let text;
|
|
31641
32353
|
try {
|
|
31642
|
-
text =
|
|
32354
|
+
text = readFileSync18(join27(dir, rel), "utf8");
|
|
31643
32355
|
} catch {
|
|
31644
32356
|
return;
|
|
31645
32357
|
}
|
|
@@ -31648,14 +32360,81 @@ function looseTsconfig(dir, gates) {
|
|
|
31648
32360
|
return;
|
|
31649
32361
|
return tsconfig.compilerOptions?.strict === true ? undefined : rel;
|
|
31650
32362
|
}
|
|
32363
|
+
function checkContext(c, root, config) {
|
|
32364
|
+
c.in("context");
|
|
32365
|
+
const paths = allInstructionPaths(config);
|
|
32366
|
+
const missing = paths.filter((p) => !existsSync20(join27(root, p)));
|
|
32367
|
+
if (missing.length > 0)
|
|
32368
|
+
c.add("warn", "instructions", `missing: ${missing.join(", ")}`, "create the files or remove them from instructions");
|
|
32369
|
+
else
|
|
32370
|
+
c.add("ok", "instructions", `${paths.length} path(s) injected into sessions`);
|
|
32371
|
+
const names = [
|
|
32372
|
+
...Object.values(config.skills.roles).flat(),
|
|
32373
|
+
...Object.values(config.skills.repos).flat()
|
|
32374
|
+
];
|
|
32375
|
+
if (names.length > 0) {
|
|
32376
|
+
const found = new Set;
|
|
32377
|
+
for (const source of config.skills.sources) {
|
|
32378
|
+
const dir = join27(root, source);
|
|
32379
|
+
if (!existsSync20(dir))
|
|
32380
|
+
continue;
|
|
32381
|
+
try {
|
|
32382
|
+
for (const name of readdirSync7(dir)) {
|
|
32383
|
+
const skill = join27(dir, name, "SKILL.md");
|
|
32384
|
+
if (existsSync20(skill))
|
|
32385
|
+
found.add(name);
|
|
32386
|
+
}
|
|
32387
|
+
} catch {}
|
|
32388
|
+
}
|
|
32389
|
+
const unknown = [...new Set(names)].filter((n) => !found.has(n));
|
|
32390
|
+
if (unknown.length > 0)
|
|
32391
|
+
c.add("warn", "skills", `not found in ${config.skills.sources.join(", ")}: ${unknown.join(", ")}`, "add the skill under .opencode/skills/ or remove it from skills.roles");
|
|
32392
|
+
else
|
|
32393
|
+
c.add("ok", "skills", `${new Set(names).size} skill(s) configured`);
|
|
32394
|
+
}
|
|
32395
|
+
const legacy = join27(root, ".factory", "legacy");
|
|
32396
|
+
if (existsSync20(legacy)) {
|
|
32397
|
+
const stamps = readdirSync7(legacy).sort();
|
|
32398
|
+
c.add("ok", "legacy setup", `${stamps.length} archived set(s) under .factory/legacy/`);
|
|
32399
|
+
}
|
|
32400
|
+
}
|
|
32401
|
+
function checkRelease(c, root, config) {
|
|
32402
|
+
c.in("release");
|
|
32403
|
+
if (!config.release.detect) {
|
|
32404
|
+
c.add("ok", "release", "detection disabled");
|
|
32405
|
+
return;
|
|
32406
|
+
}
|
|
32407
|
+
const found = [];
|
|
32408
|
+
for (const file of [
|
|
32409
|
+
".release-please-config.json",
|
|
32410
|
+
".goreleaser.yaml",
|
|
32411
|
+
".goreleaser.yml",
|
|
32412
|
+
".changeset/config.json",
|
|
32413
|
+
".github/workflows",
|
|
32414
|
+
".gitlab-ci.yml"
|
|
32415
|
+
]) {
|
|
32416
|
+
if (existsSync20(join27(root, file)))
|
|
32417
|
+
found.push(file);
|
|
32418
|
+
}
|
|
32419
|
+
for (const repo of config.repos.filter((r) => r.path !== ".")) {
|
|
32420
|
+
for (const file of ["release-please-config.json", ".goreleaser.yaml", ".gitlab-ci.yml"]) {
|
|
32421
|
+
if (existsSync20(join27(root, repo.path, file)))
|
|
32422
|
+
found.push(`${repo.path}/${file}`);
|
|
32423
|
+
}
|
|
32424
|
+
}
|
|
32425
|
+
if (found.length === 0)
|
|
32426
|
+
c.add("ok", "release", "no release tooling detected");
|
|
32427
|
+
else
|
|
32428
|
+
c.add("ok", "release", `detected ${found.join(", ")}; commits stay local and the factory does not edit release workflows`);
|
|
32429
|
+
}
|
|
31651
32430
|
function checkCaches(c, root) {
|
|
31652
32431
|
c.in("caches");
|
|
31653
|
-
const factory =
|
|
32432
|
+
const factory = join27(root, ".factory");
|
|
31654
32433
|
for (const [name, path] of [
|
|
31655
|
-
["dependency caches",
|
|
31656
|
-
["worktrees",
|
|
31657
|
-
["logs",
|
|
31658
|
-
["backups",
|
|
32434
|
+
["dependency caches", join27(factory, "cache")],
|
|
32435
|
+
["worktrees", join27(factory, "worktrees")],
|
|
32436
|
+
["logs", join27(factory, "logs")],
|
|
32437
|
+
["backups", join27(factory, "backups")]
|
|
31659
32438
|
]) {
|
|
31660
32439
|
if (!existsSync20(path))
|
|
31661
32440
|
continue;
|
|
@@ -31664,11 +32443,11 @@ function checkCaches(c, root) {
|
|
|
31664
32443
|
}
|
|
31665
32444
|
async function checkModels(c, root, config) {
|
|
31666
32445
|
c.in("models");
|
|
31667
|
-
const configured =
|
|
31668
|
-
const tmp = mkdtempSync4(
|
|
32446
|
+
const configured = modelEntries(config);
|
|
32447
|
+
const tmp = mkdtempSync4(join27(tmpdir4(), "ai-factory-doctor-"));
|
|
31669
32448
|
const res = await exec([toolBin("opencode"), "models", "--standalone"], {
|
|
31670
32449
|
cwd: root,
|
|
31671
|
-
env: { ...serverEnv(root, config, "doctor"), OPENCODE_DB:
|
|
32450
|
+
env: { ...serverEnv(root, config, "doctor"), OPENCODE_DB: join27(tmp, "opencode.db") },
|
|
31672
32451
|
replaceEnv: true,
|
|
31673
32452
|
timeoutMs: 60000
|
|
31674
32453
|
}).finally(() => rmSync8(tmp, { recursive: true, force: true }));
|
|
@@ -31684,7 +32463,7 @@ async function checkModels(c, root, config) {
|
|
|
31684
32463
|
if (available.has(id))
|
|
31685
32464
|
c.add("ok", role, model);
|
|
31686
32465
|
else
|
|
31687
|
-
c.add("fail", role, `${model} is not available to opencode`, `set the provider's credentials (env or \`opencode auth login\`) or change
|
|
32466
|
+
c.add("fail", role, `${model} is not available to opencode`, `set the provider's credentials (env or \`opencode auth login\`) or change the model in factory.config.jsonc`);
|
|
31688
32467
|
}
|
|
31689
32468
|
}
|
|
31690
32469
|
var CODE_EXTENSIONS = new Set(Object.values(PROFILES).flatMap((p) => p.extensions));
|
|
@@ -31799,7 +32578,10 @@ async function runDoctor(root) {
|
|
|
31799
32578
|
checkEnvironment(c, process.env);
|
|
31800
32579
|
checkManaged(c, root, config);
|
|
31801
32580
|
const repos = await checkRepos(c, root, config, uninitialised);
|
|
32581
|
+
checkWriteModeScripts(c, root, config, repos);
|
|
31802
32582
|
const gates = checkGates(c, root, config, repos);
|
|
32583
|
+
checkContext(c, root, config);
|
|
32584
|
+
checkRelease(c, root, config);
|
|
31803
32585
|
checkCaches(c, root);
|
|
31804
32586
|
await checkModels(c, root, config);
|
|
31805
32587
|
await checkGraph(c, root, config, lock);
|
|
@@ -31822,11 +32604,12 @@ function print(io, report) {
|
|
|
31822
32604
|
}
|
|
31823
32605
|
if (report.gates.length > 0) {
|
|
31824
32606
|
io.out("gate table (full mode; per task, scoped commands and affected tests are used)");
|
|
31825
|
-
const
|
|
32607
|
+
const label = (g) => `${g.repo} ${g.name ?? g.type}`;
|
|
32608
|
+
const width = Math.max(...report.gates.map((g) => label(g).length));
|
|
31826
32609
|
for (const g of report.gates) {
|
|
31827
32610
|
const where = g.cwd === "." ? "" : ` [${g.cwd}]`;
|
|
31828
32611
|
const what = g.command ? `${g.command} (${g.source})` : g.problem ?? "";
|
|
31829
|
-
io.out(` ${
|
|
32612
|
+
io.out(` ${label(g).padEnd(width)}${where} ${what}`);
|
|
31830
32613
|
}
|
|
31831
32614
|
}
|
|
31832
32615
|
const fails = report.checks.filter((c) => c.status === "fail").length;
|
|
@@ -31878,14 +32661,14 @@ Exit code ${ExitCode.Precondition} when a check fails; warnings exit 0.`,
|
|
|
31878
32661
|
|
|
31879
32662
|
// src/cli/commands/gate.ts
|
|
31880
32663
|
import { existsSync as existsSync25, readdirSync as readdirSync9 } from "node:fs";
|
|
31881
|
-
import { join as
|
|
32664
|
+
import { join as join34 } from "node:path";
|
|
31882
32665
|
|
|
31883
32666
|
// src/gates/run.ts
|
|
31884
|
-
import { delimiter as delimiter2, join as
|
|
32667
|
+
import { delimiter as delimiter2, join as join31 } from "node:path";
|
|
31885
32668
|
|
|
31886
32669
|
// src/addons/shortcuts.ts
|
|
31887
|
-
import { readFileSync as
|
|
31888
|
-
import { join as
|
|
32670
|
+
import { readFileSync as readFileSync19 } from "node:fs";
|
|
32671
|
+
import { join as join28 } from "node:path";
|
|
31889
32672
|
var SHORTCUT = /(?:\/\/|#|\/\*|\*|--|<!--|;)\s*(?:shortcut|ponytail):\s*(.+?)\s*(?:\*\/|-->)?\s*$/i;
|
|
31890
32673
|
function shortcutIn(line) {
|
|
31891
32674
|
return line.match(SHORTCUT)?.[1];
|
|
@@ -31922,7 +32705,7 @@ async function newShortcuts(dir, base) {
|
|
|
31922
32705
|
`).filter(Boolean)) {
|
|
31923
32706
|
let text;
|
|
31924
32707
|
try {
|
|
31925
|
-
text =
|
|
32708
|
+
text = readFileSync19(join28(dir, file), "utf8");
|
|
31926
32709
|
} catch {
|
|
31927
32710
|
continue;
|
|
31928
32711
|
}
|
|
@@ -31990,8 +32773,8 @@ function compactOutput(raw, maxLines) {
|
|
|
31990
32773
|
|
|
31991
32774
|
// src/gates/guard.ts
|
|
31992
32775
|
import { createHash as createHash3 } from "node:crypto";
|
|
31993
|
-
import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as
|
|
31994
|
-
import { dirname as dirname6, join as
|
|
32776
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync20, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "node:fs";
|
|
32777
|
+
import { dirname as dirname6, join as join29 } from "node:path";
|
|
31995
32778
|
var MISSING = "missing";
|
|
31996
32779
|
async function dirtyPaths2(worktree) {
|
|
31997
32780
|
const res = await git(["status", "--porcelain=v1", "-z", "--no-renames", "--untracked-files=all"], {
|
|
@@ -32004,13 +32787,13 @@ async function dirtyPaths2(worktree) {
|
|
|
32004
32787
|
function hashFile(abs) {
|
|
32005
32788
|
if (!existsSync21(abs))
|
|
32006
32789
|
return { hash: MISSING };
|
|
32007
|
-
const content =
|
|
32790
|
+
const content = readFileSync20(abs);
|
|
32008
32791
|
return { hash: createHash3("sha256").update(content).digest("hex"), content };
|
|
32009
32792
|
}
|
|
32010
32793
|
async function snapshot(worktree) {
|
|
32011
32794
|
const entries = new Map;
|
|
32012
32795
|
for (const path of await dirtyPaths2(worktree))
|
|
32013
|
-
entries.set(path, hashFile(
|
|
32796
|
+
entries.set(path, hashFile(join29(worktree, path)));
|
|
32014
32797
|
return { worktree, head: await gitOk(["rev-parse", "HEAD"], { cwd: worktree }), entries };
|
|
32015
32798
|
}
|
|
32016
32799
|
async function changedSince(before) {
|
|
@@ -32033,7 +32816,7 @@ async function restore(before, paths) {
|
|
|
32033
32816
|
await gitOk(["reset", "-q", "--mixed", before.head], { cwd: before.worktree });
|
|
32034
32817
|
}
|
|
32035
32818
|
for (const path of paths.filter((p) => p !== "HEAD")) {
|
|
32036
|
-
const abs =
|
|
32819
|
+
const abs = join29(before.worktree, path);
|
|
32037
32820
|
const entry = before.entries.get(path);
|
|
32038
32821
|
if (entry === undefined) {
|
|
32039
32822
|
const tracked = await gitOk(["ls-files", "--", path], { cwd: before.worktree });
|
|
@@ -32052,19 +32835,19 @@ async function restore(before, paths) {
|
|
|
32052
32835
|
|
|
32053
32836
|
// src/gates/install.ts
|
|
32054
32837
|
import { existsSync as existsSync22, mkdirSync as mkdirSync10 } from "node:fs";
|
|
32055
|
-
import { join as
|
|
32838
|
+
import { join as join30 } from "node:path";
|
|
32056
32839
|
function cacheEnv(root, repoName) {
|
|
32057
|
-
const cache =
|
|
32840
|
+
const cache = join30(root, ".factory", "cache");
|
|
32058
32841
|
const env = {
|
|
32059
|
-
npm_config_cache:
|
|
32060
|
-
npm_config_store_dir:
|
|
32061
|
-
BUN_INSTALL_CACHE_DIR:
|
|
32062
|
-
YARN_CACHE_FOLDER:
|
|
32063
|
-
UV_CACHE_DIR:
|
|
32064
|
-
GOMODCACHE:
|
|
32065
|
-
GOCACHE:
|
|
32066
|
-
GOLANGCI_LINT_CACHE:
|
|
32067
|
-
CARGO_TARGET_DIR:
|
|
32842
|
+
npm_config_cache: join30(cache, "npm"),
|
|
32843
|
+
npm_config_store_dir: join30(cache, "pnpm-store"),
|
|
32844
|
+
BUN_INSTALL_CACHE_DIR: join30(cache, "bun"),
|
|
32845
|
+
YARN_CACHE_FOLDER: join30(cache, "yarn"),
|
|
32846
|
+
UV_CACHE_DIR: join30(cache, "uv"),
|
|
32847
|
+
GOMODCACHE: join30(cache, "go", "mod"),
|
|
32848
|
+
GOCACHE: join30(cache, "go", "build"),
|
|
32849
|
+
GOLANGCI_LINT_CACHE: join30(cache, "golangci-lint"),
|
|
32850
|
+
CARGO_TARGET_DIR: join30(cache, "cargo-target", repoName)
|
|
32068
32851
|
};
|
|
32069
32852
|
for (const dir of Object.values(env))
|
|
32070
32853
|
mkdirSync10(dir, { recursive: true });
|
|
@@ -32083,7 +32866,7 @@ async function installDeps(root, worktree, repo, timeoutMs = 15 * 60000) {
|
|
|
32083
32866
|
const done = new Set;
|
|
32084
32867
|
for (const profile of profilesFor(repoLanguages(repo))) {
|
|
32085
32868
|
for (const dir of installDirs(profile, files)) {
|
|
32086
|
-
const step = profile.install.find((s) => existsSync22(
|
|
32869
|
+
const step = profile.install.find((s) => existsSync22(join30(worktree, dir, s.lockfile)));
|
|
32087
32870
|
const key = `${dir}\x00${step?.command ?? profile.id}`;
|
|
32088
32871
|
if (done.has(key))
|
|
32089
32872
|
continue;
|
|
@@ -32098,7 +32881,7 @@ async function installDeps(root, worktree, repo, timeoutMs = 15 * 60000) {
|
|
|
32098
32881
|
continue;
|
|
32099
32882
|
}
|
|
32100
32883
|
const res = await exec(["sh", "-c", step.command], {
|
|
32101
|
-
cwd:
|
|
32884
|
+
cwd: join30(worktree, dir),
|
|
32102
32885
|
env,
|
|
32103
32886
|
replaceEnv: true,
|
|
32104
32887
|
timeoutMs
|
|
@@ -32120,22 +32903,30 @@ var FAILING = new Set(["fail", "missing", "not-check-only", "timeout"]);
|
|
|
32120
32903
|
var TEST_GATES = new Set(["test", "testAffected"]);
|
|
32121
32904
|
async function runOne(input, gate) {
|
|
32122
32905
|
const before = await snapshot(input.worktree);
|
|
32123
|
-
const bins = gate.binDirs.map((b) =>
|
|
32906
|
+
const bins = gate.binDirs.map((b) => join31(input.worktree, b));
|
|
32124
32907
|
const env = {
|
|
32125
32908
|
...agentProcessEnv(),
|
|
32126
32909
|
...cacheEnv(input.root, input.repo.name),
|
|
32127
32910
|
...QUIET_ENV,
|
|
32911
|
+
...gate.env ?? {},
|
|
32128
32912
|
PATH: [...bins, process.env.PATH ?? ""].join(delimiter2)
|
|
32129
32913
|
};
|
|
32130
|
-
const argv = [
|
|
32131
|
-
|
|
32132
|
-
|
|
32914
|
+
const argv = [
|
|
32915
|
+
"sh",
|
|
32916
|
+
"-c",
|
|
32917
|
+
gate.setup === undefined ? gate.command : `set -e
|
|
32918
|
+
${gate.setup}
|
|
32919
|
+
${gate.command}`
|
|
32920
|
+
];
|
|
32921
|
+
const res = await exec(sandboxEnabled(input.config.security.sandbox) ? sandboxArgv(seatbeltProfile(checkoutPolicy(input.root, input.worktree, [join31(input.root, ".factory", "cache")])), argv) : argv, {
|
|
32922
|
+
cwd: join31(input.worktree, gate.cwd),
|
|
32133
32923
|
env,
|
|
32134
32924
|
replaceEnv: true,
|
|
32135
|
-
timeoutMs: input.timeoutMs ?? 15 * 60000
|
|
32925
|
+
timeoutMs: gate.timeoutMs ?? input.timeoutMs ?? 15 * 60000
|
|
32136
32926
|
});
|
|
32137
32927
|
const base = {
|
|
32138
32928
|
type: gate.type,
|
|
32929
|
+
...gate.name === undefined ? {} : { name: gate.name },
|
|
32139
32930
|
cwd: gate.cwd,
|
|
32140
32931
|
command: gate.command,
|
|
32141
32932
|
source: gate.source,
|
|
@@ -32143,8 +32934,46 @@ async function runOne(input, gate) {
|
|
|
32143
32934
|
durationMs: res.durationMs
|
|
32144
32935
|
};
|
|
32145
32936
|
const max = input.config.tokens.gateOutputMaxLines;
|
|
32937
|
+
const raw = `${res.stdout}
|
|
32938
|
+
${res.stderr}`;
|
|
32939
|
+
const output = redactProcess(compactOutput(raw, max));
|
|
32146
32940
|
const modified = await changedSince(before);
|
|
32147
|
-
|
|
32941
|
+
const outputs = gate.outputs ?? [];
|
|
32942
|
+
if (outputs.length > 0) {
|
|
32943
|
+
const drifted = modified.filter((path) => outputs.some((glob) => opencodeGlobRegExp(glob).test(path)));
|
|
32944
|
+
if (modified.length > 0)
|
|
32945
|
+
await restore(before, modified);
|
|
32946
|
+
if (res.timedOut)
|
|
32947
|
+
return { ...base, status: "timeout", output: `gate timed out: \`${gate.command}\`` };
|
|
32948
|
+
if (res.code !== 0)
|
|
32949
|
+
return { ...base, status: "fail", output: output || `gate failed: \`${gate.command}\`` };
|
|
32950
|
+
if (modified.includes("HEAD")) {
|
|
32951
|
+
return {
|
|
32952
|
+
...base,
|
|
32953
|
+
status: "fail",
|
|
32954
|
+
output: `generated gate moved HEAD: \`${gate.command}\` must not commit (restored)`
|
|
32955
|
+
};
|
|
32956
|
+
}
|
|
32957
|
+
const extraWrites = modified.filter((path) => !drifted.includes(path));
|
|
32958
|
+
if (extraWrites.length > 0) {
|
|
32959
|
+
return {
|
|
32960
|
+
...base,
|
|
32961
|
+
status: "fail",
|
|
32962
|
+
modified: extraWrites,
|
|
32963
|
+
output: `generated gate wrote outside its outputs: ${extraWrites.join(", ")} (restored)`
|
|
32964
|
+
};
|
|
32965
|
+
}
|
|
32966
|
+
if (drifted.length > 0) {
|
|
32967
|
+
return {
|
|
32968
|
+
...base,
|
|
32969
|
+
status: "fail",
|
|
32970
|
+
modified: drifted,
|
|
32971
|
+
output: `generated artifacts are stale: ${drifted.join(", ")} (regenerate and commit them)`
|
|
32972
|
+
};
|
|
32973
|
+
}
|
|
32974
|
+
return { ...base, status: "pass", output: "" };
|
|
32975
|
+
}
|
|
32976
|
+
if (modified.length > 0 && gate.checkOnly !== false) {
|
|
32148
32977
|
await restore(before, modified);
|
|
32149
32978
|
return {
|
|
32150
32979
|
...base,
|
|
@@ -32155,12 +32984,10 @@ async function runOne(input, gate) {
|
|
|
32155
32984
|
}
|
|
32156
32985
|
if (res.timedOut)
|
|
32157
32986
|
return { ...base, status: "timeout", output: `gate timed out: \`${gate.command}\`` };
|
|
32158
|
-
const raw = `${res.stdout}
|
|
32159
|
-
${res.stderr}`;
|
|
32160
32987
|
return {
|
|
32161
32988
|
...base,
|
|
32162
32989
|
status: res.code === 0 ? "pass" : "fail",
|
|
32163
|
-
output: res.code === 0 ? "" :
|
|
32990
|
+
output: res.code === 0 ? "" : output
|
|
32164
32991
|
};
|
|
32165
32992
|
}
|
|
32166
32993
|
function relatedFiles(root, repoPath, changed) {
|
|
@@ -32243,13 +33070,13 @@ async function runGates(input) {
|
|
|
32243
33070
|
|
|
32244
33071
|
// src/git/worktrees.ts
|
|
32245
33072
|
import { existsSync as existsSync23, mkdirSync as mkdirSync11 } from "node:fs";
|
|
32246
|
-
import { dirname as dirname7, join as
|
|
33073
|
+
import { dirname as dirname7, join as join32 } from "node:path";
|
|
32247
33074
|
var taskBranch = (id, task) => `${changeBranch(id)}--${task}`;
|
|
32248
|
-
var worktreePath = (root, id, repo, task) =>
|
|
33075
|
+
var worktreePath = (root, id, repo, task) => join32(root, ".factory", "worktrees", id, repo, task);
|
|
32249
33076
|
async function addTaskWorktree(root, repoDir, repoName, id, task) {
|
|
32250
33077
|
const path = worktreePath(root, id, repoName, task);
|
|
32251
33078
|
const branch = taskBranch(id, task);
|
|
32252
|
-
if (existsSync23(
|
|
33079
|
+
if (existsSync23(join32(path, ".git")))
|
|
32253
33080
|
return { path, branch, created: false };
|
|
32254
33081
|
mkdirSync11(dirname7(path), { recursive: true });
|
|
32255
33082
|
await gitOk(["worktree", "prune"], { cwd: repoDir });
|
|
@@ -32268,20 +33095,27 @@ async function removeTaskWorktree(repoDir, wt) {
|
|
|
32268
33095
|
}
|
|
32269
33096
|
|
|
32270
33097
|
// src/update/snapshot.ts
|
|
32271
|
-
import { cpSync, existsSync as existsSync24, mkdirSync as mkdirSync12, readdirSync as readdirSync8, readFileSync as
|
|
32272
|
-
import { join as
|
|
32273
|
-
var gateFile = (root, id) =>
|
|
33098
|
+
import { cpSync, existsSync as existsSync24, mkdirSync as mkdirSync12, readdirSync as readdirSync8, readFileSync as readFileSync21, rmSync as rmSync10 } from "node:fs";
|
|
33099
|
+
import { join as join33 } from "node:path";
|
|
33100
|
+
var gateFile = (root, id) => join33(root, ".factory", "runs", id, "snapshot", "gates.json");
|
|
32274
33101
|
function snapshotDefinitions(root, id, config, factoryVersion, replace = false) {
|
|
32275
|
-
const
|
|
32276
|
-
const
|
|
33102
|
+
const snapshotRoot = join33(root, ".factory", "worktrees", id, ".opencode");
|
|
33103
|
+
const agents = join33(root, ".opencode", "agents");
|
|
33104
|
+
const target = join33(snapshotRoot, "agents");
|
|
33105
|
+
const skills = join33(root, ".opencode", "skills");
|
|
33106
|
+
const skillTarget = join33(snapshotRoot, "skills");
|
|
32277
33107
|
if (replace) {
|
|
32278
|
-
rmSync10(
|
|
33108
|
+
rmSync10(snapshotRoot, { recursive: true, force: true });
|
|
32279
33109
|
rmSync10(gateFile(root, id), { force: true });
|
|
32280
33110
|
}
|
|
32281
33111
|
if (existsSync24(agents) && !existsSync24(target)) {
|
|
32282
33112
|
mkdirSync12(target, { recursive: true });
|
|
32283
33113
|
cpSync(agents, target, { recursive: true });
|
|
32284
33114
|
}
|
|
33115
|
+
if (existsSync24(skills) && !existsSync24(skillTarget)) {
|
|
33116
|
+
mkdirSync12(skillTarget, { recursive: true });
|
|
33117
|
+
cpSync(skills, skillTarget, { recursive: true });
|
|
33118
|
+
}
|
|
32285
33119
|
if (!existsSync24(gateFile(root, id))) {
|
|
32286
33120
|
const tables = Object.fromEntries(Object.keys(PROFILES).map((l) => [l, PROFILES[l].gates]));
|
|
32287
33121
|
writeJsonAtomic(gateFile(root, id), {
|
|
@@ -32293,15 +33127,15 @@ function snapshotDefinitions(root, id, config, factoryVersion, replace = false)
|
|
|
32293
33127
|
}
|
|
32294
33128
|
function loadGateSnapshot(root, id) {
|
|
32295
33129
|
const file = gateFile(root, id);
|
|
32296
|
-
return existsSync24(file) ? JSON.parse(
|
|
33130
|
+
return existsSync24(file) ? JSON.parse(readFileSync21(file, "utf8")) : undefined;
|
|
32297
33131
|
}
|
|
32298
33132
|
function dropAgentSnapshot(root, id) {
|
|
32299
|
-
const dir =
|
|
32300
|
-
rmSync10(
|
|
33133
|
+
const dir = join33(root, ".factory", "worktrees", id);
|
|
33134
|
+
rmSync10(join33(dir, ".opencode"), { recursive: true, force: true });
|
|
32301
33135
|
if (!existsSync24(dir))
|
|
32302
33136
|
return;
|
|
32303
33137
|
for (const repo of readdirSync8(dir)) {
|
|
32304
|
-
const repoDir =
|
|
33138
|
+
const repoDir = join33(dir, repo);
|
|
32305
33139
|
if (readdirSync8(repoDir).length === 0)
|
|
32306
33140
|
rmSync10(repoDir, { recursive: true });
|
|
32307
33141
|
}
|
|
@@ -32372,7 +33206,7 @@ async function baseOf2(dir, ref) {
|
|
|
32372
33206
|
return res.stdout.trim();
|
|
32373
33207
|
}
|
|
32374
33208
|
function findTaskWorktree(root, id, task, repoName) {
|
|
32375
|
-
const changeDir =
|
|
33209
|
+
const changeDir = join34(root, ".factory", "worktrees", id);
|
|
32376
33210
|
const repos = repoName ? [repoName] : existsSync25(changeDir) ? readdirSync9(changeDir) : [];
|
|
32377
33211
|
const hits = repos.map((r) => ({ repo: r, dir: worktreePath(root, id, r, task) })).filter((h) => existsSync25(h.dir));
|
|
32378
33212
|
const hit = hits[0];
|
|
@@ -32387,7 +33221,7 @@ function printReport(io, report) {
|
|
|
32387
33221
|
const where = r.cwd === "." ? "" : ` [${r.cwd}]`;
|
|
32388
33222
|
const what = r.command ? ` ${r.command}` : "";
|
|
32389
33223
|
const time = r.durationMs > 0 ? ` ${(r.durationMs / 1000).toFixed(1)}s` : "";
|
|
32390
|
-
io.out(` ${MARK3[r.status]} ${r.type}${where}${what}${time}${r.status === "skipped" ? ` (${r.output})` : ""}`);
|
|
33224
|
+
io.out(` ${MARK3[r.status]} ${r.name ?? r.type}${where}${what}${time}${r.status === "skipped" ? ` (${r.output})` : ""}`);
|
|
32391
33225
|
if (r.status !== "pass" && r.status !== "skipped" && r.output) {
|
|
32392
33226
|
for (const line of r.output.split(`
|
|
32393
33227
|
`))
|
|
@@ -32436,7 +33270,7 @@ var gateCommand = {
|
|
|
32436
33270
|
} else {
|
|
32437
33271
|
const repos = values.repo !== undefined ? [pick(values.repo)] : codeRepos;
|
|
32438
33272
|
for (const repo of repos) {
|
|
32439
|
-
const dir =
|
|
33273
|
+
const dir = join34(root, repo.path);
|
|
32440
33274
|
const branch = (await git(["branch", "--show-current"], { cwd: dir })).stdout.trim();
|
|
32441
33275
|
if (branch !== `change/${id}`) {
|
|
32442
33276
|
if (values.repo !== undefined) {
|
|
@@ -32471,7 +33305,7 @@ var gateCommand = {
|
|
|
32471
33305
|
|
|
32472
33306
|
// src/cli/commands/graph.ts
|
|
32473
33307
|
import { existsSync as existsSync26 } from "node:fs";
|
|
32474
|
-
import { join as
|
|
33308
|
+
import { join as join35 } from "node:path";
|
|
32475
33309
|
var graphCommand = {
|
|
32476
33310
|
name: "graph",
|
|
32477
33311
|
summary: "Build or incrementally update the workspace graph",
|
|
@@ -32488,7 +33322,7 @@ The pipeline also updates it after every task merge, so this is rarely needed by
|
|
|
32488
33322
|
const root = await findWorkspaceRoot(io.cwd);
|
|
32489
33323
|
if (root === undefined)
|
|
32490
33324
|
throw new PreconditionError("not inside a git repository");
|
|
32491
|
-
if (!existsSync26(
|
|
33325
|
+
if (!existsSync26(join35(root, LOCK_FILE)))
|
|
32492
33326
|
throw new PreconditionError("ai-factory is not installed here", ["run `ai-factory init`"]);
|
|
32493
33327
|
const tool = await checkTool("graphify", root);
|
|
32494
33328
|
if (!tool.ok)
|
|
@@ -32508,8 +33342,8 @@ The pipeline also updates it after every task merge, so this is rarely needed by
|
|
|
32508
33342
|
};
|
|
32509
33343
|
|
|
32510
33344
|
// src/cli/commands/init.ts
|
|
32511
|
-
import { existsSync as
|
|
32512
|
-
import { join as
|
|
33345
|
+
import { existsSync as existsSync28, writeFileSync as writeFileSync10 } from "node:fs";
|
|
33346
|
+
import { join as join37 } from "node:path";
|
|
32513
33347
|
|
|
32514
33348
|
// src/config/render.ts
|
|
32515
33349
|
var DEFAULT_MODELS = {
|
|
@@ -32521,6 +33355,72 @@ var DEFAULT_MODELS = {
|
|
|
32521
33355
|
explorer: "anthropic/claude-haiku-5-5"
|
|
32522
33356
|
};
|
|
32523
33357
|
var json = (value) => JSON.stringify(value);
|
|
33358
|
+
function suggestedExtraGates(detection) {
|
|
33359
|
+
const out = [];
|
|
33360
|
+
for (const repo of detection.repos) {
|
|
33361
|
+
if (repo.languages.includes("tauri")) {
|
|
33362
|
+
out.push({
|
|
33363
|
+
name: "ios-compile",
|
|
33364
|
+
type: "types",
|
|
33365
|
+
phases: ["task", "full"],
|
|
33366
|
+
when: { changed: ["src-tauri/**"] },
|
|
33367
|
+
command: "cargo check --manifest-path src-tauri/Cargo.toml --target aarch64-apple-ios"
|
|
33368
|
+
});
|
|
33369
|
+
}
|
|
33370
|
+
}
|
|
33371
|
+
for (const suite of detection.e2e) {
|
|
33372
|
+
out.push({
|
|
33373
|
+
name: `e2e-${suite.repo}`,
|
|
33374
|
+
type: "test",
|
|
33375
|
+
phases: ["full"],
|
|
33376
|
+
command: suite.command
|
|
33377
|
+
});
|
|
33378
|
+
}
|
|
33379
|
+
const wholeRepo = new Set(["docs", "kustomize", "chart", "verify"]);
|
|
33380
|
+
for (const repo of detection.tasks) {
|
|
33381
|
+
for (const name of repo.names) {
|
|
33382
|
+
if (!wholeRepo.has(name))
|
|
33383
|
+
continue;
|
|
33384
|
+
out.push({
|
|
33385
|
+
name: `${name}-${repo.repo}`,
|
|
33386
|
+
type: "test",
|
|
33387
|
+
phases: ["full"],
|
|
33388
|
+
command: `task ${name}`
|
|
33389
|
+
});
|
|
33390
|
+
}
|
|
33391
|
+
}
|
|
33392
|
+
for (const contract of detection.contracts) {
|
|
33393
|
+
if (contract.command === undefined)
|
|
33394
|
+
continue;
|
|
33395
|
+
if (contract.kind === "ipc") {
|
|
33396
|
+
out.push({
|
|
33397
|
+
name: "generated-bindings",
|
|
33398
|
+
type: "test",
|
|
33399
|
+
phases: ["task", "full"],
|
|
33400
|
+
when: { changed: ["src-tauri/**", "src/**"] },
|
|
33401
|
+
command: contract.command,
|
|
33402
|
+
outputs: contract.outputs ?? [contract.path]
|
|
33403
|
+
});
|
|
33404
|
+
} else if (contract.kind === "crd") {
|
|
33405
|
+
out.push({
|
|
33406
|
+
name: "generated-crd",
|
|
33407
|
+
type: "test",
|
|
33408
|
+
phases: ["full"],
|
|
33409
|
+
command: contract.command,
|
|
33410
|
+
outputs: contract.outputs ?? [contract.path]
|
|
33411
|
+
});
|
|
33412
|
+
} else if (contract.kind === "generated") {
|
|
33413
|
+
out.push({
|
|
33414
|
+
name: "generated-artifacts",
|
|
33415
|
+
type: "test",
|
|
33416
|
+
phases: ["full"],
|
|
33417
|
+
command: contract.command,
|
|
33418
|
+
outputs: contract.outputs ?? [contract.path]
|
|
33419
|
+
});
|
|
33420
|
+
}
|
|
33421
|
+
}
|
|
33422
|
+
return out;
|
|
33423
|
+
}
|
|
32524
33424
|
function renderInitialConfig(detection) {
|
|
32525
33425
|
const repos = detection.repos.map((r) => ` ${json(toRepoConfig(r))}`).join(`,
|
|
32526
33426
|
`);
|
|
@@ -32541,16 +33441,29 @@ ${repos}
|
|
|
32541
33441
|
],
|
|
32542
33442
|
|
|
32543
33443
|
// opencode model ids ("provider/model") per role. contractReviewer defaults to reviewer.
|
|
33444
|
+
// "roles", "repos" and "agents" can override these; the flat entries stay the fallback.
|
|
32544
33445
|
"models": {
|
|
32545
33446
|
${models}
|
|
32546
33447
|
},
|
|
32547
33448
|
|
|
33449
|
+
// Project bars injected into every session. Add docs/security.md, docs/go.md, etc.
|
|
33450
|
+
"instructions": ${json(detection.instructions)},
|
|
33451
|
+
// Skills the agents should load first, from .opencode/skills or a custom source.
|
|
33452
|
+
"skills": { "sources": [".opencode/skills"], "roles": {}, "repos": {} },
|
|
33453
|
+
// MCP tool action patterns per role (e.g. "reviewer": ["mantine_*"]).
|
|
33454
|
+
"mcp": { "roles": {} },
|
|
33455
|
+
// Optional specialist agents and task routing by touched paths.
|
|
33456
|
+
"agents": [],
|
|
33457
|
+
"routing": [],
|
|
33458
|
+
|
|
32548
33459
|
"gates": {
|
|
32549
33460
|
"maxRetries": 3,
|
|
32550
33461
|
// Gate type -> script/target name looked up in package.json, Makefile, justfile, Taskfile.
|
|
32551
33462
|
"scripts": { "format": "format:check", "lint": "lint", "types": "typecheck", "test": "test", "testAffected": "test:affected" },
|
|
32552
33463
|
// Repo name -> gate type -> literal command; wins over scripts and profile defaults.
|
|
32553
|
-
"overrides": {}
|
|
33464
|
+
"overrides": {},
|
|
33465
|
+
// Detected stack gates: iOS compile and generated-artifact drift. Edit or remove freely.
|
|
33466
|
+
"extra": ${json(suggestedExtraGates(detection))}
|
|
32554
33467
|
},
|
|
32555
33468
|
|
|
32556
33469
|
// Proposed by detection: confirm providers and consumers.
|
|
@@ -32573,6 +33486,12 @@ ${contracts}
|
|
|
32573
33486
|
"graph": { "exclude": ${json(DEFAULT_GRAPH_EXCLUDE)} },
|
|
32574
33487
|
// Agent shells and gates run in an OS sandbox where available ("auto"; macOS sandbox-exec).
|
|
32575
33488
|
"security": { "sandbox": "auto" },
|
|
33489
|
+
// Task commits: "conventional" or "gitmoji"; scopes prefixes the repo name.
|
|
33490
|
+
"commits": { "style": "conventional", "scopes": false },
|
|
33491
|
+
// Clarification before design and the no-spec quick task.
|
|
33492
|
+
"workflow": { "refine": true, "quick": { "enabled": true, "branchPrefix": "task/" } },
|
|
33493
|
+
// Detect release-please, goreleaser, changesets and forge CI (read-only).
|
|
33494
|
+
"release": { "detect": true },
|
|
32576
33495
|
|
|
32577
33496
|
// Managed files the factory must never overwrite (workspace-relative paths).
|
|
32578
33497
|
"unmanaged": []
|
|
@@ -32580,9 +33499,51 @@ ${contracts}
|
|
|
32580
33499
|
`;
|
|
32581
33500
|
}
|
|
32582
33501
|
|
|
33502
|
+
// src/workspace/legacy.ts
|
|
33503
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync13, readdirSync as readdirSync10, renameSync as renameSync6, rmdirSync as rmdirSync2 } from "node:fs";
|
|
33504
|
+
import { dirname as dirname8, join as join36 } from "node:path";
|
|
33505
|
+
var OPENSPEC_PREFIX = /^(opsx-|openspec-)/;
|
|
33506
|
+
function move(root, rel, stamp, archived) {
|
|
33507
|
+
const from = join36(root, rel);
|
|
33508
|
+
if (!existsSync27(from))
|
|
33509
|
+
return;
|
|
33510
|
+
const to = join36(root, ".factory", "legacy", stamp, rel);
|
|
33511
|
+
mkdirSync13(dirname8(to), { recursive: true });
|
|
33512
|
+
renameSync6(from, to);
|
|
33513
|
+
archived.push(rel);
|
|
33514
|
+
}
|
|
33515
|
+
function moveEntries(root, dir, keep, stamp, archived) {
|
|
33516
|
+
const abs = join36(root, dir);
|
|
33517
|
+
if (!existsSync27(abs))
|
|
33518
|
+
return;
|
|
33519
|
+
for (const name of readdirSync10(abs)) {
|
|
33520
|
+
if (keep(name))
|
|
33521
|
+
continue;
|
|
33522
|
+
move(root, `${dir}/${name}`, stamp, archived);
|
|
33523
|
+
}
|
|
33524
|
+
try {
|
|
33525
|
+
if (readdirSync10(abs).length === 0)
|
|
33526
|
+
rmdirSync2(abs);
|
|
33527
|
+
} catch {}
|
|
33528
|
+
}
|
|
33529
|
+
function archiveLegacySetups(root, repoPaths) {
|
|
33530
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
33531
|
+
const archived = [];
|
|
33532
|
+
const dirs = [".opencode", ...repoPaths.filter((p) => p !== ".").map((p) => `${p}/.opencode`)];
|
|
33533
|
+
for (const base of dirs) {
|
|
33534
|
+
for (const name of ["agent", "agents"]) {
|
|
33535
|
+
moveEntries(root, `${base}/${name}`, () => false, stamp, archived);
|
|
33536
|
+
}
|
|
33537
|
+
moveEntries(root, `${base}/commands`, (n) => OPENSPEC_PREFIX.test(n), stamp, archived);
|
|
33538
|
+
moveEntries(root, `${base}/command`, () => false, stamp, archived);
|
|
33539
|
+
moveEntries(root, `${base}/skills`, (n) => OPENSPEC_PREFIX.test(n), stamp, archived);
|
|
33540
|
+
}
|
|
33541
|
+
return { stamp, archived };
|
|
33542
|
+
}
|
|
33543
|
+
|
|
32583
33544
|
// src/cli/commands/init.ts
|
|
32584
33545
|
var OPENSPEC_CONFIG = "openspec/config.yaml";
|
|
32585
|
-
var HELP4 = `Usage: ai-factory init [--force] [--allow-downgrade] [--no-commit] [--no-graph] [--json]
|
|
33546
|
+
var HELP4 = `Usage: ai-factory init [--plan] [--force] [--allow-downgrade] [--no-commit] [--no-graph] [--json]
|
|
32586
33547
|
|
|
32587
33548
|
Installs the factory into the workspace: detects repos, languages and contracts, checks
|
|
32588
33549
|
the tools, runs \`openspec init\` for opencode, renders agents, standards and config,
|
|
@@ -32591,6 +33552,7 @@ and commits the result in the AI repo. Safe to run again: an unchanged workspace
|
|
|
32591
33552
|
no diff and no commit.
|
|
32592
33553
|
|
|
32593
33554
|
Options:
|
|
33555
|
+
--plan Show what would be created, archived or replaced; write nothing
|
|
32594
33556
|
--force Overwrite existing files that ai-factory did not create
|
|
32595
33557
|
--allow-downgrade Let this (older) version rewrite a workspace a newer one installed
|
|
32596
33558
|
--no-commit Leave the changes uncommitted
|
|
@@ -32617,6 +33579,7 @@ var initCommand = {
|
|
|
32617
33579
|
"allow-downgrade": { type: "boolean", default: false },
|
|
32618
33580
|
"no-commit": { type: "boolean", default: false },
|
|
32619
33581
|
"no-graph": { type: "boolean", default: false },
|
|
33582
|
+
plan: { type: "boolean", default: false },
|
|
32620
33583
|
json: { type: "boolean", default: false }
|
|
32621
33584
|
});
|
|
32622
33585
|
const root = await findWorkspaceRoot(io.cwd);
|
|
@@ -32635,7 +33598,7 @@ var initCommand = {
|
|
|
32635
33598
|
if (missing.length > 0) {
|
|
32636
33599
|
throw new PreconditionError("required tools are missing or unsupported", missing.map((t) => t.ok ? "" : `${t.tool}: ${t.problem} → ${t.fix}`));
|
|
32637
33600
|
}
|
|
32638
|
-
if (!values["no-commit"]) {
|
|
33601
|
+
if (!values.plan && !values["no-commit"]) {
|
|
32639
33602
|
const dirty = await dirtyInstallPaths(root, [CONFIG_FILE]);
|
|
32640
33603
|
if (dirty.length > 0) {
|
|
32641
33604
|
throw new PreconditionError("uncommitted changes in files that init commits", [
|
|
@@ -32644,14 +33607,33 @@ var initCommand = {
|
|
|
32644
33607
|
]);
|
|
32645
33608
|
}
|
|
32646
33609
|
}
|
|
32647
|
-
const configPath =
|
|
32648
|
-
const newConfig = !
|
|
33610
|
+
const configPath = join37(root, CONFIG_FILE);
|
|
33611
|
+
const newConfig = !existsSync28(configPath);
|
|
32649
33612
|
const detection = newConfig ? await detectWorkspace(root) : undefined;
|
|
32650
|
-
const
|
|
33613
|
+
const legacy = !values.plan && readLock(root) === undefined && detection !== undefined ? archiveLegacySetups(root, detection.repos.map((r) => r.path)) : undefined;
|
|
33614
|
+
const keepOpenspec = newConfig && !values.force && existsSync28(join37(root, OPENSPEC_CONFIG)) && readLock(root) === undefined;
|
|
32651
33615
|
const rendered = detection !== undefined ? renderInitialConfig(detection) : undefined;
|
|
32652
33616
|
const configText = rendered !== undefined && keepOpenspec ? setJsonc(rendered, ["unmanaged"], [OPENSPEC_CONFIG]) : rendered;
|
|
32653
33617
|
const config = configText !== undefined ? parseConfigText(configText, CONFIG_FILE) : loadConfig(root).config;
|
|
32654
33618
|
const openspecFiles = await renderOpenspecTools(root);
|
|
33619
|
+
if (values.plan) {
|
|
33620
|
+
const planned = planSync(root, [...renderWorkspace(config, { root }), ...openspecFiles], readLock(root)?.managed ?? {}, { unmanaged: config.unmanaged, force: values.force });
|
|
33621
|
+
if (values.json) {
|
|
33622
|
+
io.out(JSON.stringify({
|
|
33623
|
+
root,
|
|
33624
|
+
newConfig,
|
|
33625
|
+
notes: detection?.notes ?? [],
|
|
33626
|
+
plan: planned.map(({ path, action }) => ({ path, action }))
|
|
33627
|
+
}, null, 2));
|
|
33628
|
+
} else {
|
|
33629
|
+
io.out(`ai-factory ${FACTORY_VERSION} plan for ${root}`);
|
|
33630
|
+
if (newConfig)
|
|
33631
|
+
io.out(`would write ${CONFIG_FILE}`);
|
|
33632
|
+
for (const change of planned)
|
|
33633
|
+
io.out(`${REPORTED[change.action] ?? change.action}: ${change.path}`);
|
|
33634
|
+
}
|
|
33635
|
+
return ExitCode.Ok;
|
|
33636
|
+
}
|
|
32655
33637
|
planWorkspace(root, config, { force: values.force, tools: openspecFiles });
|
|
32656
33638
|
if (configText !== undefined)
|
|
32657
33639
|
writeFileSync10(configPath, configText);
|
|
@@ -32660,7 +33642,7 @@ var initCommand = {
|
|
|
32660
33642
|
tools: openspecFiles
|
|
32661
33643
|
});
|
|
32662
33644
|
const commit = values["no-commit"] ? undefined : await commitInstall(root, `chore(factory): init v${FACTORY_VERSION}`);
|
|
32663
|
-
const graph = values["no-graph"] ? undefined : await updateGraph(root, { full: !
|
|
33645
|
+
const graph = values["no-graph"] ? undefined : await updateGraph(root, { full: !existsSync28(graphFile(root)) });
|
|
32664
33646
|
const changed = result.plan.filter((c) => REPORTED[c.action] !== undefined);
|
|
32665
33647
|
if (values.json) {
|
|
32666
33648
|
io.out(JSON.stringify({
|
|
@@ -32679,6 +33661,9 @@ var initCommand = {
|
|
|
32679
33661
|
io.out(`tools: ${tools.map((t) => `${t.tool} ${t.ok ? t.version : "?"}`).join(", ")}`);
|
|
32680
33662
|
for (const note of detection?.notes ?? [])
|
|
32681
33663
|
io.out(`note: ${note}`);
|
|
33664
|
+
if (legacy !== undefined && legacy.archived.length > 0) {
|
|
33665
|
+
io.out(`legacy setup archived under .factory/legacy/${legacy.stamp}/ (${legacy.archived.length} path${legacy.archived.length === 1 ? "" : "s"})`);
|
|
33666
|
+
}
|
|
32682
33667
|
if (keepOpenspec)
|
|
32683
33668
|
io.out(`kept your ${OPENSPEC_CONFIG} (listed under "unmanaged"); add the factory's workspace context to it by hand if you want the architect to see it`);
|
|
32684
33669
|
if (newConfig)
|
|
@@ -32704,17 +33689,17 @@ var initCommand = {
|
|
|
32704
33689
|
};
|
|
32705
33690
|
|
|
32706
33691
|
// src/cli/commands/pipeline-commands.ts
|
|
32707
|
-
import { existsSync as
|
|
32708
|
-
import { join as
|
|
33692
|
+
import { existsSync as existsSync34, readFileSync as readFileSync27 } from "node:fs";
|
|
33693
|
+
import { join as join49, resolve as resolve5 } from "node:path";
|
|
32709
33694
|
|
|
32710
33695
|
// src/pipeline/approve.ts
|
|
32711
|
-
import { mkdirSync as
|
|
32712
|
-
import { dirname as
|
|
33696
|
+
import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync11 } from "node:fs";
|
|
33697
|
+
import { dirname as dirname9 } from "node:path";
|
|
32713
33698
|
|
|
32714
33699
|
// src/approval/approval.ts
|
|
32715
33700
|
import { createHash as createHash4 } from "node:crypto";
|
|
32716
|
-
import { existsSync as
|
|
32717
|
-
import { join as
|
|
33701
|
+
import { existsSync as existsSync29, readdirSync as readdirSync11, readFileSync as readFileSync22, statSync as statSync7 } from "node:fs";
|
|
33702
|
+
import { join as join38 } from "node:path";
|
|
32718
33703
|
function parseApproval(value) {
|
|
32719
33704
|
if (typeof value !== "object" || value === null)
|
|
32720
33705
|
return;
|
|
@@ -32722,28 +33707,28 @@ function parseApproval(value) {
|
|
|
32722
33707
|
const ok = typeof v.changeId === "string" && typeof v.approver === "string" && typeof v.specCommit === "string" && /^[0-9a-f]{40,64}$/.test(v.specCommit) && typeof v.hash === "string" && /^[0-9a-f]{64}$/.test(v.hash) && typeof v.approvedAt === "string";
|
|
32723
33708
|
return ok ? value : undefined;
|
|
32724
33709
|
}
|
|
32725
|
-
var changeDir = (root, id) =>
|
|
32726
|
-
var approvalFile = (root, id) =>
|
|
33710
|
+
var changeDir = (root, id) => join38(root, "openspec", "changes", id);
|
|
33711
|
+
var approvalFile = (root, id) => join38(root, ".factory", "approvals", `${id}.json`);
|
|
32727
33712
|
function filesUnder(dir, prefix = "") {
|
|
32728
|
-
if (!
|
|
33713
|
+
if (!existsSync29(dir))
|
|
32729
33714
|
return [];
|
|
32730
|
-
return
|
|
33715
|
+
return readdirSync11(dir).sort().flatMap((name) => {
|
|
32731
33716
|
const rel = prefix === "" ? name : `${prefix}/${name}`;
|
|
32732
|
-
const abs =
|
|
33717
|
+
const abs = join38(dir, name);
|
|
32733
33718
|
return statSync7(abs).isDirectory() ? filesUnder(abs, rel) : [rel];
|
|
32734
33719
|
});
|
|
32735
33720
|
}
|
|
32736
33721
|
function approvedFiles(root, id) {
|
|
32737
33722
|
const dir = changeDir(root, id);
|
|
32738
|
-
const top = ["proposal.md", "design.md", "tasks.md"].filter((f) =>
|
|
32739
|
-
const specs = filesUnder(
|
|
33723
|
+
const top = ["proposal.md", "design.md", "tasks.md"].filter((f) => existsSync29(join38(dir, f)));
|
|
33724
|
+
const specs = filesUnder(join38(dir, "specs"), "specs").filter((f) => f.endsWith(".md"));
|
|
32740
33725
|
return [...top, ...specs].sort();
|
|
32741
33726
|
}
|
|
32742
33727
|
function specHash(root, id) {
|
|
32743
33728
|
const hash = createHash4("sha256");
|
|
32744
33729
|
const dir = changeDir(root, id);
|
|
32745
33730
|
for (const rel of approvedFiles(root, id)) {
|
|
32746
|
-
let content =
|
|
33731
|
+
let content = readFileSync22(join38(dir, rel), "utf8").replace(/\r\n/g, `
|
|
32747
33732
|
`);
|
|
32748
33733
|
if (rel === "tasks.md")
|
|
32749
33734
|
content = normalizeCheckboxes(content);
|
|
@@ -32753,10 +33738,10 @@ function specHash(root, id) {
|
|
|
32753
33738
|
}
|
|
32754
33739
|
function readApproval(root, id) {
|
|
32755
33740
|
const file = approvalFile(root, id);
|
|
32756
|
-
if (!
|
|
33741
|
+
if (!existsSync29(file))
|
|
32757
33742
|
return;
|
|
32758
33743
|
try {
|
|
32759
|
-
return parseApproval(JSON.parse(
|
|
33744
|
+
return parseApproval(JSON.parse(readFileSync22(file, "utf8")));
|
|
32760
33745
|
} catch {
|
|
32761
33746
|
return;
|
|
32762
33747
|
}
|
|
@@ -32774,12 +33759,12 @@ function checkApprovalHash(root, id) {
|
|
|
32774
33759
|
}
|
|
32775
33760
|
|
|
32776
33761
|
// src/pipeline/design.ts
|
|
32777
|
-
import { existsSync as
|
|
32778
|
-
import { join as
|
|
33762
|
+
import { existsSync as existsSync31 } from "node:fs";
|
|
33763
|
+
import { join as join41 } from "node:path";
|
|
32779
33764
|
|
|
32780
33765
|
// src/git/branching.ts
|
|
32781
|
-
import { existsSync as
|
|
32782
|
-
import { join as
|
|
33766
|
+
import { existsSync as existsSync30 } from "node:fs";
|
|
33767
|
+
import { join as join39 } from "node:path";
|
|
32783
33768
|
async function switchToChangeBranch(dir, branch, start) {
|
|
32784
33769
|
if (await currentBranch(dir) === branch)
|
|
32785
33770
|
return { created: false };
|
|
@@ -32816,8 +33801,8 @@ async function trackedBranch(root, path) {
|
|
|
32816
33801
|
return;
|
|
32817
33802
|
}
|
|
32818
33803
|
async function branchSubmodule(root, repo, id) {
|
|
32819
|
-
const dir =
|
|
32820
|
-
if (!
|
|
33804
|
+
const dir = join39(root, repo.path);
|
|
33805
|
+
if (!existsSync30(join39(dir, ".git"))) {
|
|
32821
33806
|
throw new PreconditionError(`submodule ${repo.path} is not initialised`, [
|
|
32822
33807
|
`run \`git submodule update --init ${repo.path}\``
|
|
32823
33808
|
]);
|
|
@@ -32889,7 +33874,7 @@ function decisionValues(handoff) {
|
|
|
32889
33874
|
}
|
|
32890
33875
|
|
|
32891
33876
|
// src/pipeline/context.ts
|
|
32892
|
-
import { join as
|
|
33877
|
+
import { join as join40 } from "node:path";
|
|
32893
33878
|
|
|
32894
33879
|
// src/runtime/rate-limit.ts
|
|
32895
33880
|
function sleep(ms, signal) {
|
|
@@ -32987,7 +33972,7 @@ class Pipeline {
|
|
|
32987
33972
|
return repo;
|
|
32988
33973
|
}
|
|
32989
33974
|
repoDir(repo) {
|
|
32990
|
-
return
|
|
33975
|
+
return join40(this.root, repo.path);
|
|
32991
33976
|
}
|
|
32992
33977
|
out(line) {
|
|
32993
33978
|
this.deps.out(line);
|
|
@@ -33026,14 +34011,31 @@ class Pipeline {
|
|
|
33026
34011
|
}
|
|
33027
34012
|
}
|
|
33028
34013
|
async#call(call, signal) {
|
|
33029
|
-
const
|
|
33030
|
-
|
|
33031
|
-
|
|
33032
|
-
agent: call.agent,
|
|
33033
|
-
model,
|
|
33034
|
-
title: call.title,
|
|
33035
|
-
context: { ...call.context, changeId: call.changeId, role: call.role }
|
|
34014
|
+
const first = modelFor(this.config, call.modelRole, {
|
|
34015
|
+
...call.context.repo === undefined ? {} : { repo: call.context.repo },
|
|
34016
|
+
agent: call.agent
|
|
33036
34017
|
});
|
|
34018
|
+
let model = first;
|
|
34019
|
+
let raw;
|
|
34020
|
+
let lastError;
|
|
34021
|
+
for (const candidate of [first, ...this.config.models.fallback]) {
|
|
34022
|
+
try {
|
|
34023
|
+
raw = await this.deps.runtime.open({
|
|
34024
|
+
agent: call.agent,
|
|
34025
|
+
model: candidate,
|
|
34026
|
+
title: call.title,
|
|
34027
|
+
context: { ...call.context, changeId: call.changeId, role: call.role }
|
|
34028
|
+
});
|
|
34029
|
+
model = candidate;
|
|
34030
|
+
break;
|
|
34031
|
+
} catch (err) {
|
|
34032
|
+
lastError = err;
|
|
34033
|
+
}
|
|
34034
|
+
}
|
|
34035
|
+
if (raw === undefined) {
|
|
34036
|
+
throw lastError instanceof Error ? lastError : new Error(`could not open a session for ${call.agent}`);
|
|
34037
|
+
}
|
|
34038
|
+
const provider = model.split("/")[0] ?? model;
|
|
33037
34039
|
const session = limited(raw, this.limiter, provider);
|
|
33038
34040
|
let turn = await promptForHandoff(session, call.prompt, signal);
|
|
33039
34041
|
let usage = turn.usage;
|
|
@@ -33113,6 +34115,8 @@ ${text}`).join(`
|
|
|
33113
34115
|
|
|
33114
34116
|
`);
|
|
33115
34117
|
const contracts = input.contracts.map((c) => `- ${c.kind} ${c.provider}:${c.path} → ${c.consumers.join(", ") || "no consumers"}`);
|
|
34118
|
+
const instructions = input.instructions === undefined || input.instructions.length === 0 ? "- none configured" : input.instructions.map((p) => `- \`${p}\``).join(`
|
|
34119
|
+
`);
|
|
33116
34120
|
return `Write the OpenSpec change \`${input.id}\` in \`openspec/changes/${input.id}/\` (the folder exists; fill proposal.md, specs/<capability>/spec.md, design.md and tasks.md).
|
|
33117
34121
|
|
|
33118
34122
|
Request:
|
|
@@ -33128,6 +34132,9 @@ Contracts:
|
|
|
33128
34132
|
${contracts.length > 0 ? contracts.join(`
|
|
33129
34133
|
`) : "- none configured"}
|
|
33130
34134
|
|
|
34135
|
+
Project instructions (read these before writing the spec):
|
|
34136
|
+
${instructions}
|
|
34137
|
+
|
|
33131
34138
|
Explorer findings:
|
|
33132
34139
|
${explored || "(none)"}
|
|
33133
34140
|
|
|
@@ -33169,18 +34176,28 @@ ${conflicts.map((f) => `- ${f}`).join(`
|
|
|
33169
34176
|
Resolve every conflict so both changes are kept and correct. Remove all conflict markers. Do not run git commands that change history; the factory continues the rebase. End with a handoff (\`task: "${task.id}"\`).`;
|
|
33170
34177
|
}
|
|
33171
34178
|
function reviewPrompt(input) {
|
|
34179
|
+
const instructions = input.instructions === undefined || input.instructions.length === 0 ? "" : `
|
|
34180
|
+
Project instructions (read these before judging the diff; paths are relative to this checkout):
|
|
34181
|
+
${input.instructions.map((p) => `- \`${p}\``).join(`
|
|
34182
|
+
`)}
|
|
34183
|
+
`;
|
|
33172
34184
|
return `Review repo \`${input.repo.name}\` for this change: \`git diff ${input.base}..${input.head}\` (run it in this directory), plus the code around it.
|
|
33173
|
-
Standards in force: ${input.profiles.join(", ") || "none"} (
|
|
33174
|
-
|
|
34185
|
+
Standards in force: ${input.profiles.join(", ") || "none"} (the full standards are in the workspace's .opencode/standards/).
|
|
34186
|
+
${instructions}
|
|
33175
34187
|
Spec of the change:
|
|
33176
34188
|
${input.spec}
|
|
33177
34189
|
|
|
33178
34190
|
Use \`task: "review:${input.repo.name}"\`.`;
|
|
33179
34191
|
}
|
|
33180
34192
|
function testerPrompt(input) {
|
|
34193
|
+
const instructions = input.instructions === undefined || input.instructions.length === 0 ? "" : `
|
|
34194
|
+
Project instructions (follow these test conventions; paths are workspace-relative, so open \`$AI_FACTORY_WORKSPACE/<path>\` from a worktree):
|
|
34195
|
+
${input.instructions.map((p) => `- \`${p}\``).join(`
|
|
34196
|
+
`)}
|
|
34197
|
+
`;
|
|
33181
34198
|
return `Task ${input.task.id}: make sure every scenario below that concerns repo \`${input.task.repo}\` has a test.
|
|
33182
34199
|
You may only edit test files (${input.testGlobs.join(", ")}).
|
|
33183
|
-
|
|
34200
|
+
${instructions}
|
|
33184
34201
|
Spec of the change:
|
|
33185
34202
|
${input.spec}
|
|
33186
34203
|
|
|
@@ -33203,6 +34220,25 @@ function findingPack(f) {
|
|
|
33203
34220
|
- Impact if skipped: ${f.impact}
|
|
33204
34221
|
${f.file ? `- Location: ${f.file}${f.line ? `:${f.line}` : ""}` : ""}`;
|
|
33205
34222
|
}
|
|
34223
|
+
function quickPrompt(input) {
|
|
34224
|
+
return `Quick task in repo \`${input.repo.name}\` (no OpenSpec change):
|
|
34225
|
+
"""
|
|
34226
|
+
${input.task}
|
|
34227
|
+
"""
|
|
34228
|
+
Make the smallest complete change that satisfies the task. You may edit any file in this
|
|
34229
|
+
repository except the factory-managed files and secrets. Tests for the change belong with it.
|
|
34230
|
+
The factory runs the repository's gates after you finish; report the result in your handoff.
|
|
34231
|
+
Use \`task: "quick"\`.`;
|
|
34232
|
+
}
|
|
34233
|
+
function quickReviewPrompt(input) {
|
|
34234
|
+
return `Review the uncommitted quick-task change in repo \`${input.repo.name}\`:
|
|
34235
|
+
"""
|
|
34236
|
+
${input.task}
|
|
34237
|
+
"""
|
|
34238
|
+
Run \`git diff\` in this repository and read the code around the changes. Report only real
|
|
34239
|
+
bugs, security problems, missing tests for risky logic, and contract/convention violations.
|
|
34240
|
+
Use \`task: "quick-review"\` and the normal findings handoff.`;
|
|
34241
|
+
}
|
|
33206
34242
|
function contractReviewPrompt(input) {
|
|
33207
34243
|
return `One cross-repo review of the contracts this change touches. The deterministic contract gate already ran:
|
|
33208
34244
|
${input.contracts.map((c) => c.summary).join(`
|
|
@@ -33217,14 +34253,14 @@ Report only real mismatches between providers and consumers as findings, with \`
|
|
|
33217
34253
|
|
|
33218
34254
|
// src/pipeline/design.ts
|
|
33219
34255
|
function specDir(p, draft) {
|
|
33220
|
-
return draft === undefined ? p.root :
|
|
34256
|
+
return draft === undefined ? p.root : join41(p.root, draft);
|
|
33221
34257
|
}
|
|
33222
34258
|
function slug(text) {
|
|
33223
34259
|
const words = text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter(Boolean).slice(0, 5);
|
|
33224
34260
|
return words.join("-") || "change";
|
|
33225
34261
|
}
|
|
33226
34262
|
function uniqueId(p, wanted) {
|
|
33227
|
-
const taken = (id) => loadRun(p.root, id) !== undefined ||
|
|
34263
|
+
const taken = (id) => loadRun(p.root, id) !== undefined || existsSync31(join41(p.root, "openspec", "changes", id)) || existsSync31(join41(p.root, ".factory", "drafts", id));
|
|
33228
34264
|
let id = wanted;
|
|
33229
34265
|
for (let n = 2;taken(id); n++)
|
|
33230
34266
|
id = `${wanted}-${n}`;
|
|
@@ -33239,8 +34275,8 @@ async function branchForDesign(p, id) {
|
|
|
33239
34275
|
}
|
|
33240
34276
|
const base = loadRun(p.root, occupiedBy)?.branches["."]?.base ?? await revParse(p.root, "HEAD");
|
|
33241
34277
|
const draft = `.factory/drafts/${id}`;
|
|
33242
|
-
if (!
|
|
33243
|
-
const args = await refExists(p.root, `refs/heads/${changeBranch(id)}`) ? ["worktree", "add", "-q",
|
|
34278
|
+
if (!existsSync31(join41(p.root, draft))) {
|
|
34279
|
+
const args = await refExists(p.root, `refs/heads/${changeBranch(id)}`) ? ["worktree", "add", "-q", join41(p.root, draft), changeBranch(id)] : ["worktree", "add", "-q", "-b", changeBranch(id), join41(p.root, draft), base];
|
|
33244
34280
|
await gitOk(args, { cwd: p.root });
|
|
33245
34281
|
}
|
|
33246
34282
|
return { base, draft };
|
|
@@ -33348,7 +34384,8 @@ async function design(p, id) {
|
|
|
33348
34384
|
request: run.request,
|
|
33349
34385
|
explorations,
|
|
33350
34386
|
repos: p.codeRepos(),
|
|
33351
|
-
contracts: p.config.contracts
|
|
34387
|
+
contracts: p.config.contracts,
|
|
34388
|
+
instructions: allInstructionPaths(p.config)
|
|
33352
34389
|
}),
|
|
33353
34390
|
retry: async (_handoff, attempt) => {
|
|
33354
34391
|
if (attempt > p.config.gates.maxRetries)
|
|
@@ -33401,7 +34438,7 @@ async function approve(p, id) {
|
|
|
33401
34438
|
await p.update(id, (d) => {
|
|
33402
34439
|
assertApprovable(id, d);
|
|
33403
34440
|
const file = approvalFile(p.root, id);
|
|
33404
|
-
|
|
34441
|
+
mkdirSync14(dirname9(file), { recursive: true });
|
|
33405
34442
|
writeFileSync11(file, `${JSON.stringify(approval, null, 2)}
|
|
33406
34443
|
`);
|
|
33407
34444
|
d.phase = "queued";
|
|
@@ -33459,15 +34496,15 @@ async function verifyApproval(p, id, dir) {
|
|
|
33459
34496
|
}
|
|
33460
34497
|
|
|
33461
34498
|
// src/pipeline/contracts.ts
|
|
33462
|
-
import { mkdirSync as
|
|
33463
|
-
import { join as
|
|
34499
|
+
import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync13 } from "node:fs";
|
|
34500
|
+
import { join as join45 } from "node:path";
|
|
33464
34501
|
|
|
33465
34502
|
// src/contracts/gate.ts
|
|
33466
34503
|
var import_ajv = __toESM(require_ajv(), 1);
|
|
33467
34504
|
var import__2020 = __toESM(require__2020(), 1);
|
|
33468
34505
|
var import_ajv_formats = __toESM(require_dist(), 1);
|
|
33469
|
-
import { readFileSync as
|
|
33470
|
-
import { join as
|
|
34506
|
+
import { readFileSync as readFileSync23 } from "node:fs";
|
|
34507
|
+
import { join as join42 } from "node:path";
|
|
33471
34508
|
|
|
33472
34509
|
// src/contracts/schema-diff.ts
|
|
33473
34510
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -34003,7 +35040,7 @@ var SOURCE = /\.(ts|tsx|js|jsx|mjs|cjs|go|py|rs|java|kt|swift|rb|php|cs|vue|svel
|
|
|
34003
35040
|
function consumerSources(dir) {
|
|
34004
35041
|
return listFiles(dir, { maxDepth: 8, maxFiles: 5000 }).filter((f) => SOURCE.test(f.path) && f.size <= 256 * 1024).map((f) => {
|
|
34005
35042
|
try {
|
|
34006
|
-
return
|
|
35043
|
+
return readFileSync23(join42(dir, f.path), "utf8");
|
|
34007
35044
|
} catch {
|
|
34008
35045
|
return "";
|
|
34009
35046
|
}
|
|
@@ -34027,21 +35064,50 @@ async function runValidate(command, dir, contractAbs, maxLines) {
|
|
|
34027
35064
|
${compactOutput(`${res.stdout}
|
|
34028
35065
|
${res.stderr}`, maxLines)}`;
|
|
34029
35066
|
}
|
|
34030
|
-
function
|
|
35067
|
+
function repoOf2(config, name) {
|
|
34031
35068
|
const repo = config.repos.find((r) => r.name === name);
|
|
34032
35069
|
if (repo === undefined)
|
|
34033
35070
|
throw new Error(`unknown repo ${name}`);
|
|
34034
35071
|
return repo;
|
|
34035
35072
|
}
|
|
34036
35073
|
async function checkContract(input, contract) {
|
|
34037
|
-
const provider =
|
|
34038
|
-
const providerDir =
|
|
35074
|
+
const provider = repoOf2(input.config, contract.provider);
|
|
35075
|
+
const providerDir = join42(input.root, provider.path);
|
|
34039
35076
|
const head = await headRef(providerDir, input.id);
|
|
34040
35077
|
const base = input.bases[provider.name] ?? head;
|
|
34041
35078
|
const breaking = [];
|
|
34042
35079
|
const consumerIssues = [];
|
|
34043
35080
|
const notes = [];
|
|
34044
|
-
if (contract.kind === "
|
|
35081
|
+
if (contract.kind === "generated" || contract.kind === "crd" || contract.kind === "ipc" || contract.kind === "custom") {
|
|
35082
|
+
if (contract.command === undefined) {
|
|
35083
|
+
consumerIssues.push(`${contract.kind} contract has no command`);
|
|
35084
|
+
} else if (contract.kind === "custom") {
|
|
35085
|
+
const issue = await runValidate(contract.command, providerDir, join42(providerDir, contract.path), input.gateOutputMaxLines);
|
|
35086
|
+
if (issue)
|
|
35087
|
+
consumerIssues.push(`${provider.name}: ${issue}`);
|
|
35088
|
+
} else {
|
|
35089
|
+
const before = await snapshot(providerDir);
|
|
35090
|
+
const res = await exec(["sh", "-c", contract.command], {
|
|
35091
|
+
cwd: providerDir,
|
|
35092
|
+
env: { CI: "1", NO_COLOR: "1" },
|
|
35093
|
+
timeoutMs: 15 * 60000
|
|
35094
|
+
});
|
|
35095
|
+
const modified = await changedSince(before);
|
|
35096
|
+
if (modified.length > 0)
|
|
35097
|
+
await restore(before, modified);
|
|
35098
|
+
if (res.code !== 0) {
|
|
35099
|
+
consumerIssues.push(`\`${contract.command}\` failed:
|
|
35100
|
+
${compactOutput(`${res.stdout}
|
|
35101
|
+
${res.stderr}`, input.gateOutputMaxLines)}`);
|
|
35102
|
+
} else {
|
|
35103
|
+
const outputs = contract.outputs ?? [contract.path];
|
|
35104
|
+
const drifted = modified.filter((file) => outputs.some((glob) => file === glob || file.startsWith(`${glob.replace(/\/$/, "")}/`) || opencodeGlobRegExp(glob).test(file)));
|
|
35105
|
+
if (drifted.length > 0) {
|
|
35106
|
+
consumerIssues.push(`generated artifacts are stale: ${drifted.join(", ")}; regenerate and commit them`);
|
|
35107
|
+
}
|
|
35108
|
+
}
|
|
35109
|
+
}
|
|
35110
|
+
} else if (contract.kind === "proto") {
|
|
34045
35111
|
const oldFiles = await filesAt(providerDir, base, contract.path, ".proto");
|
|
34046
35112
|
const newFiles = await filesAt(providerDir, head, contract.path, ".proto");
|
|
34047
35113
|
if (newFiles.length === 0)
|
|
@@ -34064,7 +35130,7 @@ async function checkContract(input, contract) {
|
|
|
34064
35130
|
else {
|
|
34065
35131
|
breaking.push(...diffOpenApi(oldDoc, newDoc));
|
|
34066
35132
|
for (const consumer of contract.consumers) {
|
|
34067
|
-
const dir =
|
|
35133
|
+
const dir = join42(input.root, repoOf2(input.config, consumer).path);
|
|
34068
35134
|
for (const path of referencedRemovedPaths(oldDoc, newDoc, consumerSources(dir))) {
|
|
34069
35135
|
consumerIssues.push(`${consumer} still references ${path}, which the new contract no longer has`);
|
|
34070
35136
|
}
|
|
@@ -34096,8 +35162,8 @@ async function checkContract(input, contract) {
|
|
|
34096
35162
|
}
|
|
34097
35163
|
}
|
|
34098
35164
|
for (const [consumer, command] of Object.entries(contract.validate ?? {})) {
|
|
34099
|
-
const dir =
|
|
34100
|
-
const issue = await runValidate(command, dir,
|
|
35165
|
+
const dir = join42(input.root, repoOf2(input.config, consumer).path);
|
|
35166
|
+
const issue = await runValidate(command, dir, join42(providerDir, contract.path), input.gateOutputMaxLines);
|
|
34101
35167
|
if (issue)
|
|
34102
35168
|
consumerIssues.push(`${consumer}: ${issue}`);
|
|
34103
35169
|
}
|
|
@@ -34128,23 +35194,23 @@ function touchedContracts(config, repos) {
|
|
|
34128
35194
|
|
|
34129
35195
|
// src/pipeline/implement.ts
|
|
34130
35196
|
import {
|
|
34131
|
-
existsSync as
|
|
34132
|
-
mkdirSync as
|
|
34133
|
-
readdirSync as
|
|
34134
|
-
readFileSync as
|
|
35197
|
+
existsSync as existsSync32,
|
|
35198
|
+
mkdirSync as mkdirSync15,
|
|
35199
|
+
readdirSync as readdirSync12,
|
|
35200
|
+
readFileSync as readFileSync25,
|
|
34135
35201
|
rmSync as rmSync11,
|
|
34136
35202
|
statSync as statSync8,
|
|
34137
35203
|
writeFileSync as writeFileSync12
|
|
34138
35204
|
} from "node:fs";
|
|
34139
|
-
import { join as
|
|
35205
|
+
import { join as join44 } from "node:path";
|
|
34140
35206
|
|
|
34141
35207
|
// src/gates/definitions.ts
|
|
34142
|
-
import { readFileSync as
|
|
34143
|
-
import { join as
|
|
35208
|
+
import { readFileSync as readFileSync24 } from "node:fs";
|
|
35209
|
+
import { join as join43 } from "node:path";
|
|
34144
35210
|
var HOSTS = ["package.json", "Makefile", "justfile", "Justfile", "Taskfile.yml", "Taskfile.yaml"];
|
|
34145
35211
|
function current(dir, file) {
|
|
34146
35212
|
try {
|
|
34147
|
-
return
|
|
35213
|
+
return readFileSync24(join43(dir, file), "utf8");
|
|
34148
35214
|
} catch {
|
|
34149
35215
|
return;
|
|
34150
35216
|
}
|
|
@@ -34323,7 +35389,7 @@ class GraphUpdater {
|
|
|
34323
35389
|
}
|
|
34324
35390
|
|
|
34325
35391
|
// src/policy/scope.ts
|
|
34326
|
-
import { isAbsolute as isAbsolute2, normalize, relative as
|
|
35392
|
+
import { isAbsolute as isAbsolute2, normalize, relative as relative6, resolve as resolve4 } from "node:path";
|
|
34327
35393
|
|
|
34328
35394
|
// src/policy/paths.ts
|
|
34329
35395
|
var PROTECTED = [
|
|
@@ -34378,7 +35444,7 @@ var architectDirs = (changeId) => [
|
|
|
34378
35444
|
var IMPLEMENTING_ROLES = new Set(["implementer", "tester"]);
|
|
34379
35445
|
function checkEdit(ctx, path, approval) {
|
|
34380
35446
|
const abs = normalize(isAbsolute2(path) ? path : resolve4(ctx.directory, path));
|
|
34381
|
-
const rel =
|
|
35447
|
+
const rel = relative6(ctx.directory, abs);
|
|
34382
35448
|
if (rel === "" || rel.startsWith("..") || isAbsolute2(rel)) {
|
|
34383
35449
|
return {
|
|
34384
35450
|
allowed: false,
|
|
@@ -34753,6 +35819,25 @@ class Scheduler {
|
|
|
34753
35819
|
}
|
|
34754
35820
|
}
|
|
34755
35821
|
|
|
35822
|
+
// src/tasks/routing.ts
|
|
35823
|
+
function agentForTask(config, repo, touches) {
|
|
35824
|
+
for (const rule of config.routing) {
|
|
35825
|
+
const agent = config.agents.find((a) => a.id === rule.agent);
|
|
35826
|
+
if (agent === undefined || agent.phase !== "implementer")
|
|
35827
|
+
continue;
|
|
35828
|
+
const hit = rule.paths.some((path) => touches.some((touch) => {
|
|
35829
|
+
try {
|
|
35830
|
+
return opencodeGlobRegExp(path).test(touch) || opencodeGlobRegExp(touch).test(path);
|
|
35831
|
+
} catch {
|
|
35832
|
+
return false;
|
|
35833
|
+
}
|
|
35834
|
+
}));
|
|
35835
|
+
if (hit)
|
|
35836
|
+
return { agent: agent.id, role: "implementer", custom: true };
|
|
35837
|
+
}
|
|
35838
|
+
return { agent: `implementer-${repo}`, role: "implementer", custom: false };
|
|
35839
|
+
}
|
|
35840
|
+
|
|
34756
35841
|
// src/util/cap.ts
|
|
34757
35842
|
var CHARS_PER_TOKEN = 4;
|
|
34758
35843
|
var estimateTokens = (text) => Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
@@ -34775,7 +35860,7 @@ function gateDefinitions(p, id) {
|
|
|
34775
35860
|
return { config: p.config };
|
|
34776
35861
|
return { config: { ...p.config, gates: snapshot.gates }, gateTables: snapshot.tables };
|
|
34777
35862
|
}
|
|
34778
|
-
var packPath = (root, id, task) =>
|
|
35863
|
+
var packPath = (root, id, task) => join44(root, ".factory", "context", id, `${task}.md`);
|
|
34779
35864
|
async function activate(p, id) {
|
|
34780
35865
|
const claim = await claimActive(p.root, id);
|
|
34781
35866
|
if (claim.status === "busy") {
|
|
@@ -34792,7 +35877,7 @@ async function activate(p, id) {
|
|
|
34792
35877
|
const run = p.run(id);
|
|
34793
35878
|
const branch = changeBranch(id);
|
|
34794
35879
|
if (run.draft !== undefined) {
|
|
34795
|
-
await gitOk(["worktree", "remove",
|
|
35880
|
+
await gitOk(["worktree", "remove", join44(p.root, run.draft)], { cwd: p.root });
|
|
34796
35881
|
await p.update(id, (d) => {
|
|
34797
35882
|
delete d.draft;
|
|
34798
35883
|
});
|
|
@@ -34824,18 +35909,18 @@ async function activate(p, id) {
|
|
|
34824
35909
|
return { status: "active" };
|
|
34825
35910
|
}
|
|
34826
35911
|
function specText(p, id) {
|
|
34827
|
-
const dir =
|
|
34828
|
-
if (!
|
|
35912
|
+
const dir = join44(p.root, "openspec", "changes", id, "specs");
|
|
35913
|
+
if (!existsSync32(dir))
|
|
34829
35914
|
return "";
|
|
34830
35915
|
const out = [];
|
|
34831
35916
|
const walk = (d, rel) => {
|
|
34832
|
-
for (const name of
|
|
34833
|
-
const abs =
|
|
35917
|
+
for (const name of readdirSync12(d).sort()) {
|
|
35918
|
+
const abs = join44(d, name);
|
|
34834
35919
|
if (statSync8(abs).isDirectory())
|
|
34835
35920
|
walk(abs, `${rel}${name}/`);
|
|
34836
35921
|
else if (name.endsWith(".md"))
|
|
34837
35922
|
out.push(`### specs/${rel}${name}
|
|
34838
|
-
${
|
|
35923
|
+
${readFileSync25(abs, "utf8").trim()}`);
|
|
34839
35924
|
}
|
|
34840
35925
|
};
|
|
34841
35926
|
walk(dir, "");
|
|
@@ -34861,10 +35946,10 @@ function cappedSpec(p, id) {
|
|
|
34861
35946
|
async function ensureContextPacks(p, id) {
|
|
34862
35947
|
const cap = p.config.tokens.contextPackMaxTokens;
|
|
34863
35948
|
const tasks = p.run(id).tasks.filter((t) => t.kind === "task" && t.status === "pending");
|
|
34864
|
-
const missing = tasks.filter((t) => !
|
|
35949
|
+
const missing = tasks.filter((t) => !existsSync32(packPath(p.root, id, t.id)));
|
|
34865
35950
|
if (missing.length > 0) {
|
|
34866
35951
|
p.out(`▶ context packs (${missing.length})`);
|
|
34867
|
-
|
|
35952
|
+
mkdirSync15(join44(p.root, ".factory", "context", id), { recursive: true });
|
|
34868
35953
|
const res = await p.call({
|
|
34869
35954
|
changeId: id,
|
|
34870
35955
|
agent: "architect",
|
|
@@ -34883,7 +35968,7 @@ async function ensureContextPacks(p, id) {
|
|
|
34883
35968
|
}
|
|
34884
35969
|
for (const t of tasks) {
|
|
34885
35970
|
const file = packPath(p.root, id, t.id);
|
|
34886
|
-
const text =
|
|
35971
|
+
const text = existsSync32(file) ? capPack(readFileSync25(file, "utf8"), cap) : fallbackPack(p, id, t, cap);
|
|
34887
35972
|
writeFileSync12(file, text);
|
|
34888
35973
|
}
|
|
34889
35974
|
}
|
|
@@ -35017,7 +36102,7 @@ class PipelineTaskRunner {
|
|
|
35017
36102
|
await gitOk(["checkout", "-q", "HEAD", "--", v.path], { cwd: dir });
|
|
35018
36103
|
else {
|
|
35019
36104
|
await git(["rm", "-q", "--cached", "--ignore-unmatch", "--", v.path], { cwd: dir });
|
|
35020
|
-
rmSync11(
|
|
36105
|
+
rmSync11(join44(dir, v.path), { force: true, recursive: true });
|
|
35021
36106
|
}
|
|
35022
36107
|
}
|
|
35023
36108
|
return violations.map((v) => `${v.path}: ${v.reason}`);
|
|
@@ -35043,14 +36128,15 @@ class PipelineTaskRunner {
|
|
|
35043
36128
|
t.installed = true;
|
|
35044
36129
|
});
|
|
35045
36130
|
}
|
|
35046
|
-
const pack =
|
|
36131
|
+
const pack = existsSync32(packPath(this.p.root, this.id, task.id)) ? readFileSync25(packPath(this.p.root, this.id, task.id), "utf8") : "";
|
|
35047
36132
|
const gates = gateCommands(this.p, task, dir);
|
|
35048
36133
|
const isTest = task.kind === "test";
|
|
35049
36134
|
const prompt = isTest ? testerPrompt({
|
|
35050
36135
|
task,
|
|
35051
36136
|
spec: cappedSpec(this.p, this.id),
|
|
35052
36137
|
testGlobs: this.#context(task, dir).testGlobs ?? [],
|
|
35053
|
-
gates
|
|
36138
|
+
gates,
|
|
36139
|
+
instructions: instructionPaths(this.p.config, task.repo)
|
|
35054
36140
|
}) : taskPrompt({
|
|
35055
36141
|
task,
|
|
35056
36142
|
pack,
|
|
@@ -35058,9 +36144,10 @@ class PipelineTaskRunner {
|
|
|
35058
36144
|
...ctx.feedback === undefined ? {} : { feedback: ctx.feedback }
|
|
35059
36145
|
});
|
|
35060
36146
|
const headBefore = (await gitOk(["rev-parse", "HEAD"], { cwd: dir })).trim();
|
|
36147
|
+
const routed = agentForTask(this.p.config, task.repo, task.touches);
|
|
35061
36148
|
const res = await this.p.call({
|
|
35062
36149
|
changeId: this.id,
|
|
35063
|
-
agent: isTest ? "tester" :
|
|
36150
|
+
agent: isTest ? "tester" : routed.agent,
|
|
35064
36151
|
role: isTest ? "tester" : "implementer",
|
|
35065
36152
|
modelRole: isTest ? "tester" : "implementer",
|
|
35066
36153
|
context: this.#context(task, dir),
|
|
@@ -35111,7 +36198,7 @@ ${redefined.join(`
|
|
|
35111
36198
|
}
|
|
35112
36199
|
const report = await this.#gates(task, dir);
|
|
35113
36200
|
if (!report.passed) {
|
|
35114
|
-
const output = report.results.filter((r) => r.status !== "pass" && r.status !== "skipped").map((r) => `[${r.type}] ${r.command ?? ""}
|
|
36201
|
+
const output = report.results.filter((r) => r.status !== "pass" && r.status !== "skipped").map((r) => `[${r.name ?? r.type}] ${r.command ?? ""}
|
|
35115
36202
|
${r.output}`).join(`
|
|
35116
36203
|
|
|
35117
36204
|
`);
|
|
@@ -35122,7 +36209,10 @@ ${output}` : output
|
|
|
35122
36209
|
};
|
|
35123
36210
|
}
|
|
35124
36211
|
const type = task.kind === "test" ? "test" : task.kind === "task" ? undefined : "fix";
|
|
35125
|
-
const commit = await commitTask(dir, this.id, task.id, task.title, type
|
|
36212
|
+
const commit = await commitTask(dir, this.id, task.id, task.title, type, {
|
|
36213
|
+
style: this.p.config.commits.style,
|
|
36214
|
+
...this.p.config.commits.scopes ? { scope: task.repo } : {}
|
|
36215
|
+
});
|
|
35126
36216
|
return { status: "passed", ...commit ? { commit } : {} };
|
|
35127
36217
|
}
|
|
35128
36218
|
async merge(task) {
|
|
@@ -35137,7 +36227,7 @@ ${output}` : output
|
|
|
35137
36227
|
}
|
|
35138
36228
|
const branch = taskBranch(this.id, task.id);
|
|
35139
36229
|
const path = worktreePath(this.p.root, this.id, repo.name, task.id);
|
|
35140
|
-
if (!
|
|
36230
|
+
if (!existsSync32(path)) {
|
|
35141
36231
|
const merged = !await refExists(repoDir, `refs/heads/${branch}`) || (await git(["merge-base", "--is-ancestor", branch, changeBranch(this.id)], {
|
|
35142
36232
|
cwd: repoDir
|
|
35143
36233
|
})).code === 0;
|
|
@@ -35162,16 +36252,17 @@ ${output}` : output
|
|
|
35162
36252
|
if (outcome.status === "conflict")
|
|
35163
36253
|
return { status: "conflict", conflicts: outcome.conflicts };
|
|
35164
36254
|
const report = this.#lastRegate.get(task.id);
|
|
35165
|
-
const output = report?.results.filter((r) => r.status !== "pass" && r.status !== "skipped").map((r) => `[${r.type}] ${r.output}`).join(`
|
|
36255
|
+
const output = report?.results.filter((r) => r.status !== "pass" && r.status !== "skipped").map((r) => `[${r.name ?? r.type}] ${r.output}`).join(`
|
|
35166
36256
|
|
|
35167
36257
|
`) ?? "gates failed after the rebase";
|
|
35168
36258
|
return { status: "gates-failed", output };
|
|
35169
36259
|
}
|
|
35170
36260
|
async resolveConflict(task, conflicts) {
|
|
35171
36261
|
const path = worktreePath(this.p.root, this.id, task.repo, task.id);
|
|
36262
|
+
const routed = agentForTask(this.p.config, task.repo, task.touches);
|
|
35172
36263
|
const res = await this.p.call({
|
|
35173
36264
|
changeId: this.id,
|
|
35174
|
-
agent:
|
|
36265
|
+
agent: routed.agent,
|
|
35175
36266
|
role: "implementer",
|
|
35176
36267
|
modelRole: "implementer",
|
|
35177
36268
|
context: { ...this.#context(task, path), touches: ["**"] },
|
|
@@ -35183,8 +36274,8 @@ ${output}` : output
|
|
|
35183
36274
|
if (res.handoff?.status !== "done")
|
|
35184
36275
|
return false;
|
|
35185
36276
|
for (const file of conflicts) {
|
|
35186
|
-
const abs =
|
|
35187
|
-
if (
|
|
36277
|
+
const abs = join44(path, file);
|
|
36278
|
+
if (existsSync32(abs) && /^(<{7}|={7}|>{7})( |$)/m.test(readFileSync25(abs, "utf8")))
|
|
35188
36279
|
return false;
|
|
35189
36280
|
}
|
|
35190
36281
|
return true;
|
|
@@ -35193,7 +36284,7 @@ ${output}` : output
|
|
|
35193
36284
|
const repo = this.p.repo(task.repo);
|
|
35194
36285
|
const repoDir = this.p.repoDir(repo);
|
|
35195
36286
|
const path = worktreePath(this.p.root, this.id, repo.name, task.id);
|
|
35196
|
-
if (
|
|
36287
|
+
if (existsSync32(path)) {
|
|
35197
36288
|
await abortRebase(path);
|
|
35198
36289
|
await gitOk(["worktree", "remove", "--force", path], { cwd: repoDir });
|
|
35199
36290
|
}
|
|
@@ -35257,7 +36348,7 @@ function addFixTask(p, draft, id, task) {
|
|
|
35257
36348
|
});
|
|
35258
36349
|
recordTask(state, "added", "contract-fix");
|
|
35259
36350
|
draft.tasks.push(state);
|
|
35260
|
-
|
|
36351
|
+
mkdirSync16(join45(p.root, ".factory", "context", id), { recursive: true });
|
|
35261
36352
|
writeFileSync13(packPath(p.root, id, task.id), task.pack);
|
|
35262
36353
|
}
|
|
35263
36354
|
async function contractPhase(p, id) {
|
|
@@ -35369,11 +36460,11 @@ ${summarize(r)}
|
|
|
35369
36460
|
}
|
|
35370
36461
|
|
|
35371
36462
|
// src/pipeline/finish.ts
|
|
35372
|
-
import { existsSync as
|
|
35373
|
-
import { join as
|
|
36463
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync17, readdirSync as readdirSync13, readFileSync as readFileSync26, writeFileSync as writeFileSync14 } from "node:fs";
|
|
36464
|
+
import { join as join47 } from "node:path";
|
|
35374
36465
|
|
|
35375
36466
|
// src/git/pointers.ts
|
|
35376
|
-
import { join as
|
|
36467
|
+
import { join as join46 } from "node:path";
|
|
35377
36468
|
var PROVISIONAL_SUBJECT = (id) => `chore(submodules): bump for ${id} [provisional]`;
|
|
35378
36469
|
async function bumpPointers(root, id, repos) {
|
|
35379
36470
|
const paths = repos.map((r) => r.path);
|
|
@@ -35472,7 +36563,7 @@ async function repoint(root, config, id) {
|
|
|
35472
36563
|
for (const pointer of bump.pointers) {
|
|
35473
36564
|
const repo = config.repos.find((r) => r.path === pointer.path);
|
|
35474
36565
|
const name = repo?.name ?? pointer.path;
|
|
35475
|
-
const dir =
|
|
36566
|
+
const dir = join46(root, pointer.path);
|
|
35476
36567
|
const base = { repo: name, path: pointer.path, from: pointer.to };
|
|
35477
36568
|
const current = (await git(["rev-parse", `HEAD:${pointer.path}`], { cwd: root })).stdout.trim();
|
|
35478
36569
|
if (current !== pointer.to) {
|
|
@@ -35558,7 +36649,8 @@ async function review(p, id) {
|
|
|
35558
36649
|
base,
|
|
35559
36650
|
head: changeBranch(id),
|
|
35560
36651
|
spec,
|
|
35561
|
-
profiles: profilesFor(repoLanguages(repo)).map((pr) => pr.title)
|
|
36652
|
+
profiles: profilesFor(repoLanguages(repo)).map((pr) => pr.title),
|
|
36653
|
+
instructions: sessionInstructionPaths(p.config, name, repo.path)
|
|
35562
36654
|
})
|
|
35563
36655
|
});
|
|
35564
36656
|
if (res.handoff === undefined) {
|
|
@@ -35583,7 +36675,7 @@ async function review(p, id) {
|
|
|
35583
36675
|
kind: "review-fix"
|
|
35584
36676
|
});
|
|
35585
36677
|
added++;
|
|
35586
|
-
|
|
36678
|
+
mkdirSync17(join47(p.root, ".factory", "context", id), { recursive: true });
|
|
35587
36679
|
writeFileSync14(packPath(p.root, id, taskId), findingPack(f));
|
|
35588
36680
|
}
|
|
35589
36681
|
addTask(d, {
|
|
@@ -35626,7 +36718,7 @@ async function fullSuite(p, id) {
|
|
|
35626
36718
|
command: r.command
|
|
35627
36719
|
}))
|
|
35628
36720
|
});
|
|
35629
|
-
const output = report.results.filter((r) => r.status !== "pass" && r.status !== "skipped").map((r) => `[${r.type}] ${r.command ?? ""}
|
|
36721
|
+
const output = report.results.filter((r) => r.status !== "pass" && r.status !== "skipped").map((r) => `[${r.name ?? r.type}] ${r.command ?? ""}
|
|
35630
36722
|
${r.output}`).join(`
|
|
35631
36723
|
|
|
35632
36724
|
`);
|
|
@@ -35675,7 +36767,7 @@ function harvest(run) {
|
|
|
35675
36767
|
}
|
|
35676
36768
|
return { risks, questions, findings };
|
|
35677
36769
|
}
|
|
35678
|
-
var reportFile = (root, id, kind) =>
|
|
36770
|
+
var reportFile = (root, id, kind) => join47(root, ".factory", "reports", kind === "final" ? `${id}.md` : `${id}-paused.md`);
|
|
35679
36771
|
function writePauseReport(p, id, summary) {
|
|
35680
36772
|
const run = p.run(id);
|
|
35681
36773
|
const failed = run.tasks.filter((t) => t.status === "failed");
|
|
@@ -35706,18 +36798,18 @@ function writePauseReport(p, id, summary) {
|
|
|
35706
36798
|
...usageReport(summarizeUsage(p.root, id))
|
|
35707
36799
|
];
|
|
35708
36800
|
const file = reportFile(p.root, id, "paused");
|
|
35709
|
-
|
|
36801
|
+
mkdirSync17(join47(p.root, ".factory", "reports"), { recursive: true });
|
|
35710
36802
|
writeFileSync14(file, lines.join(`
|
|
35711
36803
|
`));
|
|
35712
36804
|
return file;
|
|
35713
36805
|
}
|
|
35714
36806
|
function archivedName(root, id) {
|
|
35715
|
-
if (
|
|
36807
|
+
if (existsSync33(join47(root, "openspec", "changes", id)))
|
|
35716
36808
|
return;
|
|
35717
|
-
const archive =
|
|
35718
|
-
if (!
|
|
36809
|
+
const archive = join47(root, "openspec", "changes", "archive");
|
|
36810
|
+
if (!existsSync33(archive))
|
|
35719
36811
|
return;
|
|
35720
|
-
return
|
|
36812
|
+
return readdirSync13(archive).filter((n) => n === id || n.endsWith(`-${id}`)).sort().at(-1);
|
|
35721
36813
|
}
|
|
35722
36814
|
async function close(p, id) {
|
|
35723
36815
|
const run = p.run(id);
|
|
@@ -35725,9 +36817,9 @@ async function close(p, id) {
|
|
|
35725
36817
|
if (open.length > 0)
|
|
35726
36818
|
throw new PreconditionError(`cannot close ${id}: tasks not merged: ${open.map((t) => t.id).join(", ")}`);
|
|
35727
36819
|
p.out("▶ close");
|
|
35728
|
-
const tasksFile =
|
|
35729
|
-
if (
|
|
35730
|
-
let text =
|
|
36820
|
+
const tasksFile = join47(p.root, "openspec", "changes", id, "tasks.md");
|
|
36821
|
+
if (existsSync33(tasksFile)) {
|
|
36822
|
+
let text = readFileSync26(tasksFile, "utf8");
|
|
35731
36823
|
for (const t of run.tasks.filter((x) => x.kind === "task"))
|
|
35732
36824
|
text = setTaskChecked(text, t.id, true);
|
|
35733
36825
|
writeFileSync14(tasksFile, text);
|
|
@@ -35818,7 +36910,7 @@ Archived as \`openspec/changes/archive/${archivedAs}\`.`,
|
|
|
35818
36910
|
...usageReport(summarizeUsage(p.root, id))
|
|
35819
36911
|
];
|
|
35820
36912
|
const file = reportFile(p.root, id, "final");
|
|
35821
|
-
|
|
36913
|
+
mkdirSync17(join47(p.root, ".factory", "reports"), { recursive: true });
|
|
35822
36914
|
writeFileSync14(file, lines.join(`
|
|
35823
36915
|
`));
|
|
35824
36916
|
await p.update(id, (d) => {
|
|
@@ -36009,12 +37101,72 @@ async function step(p, id) {
|
|
|
36009
37101
|
}
|
|
36010
37102
|
}
|
|
36011
37103
|
|
|
37104
|
+
// src/pipeline/refine.ts
|
|
37105
|
+
import { mkdirSync as mkdirSync18, writeFileSync as writeFileSync15 } from "node:fs";
|
|
37106
|
+
import { join as join48 } from "node:path";
|
|
37107
|
+
var QUESTIONS = [
|
|
37108
|
+
["scope", "Which repos, modules or capabilities does this touch?", ""],
|
|
37109
|
+
["out-of-scope", "What is explicitly out of scope?", "nothing stated"],
|
|
37110
|
+
[
|
|
37111
|
+
"verification",
|
|
37112
|
+
"How should this be verified (tests, gates, manual checks)?",
|
|
37113
|
+
"the repository's normal gates"
|
|
37114
|
+
],
|
|
37115
|
+
["constraints", "Any contracts, compatibility or platform constraints?", "none stated"],
|
|
37116
|
+
["extra", "Anything else the spec must know?", "none"]
|
|
37117
|
+
];
|
|
37118
|
+
function requestSlug(text) {
|
|
37119
|
+
const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").split("-").slice(0, 8).join("-");
|
|
37120
|
+
return slug === "" ? "request" : slug;
|
|
37121
|
+
}
|
|
37122
|
+
async function refineRequest(io, request) {
|
|
37123
|
+
if (io.ask === undefined) {
|
|
37124
|
+
throw new PreconditionError("cannot ask questions in this environment", [
|
|
37125
|
+
"run `ai-factory refine` from an interactive terminal, or pass a complete request to `run`"
|
|
37126
|
+
]);
|
|
37127
|
+
}
|
|
37128
|
+
const answers = [];
|
|
37129
|
+
for (const [key, question, initial] of QUESTIONS) {
|
|
37130
|
+
const answer = await io.ask(question, initial);
|
|
37131
|
+
if (answer === undefined) {
|
|
37132
|
+
throw new PreconditionError("refinement was cancelled");
|
|
37133
|
+
}
|
|
37134
|
+
if (answer.trim() !== "" && answer !== initial)
|
|
37135
|
+
answers.push([key, answer.trim()]);
|
|
37136
|
+
}
|
|
37137
|
+
const text = `# Refined request
|
|
37138
|
+
|
|
37139
|
+
## Original
|
|
37140
|
+
|
|
37141
|
+
${request.trim()}
|
|
37142
|
+
|
|
37143
|
+
## Clarifications
|
|
37144
|
+
|
|
37145
|
+
${answers.map(([key, answer]) => `- **${key}**: ${answer}`).join(`
|
|
37146
|
+
`)}
|
|
37147
|
+
|
|
37148
|
+
## Instructions for the architect
|
|
37149
|
+
|
|
37150
|
+
Treat the clarifications as part of the request. If they conflict with the original, the
|
|
37151
|
+
clarification wins. Keep anything not named here out of scope, and put open questions into
|
|
37152
|
+
\`openspec/changes/<id>/questions.md\` rather than guessing.
|
|
37153
|
+
`;
|
|
37154
|
+
const root = await findWorkspaceRoot(io.cwd);
|
|
37155
|
+
if (root === undefined)
|
|
37156
|
+
throw new PreconditionError("not inside a git repository");
|
|
37157
|
+
const dir = join48(root, ".factory", "requests");
|
|
37158
|
+
mkdirSync18(dir, { recursive: true });
|
|
37159
|
+
const path = join48(dir, `${requestSlug(request)}.md`);
|
|
37160
|
+
writeFileSync15(path, text);
|
|
37161
|
+
return { path, text };
|
|
37162
|
+
}
|
|
37163
|
+
|
|
36012
37164
|
// src/cli/commands/pipeline-commands.ts
|
|
36013
37165
|
async function setup(io) {
|
|
36014
37166
|
const root = await findWorkspaceRoot(io.cwd);
|
|
36015
37167
|
if (root === undefined)
|
|
36016
37168
|
throw new PreconditionError("not inside a git repository");
|
|
36017
|
-
if (!
|
|
37169
|
+
if (!existsSync34(join49(root, LOCK_FILE)))
|
|
36018
37170
|
throw new PreconditionError("ai-factory is not installed here", ["run `ai-factory init`"]);
|
|
36019
37171
|
const { config } = loadConfig(root);
|
|
36020
37172
|
pruneLogs(root, config.logs.retentionDays);
|
|
@@ -36062,18 +37214,35 @@ function changeIdArg(command, positionals) {
|
|
|
36062
37214
|
var runCommand = {
|
|
36063
37215
|
name: "run",
|
|
36064
37216
|
summary: "Start a change from a request and run it up to spec approval",
|
|
36065
|
-
help: `Usage: ai-factory run "<request>"
|
|
37217
|
+
help: `Usage: ai-factory run "<request>" [--request <file>] [--interactive]
|
|
36066
37218
|
|
|
36067
37219
|
Intake (change id and summary), a change branch in the AI repo, explorers per likely repo,
|
|
36068
37220
|
then the architect writes the OpenSpec change. Stops at spec approval: review
|
|
36069
37221
|
openspec/changes/<id>/ and run \`ai-factory approve <id>\`. If another change is active, the
|
|
36070
|
-
new one is drafted in .factory/drafts/<id> and queued after approval
|
|
37222
|
+
new one is drafted in .factory/drafts/<id> and queued after approval.
|
|
37223
|
+
|
|
37224
|
+
--request <file> Read the request from a file (e.g. one written by \`ai-factory refine\`)
|
|
37225
|
+
--interactive Ask scope/verification questions before intake`,
|
|
36071
37226
|
async run(args, io) {
|
|
36072
|
-
const { positionals } = parseCommandArgs("run", args, {
|
|
36073
|
-
|
|
37227
|
+
const { values, positionals } = parseCommandArgs("run", args, {
|
|
37228
|
+
request: { type: "string" },
|
|
37229
|
+
interactive: { type: "boolean", default: false }
|
|
37230
|
+
}, true);
|
|
37231
|
+
let request = positionals.join(" ").trim();
|
|
37232
|
+
if (values.request !== undefined) {
|
|
37233
|
+
request = readFileSync27(resolve5(io.cwd, values.request), "utf8").trim();
|
|
37234
|
+
}
|
|
36074
37235
|
if (request === "")
|
|
36075
37236
|
throw new UsageError('run: expected a request, e.g. ai-factory run "add a health endpoint"');
|
|
36076
37237
|
return withPipeline(io, "intake", async (p) => {
|
|
37238
|
+
if (values.interactive) {
|
|
37239
|
+
if (!p.config.workflow.refine) {
|
|
37240
|
+
throw new PreconditionError("workflow.refine is disabled");
|
|
37241
|
+
}
|
|
37242
|
+
const refined = await refineRequest(io, request);
|
|
37243
|
+
request = refined.text;
|
|
37244
|
+
io.out(`refined request: ${refined.path}`);
|
|
37245
|
+
}
|
|
36077
37246
|
const id = await intake(p, request);
|
|
36078
37247
|
const result = await design(p, id);
|
|
36079
37248
|
if (!result.ok) {
|
|
@@ -36131,8 +37300,282 @@ kept worktrees, finished ones stay; a queued change starts when the workspace is
|
|
|
36131
37300
|
}
|
|
36132
37301
|
};
|
|
36133
37302
|
|
|
37303
|
+
// src/pipeline/quick.ts
|
|
37304
|
+
import { rmSync as rmSync12 } from "node:fs";
|
|
37305
|
+
import { join as join50 } from "node:path";
|
|
37306
|
+
async function revert(dir, paths) {
|
|
37307
|
+
for (const path of paths) {
|
|
37308
|
+
const inHead = (await git(["cat-file", "-e", `HEAD:${path}`], { cwd: dir })).code === 0;
|
|
37309
|
+
if (inHead)
|
|
37310
|
+
await gitOk(["checkout", "-q", "HEAD", "--", path], { cwd: dir });
|
|
37311
|
+
else {
|
|
37312
|
+
await git(["rm", "-q", "--cached", "--ignore-unmatch", "--", path], { cwd: dir });
|
|
37313
|
+
rmSync12(join50(dir, path), { force: true, recursive: true });
|
|
37314
|
+
}
|
|
37315
|
+
}
|
|
37316
|
+
}
|
|
37317
|
+
function gateOutput(report) {
|
|
37318
|
+
return report.results.filter((r) => r.status !== "pass" && r.status !== "skipped").map((r) => `[${r.name ?? r.type}] ${r.command ?? ""}
|
|
37319
|
+
${r.output}`).join(`
|
|
37320
|
+
|
|
37321
|
+
`);
|
|
37322
|
+
}
|
|
37323
|
+
async function quickTask(p, input) {
|
|
37324
|
+
if (!p.config.workflow.quick.enabled) {
|
|
37325
|
+
throw new PreconditionError("workflow.quick.enabled is false");
|
|
37326
|
+
}
|
|
37327
|
+
const code = p.codeRepos();
|
|
37328
|
+
const repo = input.repo === undefined ? code.length === 1 ? code[0] : undefined : code.find((r) => r.name === input.repo);
|
|
37329
|
+
if (repo === undefined) {
|
|
37330
|
+
throw new PreconditionError(input.repo === undefined ? "quick: this workspace has several code repos; pass --repo <name>" : `quick: unknown repo "${input.repo}"`, code.map((r) => ` ${r.name} (${r.path})`));
|
|
37331
|
+
}
|
|
37332
|
+
const dir = p.repoDir(repo);
|
|
37333
|
+
await ensureClean(dir, repo.name);
|
|
37334
|
+
const slug = requestSlug(input.task);
|
|
37335
|
+
const branch = `${p.config.workflow.quick.branchPrefix}${slug}`;
|
|
37336
|
+
if (await currentBranch(dir) === branch) {
|
|
37337
|
+
throw new PreconditionError(`already on ${branch}`);
|
|
37338
|
+
}
|
|
37339
|
+
if ((await git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], { cwd: dir })).code === 0) {
|
|
37340
|
+
throw new PreconditionError(`branch ${branch} already exists`);
|
|
37341
|
+
}
|
|
37342
|
+
const base = (await gitOk(["rev-parse", "HEAD"], { cwd: dir })).trim();
|
|
37343
|
+
await gitOk(["switch", "-q", "-c", branch], { cwd: dir });
|
|
37344
|
+
const installed = await installDeps(p.root, dir, repo);
|
|
37345
|
+
const installFailure = installed.find((s) => s.status === "failed");
|
|
37346
|
+
if (installFailure !== undefined) {
|
|
37347
|
+
return {
|
|
37348
|
+
repo: repo.name,
|
|
37349
|
+
branch,
|
|
37350
|
+
ok: false,
|
|
37351
|
+
detail: `install failed: ${installFailure.detail ?? installFailure.dir}`
|
|
37352
|
+
};
|
|
37353
|
+
}
|
|
37354
|
+
const explicit = input.agent === undefined ? undefined : p.config.agents.find((a) => a.id === input.agent);
|
|
37355
|
+
if (input.agent !== undefined && explicit === undefined) {
|
|
37356
|
+
throw new PreconditionError(`quick: unknown agent "${input.agent}"`);
|
|
37357
|
+
}
|
|
37358
|
+
if (explicit !== undefined && explicit.phase !== "implementer") {
|
|
37359
|
+
throw new PreconditionError(`quick: agent "${explicit.id}" is not an implementer`);
|
|
37360
|
+
}
|
|
37361
|
+
const routed = explicit === undefined ? { agent: `implementer-${repo.name}`, role: "implementer", custom: false } : { agent: explicit.id, role: "implementer", custom: true };
|
|
37362
|
+
const context = {
|
|
37363
|
+
directory: dir,
|
|
37364
|
+
workspace: p.root,
|
|
37365
|
+
repo: repo.name,
|
|
37366
|
+
task: "quick",
|
|
37367
|
+
touches: ["**"]
|
|
37368
|
+
};
|
|
37369
|
+
let feedback;
|
|
37370
|
+
let passed = false;
|
|
37371
|
+
let output = "";
|
|
37372
|
+
for (let attempt = 1;attempt <= 3 && !passed; attempt++) {
|
|
37373
|
+
const res = await p.call({
|
|
37374
|
+
changeId: `quick-${slug}`,
|
|
37375
|
+
agent: routed.agent,
|
|
37376
|
+
role: "implementer",
|
|
37377
|
+
modelRole: "implementer",
|
|
37378
|
+
context,
|
|
37379
|
+
title: `quick ${slug} attempt ${attempt}`,
|
|
37380
|
+
step: "quick",
|
|
37381
|
+
kind: "attempt",
|
|
37382
|
+
attempt,
|
|
37383
|
+
prompt: feedback === undefined ? quickPrompt({ task: input.task, repo }) : `${quickPrompt({ task: input.task, repo })}
|
|
37384
|
+
|
|
37385
|
+
Previous attempt failed:
|
|
37386
|
+
${feedback}`
|
|
37387
|
+
});
|
|
37388
|
+
if (res.handoff === undefined || res.handoff.status !== "done") {
|
|
37389
|
+
return {
|
|
37390
|
+
repo: repo.name,
|
|
37391
|
+
branch,
|
|
37392
|
+
ok: false,
|
|
37393
|
+
detail: res.handoff?.openQuestions.join("; ") ?? res.problem ?? "agent did not finish"
|
|
37394
|
+
};
|
|
37395
|
+
}
|
|
37396
|
+
const changed = await changedFiles(dir, base);
|
|
37397
|
+
const violations = auditChanges({
|
|
37398
|
+
sessionId: "quick-audit",
|
|
37399
|
+
changeId: `quick-${slug}`,
|
|
37400
|
+
role: "implementer",
|
|
37401
|
+
...context
|
|
37402
|
+
}, changed, true);
|
|
37403
|
+
if (violations.length > 0) {
|
|
37404
|
+
await revert(dir, violations.map((v) => v.path));
|
|
37405
|
+
return {
|
|
37406
|
+
repo: repo.name,
|
|
37407
|
+
branch,
|
|
37408
|
+
ok: false,
|
|
37409
|
+
detail: `edits outside the quick scope were reverted:
|
|
37410
|
+
${violations.map((v) => `${v.path}: ${v.reason}`).join(`
|
|
37411
|
+
`)}`
|
|
37412
|
+
};
|
|
37413
|
+
}
|
|
37414
|
+
const report = await runGates({
|
|
37415
|
+
root: p.root,
|
|
37416
|
+
config: p.config,
|
|
37417
|
+
repo,
|
|
37418
|
+
worktree: dir,
|
|
37419
|
+
mode: "task",
|
|
37420
|
+
base
|
|
37421
|
+
});
|
|
37422
|
+
if (report.passed) {
|
|
37423
|
+
passed = true;
|
|
37424
|
+
break;
|
|
37425
|
+
}
|
|
37426
|
+
output = gateOutput(report);
|
|
37427
|
+
feedback = output;
|
|
37428
|
+
}
|
|
37429
|
+
if (!passed) {
|
|
37430
|
+
return { repo: repo.name, branch, ok: false, detail: `gates failed:
|
|
37431
|
+
${output}` };
|
|
37432
|
+
}
|
|
37433
|
+
if (input.review) {
|
|
37434
|
+
let findings = [];
|
|
37435
|
+
for (let round = 1;round <= 3; round++) {
|
|
37436
|
+
const review = await p.call({
|
|
37437
|
+
changeId: `quick-${slug}`,
|
|
37438
|
+
agent: "reviewer",
|
|
37439
|
+
role: "reviewer",
|
|
37440
|
+
modelRole: "reviewer",
|
|
37441
|
+
context: { directory: dir, workspace: p.root, repo: repo.name },
|
|
37442
|
+
title: `quick ${slug} review ${round}`,
|
|
37443
|
+
step: "quick-review",
|
|
37444
|
+
kind: "step",
|
|
37445
|
+
prompt: quickReviewPrompt({ task: input.task, repo })
|
|
37446
|
+
});
|
|
37447
|
+
if (review.handoff === undefined) {
|
|
37448
|
+
return {
|
|
37449
|
+
repo: repo.name,
|
|
37450
|
+
branch,
|
|
37451
|
+
ok: false,
|
|
37452
|
+
detail: review.problem ?? "review produced no handoff"
|
|
37453
|
+
};
|
|
37454
|
+
}
|
|
37455
|
+
findings = (review.handoff.findings ?? []).filter((f) => f.severity !== "nice").map((f) => `- [${f.severity}] ${f.file ?? ""}${f.line ? `:${f.line}` : ""} ${f.problem}
|
|
37456
|
+
fix: ${f.fix}`);
|
|
37457
|
+
if (findings.length === 0)
|
|
37458
|
+
break;
|
|
37459
|
+
const fix = await p.call({
|
|
37460
|
+
changeId: `quick-${slug}`,
|
|
37461
|
+
agent: routed.agent,
|
|
37462
|
+
role: "implementer",
|
|
37463
|
+
modelRole: "implementer",
|
|
37464
|
+
context,
|
|
37465
|
+
title: `quick ${slug} fix ${round}`,
|
|
37466
|
+
step: "quick",
|
|
37467
|
+
kind: "attempt",
|
|
37468
|
+
attempt: 4 + round,
|
|
37469
|
+
prompt: `Fix these review findings for the quick task:
|
|
37470
|
+
${findings.join(`
|
|
37471
|
+
`)}
|
|
37472
|
+
|
|
37473
|
+
${quickPrompt({ task: input.task, repo })}`
|
|
37474
|
+
});
|
|
37475
|
+
if (fix.handoff?.status !== "done") {
|
|
37476
|
+
return { repo: repo.name, branch, ok: false, detail: "review fix did not finish" };
|
|
37477
|
+
}
|
|
37478
|
+
const report = await runGates({
|
|
37479
|
+
root: p.root,
|
|
37480
|
+
config: p.config,
|
|
37481
|
+
repo,
|
|
37482
|
+
worktree: dir,
|
|
37483
|
+
mode: "task",
|
|
37484
|
+
base
|
|
37485
|
+
});
|
|
37486
|
+
if (!report.passed) {
|
|
37487
|
+
return {
|
|
37488
|
+
repo: repo.name,
|
|
37489
|
+
branch,
|
|
37490
|
+
ok: false,
|
|
37491
|
+
detail: `gates failed after review fix:
|
|
37492
|
+
${gateOutput(report)}`
|
|
37493
|
+
};
|
|
37494
|
+
}
|
|
37495
|
+
if (round === 3) {
|
|
37496
|
+
return {
|
|
37497
|
+
repo: repo.name,
|
|
37498
|
+
branch,
|
|
37499
|
+
ok: false,
|
|
37500
|
+
detail: `review findings remain:
|
|
37501
|
+
${findings.join(`
|
|
37502
|
+
`)}`
|
|
37503
|
+
};
|
|
37504
|
+
}
|
|
37505
|
+
}
|
|
37506
|
+
}
|
|
37507
|
+
const subject = conventionalSubject(input.task, undefined, {
|
|
37508
|
+
style: p.config.commits.style,
|
|
37509
|
+
...p.config.commits.scopes ? { scope: repo.name } : {}
|
|
37510
|
+
});
|
|
37511
|
+
const commit = await commitAll(dir, subject, { "Change-Id": `quick-${slug}` });
|
|
37512
|
+
return {
|
|
37513
|
+
repo: repo.name,
|
|
37514
|
+
branch,
|
|
37515
|
+
...commit === undefined ? {} : { commit },
|
|
37516
|
+
ok: true,
|
|
37517
|
+
detail: commit === undefined ? "nothing to commit" : `committed ${commit.slice(0, 12)}`
|
|
37518
|
+
};
|
|
37519
|
+
}
|
|
37520
|
+
|
|
37521
|
+
// src/cli/commands/quick.ts
|
|
37522
|
+
var HELP5 = `Usage: ai-factory quick "<task>" [--repo <name>] [--agent <id>] [--no-review]
|
|
37523
|
+
|
|
37524
|
+
The no-OpenSpec path: create a task/<slug> branch, implement, run the repository gates, run
|
|
37525
|
+
one review pass, then make one local commit. No spec, no approval hash, and no push.
|
|
37526
|
+
--agent selects a configured custom implementer. The branch stays checked out so you can
|
|
37527
|
+
inspect or amend it.`;
|
|
37528
|
+
var quickCommand = {
|
|
37529
|
+
name: "quick",
|
|
37530
|
+
summary: "Implement a small task on a branch without an OpenSpec change",
|
|
37531
|
+
help: HELP5,
|
|
37532
|
+
async run(args, io) {
|
|
37533
|
+
const { values, positionals } = parseCommandArgs("quick", args, {
|
|
37534
|
+
repo: { type: "string" },
|
|
37535
|
+
agent: { type: "string" },
|
|
37536
|
+
"no-review": { type: "boolean", default: false }
|
|
37537
|
+
}, true);
|
|
37538
|
+
const task = positionals.join(" ").trim();
|
|
37539
|
+
if (task === "")
|
|
37540
|
+
throw new UsageError('quick: expected a task, e.g. "fix the login typo"');
|
|
37541
|
+
return withPipeline(io, "quick", async (p) => {
|
|
37542
|
+
const result = await quickTask(p, {
|
|
37543
|
+
task,
|
|
37544
|
+
...values.repo === undefined ? {} : { repo: values.repo },
|
|
37545
|
+
...values.agent === undefined ? {} : { agent: values.agent },
|
|
37546
|
+
review: !values["no-review"]
|
|
37547
|
+
});
|
|
37548
|
+
io.out(`${result.ok ? "✓" : "✗"} ${result.repo} ${result.branch}: ${result.detail}`);
|
|
37549
|
+
return result.ok ? ExitCode.Ok : ExitCode.GateFailed;
|
|
37550
|
+
});
|
|
37551
|
+
}
|
|
37552
|
+
};
|
|
37553
|
+
|
|
37554
|
+
// src/cli/commands/refine.ts
|
|
37555
|
+
var HELP6 = `Usage: ai-factory refine "<request>"
|
|
37556
|
+
|
|
37557
|
+
Ask a few scope, verification and constraint questions, then write a refined request to
|
|
37558
|
+
.factory/requests/<slug>.md. Nothing is designed or implemented. Feed it to the pipeline
|
|
37559
|
+
with \`ai-factory run --request .factory/requests/<slug>.md\`, or use \`run --interactive\`
|
|
37560
|
+
to refine in the same command.`;
|
|
37561
|
+
var refineCommand = {
|
|
37562
|
+
name: "refine",
|
|
37563
|
+
summary: "Clarify a vague request before it becomes a spec",
|
|
37564
|
+
help: HELP6,
|
|
37565
|
+
async run(args, io) {
|
|
37566
|
+
const { positionals } = parseCommandArgs("refine", args, {}, true);
|
|
37567
|
+
const request = positionals.join(" ").trim();
|
|
37568
|
+
if (request === "")
|
|
37569
|
+
throw new UsageError('refine: expected a request, e.g. "add search"');
|
|
37570
|
+
const { path } = await refineRequest(io, request);
|
|
37571
|
+
io.out(`refined request: ${path}`);
|
|
37572
|
+
io.out(`next: ai-factory run --request ${path}`);
|
|
37573
|
+
return ExitCode.Ok;
|
|
37574
|
+
}
|
|
37575
|
+
};
|
|
37576
|
+
|
|
36134
37577
|
// src/cli/commands/repoint.ts
|
|
36135
|
-
var
|
|
37578
|
+
var HELP7 = `Usage: ai-factory repoint <change-id> [--json]
|
|
36136
37579
|
|
|
36137
37580
|
After the submodule MRs of a change are merged upstream: fetches each submodule, finds the
|
|
36138
37581
|
merged commit (fast-forward, merge commit, Change-Id trailer, or the squash commit with the
|
|
@@ -36143,7 +37586,7 @@ Exits ${ExitCode.Precondition} when any pointer is still provisional.`;
|
|
|
36143
37586
|
var repointCommand = {
|
|
36144
37587
|
name: "repoint",
|
|
36145
37588
|
summary: "Replace provisional submodule pointers after the MRs merged",
|
|
36146
|
-
help:
|
|
37589
|
+
help: HELP7,
|
|
36147
37590
|
async run(args, io) {
|
|
36148
37591
|
const { values, positionals } = parseCommandArgs("repoint", args, { json: { type: "boolean", default: false } }, true);
|
|
36149
37592
|
const id = positionals[0];
|
|
@@ -36172,8 +37615,8 @@ var repointCommand = {
|
|
|
36172
37615
|
};
|
|
36173
37616
|
|
|
36174
37617
|
// src/cli/commands/state-commands.ts
|
|
36175
|
-
import { existsSync as
|
|
36176
|
-
import { join as
|
|
37618
|
+
import { existsSync as existsSync35, readdirSync as readdirSync14 } from "node:fs";
|
|
37619
|
+
import { join as join51 } from "node:path";
|
|
36177
37620
|
async function workspace(io) {
|
|
36178
37621
|
const root = await findWorkspaceRoot(io.cwd);
|
|
36179
37622
|
if (root === undefined)
|
|
@@ -36222,9 +37665,9 @@ function taskDetail(run, task) {
|
|
|
36222
37665
|
}
|
|
36223
37666
|
function worktreesOf(root, id) {
|
|
36224
37667
|
const dir = factoryDir(root, "worktrees", id);
|
|
36225
|
-
if (!
|
|
37668
|
+
if (!existsSync35(dir))
|
|
36226
37669
|
return [];
|
|
36227
|
-
return
|
|
37670
|
+
return readdirSync14(dir).filter((repo) => repo !== ".opencode").flatMap((repo) => readdirSync14(join51(dir, repo)).map((task) => `.factory/worktrees/${id}/${repo}/${task}`));
|
|
36228
37671
|
}
|
|
36229
37672
|
function printRun(io, root, run, active, queuePos) {
|
|
36230
37673
|
const role = active?.changeId === run.id ? `active, ${driverAlive(active) ? `driven by pid ${active.pid}` : "no process driving it (resume to continue)"}` : queuePos >= 0 ? `queued #${queuePos + 1}` : "not active";
|
|
@@ -36382,20 +37825,20 @@ that were cancelled because of it. Continue the change with \`ai-factory resume
|
|
|
36382
37825
|
};
|
|
36383
37826
|
|
|
36384
37827
|
// src/cli/commands/update.ts
|
|
36385
|
-
import { rmSync as
|
|
36386
|
-
import { join as
|
|
37828
|
+
import { rmSync as rmSync14, writeFileSync as writeFileSync17 } from "node:fs";
|
|
37829
|
+
import { join as join53 } from "node:path";
|
|
36387
37830
|
|
|
36388
37831
|
// src/update/update.ts
|
|
36389
37832
|
import {
|
|
36390
37833
|
cpSync as cpSync2,
|
|
36391
|
-
existsSync as
|
|
36392
|
-
mkdirSync as
|
|
36393
|
-
readdirSync as
|
|
36394
|
-
readFileSync as
|
|
36395
|
-
rmSync as
|
|
36396
|
-
writeFileSync as
|
|
37834
|
+
existsSync as existsSync36,
|
|
37835
|
+
mkdirSync as mkdirSync19,
|
|
37836
|
+
readdirSync as readdirSync15,
|
|
37837
|
+
readFileSync as readFileSync28,
|
|
37838
|
+
rmSync as rmSync13,
|
|
37839
|
+
writeFileSync as writeFileSync16
|
|
36397
37840
|
} from "node:fs";
|
|
36398
|
-
import { dirname as
|
|
37841
|
+
import { dirname as dirname10, join as join52 } from "node:path";
|
|
36399
37842
|
|
|
36400
37843
|
// src/migrations/001-graph-exclude-defaults.ts
|
|
36401
37844
|
var V1_GRAPH_EXCLUDE = [
|
|
@@ -36421,8 +37864,19 @@ var graphExcludeDefaults = {
|
|
|
36421
37864
|
}
|
|
36422
37865
|
};
|
|
36423
37866
|
|
|
37867
|
+
// src/migrations/002-config-v3.ts
|
|
37868
|
+
var configV3 = {
|
|
37869
|
+
from: 2,
|
|
37870
|
+
to: 3,
|
|
37871
|
+
name: "config-v3",
|
|
37872
|
+
migrate(text) {
|
|
37873
|
+
parseJsonc(text);
|
|
37874
|
+
return setJsonc(text, ["schemaVersion"], 3);
|
|
37875
|
+
}
|
|
37876
|
+
};
|
|
37877
|
+
|
|
36424
37878
|
// src/migrations/index.ts
|
|
36425
|
-
var MIGRATIONS = [graphExcludeDefaults];
|
|
37879
|
+
var MIGRATIONS = [graphExcludeDefaults, configV3];
|
|
36426
37880
|
function schemaVersionOf(text) {
|
|
36427
37881
|
const { value } = parseJsonc(text);
|
|
36428
37882
|
const v = value?.schemaVersion;
|
|
@@ -36484,7 +37938,7 @@ function changesInProgress(root) {
|
|
|
36484
37938
|
return [...ids].sort();
|
|
36485
37939
|
}
|
|
36486
37940
|
async function planUpdate(root, lock, options) {
|
|
36487
|
-
const original =
|
|
37941
|
+
const original = readFileSync28(join52(root, CONFIG_FILE), "utf8");
|
|
36488
37942
|
const migration = migrateConfigText(original);
|
|
36489
37943
|
const config = parseConfigText(migration.text, CONFIG_FILE);
|
|
36490
37944
|
const plan = planWorkspace(root, config, {
|
|
@@ -36544,20 +37998,20 @@ function formatPlan(plan) {
|
|
|
36544
37998
|
var backupsDir = (root) => factoryDir(root, "backups");
|
|
36545
37999
|
function backup(root, plan) {
|
|
36546
38000
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
36547
|
-
const dir =
|
|
38001
|
+
const dir = join52(backupsDir(root), stamp);
|
|
36548
38002
|
const files = [];
|
|
36549
38003
|
const created = [];
|
|
36550
38004
|
for (const rel of plan.touched) {
|
|
36551
|
-
const abs =
|
|
36552
|
-
if (!
|
|
38005
|
+
const abs = join52(root, rel);
|
|
38006
|
+
if (!existsSync36(abs)) {
|
|
36553
38007
|
created.push(rel);
|
|
36554
38008
|
continue;
|
|
36555
38009
|
}
|
|
36556
|
-
|
|
36557
|
-
cpSync2(abs,
|
|
38010
|
+
mkdirSync19(dirname10(join52(dir, "files", rel)), { recursive: true });
|
|
38011
|
+
cpSync2(abs, join52(dir, "files", rel), { recursive: true });
|
|
36558
38012
|
files.push(rel);
|
|
36559
38013
|
}
|
|
36560
|
-
writeJsonAtomic(
|
|
38014
|
+
writeJsonAtomic(join52(dir, "manifest.json"), {
|
|
36561
38015
|
at: new Date().toISOString(),
|
|
36562
38016
|
fromVersion: plan.fromVersion,
|
|
36563
38017
|
toVersion: plan.toVersion,
|
|
@@ -36568,28 +38022,28 @@ function backup(root, plan) {
|
|
|
36568
38022
|
}
|
|
36569
38023
|
function latestBackup(root) {
|
|
36570
38024
|
const base = backupsDir(root);
|
|
36571
|
-
if (!
|
|
38025
|
+
if (!existsSync36(base))
|
|
36572
38026
|
return;
|
|
36573
|
-
for (const name of
|
|
36574
|
-
const file =
|
|
36575
|
-
if (!
|
|
38027
|
+
for (const name of readdirSync15(base).sort().reverse()) {
|
|
38028
|
+
const file = join52(base, name, "manifest.json");
|
|
38029
|
+
if (!existsSync36(file))
|
|
36576
38030
|
continue;
|
|
36577
|
-
const manifest = JSON.parse(
|
|
38031
|
+
const manifest = JSON.parse(readFileSync28(file, "utf8"));
|
|
36578
38032
|
if (!manifest.restored)
|
|
36579
|
-
return { dir:
|
|
38033
|
+
return { dir: join52(base, name), manifest };
|
|
36580
38034
|
}
|
|
36581
38035
|
return;
|
|
36582
38036
|
}
|
|
36583
38037
|
async function restoreBackup(root, dir, manifest) {
|
|
36584
38038
|
for (const rel of manifest.created)
|
|
36585
|
-
|
|
38039
|
+
rmSync13(join52(root, rel), { recursive: true, force: true });
|
|
36586
38040
|
for (const rel of manifest.files) {
|
|
36587
|
-
const target =
|
|
36588
|
-
|
|
36589
|
-
|
|
36590
|
-
cpSync2(
|
|
38041
|
+
const target = join52(root, rel);
|
|
38042
|
+
rmSync13(target, { recursive: true, force: true });
|
|
38043
|
+
mkdirSync19(dirname10(target), { recursive: true });
|
|
38044
|
+
cpSync2(join52(dir, "files", rel), target, { recursive: true });
|
|
36591
38045
|
}
|
|
36592
|
-
|
|
38046
|
+
writeFileSync16(join52(dir, "manifest.json"), `${JSON.stringify({ ...manifest, restored: true }, null, 2)}
|
|
36593
38047
|
`);
|
|
36594
38048
|
return [...manifest.files, ...manifest.created];
|
|
36595
38049
|
}
|
|
@@ -36683,7 +38137,7 @@ Upgrades this workspace to the installed factory version, as one local commit
|
|
|
36683
38137
|
if (!t.ok)
|
|
36684
38138
|
io.out(`warning: ${t.tool}: ${t.problem} → ${t.fix}`);
|
|
36685
38139
|
const backupDir = backup(root, plan);
|
|
36686
|
-
|
|
38140
|
+
writeFileSync17(join53(root, CONFIG_FILE), plan.configText);
|
|
36687
38141
|
const result = await syncWorkspace(root, plan.config, {
|
|
36688
38142
|
merge: true,
|
|
36689
38143
|
...values.restore ? { restore: true } : {},
|
|
@@ -36698,7 +38152,7 @@ Upgrades this workspace to the installed factory version, as one local commit
|
|
|
36698
38152
|
}
|
|
36699
38153
|
const commit = await commitInstall(root, `chore(factory): update to v${FACTORY_VERSION}`);
|
|
36700
38154
|
if (commit === undefined)
|
|
36701
|
-
|
|
38155
|
+
rmSync14(backupDir, { recursive: true, force: true });
|
|
36702
38156
|
if (plan.graphRebuild) {
|
|
36703
38157
|
const graph = await updateGraph(root, { full: true });
|
|
36704
38158
|
io.out(graph.ok ? "graph: rebuilt" : `graph: rebuild failed (${graph.error ?? ""}); run \`ai-factory graph --full\``);
|
|
@@ -36752,7 +38206,9 @@ var COMMANDS = [
|
|
|
36752
38206
|
rollbackCommand,
|
|
36753
38207
|
doctorCommand,
|
|
36754
38208
|
detectCommand,
|
|
38209
|
+
refineCommand,
|
|
36755
38210
|
runCommand,
|
|
38211
|
+
quickCommand,
|
|
36756
38212
|
approveCommand,
|
|
36757
38213
|
resumeCommand,
|
|
36758
38214
|
statusCommand,
|
|
@@ -36799,6 +38255,16 @@ async function askYesNo(question) {
|
|
|
36799
38255
|
rl.close();
|
|
36800
38256
|
}
|
|
36801
38257
|
}
|
|
38258
|
+
async function askText(question, initial) {
|
|
38259
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
38260
|
+
try {
|
|
38261
|
+
const suffix = initial === undefined || initial === "" ? "" : ` (${initial})`;
|
|
38262
|
+
const answer = (await rl.question(`${question}${suffix}: `)).trim();
|
|
38263
|
+
return answer === "" ? initial : answer;
|
|
38264
|
+
} finally {
|
|
38265
|
+
rl.close();
|
|
38266
|
+
}
|
|
38267
|
+
}
|
|
36802
38268
|
async function runCli(argv, io) {
|
|
36803
38269
|
let cwd = io.cwd;
|
|
36804
38270
|
let i = 0;
|
|
@@ -36810,7 +38276,7 @@ async function runCli(argv, io) {
|
|
|
36810
38276
|
io.err("error: -C needs a directory");
|
|
36811
38277
|
return ExitCode.Usage;
|
|
36812
38278
|
}
|
|
36813
|
-
cwd =
|
|
38279
|
+
cwd = resolve6(cwd, dir);
|
|
36814
38280
|
} else if (arg === "-V" || arg === "--version") {
|
|
36815
38281
|
io.out(FACTORY_VERSION);
|
|
36816
38282
|
return ExitCode.Ok;
|
|
@@ -36852,6 +38318,6 @@ var code = await runCli(process.argv.slice(2), {
|
|
|
36852
38318
|
`),
|
|
36853
38319
|
err: (text) => process.stderr.write(`${text}
|
|
36854
38320
|
`),
|
|
36855
|
-
...process.stdin.isTTY ? { confirm: askYesNo } : {}
|
|
38321
|
+
...process.stdin.isTTY ? { confirm: askYesNo, ask: askText } : {}
|
|
36856
38322
|
});
|
|
36857
38323
|
process.exit(code);
|