@wizdear/atlas-code 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent-factory.d.ts +10 -5
- package/dist/agent-factory.d.ts.map +1 -1
- package/dist/agent-factory.js +50 -13
- package/dist/agent-factory.js.map +1 -1
- package/dist/cli.d.ts +7 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +72 -16
- package/dist/cli.js.map +1 -1
- package/dist/discovery.d.ts +9 -2
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +4 -5
- package/dist/discovery.js.map +1 -1
- package/dist/extension.d.ts +9 -2
- package/dist/extension.d.ts.map +1 -1
- package/dist/extension.js +1103 -381
- package/dist/extension.js.map +1 -1
- package/dist/gate.d.ts +1 -1
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator.d.ts +0 -4
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +0 -2
- package/dist/orchestrator.js.map +1 -1
- package/dist/pipeline-editor.d.ts +2 -0
- package/dist/pipeline-editor.d.ts.map +1 -1
- package/dist/pipeline-editor.js +36 -5
- package/dist/pipeline-editor.js.map +1 -1
- package/dist/pipeline.d.ts +2 -10
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +4 -6
- package/dist/pipeline.js.map +1 -1
- package/dist/planner.d.ts +9 -2
- package/dist/planner.d.ts.map +1 -1
- package/dist/planner.js +15 -11
- package/dist/planner.js.map +1 -1
- package/dist/roles/architect.d.ts +1 -1
- package/dist/roles/architect.d.ts.map +1 -1
- package/dist/roles/architect.js +1 -1
- package/dist/roles/architect.js.map +1 -1
- package/dist/roles/cicd.d.ts +1 -1
- package/dist/roles/cicd.d.ts.map +1 -1
- package/dist/roles/cicd.js +5 -0
- package/dist/roles/cicd.js.map +1 -1
- package/dist/roles/documenter.d.ts +1 -1
- package/dist/roles/documenter.d.ts.map +1 -1
- package/dist/roles/documenter.js +11 -0
- package/dist/roles/documenter.js.map +1 -1
- package/dist/roles/index.d.ts +1 -0
- package/dist/roles/index.d.ts.map +1 -1
- package/dist/roles/index.js +3 -0
- package/dist/roles/index.js.map +1 -1
- package/dist/roles/recover.d.ts +5 -0
- package/dist/roles/recover.d.ts.map +1 -0
- package/dist/roles/recover.js +82 -0
- package/dist/roles/recover.js.map +1 -0
- package/dist/roles/reviewer.d.ts +1 -1
- package/dist/roles/reviewer.d.ts.map +1 -1
- package/dist/roles/reviewer.js +7 -1
- package/dist/roles/reviewer.js.map +1 -1
- package/dist/roles/standards-enricher.d.ts +1 -1
- package/dist/roles/standards-enricher.d.ts.map +1 -1
- package/dist/roles/standards-enricher.js +8 -0
- package/dist/roles/standards-enricher.js.map +1 -1
- package/dist/roles/tester.d.ts +1 -1
- package/dist/roles/tester.d.ts.map +1 -1
- package/dist/roles/tester.js +7 -0
- package/dist/roles/tester.js.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +6 -6
- package/dist/router.js.map +1 -1
- package/dist/standards.d.ts +37 -11
- package/dist/standards.d.ts.map +1 -1
- package/dist/standards.js +71 -89
- package/dist/standards.js.map +1 -1
- package/dist/step-executor.d.ts +15 -2
- package/dist/step-executor.d.ts.map +1 -1
- package/dist/step-executor.js +138 -30
- package/dist/step-executor.js.map +1 -1
- package/dist/store.d.ts +3 -10
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +45 -57
- package/dist/store.js.map +1 -1
- package/dist/system-architect.d.ts +9 -2
- package/dist/system-architect.d.ts.map +1 -1
- package/dist/system-architect.js +6 -10
- package/dist/system-architect.js.map +1 -1
- package/dist/telegram/bridge.d.ts +39 -0
- package/dist/telegram/bridge.d.ts.map +1 -0
- package/dist/telegram/bridge.js +380 -0
- package/dist/telegram/bridge.js.map +1 -0
- package/dist/telegram/formatter.d.ts +15 -0
- package/dist/telegram/formatter.d.ts.map +1 -0
- package/dist/telegram/formatter.js +86 -0
- package/dist/telegram/formatter.js.map +1 -0
- package/dist/telegram/renderer.d.ts +45 -0
- package/dist/telegram/renderer.d.ts.map +1 -0
- package/dist/telegram/renderer.js +150 -0
- package/dist/telegram/renderer.js.map +1 -0
- package/dist/telegram/telegram-api.d.ts +84 -0
- package/dist/telegram/telegram-api.d.ts.map +1 -0
- package/dist/telegram/telegram-api.js +134 -0
- package/dist/telegram/telegram-api.js.map +1 -0
- package/dist/types.d.ts +10 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +2 -0
- package/dist/ui.js.map +1 -1
- package/package.json +1 -1
package/dist/standards.d.ts
CHANGED
|
@@ -1,18 +1,44 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
/**
|
|
3
|
-
export
|
|
1
|
+
import type { VibeConfig } from "./types.js";
|
|
2
|
+
/** Parsed frontmatter result. */
|
|
3
|
+
export interface StandardsFrontmatter {
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
/** Single entry in the standards index. */
|
|
7
|
+
export interface StandardsIndexEntry {
|
|
8
|
+
filename: string;
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
/** Result of building the standards index. */
|
|
12
|
+
export interface StandardsIndexResult {
|
|
13
|
+
entries: StandardsIndexEntry[];
|
|
14
|
+
/** Filenames that had frontmatter auto-added. */
|
|
15
|
+
autoFixed: string[];
|
|
16
|
+
}
|
|
4
17
|
/**
|
|
5
|
-
*
|
|
18
|
+
* Parses YAML frontmatter from markdown content.
|
|
19
|
+
* Extracts the `description` field. Returns empty object if no frontmatter found.
|
|
6
20
|
*/
|
|
7
|
-
export declare function
|
|
21
|
+
export declare function parseFrontmatter(content: string): StandardsFrontmatter;
|
|
8
22
|
/**
|
|
9
|
-
*
|
|
10
|
-
* Returns
|
|
23
|
+
* Extracts the first `# Heading` from markdown content.
|
|
24
|
+
* Returns undefined if no heading found.
|
|
11
25
|
*/
|
|
12
|
-
export declare function
|
|
26
|
+
export declare function extractFirstHeading(content: string): string | undefined;
|
|
13
27
|
/**
|
|
14
|
-
*
|
|
15
|
-
* Returns an empty string if the input is empty.
|
|
28
|
+
* Generates a YAML frontmatter block with a description field.
|
|
16
29
|
*/
|
|
17
|
-
export declare function
|
|
30
|
+
export declare function generateFrontmatter(description: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Scans all standards sources, parses frontmatter descriptions,
|
|
33
|
+
* and returns a list of index entries sorted by filename.
|
|
34
|
+
*
|
|
35
|
+
* Description priority: frontmatter description > first # heading > filename (without .md).
|
|
36
|
+
* Files missing frontmatter are auto-fixed: a frontmatter block is prepended and written back.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildStandardsIndex(config: VibeConfig, projectRoot: string): Promise<StandardsIndexResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Formats index entries as a system prompt section.
|
|
41
|
+
* Returns empty string if no entries.
|
|
42
|
+
*/
|
|
43
|
+
export declare function formatStandardsIndexPrompt(entries: StandardsIndexEntry[], standardsDir?: string): string;
|
|
18
44
|
//# sourceMappingURL=standards.d.ts.map
|
package/dist/standards.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"standards.d.ts","sourceRoot":"","sources":["../src/standards.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"standards.d.ts","sourceRoot":"","sources":["../src/standards.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,iCAAiC;AACjC,MAAM,WAAW,oBAAoB;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,2CAA2C;AAC3C,MAAM,WAAW,mBAAmB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,8CAA8C;AAC9C,MAAM,WAAW,oBAAoB;IACpC,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,iDAAiD;IACjD,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAqDD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,oBAAoB,CAiBtE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGvE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA+BhH;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,mBAAmB,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAgBxG","sourcesContent":["import { access, readdir, readFile, stat, writeFile } from \"node:fs/promises\";\nimport { basename, isAbsolute, join, resolve } from \"node:path\";\nimport type { VibeConfig } from \"./types.js\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Parsed frontmatter result. */\nexport interface StandardsFrontmatter {\n\tdescription?: string;\n}\n\n/** Single entry in the standards index. */\nexport interface StandardsIndexEntry {\n\tfilename: string;\n\tdescription: string;\n}\n\n/** Result of building the standards index. */\nexport interface StandardsIndexResult {\n\tentries: StandardsIndexEntry[];\n\t/** Filenames that had frontmatter auto-added. */\n\tautoFixed: string[];\n}\n\n// ─── Internal Helpers ────────────────────────────────────────────────────────\n\n/** Checks whether a file exists. */\nasync function fileExists(path: string): Promise<boolean> {\n\ttry {\n\t\tawait access(path);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Resolves a path to an absolute path relative to projectRoot. */\nfunction resolvePath(source: string, projectRoot: string): string {\n\tif (isAbsolute(source)) {\n\t\treturn source;\n\t}\n\treturn resolve(projectRoot, source);\n}\n\n/**\n * Traverses all source paths and returns a Map<filename, absolutePath> of discovered .md files.\n * When the same filename exists in multiple sources, the later source takes precedence.\n */\nasync function resolveStandardFiles(sources: string[], projectRoot: string): Promise<Map<string, string>> {\n\tconst result = new Map<string, string>();\n\n\tfor (const source of sources) {\n\t\tconst absPath = resolvePath(source, projectRoot);\n\t\tif (!(await fileExists(absPath))) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst info = await stat(absPath);\n\t\tif (info.isDirectory()) {\n\t\t\tconst entries = await readdir(absPath);\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (entry.endsWith(\".md\")) {\n\t\t\t\t\tresult.set(entry, join(absPath, entry));\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (info.isFile()) {\n\t\t\tresult.set(basename(absPath), absPath);\n\t\t}\n\t}\n\n\treturn result;\n}\n\n// ─── Public API ──────────────────────────────────────────────────────────────\n\n/**\n * Parses YAML frontmatter from markdown content.\n * Extracts the `description` field. Returns empty object if no frontmatter found.\n */\nexport function parseFrontmatter(content: string): StandardsFrontmatter {\n\tif (!content.startsWith(\"---\")) {\n\t\treturn {};\n\t}\n\n\tconst endIndex = content.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn {};\n\t}\n\n\tconst frontmatterBlock = content.slice(3, endIndex);\n\tconst descMatch = frontmatterBlock.match(/^\\s*description\\s*:\\s*(.+)$/m);\n\tif (!descMatch) {\n\t\treturn {};\n\t}\n\n\treturn { description: descMatch[1].trim() };\n}\n\n/**\n * Extracts the first `# Heading` from markdown content.\n * Returns undefined if no heading found.\n */\nexport function extractFirstHeading(content: string): string | undefined {\n\tconst match = content.match(/^#\\s+(.+)$/m);\n\treturn match ? match[1].trim() : undefined;\n}\n\n/**\n * Generates a YAML frontmatter block with a description field.\n */\nexport function generateFrontmatter(description: string): string {\n\treturn `---\\ndescription: ${description}\\n---\\n`;\n}\n\n/**\n * Scans all standards sources, parses frontmatter descriptions,\n * and returns a list of index entries sorted by filename.\n *\n * Description priority: frontmatter description > first # heading > filename (without .md).\n * Files missing frontmatter are auto-fixed: a frontmatter block is prepended and written back.\n */\nexport async function buildStandardsIndex(config: VibeConfig, projectRoot: string): Promise<StandardsIndexResult> {\n\tconst files = await resolveStandardFiles(config.standards.sources, projectRoot);\n\tconst entries: StandardsIndexEntry[] = [];\n\tconst autoFixed: string[] = [];\n\n\tfor (const [filename, filePath] of files) {\n\t\tconst content = await readFile(filePath, \"utf-8\");\n\t\tconst frontmatter = parseFrontmatter(content);\n\n\t\tlet description: string;\n\t\tif (frontmatter.description) {\n\t\t\tdescription = frontmatter.description;\n\t\t} else {\n\t\t\tconst heading = extractFirstHeading(content);\n\t\t\tdescription = heading ?? filename.replace(/\\.md$/, \"\");\n\n\t\t\t// Auto-prepend frontmatter and write back\n\t\t\tconst newContent = generateFrontmatter(description) + content;\n\t\t\ttry {\n\t\t\t\tawait writeFile(filePath, newContent, \"utf-8\");\n\t\t\t\tautoFixed.push(filename);\n\t\t\t} catch {\n\t\t\t\t// Graceful failure: skip write, continue with derived description\n\t\t\t}\n\t\t}\n\n\t\tentries.push({ filename, description });\n\t}\n\n\tentries.sort((a, b) => a.filename.localeCompare(b.filename));\n\treturn { entries, autoFixed };\n}\n\n/**\n * Formats index entries as a system prompt section.\n * Returns empty string if no entries.\n */\nexport function formatStandardsIndexPrompt(entries: StandardsIndexEntry[], standardsDir?: string): string {\n\tif (entries.length === 0) {\n\t\treturn \"\";\n\t}\n\n\tconst dir = standardsDir ?? \".vibe/standards\";\n\tconst rows = entries.map((e) => `| \\`${e.filename}\\` | ${e.description} |`).join(\"\\n\");\n\n\treturn `## Organization Standards\n\nThe following standards are available in \\`${dir}/\\`.\nRead the ones relevant to your role and current task before proceeding.\n\n| File | Description |\n|------|-------------|\n${rows}`;\n}\n"]}
|
package/dist/standards.js
CHANGED
|
@@ -1,60 +1,6 @@
|
|
|
1
|
-
import { access, readdir, readFile, stat } from "node:fs/promises";
|
|
1
|
+
import { access, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
export const DEFAULT_ROLE_STANDARDS = {
|
|
5
|
-
planner: [".artifact-standards.md"],
|
|
6
|
-
architect: [
|
|
7
|
-
"architecture-principles.md",
|
|
8
|
-
"tech-stack.md",
|
|
9
|
-
"design-guide.md",
|
|
10
|
-
"api-design.md",
|
|
11
|
-
"security-policy.md",
|
|
12
|
-
".artifact-standards.md",
|
|
13
|
-
],
|
|
14
|
-
developer: [
|
|
15
|
-
"coding-style.md",
|
|
16
|
-
"coding-conventions.md",
|
|
17
|
-
"tech-stack.md",
|
|
18
|
-
"api-design.md",
|
|
19
|
-
"documentation.md",
|
|
20
|
-
"dependency-policy.md",
|
|
21
|
-
"performance-guidelines.md",
|
|
22
|
-
"accessibility.md",
|
|
23
|
-
"observability.md",
|
|
24
|
-
"logging.md",
|
|
25
|
-
],
|
|
26
|
-
tester: ["testing-standards.md", "coding-conventions.md", "performance-guidelines.md", ".artifact-standards.md"],
|
|
27
|
-
reviewer: [
|
|
28
|
-
"coding-style.md",
|
|
29
|
-
"coding-conventions.md",
|
|
30
|
-
"architecture-principles.md",
|
|
31
|
-
"api-design.md",
|
|
32
|
-
"testing-standards.md",
|
|
33
|
-
"documentation.md",
|
|
34
|
-
"security-policy.md",
|
|
35
|
-
"dependency-policy.md",
|
|
36
|
-
"performance-guidelines.md",
|
|
37
|
-
"accessibility.md",
|
|
38
|
-
"observability.md",
|
|
39
|
-
"logging.md",
|
|
40
|
-
".artifact-standards.md",
|
|
41
|
-
],
|
|
42
|
-
cicd: ["git-workflow.md"],
|
|
43
|
-
analyzer: ["architecture-principles.md", "tech-stack.md", ".artifact-standards.md"],
|
|
44
|
-
diagnostician: ["coding-conventions.md", "observability.md", "logging.md", ".artifact-standards.md"],
|
|
45
|
-
discovery: [".artifact-standards.md"],
|
|
46
|
-
projectAnalyzer: [".artifact-standards.md"],
|
|
47
|
-
documenter: ["documentation.md", ".artifact-standards.md"],
|
|
48
|
-
standardsEnricher: [],
|
|
49
|
-
systemArchitect: [
|
|
50
|
-
"architecture-principles.md",
|
|
51
|
-
"tech-stack.md",
|
|
52
|
-
"design-guide.md",
|
|
53
|
-
"api-design.md",
|
|
54
|
-
"security-policy.md",
|
|
55
|
-
".artifact-standards.md",
|
|
56
|
-
],
|
|
57
|
-
};
|
|
3
|
+
// ─── Internal Helpers ────────────────────────────────────────────────────────
|
|
58
4
|
/** Checks whether a file exists. */
|
|
59
5
|
async function fileExists(path) {
|
|
60
6
|
try {
|
|
@@ -98,57 +44,93 @@ async function resolveStandardFiles(sources, projectRoot) {
|
|
|
98
44
|
}
|
|
99
45
|
return result;
|
|
100
46
|
}
|
|
47
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
101
48
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
49
|
+
* Parses YAML frontmatter from markdown content.
|
|
50
|
+
* Extracts the `description` field. Returns empty object if no frontmatter found.
|
|
104
51
|
*/
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return customMapping[role] ?? DEFAULT_ROLE_STANDARDS[role];
|
|
52
|
+
export function parseFrontmatter(content) {
|
|
53
|
+
if (!content.startsWith("---")) {
|
|
54
|
+
return {};
|
|
109
55
|
}
|
|
110
|
-
|
|
56
|
+
const endIndex = content.indexOf("\n---", 3);
|
|
57
|
+
if (endIndex === -1) {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
const frontmatterBlock = content.slice(3, endIndex);
|
|
61
|
+
const descMatch = frontmatterBlock.match(/^\s*description\s*:\s*(.+)$/m);
|
|
62
|
+
if (!descMatch) {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
return { description: descMatch[1].trim() };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Extracts the first `# Heading` from markdown content.
|
|
69
|
+
* Returns undefined if no heading found.
|
|
70
|
+
*/
|
|
71
|
+
export function extractFirstHeading(content) {
|
|
72
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
73
|
+
return match ? match[1].trim() : undefined;
|
|
111
74
|
}
|
|
112
75
|
/**
|
|
113
|
-
*
|
|
76
|
+
* Generates a YAML frontmatter block with a description field.
|
|
114
77
|
*/
|
|
115
|
-
export
|
|
116
|
-
|
|
117
|
-
const mappedFiles = getRoleMappedFiles(role, config);
|
|
118
|
-
return mappedFiles.filter((fileName) => availableFiles.has(fileName));
|
|
78
|
+
export function generateFrontmatter(description) {
|
|
79
|
+
return `---\ndescription: ${description}\n---\n`;
|
|
119
80
|
}
|
|
120
81
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
82
|
+
* Scans all standards sources, parses frontmatter descriptions,
|
|
83
|
+
* and returns a list of index entries sorted by filename.
|
|
84
|
+
*
|
|
85
|
+
* Description priority: frontmatter description > first # heading > filename (without .md).
|
|
86
|
+
* Files missing frontmatter are auto-fixed: a frontmatter block is prepended and written back.
|
|
123
87
|
*/
|
|
124
|
-
export async function
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
for (const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
88
|
+
export async function buildStandardsIndex(config, projectRoot) {
|
|
89
|
+
const files = await resolveStandardFiles(config.standards.sources, projectRoot);
|
|
90
|
+
const entries = [];
|
|
91
|
+
const autoFixed = [];
|
|
92
|
+
for (const [filename, filePath] of files) {
|
|
93
|
+
const content = await readFile(filePath, "utf-8");
|
|
94
|
+
const frontmatter = parseFrontmatter(content);
|
|
95
|
+
let description;
|
|
96
|
+
if (frontmatter.description) {
|
|
97
|
+
description = frontmatter.description;
|
|
133
98
|
}
|
|
99
|
+
else {
|
|
100
|
+
const heading = extractFirstHeading(content);
|
|
101
|
+
description = heading ?? filename.replace(/\.md$/, "");
|
|
102
|
+
// Auto-prepend frontmatter and write back
|
|
103
|
+
const newContent = generateFrontmatter(description) + content;
|
|
104
|
+
try {
|
|
105
|
+
await writeFile(filePath, newContent, "utf-8");
|
|
106
|
+
autoFixed.push(filename);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Graceful failure: skip write, continue with derived description
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
entries.push({ filename, description });
|
|
134
113
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
return contents.join("\n\n---\n\n");
|
|
114
|
+
entries.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
115
|
+
return { entries, autoFixed };
|
|
139
116
|
}
|
|
140
117
|
/**
|
|
141
|
-
* Formats
|
|
142
|
-
* Returns
|
|
118
|
+
* Formats index entries as a system prompt section.
|
|
119
|
+
* Returns empty string if no entries.
|
|
143
120
|
*/
|
|
144
|
-
export function
|
|
145
|
-
if (
|
|
121
|
+
export function formatStandardsIndexPrompt(entries, standardsDir) {
|
|
122
|
+
if (entries.length === 0) {
|
|
146
123
|
return "";
|
|
147
124
|
}
|
|
125
|
+
const dir = standardsDir ?? ".vibe/standards";
|
|
126
|
+
const rows = entries.map((e) => `| \`${e.filename}\` | ${e.description} |`).join("\n");
|
|
148
127
|
return `## Organization Standards
|
|
149
128
|
|
|
150
|
-
|
|
129
|
+
The following standards are available in \`${dir}/\`.
|
|
130
|
+
Read the ones relevant to your role and current task before proceeding.
|
|
151
131
|
|
|
152
|
-
|
|
132
|
+
| File | Description |
|
|
133
|
+
|------|-------------|
|
|
134
|
+
${rows}`;
|
|
153
135
|
}
|
|
154
136
|
//# sourceMappingURL=standards.js.map
|
package/dist/standards.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"standards.js","sourceRoot":"","sources":["../src/standards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"standards.js","sourceRoot":"","sources":["../src/standards.ts"],"names":[],"mappings":"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","sourcesContent":["import { access, readdir, readFile, stat, writeFile } from \"node:fs/promises\";\nimport { basename, isAbsolute, join, resolve } from \"node:path\";\nimport type { VibeConfig } from \"./types.js\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Parsed frontmatter result. */\nexport interface StandardsFrontmatter {\n\tdescription?: string;\n}\n\n/** Single entry in the standards index. */\nexport interface StandardsIndexEntry {\n\tfilename: string;\n\tdescription: string;\n}\n\n/** Result of building the standards index. */\nexport interface StandardsIndexResult {\n\tentries: StandardsIndexEntry[];\n\t/** Filenames that had frontmatter auto-added. */\n\tautoFixed: string[];\n}\n\n// ─── Internal Helpers ────────────────────────────────────────────────────────\n\n/** Checks whether a file exists. */\nasync function fileExists(path: string): Promise<boolean> {\n\ttry {\n\t\tawait access(path);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Resolves a path to an absolute path relative to projectRoot. */\nfunction resolvePath(source: string, projectRoot: string): string {\n\tif (isAbsolute(source)) {\n\t\treturn source;\n\t}\n\treturn resolve(projectRoot, source);\n}\n\n/**\n * Traverses all source paths and returns a Map<filename, absolutePath> of discovered .md files.\n * When the same filename exists in multiple sources, the later source takes precedence.\n */\nasync function resolveStandardFiles(sources: string[], projectRoot: string): Promise<Map<string, string>> {\n\tconst result = new Map<string, string>();\n\n\tfor (const source of sources) {\n\t\tconst absPath = resolvePath(source, projectRoot);\n\t\tif (!(await fileExists(absPath))) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst info = await stat(absPath);\n\t\tif (info.isDirectory()) {\n\t\t\tconst entries = await readdir(absPath);\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (entry.endsWith(\".md\")) {\n\t\t\t\t\tresult.set(entry, join(absPath, entry));\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (info.isFile()) {\n\t\t\tresult.set(basename(absPath), absPath);\n\t\t}\n\t}\n\n\treturn result;\n}\n\n// ─── Public API ──────────────────────────────────────────────────────────────\n\n/**\n * Parses YAML frontmatter from markdown content.\n * Extracts the `description` field. Returns empty object if no frontmatter found.\n */\nexport function parseFrontmatter(content: string): StandardsFrontmatter {\n\tif (!content.startsWith(\"---\")) {\n\t\treturn {};\n\t}\n\n\tconst endIndex = content.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn {};\n\t}\n\n\tconst frontmatterBlock = content.slice(3, endIndex);\n\tconst descMatch = frontmatterBlock.match(/^\\s*description\\s*:\\s*(.+)$/m);\n\tif (!descMatch) {\n\t\treturn {};\n\t}\n\n\treturn { description: descMatch[1].trim() };\n}\n\n/**\n * Extracts the first `# Heading` from markdown content.\n * Returns undefined if no heading found.\n */\nexport function extractFirstHeading(content: string): string | undefined {\n\tconst match = content.match(/^#\\s+(.+)$/m);\n\treturn match ? match[1].trim() : undefined;\n}\n\n/**\n * Generates a YAML frontmatter block with a description field.\n */\nexport function generateFrontmatter(description: string): string {\n\treturn `---\\ndescription: ${description}\\n---\\n`;\n}\n\n/**\n * Scans all standards sources, parses frontmatter descriptions,\n * and returns a list of index entries sorted by filename.\n *\n * Description priority: frontmatter description > first # heading > filename (without .md).\n * Files missing frontmatter are auto-fixed: a frontmatter block is prepended and written back.\n */\nexport async function buildStandardsIndex(config: VibeConfig, projectRoot: string): Promise<StandardsIndexResult> {\n\tconst files = await resolveStandardFiles(config.standards.sources, projectRoot);\n\tconst entries: StandardsIndexEntry[] = [];\n\tconst autoFixed: string[] = [];\n\n\tfor (const [filename, filePath] of files) {\n\t\tconst content = await readFile(filePath, \"utf-8\");\n\t\tconst frontmatter = parseFrontmatter(content);\n\n\t\tlet description: string;\n\t\tif (frontmatter.description) {\n\t\t\tdescription = frontmatter.description;\n\t\t} else {\n\t\t\tconst heading = extractFirstHeading(content);\n\t\t\tdescription = heading ?? filename.replace(/\\.md$/, \"\");\n\n\t\t\t// Auto-prepend frontmatter and write back\n\t\t\tconst newContent = generateFrontmatter(description) + content;\n\t\t\ttry {\n\t\t\t\tawait writeFile(filePath, newContent, \"utf-8\");\n\t\t\t\tautoFixed.push(filename);\n\t\t\t} catch {\n\t\t\t\t// Graceful failure: skip write, continue with derived description\n\t\t\t}\n\t\t}\n\n\t\tentries.push({ filename, description });\n\t}\n\n\tentries.sort((a, b) => a.filename.localeCompare(b.filename));\n\treturn { entries, autoFixed };\n}\n\n/**\n * Formats index entries as a system prompt section.\n * Returns empty string if no entries.\n */\nexport function formatStandardsIndexPrompt(entries: StandardsIndexEntry[], standardsDir?: string): string {\n\tif (entries.length === 0) {\n\t\treturn \"\";\n\t}\n\n\tconst dir = standardsDir ?? \".vibe/standards\";\n\tconst rows = entries.map((e) => `| \\`${e.filename}\\` | ${e.description} |`).join(\"\\n\");\n\n\treturn `## Organization Standards\n\nThe following standards are available in \\`${dir}/\\`.\nRead the ones relevant to your role and current task before proceeding.\n\n| File | Description |\n|------|-------------|\n${rows}`;\n}\n"]}
|
package/dist/step-executor.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
2
2
|
import type { StepContext, StepExecutor, StepUsage } from "./pipeline.js";
|
|
3
|
-
import type { AgentRole, PipelineStep, TestingConfig } from "./types.js";
|
|
3
|
+
import type { AgentRole, CiCheck, PipelineStep, TestingConfig } from "./types.js";
|
|
4
4
|
/** Determines whether the role is readonly (cannot write files via tools). */
|
|
5
5
|
export declare function isReadonlyRole(role: AgentRole): boolean;
|
|
6
|
+
/** Roles that receive system-design.md as additional input context. */
|
|
7
|
+
export declare const SYSTEM_DESIGN_ROLES: ReadonlySet<AgentRole>;
|
|
6
8
|
/**
|
|
7
9
|
* Aggregates token usage from all assistant messages in a conversation.
|
|
8
10
|
* Returns undefined if no assistant messages with usage data are found.
|
|
@@ -24,10 +26,21 @@ export declare function loadInputArtifacts(inputs: string[], featureId: string,
|
|
|
24
26
|
* 3. Return the entire response if it's a single output
|
|
25
27
|
*/
|
|
26
28
|
export declare function extractArtifactContent(responseText: string, artifactName: string, isSingleOutput?: boolean): string | null;
|
|
29
|
+
/** Result of lightweight test config detection from project files. */
|
|
30
|
+
export interface DetectedTestConfig {
|
|
31
|
+
testCommand?: string;
|
|
32
|
+
ciChecks: CiCheck[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Detects test command and CI checks from project files when config.testing is empty.
|
|
36
|
+
* This is a fast file-existence check — NOT a full project-analyze re-run.
|
|
37
|
+
* Used for new projects where project-analyze ran before the project scaffold existed.
|
|
38
|
+
*/
|
|
39
|
+
export declare function detectTestConfig(projectRoot: string): Promise<DetectedTestConfig>;
|
|
27
40
|
/**
|
|
28
41
|
* Builds the prompt to send to the agent based on the step's agent role and action.
|
|
29
42
|
*/
|
|
30
|
-
export declare function buildActionPrompt(step: PipelineStep, featureId: string, inputContent: string, artifactDir: string,
|
|
43
|
+
export declare function buildActionPrompt(step: PipelineStep, featureId: string, inputContent: string, artifactDir: string, baseBranch?: string, testingConfig?: TestingConfig, projectRoot?: string): string;
|
|
31
44
|
/**
|
|
32
45
|
* Default StepExecutor implementation.
|
|
33
46
|
* Creates a role-specific agent, passes input artifacts as context,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"step-executor.d.ts","sourceRoot":"","sources":["../src/step-executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAGhE,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1E,OAAO,KAAK,EAAE,SAAS,EAAgB,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAcvF,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAEvD;AAID;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,SAAS,GAAG,SAAS,CA8B9E;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBnH;AAQD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACrC,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,cAAc,GAAE,OAAe,GAC7B,MAAM,GAAG,IAAI,CA6Bf;AAwGD;;GAEG;AACH,wBAAgB,iBAAiB,CAChC,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,eAAe,CAAC,EAAE,MAAM,EACxB,UAAU,CAAC,EAAE,MAAM,EACnB,aAAa,CAAC,EAAE,aAAa,EAC7B,WAAW,CAAC,EAAE,MAAM,GAClB,MAAM,CA4FR;AAsDD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,YAgHjC,CAAC","sourcesContent":["import type { AgentMessage } from \"@mariozechner/pi-agent-core\";\nimport { createRoleAgent, filterSerializableMessages, runAgentWithHistory } from \"./agent-factory.js\";\nimport { createModuleLogger, noopLogger } from \"./logger.js\";\nimport type { StepContext, StepExecutor, StepUsage } from \"./pipeline.js\";\nimport { getSystemPromptForRole } from \"./roles/index.js\";\nimport type { AgentRole, ArtifactName, PipelineStep, TestingConfig } from \"./types.js\";\n\n// ─── Readonly Role Detection ─────────────────────────────────────────────────\n\n/** Roles that cannot write files via tools. Artifacts are extracted from the response and saved to the store. */\nconst READONLY_ROLES: ReadonlySet<AgentRole> = new Set<AgentRole>([\n\t\"planner\",\n\t\"analyzer\",\n\t\"reviewer\",\n\t\"diagnostician\",\n\t\"discovery\",\n\t\"projectAnalyzer\",\n]);\n\n/** Determines whether the role is readonly (cannot write files via tools). */\nexport function isReadonlyRole(role: AgentRole): boolean {\n\treturn READONLY_ROLES.has(role);\n}\n\n// ─── Usage Aggregation ───────────────────────────────────────────────────────\n\n/**\n * Aggregates token usage from all assistant messages in a conversation.\n * Returns undefined if no assistant messages with usage data are found.\n */\nexport function aggregateUsage(messages: AgentMessage[]): StepUsage | undefined {\n\tlet input = 0;\n\tlet output = 0;\n\tlet cacheRead = 0;\n\tlet cacheWrite = 0;\n\tlet totalTokens = 0;\n\tlet cost = 0;\n\tlet found = false;\n\n\tfor (const msg of messages) {\n\t\tif (msg.role === \"assistant\" && \"usage\" in msg && msg.usage) {\n\t\t\tconst u = msg.usage as {\n\t\t\t\tinput: number;\n\t\t\t\toutput: number;\n\t\t\t\tcacheRead: number;\n\t\t\t\tcacheWrite: number;\n\t\t\t\ttotalTokens: number;\n\t\t\t\tcost: { total: number };\n\t\t\t};\n\t\t\tfound = true;\n\t\t\tinput += u.input;\n\t\t\toutput += u.output;\n\t\t\tcacheRead += u.cacheRead;\n\t\t\tcacheWrite += u.cacheWrite;\n\t\t\ttotalTokens += u.totalTokens;\n\t\t\tcost += u.cost.total;\n\t\t}\n\t}\n\n\treturn found ? { input, output, cacheRead, cacheWrite, totalTokens, cost } : undefined;\n}\n\n// ─── Input Artifact Loader ───────────────────────────────────────────────────\n\n/**\n * Reads the artifact files specified in step.inputs from the store\n * and combines them into a single context string.\n * Non-existent files are skipped.\n */\nexport async function loadInputArtifacts(inputs: string[], featureId: string, context: StepContext): Promise<string> {\n\tif (inputs.length === 0) {\n\t\treturn \"\";\n\t}\n\n\tconst sections: string[] = [];\n\n\tfor (const input of inputs) {\n\t\tconst hasFile = await context.store.hasArtifact(featureId, input as ArtifactName);\n\t\tif (!hasFile) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst content = await context.store.readArtifact(featureId, input as ArtifactName);\n\t\tsections.push(`## Input: ${input}\\n\\n${content}`);\n\t}\n\n\treturn sections.join(\"\\n\\n---\\n\\n\");\n}\n\n// ─── Artifact Extraction ─────────────────────────────────────────────────────\n\nfunction escapeRegex(str: string): string {\n\treturn str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Extracts artifact content from agent response text.\n * Used when a readonly role outputs markdown artifacts as text.\n *\n * Extraction strategies:\n * 1. Extract from ```artifactName code block (with nested fence pairing)\n * 2. Extract from ## artifactName section\n * 3. Return the entire response if it's a single output\n */\nexport function extractArtifactContent(\n\tresponseText: string,\n\tartifactName: string,\n\tisSingleOutput: boolean = false,\n): string | null {\n\tif (!responseText.trim()) {\n\t\treturn null;\n\t}\n\n\t// Pattern 1: ```artifactName\\n...\\n``` with nested code fence pairing.\n\t// A simple non-greedy regex fails when the content itself contains ``` fences\n\t// (e.g., directory trees or code examples inside a project-context.md artifact).\n\t// Instead, we find the opening fence and then iterate through subsequent fence\n\t// markers, pairing them (inner-open, inner-close) until we find the unpaired\n\t// closing fence for the outer block.\n\tconst content1 = extractFromCodeFence(responseText, artifactName);\n\tif (content1 !== null) {\n\t\treturn content1;\n\t}\n\n\t// Pattern 2: Content after ## artifactName (without m flag so $ matches end of string)\n\tconst pattern2 = new RegExp(`##\\\\s+${escapeRegex(artifactName)}\\\\s*\\\\n([\\\\s\\\\S]*?)(?=\\\\n##\\\\s|$)`);\n\tconst match2 = pattern2.exec(responseText);\n\tif (match2) {\n\t\treturn match2[1].trim();\n\t}\n\n\t// Pattern 3: Use the entire response if it's a single output\n\tif (isSingleOutput) {\n\t\treturn responseText.trim();\n\t}\n\n\treturn null;\n}\n\n/**\n * Extracts content from a fenced code block that may contain nested code fences.\n *\n * Algorithm:\n * 1. Find the opening fence (``` followed by artifactName)\n * 2. After the opening, scan for all ``` markers on their own lines\n * 3. Inner fences pair up sequentially (open+close). The first unpaired marker\n * is the closing fence of the outer block.\n */\nfunction extractFromCodeFence(text: string, artifactName: string): string | null {\n\t// Match the opening fence line: ```artifactName (possibly with trailing whitespace)\n\tconst openPattern = new RegExp(`^(\\`{3,})${escapeRegex(artifactName)}[ \\\\t]*$`, \"m\");\n\tconst openMatch = openPattern.exec(text);\n\tif (!openMatch) {\n\t\treturn null;\n\t}\n\n\tconst openTickCount = openMatch[1].length;\n\tconst contentStart = openMatch.index + openMatch[0].length + 1; // +1 for the newline\n\n\t// Collect fence markers after the opening fence, stopping at the next\n\t// artifact-level opening fence (a fence whose info string looks like a\n\t// file path — contains '.' or '/'). This prevents consuming fences that\n\t// belong to subsequent artifacts in multi-artifact responses.\n\tconst rest = text.slice(contentStart);\n\tconst fencePattern = /^(`{3,})[ \\t]*(\\S*)?[ \\t]*$/gm;\n\n\tinterface FenceMarker {\n\t\tindex: number;\n\t\ttickCount: number;\n\t\tinfoString: string;\n\t}\n\n\tconst markers: FenceMarker[] = [];\n\tfor (;;) {\n\t\tconst match = fencePattern.exec(rest);\n\t\tif (match === null) break;\n\t\tconst infoString = match[2] || \"\";\n\n\t\t// Stop at the next artifact-level opening fence (file-like info string)\n\t\tif (infoString !== \"\" && isArtifactInfoString(infoString)) {\n\t\t\tbreak;\n\t\t}\n\n\t\tmarkers.push({\n\t\t\tindex: match.index,\n\t\t\ttickCount: match[1].length,\n\t\t\tinfoString,\n\t\t});\n\t}\n\n\tif (markers.length === 0) {\n\t\treturn null;\n\t}\n\n\t// Within the bounded markers, find the outer closing fence.\n\t// Use the last bare fence (no info string) with >= openTickCount backticks.\n\t// Inner bare fences pair up (open+close) before it; the remaining one is\n\t// the outer close.\n\tfor (let i = markers.length - 1; i >= 0; i--) {\n\t\tconst m = markers[i];\n\t\tif (m.infoString === \"\" && m.tickCount >= openTickCount) {\n\t\t\tconst content = rest.slice(0, m.index);\n\t\t\treturn content.trim();\n\t\t}\n\t}\n\n\t// No valid closing fence found — return null (fall through to other patterns)\n\treturn null;\n}\n\n/**\n * Determines if a code fence info string looks like an artifact file path\n * rather than a code language identifier.\n *\n * Artifact info strings contain '.' (file extension) or '/' (path separator),\n * e.g., \"spec.md\", \"feat-auth/spec.md\", \"plan.json\".\n * Code language info strings are simple identifiers like \"python\", \"bash\", \"json\", \"text\".\n */\nfunction isArtifactInfoString(infoString: string): boolean {\n\treturn infoString.includes(\".\") || infoString.includes(\"/\");\n}\n\n// ─── Skill Search Instructions ───────────────────────────────────────────────\n\n/**\n * Instructions for searching past artifacts to add to specific actions when availableSkills is present.\n * The \"past feature artifacts\" keyword matches the qmd-memory description in Available Skills.\n */\nconst SKILL_SEARCH_INSTRUCTIONS: Readonly<Record<string, string>> = {\n\tplan: \"Before planning, search past feature artifacts for similar features, design patterns, or recurring decisions.\",\n\tdesign: \"Before designing, search past feature artifacts for related design patterns and architectural decisions.\",\n\ttest: \"Search past feature artifacts for related test patterns, integration points, and previously discovered defects before writing tests.\",\n\tregression:\n\t\t\"Search past feature artifacts for areas previously affected by regressions before running regression tests.\",\n\treview: \"Search past feature artifacts for recurring review feedback and common issues before reviewing.\",\n\tinvestigate: \"Search past feature artifacts for similar issues and prior investigation findings.\",\n\tdiagnose: \"Search past feature artifacts for similar bug patterns and fix strategies.\",\n};\n\n// ─── Action Prompt Builder ───────────────────────────────────────────────────\n\n/**\n * Builds the prompt to send to the agent based on the step's agent role and action.\n */\nexport function buildActionPrompt(\n\tstep: PipelineStep,\n\tfeatureId: string,\n\tinputContent: string,\n\tartifactDir: string,\n\tavailableSkills?: string,\n\tbaseBranch?: string,\n\ttestingConfig?: TestingConfig,\n\tprojectRoot?: string,\n): string {\n\tconst parts: string[] = [];\n\n\tparts.push(`You are working on feature \"${featureId}\".`);\n\tif (projectRoot) {\n\t\tparts.push(\n\t\t\t`The project root is \\`${projectRoot}\\`. All tool commands (bash, read, write, edit) ` +\n\t\t\t\t`execute relative to this directory. Do NOT prefix commands with \\`cd\\` to a different base directory.`,\n\t\t);\n\t}\n\tparts.push(\"\");\n\tparts.push(`## Task: ${step.action}`);\n\tparts.push(\"\");\n\tparts.push(getActionInstructions(step.agent, step.action, baseBranch));\n\n\tif (availableSkills) {\n\t\tconst skillInstruction = SKILL_SEARCH_INSTRUCTIONS[step.action];\n\t\tif (skillInstruction) {\n\t\t\tparts.push(\"\");\n\t\t\tparts.push(skillInstruction);\n\t\t}\n\t}\n\n\tif (inputContent) {\n\t\tparts.push(\"\");\n\t\tparts.push(\"## Input Artifacts\");\n\t\tparts.push(\"\");\n\t\tparts.push(inputContent);\n\t}\n\n\tif (step.outputs.length > 0) {\n\t\tparts.push(\"\");\n\t\tparts.push(\"## Output Artifacts\");\n\t\tparts.push(\"\");\n\t\tparts.push(\"You must produce the following artifacts:\");\n\t\tfor (const output of step.outputs) {\n\t\t\tparts.push(`- \\`${output}\\` → \\`${artifactDir}/${output}\\``);\n\t\t}\n\n\t\tif (isReadonlyRole(step.agent)) {\n\t\t\tparts.push(\"\");\n\t\t\tparts.push(\n\t\t\t\t\"Since you cannot write files directly, output the content of each artifact \" +\n\t\t\t\t\t\"in a markdown code block with the artifact filename as the info string. \" +\n\t\t\t\t\t\"For example: ```spec.md\\\\n(content)\\\\n```\",\n\t\t\t);\n\t\t} else {\n\t\t\tparts.push(\"\");\n\t\t\tparts.push(\"Write each artifact directly to the specified file path using your tools.\");\n\t\t}\n\t}\n\n\t// Inject testing config for developer and tester actions\n\tif (testingConfig?.testCommand) {\n\t\tconst shouldInject =\n\t\t\t(step.agent === \"developer\" && (step.action === \"implement\" || step.action === \"fix\")) ||\n\t\t\t(step.agent === \"tester\" && (step.action === \"test\" || step.action === \"regression\"));\n\n\t\tif (shouldInject) {\n\t\t\tparts.push(\"\");\n\t\t\tparts.push(\"## Project Test Configuration\");\n\t\t\tparts.push(\"\");\n\t\t\tparts.push(`- **Test command**: \\`${testingConfig.testCommand}\\``);\n\t\t\tparts.push(`- **Timeout**: ${testingConfig.testTimeout} seconds`);\n\t\t\tparts.push(`- **Run existing tests**: ${testingConfig.runExistingTests}`);\n\t\t\tif (testingConfig.excludePatterns.length > 0) {\n\t\t\t\tparts.push(`- **Exclude patterns**: ${testingConfig.excludePatterns.join(\", \")}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Inject CI checks for developer and tester actions (tester can write/edit files too)\n\tif (testingConfig?.ciChecks && testingConfig.ciChecks.length > 0) {\n\t\tconst shouldInjectCiChecks =\n\t\t\t(step.agent === \"developer\" && (step.action === \"implement\" || step.action === \"fix\")) ||\n\t\t\t(step.agent === \"tester\" && (step.action === \"test\" || step.action === \"regression\"));\n\n\t\tif (shouldInjectCiChecks) {\n\t\t\tparts.push(\"\");\n\t\t\tparts.push(\"## CI Verification Commands\");\n\t\t\tparts.push(\"\");\n\t\t\tparts.push(\n\t\t\t\t\"You MUST run the following checks after implementation. All must pass before the task is complete.\",\n\t\t\t);\n\t\t\tparts.push(\"\");\n\t\t\tfor (const check of testingConfig.ciChecks) {\n\t\t\t\tparts.push(`- **${check.name}**: \\`${check.command}\\``);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn parts.join(\"\\n\");\n}\n\n/**\n * Returns instructions for the given action.\n */\nfunction getActionInstructions(agent: AgentRole, action: string, baseBranch?: string): string {\n\tswitch (action) {\n\t\tcase \"plan\":\n\t\t\treturn \"Analyze the requirements and decompose them into features. Produce a spec.md for each feature and update plan.json.\";\n\n\t\tcase \"design\":\n\t\t\treturn \"Read the spec and produce a detailed design document with file structure, interfaces, and implementation guide.\";\n\n\t\tcase \"implement\":\n\t\t\treturn \"Implement the feature according to the spec and design documents. Write source code files and ensure they compile.\";\n\n\t\tcase \"fix\":\n\t\t\treturn \"Fix the bug as described in the diagnosis. Apply the minimal change necessary.\";\n\n\t\tcase \"test\":\n\t\t\treturn \"Write comprehensive tests for the feature and run them. Produce a test-report.md with results.\";\n\n\t\tcase \"regression\":\n\t\t\treturn \"Run existing tests to verify no regressions were introduced. Produce a regression-report.md with results.\";\n\n\t\tcase \"review\":\n\t\t\treturn \"Review the implementation for correctness, quality, architecture compliance, and standards compliance. Produce a review.md with your verdict.\";\n\n\t\tcase \"branch\":\n\t\t\treturn baseBranch\n\t\t\t\t? `Create a feature branch from \\`${baseBranch}\\`. Run \\`git checkout ${baseBranch}\\` first, then create the branch.`\n\t\t\t\t: `Create a feature branch for ${agent === \"cicd\" ? \"this feature\" : \"the implementation\"}.`;\n\n\t\tcase \"merge\":\n\t\t\treturn baseBranch\n\t\t\t\t? `Merge the feature branch into \\`${baseBranch}\\` using \\`--no-ff\\`. Run \\`git checkout ${baseBranch}\\` first, then merge. Do NOT run \\`git push\\` — push is handled by the orchestration system.`\n\t\t\t\t: \"Merge the feature branch after all checks pass. Do NOT run `git push`.\";\n\n\t\tcase \"analyze\":\n\t\t\treturn \"Analyze the impact of the proposed change on the existing codebase. Produce an impact-report.md.\";\n\n\t\tcase \"investigate\":\n\t\t\treturn \"Trace through the code to identify the root cause of the issue. Use read and bash tools to explore the codebase, run tests, add debug output if needed. Do not produce any artifacts — this is an exploratory step. Your findings will inform the subsequent diagnosis step.\";\n\n\t\tcase \"diagnose\":\n\t\t\treturn \"Analyze the bug, reproduce it, identify the root cause, and produce a diagnosis.md.\";\n\n\t\tdefault:\n\t\t\treturn `Execute the \"${action}\" task for this feature.`;\n\t}\n}\n\n// ─── Default Step Executor ───────────────────────────────────────────────────\n\n/**\n * Default StepExecutor implementation.\n * Creates a role-specific agent, passes input artifacts as context,\n * runs the agent, and saves the output artifacts.\n */\nexport const defaultStepExecutor: StepExecutor = async (step, featureId, context) => {\n\tconst { store, config, projectRoot } = context;\n\tconst log = createModuleLogger(context.logger ?? noopLogger, \"step-executor\");\n\n\t// 1. Ensure feature directory exists\n\tawait store.ensureFeatureDir(featureId);\n\n\t// 2. Load input artifacts\n\tlog.debug(`Loading input artifacts: [${step.inputs.join(\", \")}]`, { featureId, agent: step.agent });\n\tlet inputContent = await loadInputArtifacts(step.inputs, featureId, context);\n\n\t// 2b. Inject system-design.md for architect and reviewer roles (system-level architecture context)\n\tif ((step.agent === \"architect\" || step.agent === \"reviewer\") && (await store.hasSystemDesign())) {\n\t\tconst systemDesign = await store.readSystemDesign();\n\t\tconst systemDesignSection = `## Input: system-design.md (System Architecture)\\n\\n${systemDesign}`;\n\t\tinputContent = inputContent ? `${inputContent}\\n\\n---\\n\\n${systemDesignSection}` : systemDesignSection;\n\t\tlog.debug(`Injected system-design.md into ${step.agent} context`, { featureId });\n\t}\n\n\t// 3. Get system prompt\n\tconst systemPrompt = getSystemPromptForRole(step.agent);\n\n\t// 4. Artifact directory path\n\tconst artifactDir = store.getFeatureDir(featureId);\n\n\t// 5. Build execution prompt\n\tlet actionPrompt = buildActionPrompt(\n\t\tstep,\n\t\tfeatureId,\n\t\tinputContent,\n\t\tartifactDir,\n\t\tcontext.availableSkills,\n\t\tconfig.baseBranch,\n\t\tconfig.testing,\n\t\tprojectRoot,\n\t);\n\n\t// 6a. Load prior action history for the same role (only when step opts in)\n\tlet initialMessages: import(\"@mariozechner/pi-agent-core\").AgentMessage[] | undefined;\n\tif (step.inheritPriorHistory) {\n\t\tconst priorCheckpoints = await store.loadPriorAgentHistory(featureId, step.agent, step.action);\n\t\tif (priorCheckpoints.length > 0) {\n\t\t\tlog.debug(`Loading prior action history for ${step.agent}: ${priorCheckpoints.length} checkpoint(s)`, {\n\t\t\t\tfeatureId,\n\t\t\t\tagent: step.agent,\n\t\t\t});\n\t\t\tinitialMessages = priorCheckpoints.flatMap((cp) => cp.messages);\n\t\t}\n\t}\n\n\t// 6b. If retry history exists for the current action, append it (checkpoint restoration on retry)\n\tconst hasHistory = await store.hasAgentHistory(featureId, step.agent, step.action);\n\tif (hasHistory) {\n\t\tconst checkpoint = await store.loadAgentHistory(featureId, step.agent, step.action);\n\t\tif (checkpoint) {\n\t\t\tinitialMessages = [...(initialMessages ?? []), ...checkpoint.messages];\n\t\t\tactionPrompt = `[RETRY] Previous attempt for this step exists in your conversation history. Review what went wrong and try a different approach.\\n\\n${actionPrompt}`;\n\t\t}\n\t}\n\n\t// 7. Create agent (inject prior history)\n\tlog.info(`Creating agent: ${step.agent} for action: ${step.action}`, { featureId, hasHistory: !!initialMessages });\n\tconst agent = await createRoleAgent({\n\t\trole: step.agent,\n\t\tsystemPrompt,\n\t\tconfig,\n\t\tprojectRoot,\n\t\tmodel: context.model,\n\t\tfeatureContext: inputContent || undefined,\n\t\tgetApiKey: context.getApiKey,\n\t\tinitialMessages,\n\t\tavailableSkills: context.availableSkills,\n\t\tprojectContext: context.projectContext,\n\t});\n\n\t// 8. Agent registration callback\n\tcontext.onAgentCreated?.(agent);\n\n\ttry {\n\t\t// 9. Run agent (with history)\n\t\tlog.info(`Agent executing: ${step.agent}:${step.action}`, { featureId });\n\t\tconst result = await runAgentWithHistory(agent, actionPrompt);\n\n\t\t// 9b. Aggregate token usage\n\t\tcontext.lastStepUsage = aggregateUsage(result.messages);\n\n\t\t// 10. Save history checkpoint\n\t\tawait store.saveAgentHistory(featureId, {\n\t\t\trole: step.agent,\n\t\t\taction: step.action,\n\t\t\tmessages: filterSerializableMessages(result.messages),\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t});\n\n\t\t// 11. Save artifacts for readonly roles\n\t\tif (isReadonlyRole(step.agent) && step.outputs.length > 0) {\n\t\t\tconst isSingle = step.outputs.length === 1;\n\t\t\tfor (const output of step.outputs) {\n\t\t\t\tconst content = extractArtifactContent(result.text, output, isSingle);\n\t\t\t\tif (content) {\n\t\t\t\t\tlog.debug(`Extracted artifact: ${output}`, { featureId, agent: step.agent });\n\t\t\t\t\tawait store.writeArtifact(featureId, output as ArtifactName, content);\n\t\t\t\t} else {\n\t\t\t\t\tlog.warn(`Failed to extract artifact: ${output}`, { featureId, agent: step.agent });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tlog.info(`Agent completed: ${step.agent}:${step.action}`, { featureId });\n\t} finally {\n\t\t// 12. Agent release callback\n\t\tcontext.onAgentFinished?.();\n\t}\n};\n"]}
|
|
1
|
+
{"version":3,"file":"step-executor.d.ts","sourceRoot":"","sources":["../src/step-executor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAGhE,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1E,OAAO,KAAK,EAAE,SAAS,EAAgB,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAchG,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAEvD;AAID,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,SAAS,CAMrD,CAAC;AAIH;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,SAAS,GAAG,SAAS,CA8B9E;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBnH;AAQD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACrC,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,cAAc,GAAE,OAAe,GAC7B,MAAM,GAAG,IAAI,CA6Bf;AAuFD,sEAAsE;AACtE,MAAM,WAAW,kBAAkB;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACpB;AAYD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAsDvF;AAID;;GAEG;AACH,wBAAgB,iBAAiB,CAChC,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EACnB,aAAa,CAAC,EAAE,aAAa,EAC7B,WAAW,CAAC,EAAE,MAAM,GAClB,MAAM,CAoFR;AAwFD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,YAuIjC,CAAC","sourcesContent":["import { access, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport type { AgentMessage } from \"@mariozechner/pi-agent-core\";\nimport { createRoleAgent, filterSerializableMessages, runAgentWithHistory } from \"./agent-factory.js\";\nimport { createModuleLogger, noopLogger } from \"./logger.js\";\nimport type { StepContext, StepExecutor, StepUsage } from \"./pipeline.js\";\nimport { getSystemPromptForRole } from \"./roles/index.js\";\nimport type { AgentRole, ArtifactName, CiCheck, PipelineStep, TestingConfig } from \"./types.js\";\n\n// ─── Readonly Role Detection ─────────────────────────────────────────────────\n\n/** Roles that cannot write files via tools. Artifacts are extracted from the response and saved to the store. */\nconst READONLY_ROLES: ReadonlySet<AgentRole> = new Set<AgentRole>([\n\t\"planner\",\n\t\"analyzer\",\n\t\"reviewer\",\n\t\"diagnostician\",\n\t\"discovery\",\n\t\"projectAnalyzer\",\n]);\n\n/** Determines whether the role is readonly (cannot write files via tools). */\nexport function isReadonlyRole(role: AgentRole): boolean {\n\treturn READONLY_ROLES.has(role);\n}\n\n// ─── System Design Injection ─────────────────────────────────────────────────\n\n/** Roles that receive system-design.md as additional input context. */\nexport const SYSTEM_DESIGN_ROLES: ReadonlySet<AgentRole> = new Set<AgentRole>([\n\t\"architect\",\n\t\"reviewer\",\n\t\"developer\",\n\t\"tester\",\n\t\"diagnostician\",\n]);\n\n// ─── Usage Aggregation ───────────────────────────────────────────────────────\n\n/**\n * Aggregates token usage from all assistant messages in a conversation.\n * Returns undefined if no assistant messages with usage data are found.\n */\nexport function aggregateUsage(messages: AgentMessage[]): StepUsage | undefined {\n\tlet input = 0;\n\tlet output = 0;\n\tlet cacheRead = 0;\n\tlet cacheWrite = 0;\n\tlet totalTokens = 0;\n\tlet cost = 0;\n\tlet found = false;\n\n\tfor (const msg of messages) {\n\t\tif (msg.role === \"assistant\" && \"usage\" in msg && msg.usage) {\n\t\t\tconst u = msg.usage as {\n\t\t\t\tinput: number;\n\t\t\t\toutput: number;\n\t\t\t\tcacheRead: number;\n\t\t\t\tcacheWrite: number;\n\t\t\t\ttotalTokens: number;\n\t\t\t\tcost: { total: number };\n\t\t\t};\n\t\t\tfound = true;\n\t\t\tinput += u.input;\n\t\t\toutput += u.output;\n\t\t\tcacheRead += u.cacheRead;\n\t\t\tcacheWrite += u.cacheWrite;\n\t\t\ttotalTokens += u.totalTokens;\n\t\t\tcost += u.cost.total;\n\t\t}\n\t}\n\n\treturn found ? { input, output, cacheRead, cacheWrite, totalTokens, cost } : undefined;\n}\n\n// ─── Input Artifact Loader ───────────────────────────────────────────────────\n\n/**\n * Reads the artifact files specified in step.inputs from the store\n * and combines them into a single context string.\n * Non-existent files are skipped.\n */\nexport async function loadInputArtifacts(inputs: string[], featureId: string, context: StepContext): Promise<string> {\n\tif (inputs.length === 0) {\n\t\treturn \"\";\n\t}\n\n\tconst sections: string[] = [];\n\n\tfor (const input of inputs) {\n\t\tconst hasFile = await context.store.hasArtifact(featureId, input as ArtifactName);\n\t\tif (!hasFile) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst content = await context.store.readArtifact(featureId, input as ArtifactName);\n\t\tsections.push(`## Input: ${input}\\n\\n${content}`);\n\t}\n\n\treturn sections.join(\"\\n\\n---\\n\\n\");\n}\n\n// ─── Artifact Extraction ─────────────────────────────────────────────────────\n\nfunction escapeRegex(str: string): string {\n\treturn str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Extracts artifact content from agent response text.\n * Used when a readonly role outputs markdown artifacts as text.\n *\n * Extraction strategies:\n * 1. Extract from ```artifactName code block (with nested fence pairing)\n * 2. Extract from ## artifactName section\n * 3. Return the entire response if it's a single output\n */\nexport function extractArtifactContent(\n\tresponseText: string,\n\tartifactName: string,\n\tisSingleOutput: boolean = false,\n): string | null {\n\tif (!responseText.trim()) {\n\t\treturn null;\n\t}\n\n\t// Pattern 1: ```artifactName\\n...\\n``` with nested code fence pairing.\n\t// A simple non-greedy regex fails when the content itself contains ``` fences\n\t// (e.g., directory trees or code examples inside a project-context.md artifact).\n\t// Instead, we find the opening fence and then iterate through subsequent fence\n\t// markers, pairing them (inner-open, inner-close) until we find the unpaired\n\t// closing fence for the outer block.\n\tconst content1 = extractFromCodeFence(responseText, artifactName);\n\tif (content1 !== null) {\n\t\treturn content1;\n\t}\n\n\t// Pattern 2: Content after ## artifactName (without m flag so $ matches end of string)\n\tconst pattern2 = new RegExp(`##\\\\s+${escapeRegex(artifactName)}\\\\s*\\\\n([\\\\s\\\\S]*?)(?=\\\\n##\\\\s|$)`);\n\tconst match2 = pattern2.exec(responseText);\n\tif (match2) {\n\t\treturn match2[1].trim();\n\t}\n\n\t// Pattern 3: Use the entire response if it's a single output\n\tif (isSingleOutput) {\n\t\treturn responseText.trim();\n\t}\n\n\treturn null;\n}\n\n/**\n * Extracts content from a fenced code block that may contain nested code fences.\n *\n * Algorithm:\n * 1. Find the opening fence (``` followed by artifactName)\n * 2. After the opening, scan for all ``` markers on their own lines\n * 3. Inner fences pair up sequentially (open+close). The first unpaired marker\n * is the closing fence of the outer block.\n */\nfunction extractFromCodeFence(text: string, artifactName: string): string | null {\n\t// Match the opening fence line: ```artifactName (possibly with trailing whitespace)\n\tconst openPattern = new RegExp(`^(\\`{3,})${escapeRegex(artifactName)}[ \\\\t]*$`, \"m\");\n\tconst openMatch = openPattern.exec(text);\n\tif (!openMatch) {\n\t\treturn null;\n\t}\n\n\tconst openTickCount = openMatch[1].length;\n\tconst contentStart = openMatch.index + openMatch[0].length + 1; // +1 for the newline\n\n\t// Collect fence markers after the opening fence, stopping at the next\n\t// artifact-level opening fence (a fence whose info string looks like a\n\t// file path — contains '.' or '/'). This prevents consuming fences that\n\t// belong to subsequent artifacts in multi-artifact responses.\n\tconst rest = text.slice(contentStart);\n\tconst fencePattern = /^(`{3,})[ \\t]*(\\S*)?[ \\t]*$/gm;\n\n\tinterface FenceMarker {\n\t\tindex: number;\n\t\ttickCount: number;\n\t\tinfoString: string;\n\t}\n\n\tconst markers: FenceMarker[] = [];\n\tfor (;;) {\n\t\tconst match = fencePattern.exec(rest);\n\t\tif (match === null) break;\n\t\tconst infoString = match[2] || \"\";\n\n\t\t// Stop at the next artifact-level opening fence (file-like info string)\n\t\tif (infoString !== \"\" && isArtifactInfoString(infoString)) {\n\t\t\tbreak;\n\t\t}\n\n\t\tmarkers.push({\n\t\t\tindex: match.index,\n\t\t\ttickCount: match[1].length,\n\t\t\tinfoString,\n\t\t});\n\t}\n\n\tif (markers.length === 0) {\n\t\treturn null;\n\t}\n\n\t// Within the bounded markers, find the outer closing fence.\n\t// Use the last bare fence (no info string) with >= openTickCount backticks.\n\t// Inner bare fences pair up (open+close) before it; the remaining one is\n\t// the outer close.\n\tfor (let i = markers.length - 1; i >= 0; i--) {\n\t\tconst m = markers[i];\n\t\tif (m.infoString === \"\" && m.tickCount >= openTickCount) {\n\t\t\tconst content = rest.slice(0, m.index);\n\t\t\treturn content.trim();\n\t\t}\n\t}\n\n\t// No valid closing fence found — return null (fall through to other patterns)\n\treturn null;\n}\n\n/**\n * Determines if a code fence info string looks like an artifact file path\n * rather than a code language identifier.\n *\n * Artifact info strings contain '.' (file extension) or '/' (path separator),\n * e.g., \"spec.md\", \"feat-auth/spec.md\", \"plan.json\".\n * Code language info strings are simple identifiers like \"python\", \"bash\", \"json\", \"text\".\n */\nfunction isArtifactInfoString(infoString: string): boolean {\n\treturn infoString.includes(\".\") || infoString.includes(\"/\");\n}\n\n// ─── Lightweight Test Config Detection ───────────────────────────────────────\n\n/** Result of lightweight test config detection from project files. */\nexport interface DetectedTestConfig {\n\ttestCommand?: string;\n\tciChecks: CiCheck[];\n}\n\n/** Checks whether a file exists at the given path. */\nasync function fileExistsAt(path: string): Promise<boolean> {\n\ttry {\n\t\tawait access(path);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Detects test command and CI checks from project files when config.testing is empty.\n * This is a fast file-existence check — NOT a full project-analyze re-run.\n * Used for new projects where project-analyze ran before the project scaffold existed.\n */\nexport async function detectTestConfig(projectRoot: string): Promise<DetectedTestConfig> {\n\tconst result: DetectedTestConfig = { ciChecks: [] };\n\n\t// 1. Read package.json scripts.test\n\ttry {\n\t\tconst raw = await readFile(join(projectRoot, \"package.json\"), \"utf-8\");\n\t\tconst pkg = JSON.parse(raw) as { scripts?: Record<string, string> };\n\t\tconst testScript = pkg.scripts?.test;\n\t\tif (testScript && testScript !== 'echo \"Error: no test specified\" && exit 1') {\n\t\t\tresult.testCommand = testScript;\n\t\t}\n\t} catch {\n\t\t/* no package.json */\n\t}\n\n\t// 2. Check for vitest/jest config → fallback test command\n\tif (!result.testCommand) {\n\t\tconst testConfigs: Array<{ file: string; command: string }> = [\n\t\t\t{ file: \"vitest.config.ts\", command: \"npx vitest run\" },\n\t\t\t{ file: \"vitest.config.js\", command: \"npx vitest run\" },\n\t\t\t{ file: \"vite.config.ts\", command: \"npx vitest run\" },\n\t\t\t{ file: \"jest.config.ts\", command: \"npx jest\" },\n\t\t\t{ file: \"jest.config.js\", command: \"npx jest\" },\n\t\t];\n\t\tfor (const { file, command } of testConfigs) {\n\t\t\tif (await fileExistsAt(join(projectRoot, file))) {\n\t\t\t\tresult.testCommand = command;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// 3. Check for tsconfig.json → tsc CI check\n\tif (await fileExistsAt(join(projectRoot, \"tsconfig.json\"))) {\n\t\tresult.ciChecks.push({ name: \"TypeScript\", command: \"npx tsc --noEmit\" });\n\t}\n\n\t// 4. Check for eslint config → eslint CI check\n\tconst eslintConfigs = [\n\t\t\"eslint.config.js\",\n\t\t\"eslint.config.ts\",\n\t\t\"eslint.config.mjs\",\n\t\t\".eslintrc.js\",\n\t\t\".eslintrc.json\",\n\t\t\".eslintrc.yml\",\n\t];\n\tfor (const file of eslintConfigs) {\n\t\tif (await fileExistsAt(join(projectRoot, file))) {\n\t\t\tresult.ciChecks.push({ name: \"ESLint\", command: \"npx eslint .\" });\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n// ─── Action Prompt Builder ───────────────────────────────────────────────────\n\n/**\n * Builds the prompt to send to the agent based on the step's agent role and action.\n */\nexport function buildActionPrompt(\n\tstep: PipelineStep,\n\tfeatureId: string,\n\tinputContent: string,\n\tartifactDir: string,\n\tbaseBranch?: string,\n\ttestingConfig?: TestingConfig,\n\tprojectRoot?: string,\n): string {\n\tconst parts: string[] = [];\n\n\tparts.push(`You are working on feature \"${featureId}\".`);\n\tif (projectRoot) {\n\t\tparts.push(\n\t\t\t`The project root is \\`${projectRoot}\\`. All tool commands (bash, read, write, edit) ` +\n\t\t\t\t`execute relative to this directory. Do NOT prefix commands with \\`cd\\` to a different base directory.`,\n\t\t);\n\t}\n\tparts.push(\"\");\n\tparts.push(`## Task: ${step.action}`);\n\tparts.push(\"\");\n\tparts.push(getActionInstructions(step.agent, step.action, baseBranch));\n\n\tif (inputContent) {\n\t\tparts.push(\"\");\n\t\tparts.push(\"## Input Artifacts\");\n\t\tparts.push(\"\");\n\t\tparts.push(inputContent);\n\t}\n\n\tif (step.outputs.length > 0) {\n\t\tparts.push(\"\");\n\t\tparts.push(\"## Output Artifacts\");\n\t\tparts.push(\"\");\n\t\tparts.push(\"You must produce the following artifacts:\");\n\t\tfor (const output of step.outputs) {\n\t\t\tparts.push(`- \\`${output}\\` → \\`${artifactDir}/${output}\\``);\n\t\t}\n\n\t\tif (isReadonlyRole(step.agent)) {\n\t\t\tparts.push(\"\");\n\t\t\tparts.push(\n\t\t\t\t\"Since you cannot write files directly, output the content of each artifact \" +\n\t\t\t\t\t\"in a markdown code block with the artifact filename as the info string. \" +\n\t\t\t\t\t\"For example: ```spec.md\\\\n(content)\\\\n```\",\n\t\t\t);\n\t\t} else {\n\t\t\tparts.push(\"\");\n\t\t\tparts.push(\"Write each artifact directly to the specified file path using your tools.\");\n\t\t}\n\t}\n\n\t// Inject testing config for developer and tester actions\n\tif (testingConfig?.testCommand) {\n\t\tconst shouldInject =\n\t\t\t(step.agent === \"developer\" && (step.action === \"implement\" || step.action === \"fix\")) ||\n\t\t\t(step.agent === \"tester\" && (step.action === \"test\" || step.action === \"regression\"));\n\n\t\tif (shouldInject) {\n\t\t\tparts.push(\"\");\n\t\t\tparts.push(\"## Project Test Configuration\");\n\t\t\tparts.push(\"\");\n\t\t\tparts.push(`- **Test command**: \\`${testingConfig.testCommand}\\``);\n\t\t\tparts.push(`- **Timeout**: ${testingConfig.testTimeout} seconds`);\n\t\t\tparts.push(`- **Run existing tests**: ${testingConfig.runExistingTests}`);\n\t\t\tif (testingConfig.excludePatterns.length > 0) {\n\t\t\t\tparts.push(`- **Exclude patterns**: ${testingConfig.excludePatterns.join(\", \")}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Inject CI checks for developer and tester actions (tester can write/edit files too)\n\tif (testingConfig?.ciChecks && testingConfig.ciChecks.length > 0) {\n\t\tconst shouldInjectCiChecks =\n\t\t\t(step.agent === \"developer\" && (step.action === \"implement\" || step.action === \"fix\")) ||\n\t\t\t(step.agent === \"tester\" && (step.action === \"test\" || step.action === \"regression\"));\n\n\t\tif (shouldInjectCiChecks) {\n\t\t\tparts.push(\"\");\n\t\t\tparts.push(\"## CI Verification Commands\");\n\t\t\tparts.push(\"\");\n\t\t\tparts.push(\n\t\t\t\t\"You MUST run the following checks after implementation. All must pass before the task is complete.\",\n\t\t\t);\n\t\t\tparts.push(\"\");\n\t\t\tfor (const check of testingConfig.ciChecks) {\n\t\t\t\tparts.push(`- **${check.name}**: \\`${check.command}\\``);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn parts.join(\"\\n\");\n}\n\n/**\n * Returns instructions for the given action.\n */\nfunction getActionInstructions(agent: AgentRole, action: string, baseBranch?: string): string {\n\tswitch (action) {\n\t\tcase \"plan\":\n\t\t\treturn \"Analyze the requirements and decompose them into features. Produce a spec.md for each feature and update plan.json.\";\n\n\t\tcase \"design\":\n\t\t\treturn \"Read the spec and produce a detailed design document with file structure, interfaces, and implementation guide.\";\n\n\t\tcase \"implement\":\n\t\t\treturn \"Implement the feature according to the spec and design documents. Write source code files and ensure they compile.\";\n\n\t\tcase \"fix\":\n\t\t\treturn \"Fix the bug as described in the diagnosis. Apply the minimal change necessary.\";\n\n\t\tcase \"test\":\n\t\t\treturn baseBranch\n\t\t\t\t? \"Write comprehensive tests for the feature and run them. Produce a test-report.md with results.\\n\\n\" +\n\t\t\t\t\t\t`**Execution order**: ` +\n\t\t\t\t\t\t`1) Run \\`git diff ${baseBranch}..HEAD --name-only\\` to identify changed files. ` +\n\t\t\t\t\t\t`2) Read the implementation files to understand types and interfaces. ` +\n\t\t\t\t\t\t`3) Write ALL test files before running anything. ` +\n\t\t\t\t\t\t`4) Run the test suite ONCE. If tests fail, fix all failures, then run ONCE more. ` +\n\t\t\t\t\t\t`5) Run CI verification commands (type-check, lint) ONCE at the end, after all tests pass. ` +\n\t\t\t\t\t\t`Do not interleave writing and running — batch all writes first, then run.`\n\t\t\t\t: \"Write comprehensive tests for the feature and run them. Produce a test-report.md with results.\\n\\n\" +\n\t\t\t\t\t\t\"**Execution order**: \" +\n\t\t\t\t\t\t\"1) Read the implementation files to understand types and interfaces. \" +\n\t\t\t\t\t\t\"2) Write ALL test files before running anything. \" +\n\t\t\t\t\t\t\"3) Run the test suite ONCE. If tests fail, fix all failures, then run ONCE more. \" +\n\t\t\t\t\t\t\"4) Run CI verification commands (type-check, lint) ONCE at the end, after all tests pass. \" +\n\t\t\t\t\t\t\"Do not interleave writing and running — batch all writes first, then run.\";\n\n\t\tcase \"regression\":\n\t\t\treturn baseBranch\n\t\t\t\t? \"Run existing tests to verify no regressions were introduced. Produce a regression-report.md with results.\\n\\n\" +\n\t\t\t\t\t\t`**Execution order**: ` +\n\t\t\t\t\t\t`1) Run \\`git diff ${baseBranch}..HEAD --name-only\\` to identify changed files and test files. ` +\n\t\t\t\t\t\t`2) Run the full test suite ONCE with a single command. ` +\n\t\t\t\t\t\t`3) Run CI verification commands (type-check, lint) ONCE at the end. ` +\n\t\t\t\t\t\t`Do not run tests file-by-file — use a single test runner invocation.`\n\t\t\t\t: \"Run existing tests to verify no regressions were introduced. Produce a regression-report.md with results.\\n\\n\" +\n\t\t\t\t\t\t\"**Execution order**: \" +\n\t\t\t\t\t\t\"1) Identify the test files to run from test-report.md. \" +\n\t\t\t\t\t\t\"2) Run the full test suite ONCE with a single command. \" +\n\t\t\t\t\t\t\"3) Run CI verification commands (type-check, lint) ONCE at the end. \" +\n\t\t\t\t\t\t\"Do not run tests file-by-file — use a single test runner invocation.\";\n\n\t\tcase \"review\":\n\t\t\treturn baseBranch\n\t\t\t\t? `Review the implementation for correctness, quality, architecture compliance, and standards compliance. Produce a review.md with your verdict.\\n\\n` +\n\t\t\t\t\t\t`**Start by running \\`git diff ${baseBranch}..HEAD\\`** to see all changes at once. ` +\n\t\t\t\t\t\t`Use \\`git diff --stat ${baseBranch}..HEAD\\` for an overview.`\n\t\t\t\t: \"Review the implementation for correctness, quality, architecture compliance, and standards compliance. Produce a review.md with your verdict.\";\n\n\t\tcase \"branch\":\n\t\t\treturn baseBranch\n\t\t\t\t? `Create a feature branch from \\`${baseBranch}\\`. Run \\`git checkout ${baseBranch}\\` first, then create the branch.`\n\t\t\t\t: `Create a feature branch for ${agent === \"cicd\" ? \"this feature\" : \"the implementation\"}.`;\n\n\t\tcase \"merge\":\n\t\t\treturn baseBranch\n\t\t\t\t? `Merge the feature branch into \\`${baseBranch}\\` using \\`--no-ff\\`.\\n\\n` +\n\t\t\t\t\t\t`**Start by running \\`git diff ${baseBranch}..HEAD --name-only\\`** to get the complete list of changed files. ` +\n\t\t\t\t\t\t`Use this list for \\`git add\\` in a single command — do NOT use \\`ls\\`, \\`find\\`, or \\`git status\\` to discover files.\\n\\n` +\n\t\t\t\t\t\t`Run \\`git checkout ${baseBranch}\\` first, then merge. Do NOT run \\`git push\\` — push is handled by the orchestration system.`\n\t\t\t\t: \"Merge the feature branch after all checks pass. Do NOT run `git push`.\";\n\n\t\tcase \"analyze\":\n\t\t\treturn \"Analyze the impact of the proposed change on the existing codebase. Produce an impact-report.md.\";\n\n\t\tcase \"investigate\":\n\t\t\treturn \"Trace through the code to identify the root cause of the issue. Use read and bash tools to explore the codebase, run tests, add debug output if needed. Do not produce any artifacts — this is an exploratory step. Your findings will inform the subsequent diagnosis step.\";\n\n\t\tcase \"diagnose\":\n\t\t\treturn \"Analyze the bug, reproduce it, identify the root cause, and produce a diagnosis.md.\";\n\n\t\tdefault:\n\t\t\treturn `Execute the \"${action}\" task for this feature.`;\n\t}\n}\n\n// ─── Default Step Executor ───────────────────────────────────────────────────\n\n/**\n * Default StepExecutor implementation.\n * Creates a role-specific agent, passes input artifacts as context,\n * runs the agent, and saves the output artifacts.\n */\nexport const defaultStepExecutor: StepExecutor = async (step, featureId, context) => {\n\tconst { store, config, projectRoot } = context;\n\tconst log = createModuleLogger(context.logger ?? noopLogger, \"step-executor\");\n\n\t// 1. Ensure feature directory exists\n\tawait store.ensureFeatureDir(featureId);\n\n\t// 2. Load input artifacts\n\tlog.debug(`Loading input artifacts: [${step.inputs.join(\", \")}]`, { featureId, agent: step.agent });\n\tlet inputContent = await loadInputArtifacts(step.inputs, featureId, context);\n\n\t// 2b. Inject system-design.md for roles that need system-level architecture context\n\tif (SYSTEM_DESIGN_ROLES.has(step.agent) && (await store.hasSystemDesign())) {\n\t\tconst systemDesign = await store.readSystemDesign();\n\t\tconst systemDesignSection = `## Input: system-design.md (System Architecture)\\n\\n${systemDesign}`;\n\t\tinputContent = inputContent ? `${inputContent}\\n\\n---\\n\\n${systemDesignSection}` : systemDesignSection;\n\t\tlog.debug(`Injected system-design.md into ${step.agent} context`, { featureId });\n\t}\n\n\t// 3. Get system prompt\n\tconst systemPrompt = getSystemPromptForRole(step.agent);\n\n\t// 4. Artifact directory path\n\tconst artifactDir = store.getFeatureDir(featureId);\n\n\t// 4b. Lightweight test config detection for new projects\n\t// When config.testing.testCommand is empty (project-analyze ran before project existed),\n\t// detect from package.json and config files so the tester has concrete commands to use.\n\tlet effectiveTestingConfig = config.testing;\n\tif (\n\t\tstep.agent === \"tester\" &&\n\t\t(step.action === \"test\" || step.action === \"regression\") &&\n\t\t!config.testing?.testCommand\n\t) {\n\t\tconst detected = await detectTestConfig(projectRoot);\n\t\tif (detected.testCommand || detected.ciChecks.length > 0) {\n\t\t\teffectiveTestingConfig = {\n\t\t\t\ttestCommand: detected.testCommand ?? \"\",\n\t\t\t\ttestTimeout: config.testing?.testTimeout ?? 300,\n\t\t\t\trunExistingTests: config.testing?.runExistingTests ?? false,\n\t\t\t\texcludePatterns: config.testing?.excludePatterns ?? [],\n\t\t\t\tciChecks: [...(config.testing?.ciChecks ?? []), ...detected.ciChecks],\n\t\t\t};\n\t\t\tlog.debug(\n\t\t\t\t`Detected test config for new project: testCommand=${detected.testCommand ?? \"(none)\"}, ciChecks=${detected.ciChecks.length}`,\n\t\t\t\t{ featureId },\n\t\t\t);\n\t\t}\n\t}\n\n\t// 5. Build execution prompt\n\tlet actionPrompt = buildActionPrompt(\n\t\tstep,\n\t\tfeatureId,\n\t\tinputContent,\n\t\tartifactDir,\n\t\tconfig.baseBranch,\n\t\teffectiveTestingConfig,\n\t\tprojectRoot,\n\t);\n\n\t// 6a. Load prior action history for the same role (only when step opts in)\n\tlet initialMessages: import(\"@mariozechner/pi-agent-core\").AgentMessage[] | undefined;\n\tif (step.inheritPriorHistory) {\n\t\tconst priorCheckpoints = await store.loadPriorAgentHistory(featureId, step.agent, step.action);\n\t\tif (priorCheckpoints.length > 0) {\n\t\t\tlog.debug(`Loading prior action history for ${step.agent}: ${priorCheckpoints.length} checkpoint(s)`, {\n\t\t\t\tfeatureId,\n\t\t\t\tagent: step.agent,\n\t\t\t});\n\t\t\tinitialMessages = priorCheckpoints.flatMap((cp) => cp.messages);\n\t\t}\n\t}\n\n\t// 6b. If retry history exists for the current action, append it (checkpoint restoration on retry)\n\tconst hasHistory = await store.hasAgentHistory(featureId, step.agent, step.action);\n\tif (hasHistory) {\n\t\tconst checkpoint = await store.loadAgentHistory(featureId, step.agent, step.action);\n\t\tif (checkpoint) {\n\t\t\tinitialMessages = [...(initialMessages ?? []), ...checkpoint.messages];\n\t\t\tactionPrompt = `[RETRY] Previous attempt for this step exists in your conversation history. Review what went wrong and try a different approach.\\n\\n${actionPrompt}`;\n\t\t}\n\t}\n\n\t// 7. Create agent (inject prior history)\n\tlog.info(`Creating agent: ${step.agent} for action: ${step.action}`, { featureId, hasHistory: !!initialMessages });\n\tconst agent = await createRoleAgent({\n\t\trole: step.agent,\n\t\tsystemPrompt,\n\t\tconfig,\n\t\tprojectRoot,\n\t\tmodel: context.model,\n\t\tfeatureContext: inputContent || undefined,\n\t\tgetApiKey: context.getApiKey,\n\t\tinitialMessages,\n\t});\n\n\t// 8. Agent registration callback\n\tcontext.onAgentCreated?.(agent);\n\n\ttry {\n\t\t// 9. Run agent (with history)\n\t\tlog.info(`Agent executing: ${step.agent}:${step.action}`, { featureId });\n\t\tconst result = await runAgentWithHistory(agent, actionPrompt);\n\n\t\t// 9b. Aggregate token usage and capture response text\n\t\tcontext.lastStepUsage = aggregateUsage(result.messages);\n\t\tcontext.lastStepResponseText = result.text;\n\n\t\t// 10. Save history checkpoint\n\t\tawait store.saveAgentHistory(featureId, {\n\t\t\trole: step.agent,\n\t\t\taction: step.action,\n\t\t\tmessages: filterSerializableMessages(result.messages),\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t});\n\n\t\t// 11. Save artifacts for readonly roles\n\t\tif (isReadonlyRole(step.agent) && step.outputs.length > 0) {\n\t\t\tconst isSingle = step.outputs.length === 1;\n\t\t\tfor (const output of step.outputs) {\n\t\t\t\tconst content = extractArtifactContent(result.text, output, isSingle);\n\t\t\t\tif (content) {\n\t\t\t\t\tlog.debug(`Extracted artifact: ${output}`, { featureId, agent: step.agent });\n\t\t\t\t\tawait store.writeArtifact(featureId, output as ArtifactName, content);\n\t\t\t\t} else {\n\t\t\t\t\tlog.warn(`Failed to extract artifact: ${output}`, { featureId, agent: step.agent });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tlog.info(`Agent completed: ${step.agent}:${step.action}`, { featureId });\n\t} finally {\n\t\t// 12. Agent release callback\n\t\tcontext.onAgentFinished?.();\n\t}\n};\n"]}
|