@warpgogol/forge 2.8.0 → 2.8.2
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/AGENTS.md +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/os/session/handlers/validate.d.ts.map +1 -1
- package/dist/os/session/handlers/validate.js +23 -1
- package/dist/os/session/handlers/validate.js.map +1 -1
- package/dist/os/session/types.d.ts +31 -1
- package/dist/os/session/types.d.ts.map +1 -1
- package/dist/os/session/types.js +7 -0
- package/dist/os/session/types.js.map +1 -1
- package/os/adr/adr.module.ts +158 -0
- package/os/adr/frontmatter-io.ts +80 -0
- package/os/adr/handlers/archive.ts +244 -0
- package/os/adr/handlers/implement-stamp.ts +331 -0
- package/os/adr/handlers/list-create.ts +207 -0
- package/os/adr/handlers/validate.test.ts +232 -0
- package/os/adr/handlers/validate.ts +429 -0
- package/os/adr/index.ts +33 -0
- package/os/adr/types.ts +141 -0
- package/os/audit/audit.module.ts +48 -0
- package/os/audit/frontmatter-io.ts +84 -0
- package/os/audit/handlers/archive.ts +239 -0
- package/os/audit/index.ts +23 -0
- package/os/audit/types.ts +29 -0
- package/os/compass/compass.module.ts +186 -0
- package/os/compass/handlers/compass-audit-handler.ts +382 -0
- package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
- package/os/compass/handlers/compass-inventory-handler.ts +294 -0
- package/os/compass/handlers/compass-inventory.ts +521 -0
- package/os/compass/handlers/git-revision.ts +137 -0
- package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
- package/os/compass/handlers/resolve-scan-root.ts +79 -0
- package/os/compass/index.ts +33 -0
- package/os/core/core.module.ts +948 -0
- package/os/core/handlers/assets-check.ts +122 -0
- package/os/core/handlers/assets-helpers.ts +174 -0
- package/os/core/handlers/assets-list.ts +99 -0
- package/os/core/handlers/build.ts +155 -0
- package/os/core/handlers/determinism-check.ts +354 -0
- package/os/core/handlers/dev.ts +127 -0
- package/os/core/handlers/invariant-engine.test.ts +681 -0
- package/os/core/handlers/knowledge-compact.ts +248 -0
- package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
- package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
- package/os/core/handlers/note-link-validate.test.ts +117 -0
- package/os/core/handlers/note-orphan-detect.test.ts +94 -0
- package/os/core/handlers/package-health.test.ts +226 -0
- package/os/core/handlers/package-health.ts +231 -0
- package/os/core/handlers/pinned-check.ts +200 -0
- package/os/core/handlers/pinned-init.ts +333 -0
- package/os/core/handlers/pinned-types.ts +50 -0
- package/os/core/handlers/pinned-validate.ts +301 -0
- package/os/core/handlers/profile-resolve.ts +92 -0
- package/os/core/handlers/release-prepare.ts +294 -0
- package/os/core/handlers/release-publish.ts +211 -0
- package/os/core/handlers/validate.ts +262 -0
- package/os/core/handlers/workspace-deps.ts +55 -0
- package/os/core/index.ts +13 -0
- package/os/exploration/exploration.module.ts +90 -0
- package/os/exploration/frontmatter-io.ts +77 -0
- package/os/exploration/handlers/archive.ts +159 -0
- package/os/exploration/handlers/list.ts +77 -0
- package/os/exploration/handlers/show.ts +107 -0
- package/os/exploration/index.ts +25 -0
- package/os/exploration/types.ts +65 -0
- package/os/mission/handlers/archive.test.ts +391 -0
- package/os/mission/handlers/archive.ts +453 -0
- package/os/mission/index.ts +1 -0
- package/os/mission/mission.module.ts +48 -0
- package/os/mission/types.ts +41 -0
- package/os/naming/index.ts +14 -0
- package/os/naming/naming-convention.test.ts +163 -0
- package/os/naming/naming-convention.ts +378 -0
- package/os/naming/naming.module.ts +36 -0
- package/os/notes/index.ts +10 -0
- package/os/notes/notes.module.ts +109 -0
- package/os/plan/frontmatter-io.ts +82 -0
- package/os/plan/handlers/archive.ts +238 -0
- package/os/plan/index.ts +23 -0
- package/os/plan/plan.module.ts +48 -0
- package/os/plan/types.ts +29 -0
- package/os/program/discovery.ts +294 -0
- package/os/program/handlers/complete.ts +419 -0
- package/os/program/handlers/lease.ts +433 -0
- package/os/program/handlers/seal.ts +332 -0
- package/os/program/handlers/validate.ts +213 -0
- package/os/program/lease.ts +156 -0
- package/os/program/program.module.ts +212 -0
- package/os/program/schemas.ts +218 -0
- package/os/program/state.ts +264 -0
- package/os/rfc/acceptance.ts +344 -0
- package/os/rfc/decision-log.ts +310 -0
- package/os/rfc/dna-trace.ts +280 -0
- package/os/rfc/frontmatter-io.test.ts +132 -0
- package/os/rfc/frontmatter-io.ts +130 -0
- package/os/rfc/handlers/archive.ts +300 -0
- package/os/rfc/handlers/check.ts +238 -0
- package/os/rfc/handlers/implement-stamp.ts +515 -0
- package/os/rfc/handlers/index-graph.ts +237 -0
- package/os/rfc/handlers/lifecycle.test.ts +127 -0
- package/os/rfc/handlers/lifecycle.ts +249 -0
- package/os/rfc/handlers/list-create.ts +296 -0
- package/os/rfc/handlers/pipeline-status.ts +173 -0
- package/os/rfc/handlers/shared.ts +121 -0
- package/os/rfc/handlers/supersede-propose.ts +241 -0
- package/os/rfc/handlers/validate-rules.test.ts +891 -0
- package/os/rfc/handlers/validate-rules.ts +984 -0
- package/os/rfc/handlers/validate.ts +166 -0
- package/os/rfc/handlers.ts +19 -0
- package/os/rfc/index.ts +97 -0
- package/os/rfc/rfc.module.ts +416 -0
- package/os/rfc/types.ts +596 -0
- package/os/rfc/verification-evidence.ts +244 -0
- package/os/session/atif-parser.ts +166 -0
- package/os/session/frontmatter-io.ts +163 -0
- package/os/session/handlers/archive.ts +288 -0
- package/os/session/handlers/list.ts +174 -0
- package/os/session/handlers/save.ts +348 -0
- package/os/session/handlers/validate.ts +250 -0
- package/os/session/index.ts +50 -0
- package/os/session/session.module.ts +126 -0
- package/os/session/types.ts +203 -0
- package/os/spec/live-spec-list-show-validate.test.ts +257 -0
- package/os/spec/live-spec-list.ts +78 -0
- package/os/spec/live-spec-merge.test.ts +261 -0
- package/os/spec/live-spec-merge.ts +413 -0
- package/os/spec/live-spec-show.ts +117 -0
- package/os/spec/live-spec-types.ts +93 -0
- package/os/spec/live-spec-validate.ts +170 -0
- package/os/spec/spec-materialize.ts +390 -0
- package/os/spec/spec-schema.ts +174 -0
- package/os/spec/spec-status.ts +284 -0
- package/os/spec/spec-validate.ts +513 -0
- package/os/spec/spec.module.ts +135 -0
- package/os/werkstatt/handlers/lock.ts +168 -0
- package/os/werkstatt/handlers/schema.ts +47 -0
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
- package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
- package/os/werkstatt/index.ts +39 -0
- package/os/werkstatt/werkstatt.module.ts +62 -0
- package/os/workflow/handlers.ts +304 -0
- package/os/workflow/index.ts +28 -0
- package/os/workflow/types.ts +99 -0
- package/os/workflow/workflow.module.ts +54 -0
- package/package.json +3 -3
- package/profiles/godot-csharp.yaml +4 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/skills/_shared/fo-session-summary.md +132 -16
- package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +5 -0
- package/skills/fo/fo-idea/SKILL.md +1 -1
- package/skills/fo/fo-review/SKILL.md +1 -1
- package/skills/fo/fo-session-retro/SKILL.md +8 -0
- package/skills/fo/fo-session-save/SKILL.md +8 -0
- package/src/cli-output.ts +59 -0
- package/src/config/__tests__/resolve-terminology.test.ts +45 -0
- package/src/config/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -0
- package/src/knowledge/__tests__/promote.test.ts +219 -0
- package/src/knowledge/__tests__/serialize.test.ts +192 -0
- package/src/knowledge/budgets.ts +202 -0
- package/src/knowledge/compact.ts +405 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/parse.ts +227 -0
- package/src/knowledge/promote.ts +156 -0
- package/src/knowledge/schema.ts +112 -0
- package/src/knowledge/serialize.ts +72 -0
- package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
- package/src/migration-adapters/__tests__/registry.test.ts +131 -0
- package/src/migration-adapters/__tests__/types.test.ts +86 -0
- package/src/migration-adapters/git-utils.ts +71 -0
- package/src/migration-adapters/ignored-files.ts +221 -0
- package/src/migration-adapters/index.ts +21 -0
- package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
- package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
- package/src/migration-adapters/registry.ts +61 -0
- package/src/migration-adapters/types.ts +68 -0
- package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
- package/src/onboarding/agents-generate.ts +443 -0
- package/src/onboarding/create.ts +427 -0
- package/src/onboarding/doctor.ts +1297 -0
- package/src/onboarding/init.ts +337 -0
- package/src/onboarding/invariant-engine.ts +448 -0
- package/src/onboarding/memory-scaffold.ts +189 -0
- package/src/onboarding/nested-agents-generate.ts +93 -0
- package/src/onboarding/nested-agents-templates.ts +233 -0
- package/src/onboarding/profile-validate.ts +139 -0
- package/src/onboarding/scaffold-project.ts +299 -0
- package/src/onboarding/scaffold.ts +143 -0
- package/src/onboarding/upgrade.ts +558 -0
- package/src/onboarding/workspace-discovery.ts +167 -0
- package/src/profiles/__tests__/profile-schema.test.ts +228 -0
- package/src/profiles/__tests__/stack-profile.test.ts +190 -0
- package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
- package/src/profiles/profile-schema.ts +350 -0
- package/src/profiles/stack-profile.ts +188 -0
- package/src/profiles/terminology-utils.ts +36 -0
- package/src/registry.ts +185 -0
- package/src/skill-schema.ts +37 -0
- package/src/tests/acceptance-criteria.test.ts +99 -0
- package/src/tests/adr-implement-stamp.test.ts +368 -0
- package/src/tests/agents-generate-domain.test.ts +317 -0
- package/src/tests/agents-generate.test.ts +458 -0
- package/src/tests/bindings-schema.test.ts +314 -0
- package/src/tests/budgets.test.ts +213 -0
- package/src/tests/cli-output.test.ts +196 -0
- package/src/tests/compact.test.ts +796 -0
- package/src/tests/create.test.ts +289 -0
- package/src/tests/doctor-autonomy.test.ts +98 -0
- package/src/tests/doctor-bindings.test.ts +286 -0
- package/src/tests/doctor-domain.test.ts +113 -0
- package/src/tests/exploration-handlers.test.ts +256 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
- package/src/tests/forge-config.test.ts +420 -0
- package/src/tests/fs-atomic.test.ts +96 -0
- package/src/tests/fs.test.ts +103 -0
- package/src/tests/generated-marker.test.ts +161 -0
- package/src/tests/hash.test.ts +37 -0
- package/src/tests/implement-stamp.test.ts +505 -0
- package/src/tests/init-bindings.test.ts +183 -0
- package/src/tests/knowledge-parse.test.ts +307 -0
- package/src/tests/knowledge-pbt.test.ts +164 -0
- package/src/tests/memory-scaffold.test.ts +192 -0
- package/src/tests/migration-adapters.test.ts +480 -0
- package/src/tests/package-files.test.ts +54 -0
- package/src/tests/pinned-check.test.ts +194 -0
- package/src/tests/pinned-init.test.ts +126 -0
- package/src/tests/profile-schema.test.ts +194 -0
- package/src/tests/profile-validate.test.ts +120 -0
- package/src/tests/program-lease.test.ts +245 -0
- package/src/tests/program-paths-extended.test.ts +117 -0
- package/src/tests/program-paths.test.ts +98 -0
- package/src/tests/program-property.test.ts +139 -0
- package/src/tests/program-schemas.test.ts +445 -0
- package/src/tests/program-spec-node.test.ts +250 -0
- package/src/tests/promote.test.ts +481 -0
- package/src/tests/registry.test.ts +73 -0
- package/src/tests/scaffold-project.test.ts +150 -0
- package/src/tests/session-handlers.test.ts +503 -0
- package/src/tests/session-pbt.test.ts +190 -0
- package/src/tests/skill-schema.test.ts +146 -0
- package/src/tests/skill-validate-knowledge.test.ts +26 -0
- package/src/tests/skill-validate.test.ts +254 -0
- package/src/tests/stack-profile.test.ts +221 -0
- package/src/tests/string-utils.test.ts +42 -0
- package/src/tests/upgrade.test.ts +428 -0
- package/src/tests/werkstatt-lock.test.ts +343 -0
- package/src/tests/workspace-discovery-domain.test.ts +112 -0
- package/src/tests/workspace-discovery.test.ts +135 -0
- package/src/types.ts +221 -0
- package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
- package/src/utils/__tests__/fs-trash.test.ts +45 -0
- package/src/utils/fs-atomic.ts +93 -0
- package/src/utils/fs-idempotent.ts +41 -0
- package/src/utils/fs-trash-sync.ts +37 -0
- package/src/utils/fs-trash.ts +24 -0
- package/src/utils/fs.ts +74 -0
- package/src/utils/generated-marker.ts +166 -0
- package/src/utils/hash.ts +19 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/string-utils.ts +19 -0
- package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
- package/src/validators/note-frontmatter-validate.ts +142 -0
- package/src/validators/note-link-validate.ts +145 -0
- package/src/validators/note-orphan-detect.ts +146 -0
- package/src/validators/port-validate.ts +97 -0
- package/src/validators/skill-validate.ts +831 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>forge.agents.generate — regenerates AGENTS.md deterministically from forge.yaml + skill registry. Carries the standard generated-file marker.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not overwrite a hand-written AGENTS.md (no generated marker) — refuse with exit 1.</item>
|
|
6
|
+
<item>Do not read or modify forge.yaml — only read it via loadForgeConfig.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0391: initial forge.agents.generate handler.</item>
|
|
11
|
+
<item>RFC-0393: added Capabilities section rendered from resolved bindings.</item>
|
|
12
|
+
<item>RFC-0548: added Core behavioral layer section with intent-to-skill routing table from triggers, fixed policy text, and conditional extended layer (RFC-0549) based on register.</item>
|
|
13
|
+
<item>RFC-0549: replaced extended layer stub with full nine-section content from extended-behavioral-layer.ts.</item>
|
|
14
|
+
<item>RFC-0551: added register-conditional commit policy to core behavioral layer.</item>
|
|
15
|
+
<item>RFC-0611: added nested AGENTS.md generation for workspace directories + dryRun support.</item>
|
|
16
|
+
<item>RFC-0640: load workspaceTypes from stack profile and pass to generateNestedAgentsMd for profile-driven workspace detection.</item>
|
|
17
|
+
<item>RFC-0643: terminology substitution on final content, root template selection by register, details field in result.</item>
|
|
18
|
+
<item>RFC-0664: added project memory layer read discipline section to generated AGENTS.md.</item>
|
|
19
|
+
<item>Profile-driven root AGENTS.md template via rootAgentsMdTemplate field — profiles can now provide stack-specific root templates instead of generic business/creative templates.</item>
|
|
20
|
+
</CHANGE_SUMMARY>
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { parse as parseYaml } from "yaml";
|
|
26
|
+
import { loadForgeConfig, resolveBinding, resolveForgeRoot } from "../config/forge-config.ts";
|
|
27
|
+
import { resolveAllTerminology } from "../profiles/terminology-utils.ts";
|
|
28
|
+
import { FORGE_SKILLS } from "../registry.ts";
|
|
29
|
+
import { buildGeneratedHeader, hasGeneratedMarker, writeFileIfChanged } from "../utils/index.ts";
|
|
30
|
+
import { generateNestedAgentsMd } from "./nested-agents-generate.ts";
|
|
31
|
+
import { listStackProfiles } from "../profiles/stack-profile.ts";
|
|
32
|
+
import type { StackProfile } from "../profiles/stack-profile.ts";
|
|
33
|
+
import type { ProfileWorkspaceType } from "../profiles/profile-schema.ts";
|
|
34
|
+
import type {
|
|
35
|
+
ForgeCommandInput,
|
|
36
|
+
ForgeCommandResult,
|
|
37
|
+
ForgeRuntimeContext,
|
|
38
|
+
} from "../types.ts";
|
|
39
|
+
|
|
40
|
+
type BehavioralRegister = "business" | "creative";
|
|
41
|
+
|
|
42
|
+
// RFC-0643: Template placeholder substitution for {{terminology.key}} patterns
|
|
43
|
+
export function substituteTemplate(
|
|
44
|
+
content: string,
|
|
45
|
+
terminology: Record<string, string>,
|
|
46
|
+
): string {
|
|
47
|
+
return content.replace(
|
|
48
|
+
/\{\{terminology\.(\w+)\}\}/g,
|
|
49
|
+
(_, key: string) => terminology[key] ?? key,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// RFC-0643: Template context for root AGENTS.md generation
|
|
54
|
+
export interface TemplateContext {
|
|
55
|
+
terminology: Record<string, string>;
|
|
56
|
+
register: BehavioralRegister;
|
|
57
|
+
domain: string | null;
|
|
58
|
+
workspaceType?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// RFC-0643: Root template paths
|
|
62
|
+
const BUSINESS_ROOT_TEMPLATE = path.join(import.meta.dirname, "templates", "root-agents-business.md");
|
|
63
|
+
const CREATIVE_ROOT_TEMPLATE = path.join(import.meta.dirname, "templates", "root-agents-creative.md");
|
|
64
|
+
|
|
65
|
+
// Behavioral layer template paths
|
|
66
|
+
const BEHAVIORAL_LAYER_CORE_TEMPLATE = path.join(import.meta.dirname, "templates", "behavioral-layer-core.md");
|
|
67
|
+
const BEHAVIORAL_LAYER_EXTENDED_TEMPLATE = path.join(import.meta.dirname, "templates", "behavioral-layer-extended.md");
|
|
68
|
+
|
|
69
|
+
export function selectRootTemplate(
|
|
70
|
+
register: BehavioralRegister,
|
|
71
|
+
profile?: StackProfile,
|
|
72
|
+
): string {
|
|
73
|
+
// Profile-driven root template (rootAgentsMdTemplate)
|
|
74
|
+
if (profile?.rootAgentsMdTemplate) {
|
|
75
|
+
const templateRel = profile.rootAgentsMdTemplate;
|
|
76
|
+
// Path traversal guard: reject absolute paths and ..
|
|
77
|
+
if (!path.isAbsolute(templateRel) && !templateRel.includes("..")) {
|
|
78
|
+
const forgeRoot = path.resolve(import.meta.dirname, "..", "..");
|
|
79
|
+
const profilesDir = path.join(forgeRoot, "profiles");
|
|
80
|
+
const templatePath = path.resolve(profilesDir, templateRel);
|
|
81
|
+
// Ensure resolved path is within profiles directory
|
|
82
|
+
if (templatePath.startsWith(profilesDir + path.sep) || templatePath === profilesDir) {
|
|
83
|
+
try {
|
|
84
|
+
return fs.readFileSync(templatePath, "utf8");
|
|
85
|
+
} catch {
|
|
86
|
+
// Template file not found — fall through to register-based template
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const templatePath = register === "creative" ? CREATIVE_ROOT_TEMPLATE : BUSINESS_ROOT_TEMPLATE;
|
|
93
|
+
return fs.readFileSync(templatePath, "utf8");
|
|
94
|
+
} catch {
|
|
95
|
+
// Template file not found — fall back to empty string (dynamic sections will still be appended)
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function replaceProjectPlaceholders(
|
|
101
|
+
template: string,
|
|
102
|
+
config: ReturnType<typeof loadForgeConfig>,
|
|
103
|
+
dynamicSections: string,
|
|
104
|
+
): string {
|
|
105
|
+
let result = template;
|
|
106
|
+
result = result.replace(/\{\{projectName\}\}/g, config.project.name);
|
|
107
|
+
result = result.replace(/\{\{projectStack\}\}/g, config.project.stack.length > 0 ? config.project.stack.join(", ") : "(none)");
|
|
108
|
+
result = result.replace(/\{\{projectPm\}\}/g, config.project.packageManager);
|
|
109
|
+
result = result.replace(/\{\{rfcsDir\}\}/g, config.paths.rfcsDir);
|
|
110
|
+
result = result.replace(/\{\{adrsDir\}\}/g, config.paths.adrsDir);
|
|
111
|
+
result = result.replace(/\{\{plansDir\}\}/g, config.paths.plansDir);
|
|
112
|
+
result = result.replace(/\{\{auditsDir\}\}/g, config.paths.auditsDir);
|
|
113
|
+
result = result.replace(/\{\{specsDir\}\}/g, config.paths.specsDir);
|
|
114
|
+
result = result.replace(/\{\{skillsDir\}\}/g, config.paths.skillsDir);
|
|
115
|
+
result = result.replace(/\{\{dynamicSections\}\}/g, dynamicSections);
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readRegister(workspaceRoot: string): BehavioralRegister {
|
|
120
|
+
const prefsPath = path.join(workspaceRoot, "PREFERENCES.md");
|
|
121
|
+
try {
|
|
122
|
+
const content = fs.readFileSync(prefsPath, "utf8");
|
|
123
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
124
|
+
if (fmMatch) {
|
|
125
|
+
const fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
|
|
126
|
+
if (fm["register"] === "creative") return "creative";
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// PREFERENCES.md missing or unreadable — default to business
|
|
130
|
+
}
|
|
131
|
+
return "business";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extractTriggers(workspaceRoot: string): Array<{ name: string; triggers: string[] }> {
|
|
135
|
+
const forgeRoot = path.join(workspaceRoot, "packages", "forge");
|
|
136
|
+
const result: Array<{ name: string; triggers: string[] }> = [];
|
|
137
|
+
for (const skill of FORGE_SKILLS) {
|
|
138
|
+
if (skill.category !== "fo") continue;
|
|
139
|
+
const skillPath = path.join(forgeRoot, skill.path);
|
|
140
|
+
try {
|
|
141
|
+
const content = fs.readFileSync(skillPath, "utf8");
|
|
142
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
143
|
+
if (!fmMatch) continue;
|
|
144
|
+
const fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
|
|
145
|
+
if (Array.isArray(fm["triggers"])) {
|
|
146
|
+
result.push({
|
|
147
|
+
name: skill.name,
|
|
148
|
+
triggers: (fm["triggers"] as unknown[]).filter((v): v is string => typeof v === "string"),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
// SKILL.md not found or unreadable — skip
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function generateTriggersTable(triggers: Array<{ name: string; triggers: string[] }>): string {
|
|
159
|
+
const rows: string[] = [];
|
|
160
|
+
for (const { name, triggers: skillTriggers } of triggers) {
|
|
161
|
+
if (skillTriggers.length === 0) continue;
|
|
162
|
+
const display = skillTriggers.map((t) => `"${t}"`).join(", ");
|
|
163
|
+
rows.push(`| ${display} | \`${name}\` |`);
|
|
164
|
+
}
|
|
165
|
+
return rows.length > 0 ? rows.join("\n") + "\n" : "";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function generateBehavioralLayer(
|
|
169
|
+
workspaceRoot: string,
|
|
170
|
+
register: BehavioralRegister,
|
|
171
|
+
): string {
|
|
172
|
+
const triggers = extractTriggers(workspaceRoot);
|
|
173
|
+
|
|
174
|
+
let template: string;
|
|
175
|
+
try {
|
|
176
|
+
template = fs.readFileSync(BEHAVIORAL_LAYER_CORE_TEMPLATE, "utf8").trimEnd();
|
|
177
|
+
} catch {
|
|
178
|
+
return "";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const triggersTable = generateTriggersTable(triggers);
|
|
182
|
+
|
|
183
|
+
let extendedLayer = "";
|
|
184
|
+
if (register === "creative") {
|
|
185
|
+
try {
|
|
186
|
+
extendedLayer = fs.readFileSync(BEHAVIORAL_LAYER_EXTENDED_TEMPLATE, "utf8").trimEnd() + "\n\n";
|
|
187
|
+
} catch {
|
|
188
|
+
// Extended template missing — skip
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const result = template
|
|
193
|
+
.replace(/\{\{triggersTable\}\}/g, triggersTable)
|
|
194
|
+
.replace(/\{\{register\}\}/g, register)
|
|
195
|
+
.replace(/\{\{extendedLayer\}\}/g, extendedLayer);
|
|
196
|
+
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
interface AgentsGenerateResult {
|
|
201
|
+
command: string;
|
|
202
|
+
status: "pass" | "fail";
|
|
203
|
+
configPath: string;
|
|
204
|
+
generated: string[];
|
|
205
|
+
skipped: string[];
|
|
206
|
+
errors: string[];
|
|
207
|
+
renderedFiles?: { [relPath: string]: string };
|
|
208
|
+
details?: Array<{ path: string; domain?: string; register?: string; workspaceType?: string }>;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function runAgentsGenerate(
|
|
212
|
+
_input: ForgeCommandInput,
|
|
213
|
+
context: ForgeRuntimeContext,
|
|
214
|
+
): Promise<ForgeCommandResult<AgentsGenerateResult>> {
|
|
215
|
+
const { workspaceRoot, logger, outputFormat, dryRun } = context;
|
|
216
|
+
|
|
217
|
+
let config;
|
|
218
|
+
try {
|
|
219
|
+
config = loadForgeConfig(workspaceRoot, context.forgeRoot);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
const msg = (err as Error).message;
|
|
222
|
+
if (outputFormat === "pretty") {
|
|
223
|
+
logger.error(msg);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
data: {
|
|
227
|
+
command: "forge.agents.generate",
|
|
228
|
+
status: "fail",
|
|
229
|
+
configPath: "forge.yaml",
|
|
230
|
+
generated: [],
|
|
231
|
+
skipped: [],
|
|
232
|
+
errors: [msg],
|
|
233
|
+
},
|
|
234
|
+
exitCode: 1,
|
|
235
|
+
summary: `forge.agents.generate: failed — ${msg}`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const agentsMdPath = path.join(workspaceRoot, "AGENTS.md");
|
|
240
|
+
|
|
241
|
+
// Edit guard: refuse to overwrite a hand-written AGENTS.md (skipped in dryRun)
|
|
242
|
+
if (!dryRun && fs.existsSync(agentsMdPath)) {
|
|
243
|
+
const existing = fs.readFileSync(agentsMdPath, "utf8");
|
|
244
|
+
if (!hasGeneratedMarker(existing)) {
|
|
245
|
+
const msg = `AGENTS.md exists without a generated marker — refusing to overwrite a hand-written file. Delete or rename it first, then re-run forge.agents.generate.`;
|
|
246
|
+
if (outputFormat === "pretty") {
|
|
247
|
+
logger.error(msg);
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
data: {
|
|
251
|
+
command: "forge.agents.generate",
|
|
252
|
+
status: "fail",
|
|
253
|
+
configPath: "forge.yaml",
|
|
254
|
+
generated: [],
|
|
255
|
+
skipped: [],
|
|
256
|
+
errors: [msg],
|
|
257
|
+
},
|
|
258
|
+
exitCode: 1,
|
|
259
|
+
summary: `forge.agents.generate: failed — hand-written AGENTS.md`,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Generate AGENTS.md content
|
|
265
|
+
const header = buildGeneratedHeader({
|
|
266
|
+
filePath: "AGENTS.md",
|
|
267
|
+
ownerCommand: "forge.agents.generate",
|
|
268
|
+
commandPrefix: "forge",
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// RFC-0643: determine register from PREFERENCES.md or profile
|
|
272
|
+
const register = readRegister(workspaceRoot);
|
|
273
|
+
|
|
274
|
+
// Build dynamic sections (skills table, capabilities, behavioral layer)
|
|
275
|
+
const dynamicLines: string[] = [];
|
|
276
|
+
|
|
277
|
+
// Skills table
|
|
278
|
+
dynamicLines.push("## Skills");
|
|
279
|
+
dynamicLines.push("");
|
|
280
|
+
dynamicLines.push("| Name | Category | Invocation | Concerns |");
|
|
281
|
+
dynamicLines.push("| --- | --- | --- | --- |");
|
|
282
|
+
for (const skill of FORGE_SKILLS) {
|
|
283
|
+
dynamicLines.push(`| ${skill.name} | ${skill.category} | ${skill.invocation} | ${skill.concerns} |`);
|
|
284
|
+
}
|
|
285
|
+
dynamicLines.push("");
|
|
286
|
+
|
|
287
|
+
// Capabilities section (RFC-0393)
|
|
288
|
+
if (config.bindings) {
|
|
289
|
+
dynamicLines.push("## Capabilities");
|
|
290
|
+
dynamicLines.push("");
|
|
291
|
+
dynamicLines.push("Bindings resolved from `forge.yaml`:");
|
|
292
|
+
dynamicLines.push("");
|
|
293
|
+
dynamicLines.push("| Key | Status | Value |");
|
|
294
|
+
dynamicLines.push("| --- | --- | --- |");
|
|
295
|
+
|
|
296
|
+
const bindingKeys = [
|
|
297
|
+
"commands.validateRfc",
|
|
298
|
+
"commands.validateAdr",
|
|
299
|
+
"commands.typecheck",
|
|
300
|
+
"commands.test",
|
|
301
|
+
"commands.scopedBuild",
|
|
302
|
+
"commands.specValidate",
|
|
303
|
+
"commands.sessionSave",
|
|
304
|
+
"paths.invariantsFile",
|
|
305
|
+
"paths.reviewsDir",
|
|
306
|
+
"paths.handoffsDir",
|
|
307
|
+
"paths.sessionsDir",
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
for (const key of bindingKeys) {
|
|
311
|
+
const value = resolveBinding(config, key);
|
|
312
|
+
const status = value === null ? "absent" : "resolved";
|
|
313
|
+
const display = value === null ? "—" : typeof value === "string" ? `\`${value}\`` : String(value);
|
|
314
|
+
dynamicLines.push(`| ${key} | ${status} | ${display} |`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// compassDocs array
|
|
318
|
+
const compassDocs = resolveBinding(config, "paths.compassDocs");
|
|
319
|
+
if (Array.isArray(compassDocs) && compassDocs.length > 0) {
|
|
320
|
+
dynamicLines.push(`| paths.compassDocs | resolved | ${compassDocs.map((d) => `\`${d}\``).join(", ")} |`);
|
|
321
|
+
} else {
|
|
322
|
+
dynamicLines.push(`| paths.compassDocs | absent | — |`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// terminology
|
|
326
|
+
if (config.bindings.terminology && Object.keys(config.bindings.terminology).length > 0) {
|
|
327
|
+
dynamicLines.push("");
|
|
328
|
+
dynamicLines.push("**Terminology:**");
|
|
329
|
+
dynamicLines.push("");
|
|
330
|
+
for (const [term, value] of Object.entries(config.bindings.terminology)) {
|
|
331
|
+
dynamicLines.push(`- ${term}: ${value}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
dynamicLines.push("");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Behavioral layer section (RFC-0548)
|
|
339
|
+
const behavioralLayer = generateBehavioralLayer(workspaceRoot, register);
|
|
340
|
+
dynamicLines.push(behavioralLayer);
|
|
341
|
+
dynamicLines.push("");
|
|
342
|
+
|
|
343
|
+
// RFC-0664: Project memory layer read discipline
|
|
344
|
+
dynamicLines.push("## Project memory layer");
|
|
345
|
+
dynamicLines.push("");
|
|
346
|
+
dynamicLines.push("`.agents/memory/MEMORY.md` is the curated hot store (versioned). `.agents/memory/daily/YYYY-MM-DD.md` are append-only warm logs (git-ignored).");
|
|
347
|
+
dynamicLines.push("");
|
|
348
|
+
dynamicLines.push("**Session-start read discipline (advisory):** At session start, read `MEMORY.md` (always), then `daily/<today>.md` and `daily/<yesterday>.md` (if present). Older daily files are cold — use grep when a task references past context.");
|
|
349
|
+
dynamicLines.push("");
|
|
350
|
+
|
|
351
|
+
// RFC-0643: Load root template, replace project placeholders, insert dynamic sections
|
|
352
|
+
const profile = config.profile as StackProfile | undefined;
|
|
353
|
+
const rootTemplate = selectRootTemplate(register, profile);
|
|
354
|
+
const dynamicSections = dynamicLines.join("\n");
|
|
355
|
+
let content = replaceProjectPlaceholders(rootTemplate, config, dynamicSections);
|
|
356
|
+
|
|
357
|
+
// Prepend generated header (above template content)
|
|
358
|
+
content = header + "\n\n" + content;
|
|
359
|
+
|
|
360
|
+
// RFC-0643: Apply terminology substitution on final assembled content
|
|
361
|
+
const resolvedTerminology = resolveAllTerminology(config, profile);
|
|
362
|
+
content = substituteTemplate(content, resolvedTerminology);
|
|
363
|
+
|
|
364
|
+
const generated: string[] = ["AGENTS.md"];
|
|
365
|
+
const skipped: string[] = [];
|
|
366
|
+
const renderedFiles: { [relPath: string]: string } = {};
|
|
367
|
+
const details: Array<{ path: string; domain?: string; register?: string; workspaceType?: string }> = [];
|
|
368
|
+
details.push({
|
|
369
|
+
path: "AGENTS.md",
|
|
370
|
+
domain: profile?.domain,
|
|
371
|
+
register,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (dryRun) {
|
|
375
|
+
renderedFiles["AGENTS.md"] = content;
|
|
376
|
+
} else {
|
|
377
|
+
await writeFileIfChanged(agentsMdPath, content);
|
|
378
|
+
if (outputFormat === "pretty") {
|
|
379
|
+
logger.success(`Generated ${path.relative(workspaceRoot, agentsMdPath)}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Nested AGENTS.md generation (RFC-0611)
|
|
384
|
+
// RFC-0640: load workspaceTypes from stack profile for profile-driven detection
|
|
385
|
+
// RFC-0643: prefer config.profile (loaded by loadForgeConfig), fallback to stack-based lookup
|
|
386
|
+
let workspaceTypes: ProfileWorkspaceType[] | undefined;
|
|
387
|
+
if (profile?.workspaceTypes && profile.workspaceTypes.length > 0) {
|
|
388
|
+
workspaceTypes = profile.workspaceTypes;
|
|
389
|
+
} else {
|
|
390
|
+
try {
|
|
391
|
+
const forgeRoot = resolveForgeRoot(workspaceRoot);
|
|
392
|
+
const profiles = listStackProfiles(forgeRoot);
|
|
393
|
+
for (const stackId of config.project.stack) {
|
|
394
|
+
const stackProfile = profiles.find((p) => p.id === stackId);
|
|
395
|
+
if (stackProfile?.workspaceTypes && stackProfile.workspaceTypes.length > 0) {
|
|
396
|
+
workspaceTypes = stackProfile.workspaceTypes;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} catch {
|
|
401
|
+
// forge root not resolvable — fallback to hardcoded detection
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const nestedResult = await generateNestedAgentsMd(workspaceRoot, config, dryRun, workspaceTypes);
|
|
405
|
+
generated.push(...nestedResult.generated);
|
|
406
|
+
skipped.push(...nestedResult.skipped);
|
|
407
|
+
Object.assign(renderedFiles, nestedResult.renderedFiles);
|
|
408
|
+
|
|
409
|
+
// RFC-0643: add details for nested files
|
|
410
|
+
for (const rel of nestedResult.generated) {
|
|
411
|
+
details.push({
|
|
412
|
+
path: rel,
|
|
413
|
+
domain: profile?.domain,
|
|
414
|
+
workspaceType: nestedResult.workspaceTypeMap?.[rel],
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!dryRun && outputFormat === "pretty") {
|
|
419
|
+
for (const rel of nestedResult.generated) {
|
|
420
|
+
logger.success(`Generated ${rel}`);
|
|
421
|
+
}
|
|
422
|
+
for (const rel of nestedResult.skipped) {
|
|
423
|
+
logger.warn(`Skipped ${rel}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
data: {
|
|
429
|
+
command: "forge.agents.generate",
|
|
430
|
+
status: "pass",
|
|
431
|
+
configPath: "forge.yaml",
|
|
432
|
+
generated,
|
|
433
|
+
skipped,
|
|
434
|
+
errors: [],
|
|
435
|
+
details,
|
|
436
|
+
...(dryRun ? { renderedFiles } : {}),
|
|
437
|
+
},
|
|
438
|
+
exitCode: 0,
|
|
439
|
+
summary: dryRun
|
|
440
|
+
? `forge.agents.generate: [dry-run] would generate ${generated.length} file(s), skip ${skipped.length}`
|
|
441
|
+
: `forge.agents.generate: OK — ${generated.length} file(s) generated, ${skipped.length} skipped`,
|
|
442
|
+
};
|
|
443
|
+
}
|