agent-workflow-kit-cli 1.2.1 → 1.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/dist/cli/index.js CHANGED
@@ -9,12 +9,17 @@ import { runAdd } from "./commands/add.js";
9
9
  import { runSync } from "./commands/sync.js";
10
10
  import { runDoctor } from "./commands/doctor.js";
11
11
  import { runExport } from "./commands/export.js";
12
+ import { runWorkflowCommand, resumeWorkflowCommand } from "./commands/run.js";
13
+ import { runProfileCheck } from "./commands/profile.js";
14
+ import { listWorkflows, showWorkflow, validateWorkflow, runWorkflowById } from "./commands/workflow.js";
15
+ import { listRoles, showRole, validateRoles } from "./commands/role.js";
16
+ import { createAdrCommand, listAdrsCommand, showAdrCommand, searchAdrsCommand } from "./commands/adr.js";
12
17
  const program = new Command();
13
18
  export function runCli() {
14
19
  program
15
20
  .name("agent-workflow-kit")
16
21
  .description("Generate AI coding workflows/rules/templates for Codex and Antigravity")
17
- .version("1.2.1");
22
+ .version("1.3.1");
18
23
  program
19
24
  .command("init")
20
25
  .description("Initialize agent guidelines and skills for the repository")
@@ -75,14 +80,200 @@ export function runCli() {
75
80
  .command("export <target>")
76
81
  .description("Export custom workflows/skills for the target agent (e.g., 'antigravity')")
77
82
  .option("--no-clipboard", "Do not copy the exported instructions to clipboard", false)
83
+ .option("-o, --output <file>", "Write the exported guidelines to a specific file")
78
84
  .action(async (target, options) => {
79
85
  try {
80
- await runExport(target, { clipboard: options.clipboard });
86
+ await runExport(target, { clipboard: options.clipboard, output: options.output });
81
87
  }
82
88
  catch (err) {
83
89
  console.error(chalk.red(`Error running export: ${err instanceof Error ? err.message : String(err)}`));
84
90
  process.exit(1);
85
91
  }
86
92
  });
93
+ program
94
+ .command("run <workflow>")
95
+ .description("Execute an AWOS graph workflow")
96
+ .option("--inputs <inputs>", "Path to input JSON file or inline JSON string")
97
+ .option("--dry-run", "Execute workflow steps in simulation mode", false)
98
+ .action(async (workflow, options) => {
99
+ try {
100
+ await runWorkflowCommand(workflow, options);
101
+ }
102
+ catch (err) {
103
+ console.error(chalk.red(`Error running workflow: ${err instanceof Error ? err.message : String(err)}`));
104
+ process.exit(1);
105
+ }
106
+ });
107
+ program
108
+ .command("resume <runId>")
109
+ .description("Resume a suspended AWOS workflow")
110
+ .action(async (runId) => {
111
+ try {
112
+ await resumeWorkflowCommand(runId);
113
+ }
114
+ catch (err) {
115
+ console.error(chalk.red(`Error resuming workflow: ${err instanceof Error ? err.message : String(err)}`));
116
+ process.exit(1);
117
+ }
118
+ });
119
+ program
120
+ .command("profile")
121
+ .description("Validate directory architecture structure and rules boundaries")
122
+ .option("--profile <profile>", "Target profile name (e.g. clean-architecture)")
123
+ .action(async (options) => {
124
+ try {
125
+ await runProfileCheck(options);
126
+ }
127
+ catch (err) {
128
+ console.error(chalk.red(`Error validation profile rules: ${err instanceof Error ? err.message : String(err)}`));
129
+ process.exit(1);
130
+ }
131
+ });
132
+ // --- Workflow Group Subcommands ---
133
+ const workflowCmd = program.command("workflow").description("Manage and execute AWOS workflow packs");
134
+ workflowCmd
135
+ .command("list")
136
+ .description("List discovered workflow packs")
137
+ .action(async () => {
138
+ try {
139
+ await listWorkflows();
140
+ }
141
+ catch (err) {
142
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
143
+ process.exit(1);
144
+ }
145
+ });
146
+ workflowCmd
147
+ .command("show <id>")
148
+ .description("Show workflow pack steps and metadata")
149
+ .action(async (id) => {
150
+ try {
151
+ await showWorkflow(id);
152
+ }
153
+ catch (err) {
154
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
155
+ process.exit(1);
156
+ }
157
+ });
158
+ workflowCmd
159
+ .command("validate <id>")
160
+ .description("Validate a workflow pack schema and graph cyclic constraints")
161
+ .action(async (id) => {
162
+ try {
163
+ await validateWorkflow(id);
164
+ }
165
+ catch (err) {
166
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
167
+ process.exit(1);
168
+ }
169
+ });
170
+ workflowCmd
171
+ .command("run <id>")
172
+ .description("Run a registered workflow pack by ID")
173
+ .option("--inputs <inputs>", "Path to inputs parameter JSON file or inline string")
174
+ .option("--dry-run", "Run nodes in dry-run mode", false)
175
+ .action(async (id, options) => {
176
+ try {
177
+ await runWorkflowById(id, options);
178
+ }
179
+ catch (err) {
180
+ console.error(chalk.red(`Error running workflow: ${err instanceof Error ? err.message : String(err)}`));
181
+ process.exit(1);
182
+ }
183
+ });
184
+ // --- Role Group Subcommands ---
185
+ const roleCmd = program.command("role").description("Manage agent role profiles");
186
+ roleCmd
187
+ .command("list")
188
+ .description("List all discovered role profiles in catalog")
189
+ .action(async () => {
190
+ try {
191
+ await listRoles();
192
+ }
193
+ catch (err) {
194
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
195
+ process.exit(1);
196
+ }
197
+ });
198
+ roleCmd
199
+ .command("show <id>")
200
+ .description("Show agent role details, checklists and inputs schema")
201
+ .action(async (id) => {
202
+ try {
203
+ await showRole(id);
204
+ }
205
+ catch (err) {
206
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
207
+ process.exit(1);
208
+ }
209
+ });
210
+ roleCmd
211
+ .command("validate")
212
+ .description("Validate all roles catalog files schema parameters")
213
+ .action(async () => {
214
+ try {
215
+ await validateRoles();
216
+ }
217
+ catch (err) {
218
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
219
+ process.exit(1);
220
+ }
221
+ });
222
+ // --- ADR Group Subcommands ---
223
+ const adrCmd = program.command("adr").description("Manage Architectural Decision Records");
224
+ adrCmd
225
+ .command("create")
226
+ .description("Create a new numbered architectural decision record (ADR)")
227
+ .option("--title <title>", "ADR decision title")
228
+ .option("--status <status>", "Initial status: proposed | accepted | rejected | superseded", "proposed")
229
+ .option("--context <context>", "Context background explanation")
230
+ .option("--decision <decision>", "Architectural decision detail statement")
231
+ .option("--consequences <consequences>", "Repercussions of decision taken")
232
+ .option("--decision-maker <maker>", "Role or name of decider", "AWOS System")
233
+ .action(async (options) => {
234
+ try {
235
+ await createAdrCommand(options);
236
+ }
237
+ catch (err) {
238
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
239
+ process.exit(1);
240
+ }
241
+ });
242
+ adrCmd
243
+ .command("list")
244
+ .description("List all saved ADR documents")
245
+ .action(async () => {
246
+ try {
247
+ await listAdrsCommand();
248
+ }
249
+ catch (err) {
250
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
251
+ process.exit(1);
252
+ }
253
+ });
254
+ adrCmd
255
+ .command("show <id>")
256
+ .description("Display details of a numbered ADR document")
257
+ .action(async (id) => {
258
+ try {
259
+ await showAdrCommand(id);
260
+ }
261
+ catch (err) {
262
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
263
+ process.exit(1);
264
+ }
265
+ });
266
+ adrCmd
267
+ .command("search <keyword>")
268
+ .description("Search all ADR documents for keyword text matches")
269
+ .action(async (keyword) => {
270
+ try {
271
+ await searchAdrsCommand(keyword);
272
+ }
273
+ catch (err) {
274
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
275
+ process.exit(1);
276
+ }
277
+ });
87
278
  program.parse(process.argv);
88
279
  }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import { promises as fs } from "fs";
6
+ import path from "path";
7
+ import Handlebars from "handlebars";
8
+ const ADR_DIR = "docs/adr";
9
+ const DEFAULT_TEMPLATE = `---
10
+ id: {{id}}
11
+ title: {{{title}}}
12
+ status: {{status}}
13
+ date: {{date}}
14
+ {{#if metadata.decisionMakerRole}}
15
+ decisionMakerRole: {{metadata.decisionMakerRole}}
16
+ {{/if}}
17
+ {{#if metadata.affectedModules}}
18
+ affectedModules: {{#each metadata.affectedModules}}{{#if @index}}, {{/if}}{{this}}{{/each}}
19
+ {{/if}}
20
+ ---
21
+
22
+ # {{{title}}}
23
+
24
+ ## Context
25
+ {{{context}}}
26
+
27
+ ## Decision
28
+ {{{decision}}}
29
+
30
+ ## Consequences
31
+ {{{consequences}}}
32
+ `;
33
+ export class ADRService {
34
+ /**
35
+ * Helper to parse a markdown ADR file back into a structured ADRDefinition.
36
+ */
37
+ static parseMarkdown(content, id) {
38
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
39
+ const match = content.match(frontmatterRegex);
40
+ let title = "Untitled ADR";
41
+ let status = 'proposed';
42
+ let date = new Date().toISOString().split("T")[0];
43
+ let body = content;
44
+ const metadata = {};
45
+ if (match) {
46
+ const frontmatter = match[1];
47
+ body = match[2];
48
+ const lines = frontmatter.split("\n");
49
+ for (const line of lines) {
50
+ const trimmed = line.trim();
51
+ if (!trimmed || trimmed.startsWith("#"))
52
+ continue;
53
+ const eqIdx = trimmed.indexOf(":");
54
+ if (eqIdx !== -1) {
55
+ const key = trimmed.substring(0, eqIdx).trim();
56
+ const value = trimmed.substring(eqIdx + 1).trim();
57
+ if (key === "title")
58
+ title = value.replace(/^['"]|['"]$/g, "");
59
+ else if (key === "status") {
60
+ const parsedStatus = value.replace(/^['"]|['"]$/g, "");
61
+ if (['proposed', 'accepted', 'rejected', 'superseded'].includes(parsedStatus)) {
62
+ status = parsedStatus;
63
+ }
64
+ }
65
+ else if (key === "date")
66
+ date = value;
67
+ else if (key === "decisionMakerRole")
68
+ metadata.decisionMakerRole = value;
69
+ else if (key === "affectedModules") {
70
+ metadata.affectedModules = value.split(",").map((s) => s.trim());
71
+ }
72
+ }
73
+ }
74
+ }
75
+ // Extract headers for sections (Context, Decision, Consequences)
76
+ let context = "";
77
+ let decision = "";
78
+ let consequences = "";
79
+ const contextMatch = body.match(/##\s+Context\r?\n([\s\S]*?)(?=##\s+Decision|##\s+Consequences|$)/i);
80
+ const decisionMatch = body.match(/##\s+Decision\r?\n([\s\S]*?)(?=##\s+Context|##\s+Consequences|$)/i);
81
+ const consequencesMatch = body.match(/##\s+Consequences\r?\n([\s\S]*?)(?=##\s+Context|##\s+Decision|$)/i);
82
+ if (contextMatch)
83
+ context = contextMatch[1].trim();
84
+ if (decisionMatch)
85
+ decision = decisionMatch[1].trim();
86
+ if (consequencesMatch)
87
+ consequences = consequencesMatch[1].trim();
88
+ // In case of headers missing
89
+ if (!context && !decision && !consequences) {
90
+ context = body.trim();
91
+ }
92
+ return {
93
+ id,
94
+ title,
95
+ date,
96
+ status,
97
+ context,
98
+ decision,
99
+ consequences,
100
+ metadata,
101
+ };
102
+ }
103
+ /**
104
+ * Scans docs/adr/ and calculates the next incremental number (e.g. ADR-0003).
105
+ */
106
+ static async getNextId(workspaceRoot) {
107
+ const fullDir = path.join(workspaceRoot, ADR_DIR);
108
+ try {
109
+ const files = await fs.readdir(fullDir);
110
+ let maxNum = 0;
111
+ for (const file of files) {
112
+ const match = file.match(/^ADR-(\d{4})\.md$/);
113
+ if (match) {
114
+ const num = parseInt(match[1], 10);
115
+ if (num > maxNum) {
116
+ maxNum = num;
117
+ }
118
+ }
119
+ }
120
+ const nextNum = maxNum + 1;
121
+ return `ADR-${String(nextNum).padStart(4, "0")}`;
122
+ }
123
+ catch {
124
+ return "ADR-0001";
125
+ }
126
+ }
127
+ /**
128
+ * Generates a new ADR file in the docs/adr/ directory.
129
+ */
130
+ static async create(workspaceRoot, adr, templateString) {
131
+ const id = await ADRService.getNextId(workspaceRoot);
132
+ const date = new Date().toISOString().split("T")[0];
133
+ const fullDir = path.join(workspaceRoot, ADR_DIR);
134
+ const definition = {
135
+ ...adr,
136
+ id,
137
+ date,
138
+ };
139
+ const template = Handlebars.compile(templateString || DEFAULT_TEMPLATE);
140
+ const rendered = template(definition);
141
+ await fs.mkdir(fullDir, { recursive: true });
142
+ const filePath = path.join(fullDir, `${id}.md`);
143
+ await fs.writeFile(filePath, rendered, "utf8");
144
+ return definition;
145
+ }
146
+ /**
147
+ * Loads a specific ADR by ID.
148
+ */
149
+ static async load(workspaceRoot, id) {
150
+ const fullDir = path.join(workspaceRoot, ADR_DIR);
151
+ const filePath = path.join(fullDir, `${id}.md`);
152
+ try {
153
+ const content = await fs.readFile(filePath, "utf8");
154
+ return ADRService.parseMarkdown(content, id);
155
+ }
156
+ catch (err) {
157
+ throw new Error(`Failed to load ADR '${id}': ${err instanceof Error ? err.message : String(err)}`);
158
+ }
159
+ }
160
+ /**
161
+ * Lists all ADRs sorted by ID.
162
+ */
163
+ static async list(workspaceRoot) {
164
+ const fullDir = path.join(workspaceRoot, ADR_DIR);
165
+ const adrs = [];
166
+ try {
167
+ const files = await fs.readdir(fullDir);
168
+ for (const file of files) {
169
+ if (file.match(/^ADR-\d{4}\.md$/)) {
170
+ const id = path.basename(file, ".md");
171
+ const adr = await ADRService.load(workspaceRoot, id);
172
+ adrs.push(adr);
173
+ }
174
+ }
175
+ }
176
+ catch {
177
+ // Directory may not exist yet
178
+ }
179
+ return adrs.sort((a, b) => a.id.localeCompare(b.id));
180
+ }
181
+ /**
182
+ * Performs case-insensitive search across titles, contexts, decisions, and consequences.
183
+ */
184
+ static async search(workspaceRoot, keyword) {
185
+ const all = await ADRService.list(workspaceRoot);
186
+ const searchLower = keyword.toLowerCase();
187
+ return all.filter((adr) => {
188
+ return (adr.title.toLowerCase().includes(searchLower) ||
189
+ adr.context.toLowerCase().includes(searchLower) ||
190
+ adr.decision.toLowerCase().includes(searchLower) ||
191
+ adr.consequences.toLowerCase().includes(searchLower));
192
+ });
193
+ }
194
+ /**
195
+ * Updates status metadata parameter for an ADR.
196
+ */
197
+ static async updateStatus(workspaceRoot, id, newStatus) {
198
+ const adr = await ADRService.load(workspaceRoot, id);
199
+ adr.status = newStatus;
200
+ // Render it back
201
+ const template = Handlebars.compile(DEFAULT_TEMPLATE);
202
+ const rendered = template(adr);
203
+ const fullDir = path.join(workspaceRoot, ADR_DIR);
204
+ const filePath = path.join(fullDir, `${id}.md`);
205
+ await fs.writeFile(filePath, rendered, "utf8");
206
+ return adr;
207
+ }
208
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import { promises as fs } from "fs";
6
+ import path from "path";
7
+ import crypto from "crypto";
8
+ import { detectProjectModules } from "../detector.js";
9
+ import { parseImports } from "../parser.js";
10
+ import { loadAWOSPlugins } from "./registry.js";
11
+ const CACHE_DIR = ".agents/cache";
12
+ const CACHE_FILE = path.join(CACHE_DIR, "repo_context.json");
13
+ /**
14
+ * Calculates a composite MD5/SHA256 hash of all configuration manifest files in the repository.
15
+ */
16
+ export async function calculateManifestHash(workspaceRoot) {
17
+ const manifests = [
18
+ "package.json",
19
+ "package-lock.json",
20
+ "pom.xml",
21
+ "build.gradle",
22
+ "build.gradle.kts",
23
+ "pyproject.toml",
24
+ "requirements.txt",
25
+ "Pipfile",
26
+ "tsconfig.json",
27
+ ];
28
+ const hash = crypto.createHash("sha256");
29
+ let foundAny = false;
30
+ for (const file of manifests) {
31
+ const fullPath = path.join(workspaceRoot, file);
32
+ try {
33
+ const stat = await fs.stat(fullPath);
34
+ if (stat.isFile()) {
35
+ const content = await fs.readFile(fullPath);
36
+ hash.update(file);
37
+ hash.update(content);
38
+ foundAny = true;
39
+ }
40
+ }
41
+ catch {
42
+ // Ignore missing manifest files
43
+ }
44
+ }
45
+ // Also include directory structure hash of sub-modules
46
+ try {
47
+ const submodules = await detectProjectModules(workspaceRoot);
48
+ for (const sub of submodules) {
49
+ hash.update(sub.name);
50
+ hash.update(sub.stacks.join(","));
51
+ }
52
+ }
53
+ catch {
54
+ // Ignore errors
55
+ }
56
+ return foundAny ? hash.digest("hex") : "no-manifests-hash";
57
+ }
58
+ /**
59
+ * Parses files in a directory to identify imports and build a dependency relationship between directories.
60
+ */
61
+ async function analyzeImportGraph(workspaceRoot, modules) {
62
+ const dependencies = {};
63
+ for (const mod of modules) {
64
+ dependencies[mod] = new Set();
65
+ }
66
+ // Helper to find files recursively
67
+ async function scanDir(dir, currentModule) {
68
+ try {
69
+ const entries = await fs.readdir(dir, { withFileTypes: true });
70
+ for (const entry of entries) {
71
+ const fullPath = path.join(dir, entry.name);
72
+ if (entry.isDirectory()) {
73
+ if (entry.name.startsWith(".") ||
74
+ entry.name === "node_modules" ||
75
+ entry.name === "dist" ||
76
+ entry.name === "build" ||
77
+ entry.name === "target") {
78
+ continue;
79
+ }
80
+ await scanDir(fullPath, currentModule);
81
+ }
82
+ else if (entry.isFile() && /\.(ts|tsx|java|py)$/.test(entry.name)) {
83
+ const content = await fs.readFile(fullPath, "utf8");
84
+ const parsedImports = await parseImports(fullPath, content);
85
+ for (const importedRef of parsedImports) {
86
+ for (const mod of modules) {
87
+ if (mod !== currentModule &&
88
+ (importedRef.includes(`/${mod}`) || importedRef.includes(`.${mod}`))) {
89
+ dependencies[currentModule].add(mod);
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ catch {
97
+ // Ignore read errors
98
+ }
99
+ }
100
+ for (const mod of modules) {
101
+ const modulePath = path.join(workspaceRoot, mod);
102
+ await scanDir(modulePath, mod);
103
+ }
104
+ // Convert Sets to arrays
105
+ const result = {};
106
+ for (const [key, val] of Object.entries(dependencies)) {
107
+ result[key] = Array.from(val);
108
+ }
109
+ return result;
110
+ }
111
+ /**
112
+ * Builds a complete RepositoryContext for the given directory.
113
+ */
114
+ export async function buildRepositoryContext(workspaceRoot) {
115
+ const manifestHash = await calculateManifestHash(workspaceRoot);
116
+ // 1. Detect modules
117
+ const detectedModules = await detectProjectModules(workspaceRoot);
118
+ const moduleNames = detectedModules.map((m) => m.name === "." ? "root" : m.name);
119
+ // Analyze import boundaries
120
+ const importGraph = await analyzeImportGraph(workspaceRoot, detectedModules.map((m) => m.name));
121
+ const moduleBoundaries = detectedModules.map((m) => {
122
+ const name = m.name === "." ? "root" : m.name;
123
+ const dependencies = importGraph[m.name] || [];
124
+ return {
125
+ name,
126
+ path: m.dir,
127
+ dependencies,
128
+ };
129
+ });
130
+ // Determine main stack & architecture
131
+ let mainStack = "custom";
132
+ const stackCounts = {};
133
+ for (const m of detectedModules) {
134
+ for (const s of m.stacks) {
135
+ stackCounts[s] = (stackCounts[s] || 0) + 1;
136
+ }
137
+ }
138
+ const sortedStacks = Object.entries(stackCounts).sort((a, b) => b[1] - a[1]);
139
+ if (sortedStacks.length > 0) {
140
+ mainStack = sortedStacks[0][0];
141
+ }
142
+ // Determine default architecture style based on stack
143
+ let architecture = "layered";
144
+ if (mainStack === "spring-boot") {
145
+ architecture = "clean-architecture";
146
+ }
147
+ else if (mainStack === "react-ts") {
148
+ architecture = "feature-first";
149
+ }
150
+ else if (mainStack === "fastapi") {
151
+ architecture = "vertical-slice";
152
+ }
153
+ // Testing strategy defaults
154
+ const frameworks = [];
155
+ if (mainStack === "react-ts")
156
+ frameworks.push("vitest", "testing-library");
157
+ else if (mainStack === "spring-boot")
158
+ frameworks.push("junit", "mockito");
159
+ else if (mainStack === "fastapi")
160
+ frameworks.push("pytest");
161
+ const testing = {
162
+ frameworks,
163
+ coverageGoal: 80,
164
+ };
165
+ // Validation libraries defaults
166
+ const validationLibraries = [];
167
+ if (mainStack === "spring-boot") {
168
+ validationLibraries.push("jakarta.validation");
169
+ }
170
+ else if (mainStack === "react-ts") {
171
+ validationLibraries.push("zod");
172
+ }
173
+ else if (mainStack === "fastapi") {
174
+ validationLibraries.push("pydantic");
175
+ }
176
+ const registry = await loadAWOSPlugins(workspaceRoot);
177
+ let context = {
178
+ stack: mainStack,
179
+ architecture,
180
+ modules: moduleBoundaries,
181
+ testing,
182
+ validation: {
183
+ libraries: validationLibraries,
184
+ },
185
+ hash: manifestHash,
186
+ };
187
+ for (const analyzer of registry.analyzers) {
188
+ try {
189
+ const partial = await analyzer.detect(workspaceRoot);
190
+ context = {
191
+ ...context,
192
+ ...partial,
193
+ testing: {
194
+ ...context.testing,
195
+ ...(partial.testing || {}),
196
+ },
197
+ validation: {
198
+ ...context.validation,
199
+ ...(partial.validation || {}),
200
+ },
201
+ };
202
+ }
203
+ catch (err) {
204
+ console.warn(`[AWOS Intelligence] Custom analyzer failed:`, err);
205
+ }
206
+ }
207
+ return context;
208
+ }
209
+ /**
210
+ * Loads the RepositoryContext, using cached results if valid.
211
+ */
212
+ export async function getRepositoryContext(workspaceRoot) {
213
+ const currentHash = await calculateManifestHash(workspaceRoot);
214
+ const fullCachePath = path.join(workspaceRoot, CACHE_FILE);
215
+ try {
216
+ const cacheContent = await fs.readFile(fullCachePath, "utf8");
217
+ const cachedCtx = JSON.parse(cacheContent);
218
+ if (cachedCtx.hash === currentHash) {
219
+ return cachedCtx;
220
+ }
221
+ }
222
+ catch {
223
+ // Cache miss or read failure
224
+ }
225
+ // Re-build and write to cache
226
+ const context = await buildRepositoryContext(workspaceRoot);
227
+ try {
228
+ await fs.mkdir(path.dirname(fullCachePath), { recursive: true });
229
+ await fs.writeFile(fullCachePath, JSON.stringify(context, null, 2), "utf8");
230
+ }
231
+ catch {
232
+ // Non-blocking cache write failure
233
+ }
234
+ return context;
235
+ }