@open-agent-toolkit/control-plane 0.0.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/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/project.d.ts +4 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +191 -0
- package/dist/recommender/boundary.d.ts +3 -0
- package/dist/recommender/boundary.d.ts.map +1 -0
- package/dist/recommender/boundary.js +22 -0
- package/dist/recommender/router.d.ts +3 -0
- package/dist/recommender/router.d.ts.map +1 -0
- package/dist/recommender/router.js +182 -0
- package/dist/shared/utils/errors.d.ts +2 -0
- package/dist/shared/utils/errors.d.ts.map +1 -0
- package/dist/shared/utils/errors.js +6 -0
- package/dist/shared/utils/frontmatter.d.ts +3 -0
- package/dist/shared/utils/frontmatter.d.ts.map +1 -0
- package/dist/shared/utils/frontmatter.js +22 -0
- package/dist/shared/utils/normalize.d.ts +5 -0
- package/dist/shared/utils/normalize.d.ts.map +1 -0
- package/dist/shared/utils/normalize.js +29 -0
- package/dist/state/artifacts.d.ts +3 -0
- package/dist/state/artifacts.d.ts.map +1 -0
- package/dist/state/artifacts.js +52 -0
- package/dist/state/parser.d.ts +27 -0
- package/dist/state/parser.d.ts.map +1 -0
- package/dist/state/parser.js +120 -0
- package/dist/state/reviews.d.ts +4 -0
- package/dist/state/reviews.d.ts.map +1 -0
- package/dist/state/reviews.js +60 -0
- package/dist/state/tasks.d.ts +3 -0
- package/dist/state/tasks.d.ts.map +1 -0
- package/dist/state/tasks.js +85 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Stang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @open-agent-toolkit/control-plane
|
|
2
|
+
|
|
3
|
+
Read-only OAT control-plane library for parsing project artifacts into structured state.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
`@open-agent-toolkit/control-plane` is the typed "read" layer behind OAT project inspection.
|
|
8
|
+
|
|
9
|
+
It is responsible for:
|
|
10
|
+
|
|
11
|
+
- parsing OAT project artifacts from disk
|
|
12
|
+
- aggregating task progress, artifact status, and review state
|
|
13
|
+
- recommending the next workflow skill from parsed project state
|
|
14
|
+
- returning stable typed objects for CLI and future UI consumers
|
|
15
|
+
|
|
16
|
+
The package is intentionally pure and read-only. It has no CLI, UI, or server dependencies beyond Node.js filesystem/path builtins and `yaml` for frontmatter parsing.
|
|
17
|
+
|
|
18
|
+
## Public API
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {
|
|
22
|
+
getProjectState,
|
|
23
|
+
listProjects,
|
|
24
|
+
recommendSkill,
|
|
25
|
+
} from '@open-agent-toolkit/control-plane';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### `getProjectState(projectPath)`
|
|
29
|
+
|
|
30
|
+
Reads one OAT project directory and returns a full `ProjectState` snapshot, including:
|
|
31
|
+
|
|
32
|
+
- phase and lifecycle status
|
|
33
|
+
- task progress and current task
|
|
34
|
+
- artifact and review status
|
|
35
|
+
- blocker and HiLL metadata
|
|
36
|
+
- PR/docs timestamps and recommendation output
|
|
37
|
+
|
|
38
|
+
### `listProjects(projectsRoot)`
|
|
39
|
+
|
|
40
|
+
Reads all projects under a configured projects root and returns lightweight `ProjectSummary` records suitable for list or dashboard surfaces.
|
|
41
|
+
|
|
42
|
+
### `recommendSkill(projectState)`
|
|
43
|
+
|
|
44
|
+
Pure function that maps parsed project state to the next recommended OAT workflow skill.
|
|
45
|
+
|
|
46
|
+
## Current Consumers
|
|
47
|
+
|
|
48
|
+
- `packages/cli/src/commands/project/status.ts`
|
|
49
|
+
- `packages/cli/src/commands/project/list.ts`
|
|
50
|
+
|
|
51
|
+
The CLI also uses adjacent config-resolution code for `oat config dump`, but the control plane remains focused on project artifact parsing rather than config ownership.
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pnpm --filter @open-agent-toolkit/control-plane test
|
|
57
|
+
pnpm --filter @open-agent-toolkit/control-plane lint
|
|
58
|
+
pnpm --filter @open-agent-toolkit/control-plane type-check
|
|
59
|
+
pnpm --filter @open-agent-toolkit/control-plane build
|
|
60
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProjectState, ProjectSummary } from './types.js';
|
|
2
|
+
export declare function getProjectState(projectPath: string): Promise<ProjectState>;
|
|
3
|
+
export declare function listProjects(projectsRoot: string): Promise<ProjectSummary[]>;
|
|
4
|
+
//# sourceMappingURL=project.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAgB,MAAM,SAAS,CAAC;AAE1E,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CA0DvB;AAED,wBAAsB,YAAY,CAChC,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,cAAc,EAAE,CAAC,CA+E3B"}
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { access, readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative } from 'node:path';
|
|
3
|
+
import { recommendSkill } from './recommender/router.js';
|
|
4
|
+
import { scanArtifacts } from './state/artifacts.js';
|
|
5
|
+
import { parseStateFrontmatter } from './state/parser.js';
|
|
6
|
+
import { parseReviewTable, scanUnprocessedReviews } from './state/reviews.js';
|
|
7
|
+
import { parseTaskProgress } from './state/tasks.js';
|
|
8
|
+
export async function getProjectState(projectPath) {
|
|
9
|
+
const displayPath = await resolveProjectDisplayPath(projectPath);
|
|
10
|
+
const [stateContent, planContent, implementationContent, artifacts, reviewFiles,] = await Promise.all([
|
|
11
|
+
readOptionalFile(join(projectPath, 'state.md')),
|
|
12
|
+
readOptionalFile(join(projectPath, 'plan.md')),
|
|
13
|
+
readOptionalFile(join(projectPath, 'implementation.md')),
|
|
14
|
+
scanArtifacts(projectPath),
|
|
15
|
+
scanUnprocessedReviews(projectPath),
|
|
16
|
+
]);
|
|
17
|
+
const parsedState = parseStateFrontmatter(stateContent);
|
|
18
|
+
const reviews = mergeReviews(parseReviewTable(planContent), reviewFiles, projectPath);
|
|
19
|
+
const progress = parseTaskProgress(planContent, implementationContent);
|
|
20
|
+
const projectStateWithoutRecommendation = {
|
|
21
|
+
name: basename(projectPath),
|
|
22
|
+
path: displayPath,
|
|
23
|
+
phase: parsedState.phase ?? 'discovery',
|
|
24
|
+
phaseStatus: parsedState.phaseStatus ?? 'in_progress',
|
|
25
|
+
workflowMode: parsedState.workflowMode ?? 'spec-driven',
|
|
26
|
+
executionMode: parsedState.executionMode,
|
|
27
|
+
lifecycle: parsedState.lifecycle ?? 'active',
|
|
28
|
+
pauseTimestamp: parsedState.pauseTimestamp,
|
|
29
|
+
pauseReason: parsedState.pauseReason,
|
|
30
|
+
progress,
|
|
31
|
+
artifacts,
|
|
32
|
+
reviews,
|
|
33
|
+
blockers: parsedState.blockers,
|
|
34
|
+
hillCheckpoints: parsedState.hillCheckpoints,
|
|
35
|
+
hillCompleted: parsedState.hillCompleted,
|
|
36
|
+
prStatus: parsedState.prStatus,
|
|
37
|
+
prUrl: parsedState.prUrl,
|
|
38
|
+
docsUpdated: parsedState.docsUpdated,
|
|
39
|
+
lastCommit: parsedState.lastCommit,
|
|
40
|
+
timestamps: {
|
|
41
|
+
created: parsedState.projectCreated ?? '',
|
|
42
|
+
completed: parsedState.projectCompleted,
|
|
43
|
+
stateUpdated: parsedState.projectStateUpdated ?? '',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
...projectStateWithoutRecommendation,
|
|
48
|
+
recommendation: recommendSkill(projectStateWithoutRecommendation),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export async function listProjects(projectsRoot) {
|
|
52
|
+
const repoRoot = await findRepoRoot(projectsRoot);
|
|
53
|
+
const entries = await readdir(projectsRoot, { withFileTypes: true });
|
|
54
|
+
const projectDirs = entries
|
|
55
|
+
.filter((entry) => entry.isDirectory())
|
|
56
|
+
.map((entry) => join(projectsRoot, entry.name));
|
|
57
|
+
const summaries = await Promise.all(projectDirs.map(async (projectDir) => {
|
|
58
|
+
const statePath = join(projectDir, 'state.md');
|
|
59
|
+
const planPath = join(projectDir, 'plan.md');
|
|
60
|
+
const implementationPath = join(projectDir, 'implementation.md');
|
|
61
|
+
const stateContent = await readOptionalFile(statePath);
|
|
62
|
+
if (!stateContent) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const [planContent, implementationContent, artifacts, reviewFiles] = await Promise.all([
|
|
66
|
+
readOptionalFile(planPath),
|
|
67
|
+
readOptionalFile(implementationPath),
|
|
68
|
+
scanArtifacts(projectDir),
|
|
69
|
+
scanUnprocessedReviews(projectDir),
|
|
70
|
+
]);
|
|
71
|
+
const parsedState = parseStateFrontmatter(stateContent);
|
|
72
|
+
const reviews = mergeReviews(parseReviewTable(planContent), reviewFiles, projectDir);
|
|
73
|
+
const progress = parseTaskProgress(planContent, implementationContent);
|
|
74
|
+
const stateWithoutRecommendation = {
|
|
75
|
+
name: basename(projectDir),
|
|
76
|
+
path: formatProjectDisplayPath(projectDir, repoRoot),
|
|
77
|
+
phase: parsedState.phase ?? 'discovery',
|
|
78
|
+
phaseStatus: parsedState.phaseStatus ?? 'in_progress',
|
|
79
|
+
workflowMode: parsedState.workflowMode ?? 'spec-driven',
|
|
80
|
+
executionMode: parsedState.executionMode,
|
|
81
|
+
lifecycle: parsedState.lifecycle ?? 'active',
|
|
82
|
+
pauseTimestamp: parsedState.pauseTimestamp,
|
|
83
|
+
pauseReason: parsedState.pauseReason,
|
|
84
|
+
progress,
|
|
85
|
+
artifacts,
|
|
86
|
+
reviews,
|
|
87
|
+
blockers: parsedState.blockers,
|
|
88
|
+
hillCheckpoints: parsedState.hillCheckpoints,
|
|
89
|
+
hillCompleted: parsedState.hillCompleted,
|
|
90
|
+
prStatus: parsedState.prStatus,
|
|
91
|
+
prUrl: parsedState.prUrl,
|
|
92
|
+
docsUpdated: parsedState.docsUpdated,
|
|
93
|
+
lastCommit: parsedState.lastCommit,
|
|
94
|
+
timestamps: {
|
|
95
|
+
created: parsedState.projectCreated ?? '',
|
|
96
|
+
completed: parsedState.projectCompleted,
|
|
97
|
+
stateUpdated: parsedState.projectStateUpdated ?? '',
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
name: stateWithoutRecommendation.name,
|
|
102
|
+
path: stateWithoutRecommendation.path,
|
|
103
|
+
phase: stateWithoutRecommendation.phase,
|
|
104
|
+
phaseStatus: stateWithoutRecommendation.phaseStatus,
|
|
105
|
+
workflowMode: stateWithoutRecommendation.workflowMode,
|
|
106
|
+
lifecycle: stateWithoutRecommendation.lifecycle,
|
|
107
|
+
progress: {
|
|
108
|
+
completed: stateWithoutRecommendation.progress.completed,
|
|
109
|
+
total: stateWithoutRecommendation.progress.total,
|
|
110
|
+
},
|
|
111
|
+
recommendation: recommendSkill(stateWithoutRecommendation),
|
|
112
|
+
};
|
|
113
|
+
}));
|
|
114
|
+
return summaries
|
|
115
|
+
.filter((summary) => summary !== null)
|
|
116
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
117
|
+
}
|
|
118
|
+
async function resolveProjectDisplayPath(projectPath) {
|
|
119
|
+
if (!isAbsolute(projectPath)) {
|
|
120
|
+
return normalizePath(projectPath);
|
|
121
|
+
}
|
|
122
|
+
const repoRoot = await findRepoRoot(projectPath);
|
|
123
|
+
return formatProjectDisplayPath(projectPath, repoRoot);
|
|
124
|
+
}
|
|
125
|
+
function formatProjectDisplayPath(projectPath, repoRoot) {
|
|
126
|
+
if (!isAbsolute(projectPath)) {
|
|
127
|
+
return normalizePath(projectPath);
|
|
128
|
+
}
|
|
129
|
+
if (!repoRoot) {
|
|
130
|
+
return projectPath;
|
|
131
|
+
}
|
|
132
|
+
return normalizePath(relative(repoRoot, projectPath));
|
|
133
|
+
}
|
|
134
|
+
async function findRepoRoot(startPath) {
|
|
135
|
+
let currentPath = startPath;
|
|
136
|
+
while (true) {
|
|
137
|
+
if ((await pathExists(join(currentPath, '.git'))) ||
|
|
138
|
+
(await pathExists(join(currentPath, '.oat', 'config.json')))) {
|
|
139
|
+
return currentPath;
|
|
140
|
+
}
|
|
141
|
+
const parentPath = dirname(currentPath);
|
|
142
|
+
if (parentPath === currentPath) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
currentPath = parentPath;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function pathExists(path) {
|
|
149
|
+
try {
|
|
150
|
+
await access(path);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function normalizePath(path) {
|
|
158
|
+
return path.replace(/\\/g, '/');
|
|
159
|
+
}
|
|
160
|
+
async function readOptionalFile(path) {
|
|
161
|
+
try {
|
|
162
|
+
return await readFile(path, 'utf8');
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (typeof error === 'object' &&
|
|
166
|
+
error !== null &&
|
|
167
|
+
'code' in error &&
|
|
168
|
+
error.code === 'ENOENT') {
|
|
169
|
+
return '';
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function mergeReviews(tableReviews, reviewFiles, projectPath) {
|
|
175
|
+
const merged = [...tableReviews];
|
|
176
|
+
const existingArtifacts = new Set(tableReviews.map((review) => review.artifact));
|
|
177
|
+
for (const reviewFile of reviewFiles) {
|
|
178
|
+
const relativeArtifact = relative(projectPath, reviewFile).replace(/\\/g, '/');
|
|
179
|
+
if (existingArtifacts.has(relativeArtifact)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
merged.push({
|
|
183
|
+
scope: basename(reviewFile, '.md'),
|
|
184
|
+
type: 'code',
|
|
185
|
+
status: 'received',
|
|
186
|
+
date: '-',
|
|
187
|
+
artifact: relativeArtifact,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return merged;
|
|
191
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundary.d.ts","sourceRoot":"","sources":["../../src/recommender/boundary.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAQ7C,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,OAAO,EAAE,MAAM,GACd,YAAY,CAiBd"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { normalizeNullableString, parseBoolean, } from '../shared/utils/normalize.js';
|
|
2
|
+
const TEMPLATE_PATTERNS = [
|
|
3
|
+
/\{Project Name\}/,
|
|
4
|
+
/\{Copy of/,
|
|
5
|
+
/\{Clear description/,
|
|
6
|
+
];
|
|
7
|
+
export function detectBoundaryTier(frontmatter, content) {
|
|
8
|
+
const status = normalizeNullableString(frontmatter.oat_status);
|
|
9
|
+
const isTemplate = parseBoolean(frontmatter.oat_template);
|
|
10
|
+
if (status === 'complete') {
|
|
11
|
+
return 1;
|
|
12
|
+
}
|
|
13
|
+
if (status === 'in_progress' &&
|
|
14
|
+
!isTemplate &&
|
|
15
|
+
!hasTemplatePlaceholders(content)) {
|
|
16
|
+
return 2;
|
|
17
|
+
}
|
|
18
|
+
return 3;
|
|
19
|
+
}
|
|
20
|
+
function hasTemplatePlaceholders(content) {
|
|
21
|
+
return TEMPLATE_PATTERNS.some((pattern) => pattern.test(content));
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/recommender/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,YAAY,EAEZ,mBAAmB,EAEpB,MAAM,UAAU,CAAC;AAiElB,wBAAgB,cAAc,CAC5B,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,gBAAgB,CAAC,GAC1C,mBAAmB,CA8BrB"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const CURRENT_PHASE_SKILLS = {
|
|
2
|
+
discovery: 'oat-project-discover',
|
|
3
|
+
spec: 'oat-project-spec',
|
|
4
|
+
design: 'oat-project-design',
|
|
5
|
+
plan: 'oat-project-plan',
|
|
6
|
+
};
|
|
7
|
+
const CURRENT_ARTIFACT_BY_PHASE = {
|
|
8
|
+
discovery: 'discovery',
|
|
9
|
+
spec: 'spec',
|
|
10
|
+
design: 'design',
|
|
11
|
+
plan: 'plan',
|
|
12
|
+
implement: 'implementation',
|
|
13
|
+
};
|
|
14
|
+
const SPEC_DRIVEN_ROUTES = {
|
|
15
|
+
'discovery:in_progress:3': 'oat-project-discover',
|
|
16
|
+
'discovery:in_progress:2': 'oat-project-spec',
|
|
17
|
+
'discovery:complete:1': 'oat-project-spec',
|
|
18
|
+
'spec:in_progress:3': 'oat-project-spec',
|
|
19
|
+
'spec:in_progress:2': 'oat-project-design',
|
|
20
|
+
'spec:complete:1': 'oat-project-design',
|
|
21
|
+
'design:in_progress:3': 'oat-project-design',
|
|
22
|
+
'design:in_progress:2': 'oat-project-plan',
|
|
23
|
+
'design:complete:1': 'oat-project-plan',
|
|
24
|
+
'plan:in_progress:3': 'oat-project-plan',
|
|
25
|
+
'plan:in_progress:2': 'oat-project-implement',
|
|
26
|
+
'plan:complete:1': 'oat-project-implement',
|
|
27
|
+
'implement:in_progress:any': 'oat-project-implement',
|
|
28
|
+
};
|
|
29
|
+
const QUICK_ROUTES = {
|
|
30
|
+
'discovery:in_progress:3': 'oat-project-discover',
|
|
31
|
+
'discovery:in_progress:2': 'oat-project-plan',
|
|
32
|
+
'discovery:complete:1': 'oat-project-plan',
|
|
33
|
+
'plan:in_progress:3': 'oat-project-plan',
|
|
34
|
+
'plan:in_progress:2': 'oat-project-implement',
|
|
35
|
+
'plan:complete:1': 'oat-project-implement',
|
|
36
|
+
'implement:in_progress:any': 'oat-project-implement',
|
|
37
|
+
};
|
|
38
|
+
const IMPORT_ROUTES = {
|
|
39
|
+
'plan:in_progress:3': 'oat-project-import-plan',
|
|
40
|
+
'plan:in_progress:2': 'oat-project-implement',
|
|
41
|
+
'plan:complete:1': 'oat-project-implement',
|
|
42
|
+
'implement:in_progress:any': 'oat-project-implement',
|
|
43
|
+
};
|
|
44
|
+
export function recommendSkill(state) {
|
|
45
|
+
const hillOverride = getHillOverride(state);
|
|
46
|
+
if (hillOverride) {
|
|
47
|
+
return hillOverride;
|
|
48
|
+
}
|
|
49
|
+
if (state.phase === 'implement' &&
|
|
50
|
+
(state.phaseStatus === 'complete' || state.phaseStatus === 'pr_open')) {
|
|
51
|
+
return getPostImplementationRecommendation(state);
|
|
52
|
+
}
|
|
53
|
+
const currentArtifact = getCurrentArtifact(state);
|
|
54
|
+
if (currentArtifact?.readyFor && currentArtifact.status === 'complete') {
|
|
55
|
+
return {
|
|
56
|
+
skill: normalizeImplementationSkill(currentArtifact.readyFor, state),
|
|
57
|
+
reason: 'Current artifact is complete and explicitly points to the next skill',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const earlyRoute = getEarlyPhaseRoute(state, currentArtifact?.boundaryTier ?? 3);
|
|
61
|
+
return {
|
|
62
|
+
skill: normalizeImplementationSkill(earlyRoute, state),
|
|
63
|
+
reason: `Route ${state.workflowMode} ${state.phase} work based on boundary tier`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function getHillOverride(state) {
|
|
67
|
+
if (state.phase === 'implement') {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (state.hillCheckpoints.includes(state.phase) &&
|
|
71
|
+
!state.hillCompleted.includes(state.phase)) {
|
|
72
|
+
return {
|
|
73
|
+
skill: CURRENT_PHASE_SKILLS[state.phase],
|
|
74
|
+
reason: 'HiLL gate is pending for the current phase',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function getPostImplementationRecommendation(state) {
|
|
80
|
+
if (hasIncompleteRevisionPhase(state)) {
|
|
81
|
+
return {
|
|
82
|
+
skill: normalizeImplementationSkill('oat-project-implement', state),
|
|
83
|
+
reason: 'Revision work remains incomplete',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (hasUnprocessedReviewFeedback(state)) {
|
|
87
|
+
return {
|
|
88
|
+
skill: 'oat-project-review-receive',
|
|
89
|
+
reason: 'Unprocessed review feedback exists',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const finalReview = state.reviews.find((review) => review.scope === 'final' && review.type === 'code');
|
|
93
|
+
if (!finalReview || finalReview.status === 'pending') {
|
|
94
|
+
return {
|
|
95
|
+
skill: 'oat-project-review-provide',
|
|
96
|
+
reason: 'Final code review is required',
|
|
97
|
+
context: 'code final',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (finalReview.status === 'fixes_completed') {
|
|
101
|
+
return {
|
|
102
|
+
skill: 'oat-project-review-provide',
|
|
103
|
+
reason: 'Review fixes are complete and need re-review',
|
|
104
|
+
context: 'code final',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (finalReview.status !== 'passed') {
|
|
108
|
+
return {
|
|
109
|
+
skill: 'oat-project-review-receive',
|
|
110
|
+
reason: 'Final review findings still need processing',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const summaryArtifact = state.artifacts.find((artifact) => artifact.type === 'summary');
|
|
114
|
+
if (!summaryArtifact || summaryArtifact.status !== 'complete') {
|
|
115
|
+
return {
|
|
116
|
+
skill: 'oat-project-summary',
|
|
117
|
+
reason: 'Final review passed but summary is not complete',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (state.phaseStatus !== 'pr_open') {
|
|
121
|
+
return {
|
|
122
|
+
skill: 'oat-project-pr-final',
|
|
123
|
+
reason: 'Summary is complete and the final PR has not been opened',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
skill: 'oat-project-complete',
|
|
128
|
+
reason: 'PR is open and the project is ready for completion',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function hasIncompleteRevisionPhase(state) {
|
|
132
|
+
return state.progress.phases.some((phase) => phase.isRevision && phase.completed < phase.total);
|
|
133
|
+
}
|
|
134
|
+
function hasUnprocessedReviewFeedback(state) {
|
|
135
|
+
return state.reviews.some((review) => {
|
|
136
|
+
if (review.scope === 'final' && review.type === 'code') {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return !isProcessedReviewStatus(review);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function isProcessedReviewStatus(review) {
|
|
143
|
+
return (review.status === 'passed' ||
|
|
144
|
+
review.status === 'fixes_added' ||
|
|
145
|
+
review.status === 'fixes_completed');
|
|
146
|
+
}
|
|
147
|
+
function getCurrentArtifact(state) {
|
|
148
|
+
return state.artifacts.find((artifact) => artifact.type === CURRENT_ARTIFACT_BY_PHASE[state.phase]);
|
|
149
|
+
}
|
|
150
|
+
function getEarlyPhaseRoute(state, boundaryTier) {
|
|
151
|
+
if (state.phase === 'implement' && state.phaseStatus === 'in_progress') {
|
|
152
|
+
return normalizeImplementationSkill('oat-project-implement', state);
|
|
153
|
+
}
|
|
154
|
+
const routes = getWorkflowRoutes(state.workflowMode);
|
|
155
|
+
const key = `${state.phase}:${state.phaseStatus}:${boundaryTier}`;
|
|
156
|
+
const route = routes[key];
|
|
157
|
+
if (route) {
|
|
158
|
+
return route;
|
|
159
|
+
}
|
|
160
|
+
if (state.phase === 'implement') {
|
|
161
|
+
return 'oat-project-implement';
|
|
162
|
+
}
|
|
163
|
+
return CURRENT_PHASE_SKILLS[state.phase];
|
|
164
|
+
}
|
|
165
|
+
function getWorkflowRoutes(workflowMode) {
|
|
166
|
+
switch (workflowMode) {
|
|
167
|
+
case 'quick':
|
|
168
|
+
return QUICK_ROUTES;
|
|
169
|
+
case 'import':
|
|
170
|
+
return IMPORT_ROUTES;
|
|
171
|
+
case 'spec-driven':
|
|
172
|
+
default:
|
|
173
|
+
return SPEC_DRIVEN_ROUTES;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function normalizeImplementationSkill(skill, state) {
|
|
177
|
+
if (skill === 'oat-project-implement' &&
|
|
178
|
+
state.executionMode === 'subagent-driven') {
|
|
179
|
+
return 'oat-project-subagent-implement';
|
|
180
|
+
}
|
|
181
|
+
return skill;
|
|
182
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/shared/utils/errors.ts"],"names":[],"mappings":"AAAA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAO1D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../../src/shared/utils/frontmatter.ts"],"names":[],"mappings":"AAIA,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGjE;AAED,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import YAML from 'yaml';
|
|
2
|
+
const FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/;
|
|
3
|
+
export function extractFrontmatter(content) {
|
|
4
|
+
const match = content.match(FRONTMATTER_PATTERN);
|
|
5
|
+
return match?.[1] ?? null;
|
|
6
|
+
}
|
|
7
|
+
export function parseFrontmatterRecord(content) {
|
|
8
|
+
const frontmatter = extractFrontmatter(content);
|
|
9
|
+
if (frontmatter == null) {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const parsed = YAML.parse(frontmatter);
|
|
14
|
+
return isRecord(parsed) ? parsed : {};
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === 'object' && value !== null;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../../src/shared/utils/normalize.ts"],"names":[],"mappings":"AAEA,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,OAAO,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GAClD,MAAM,GAAG,IAAI,CAgBf;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAiBpD"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const PLACEHOLDER_PATTERN = /^\{[^}]+\}$/;
|
|
2
|
+
export function normalizeNullableString(value, options = {}) {
|
|
3
|
+
if (typeof value !== 'string') {
|
|
4
|
+
return value == null ? null : String(value);
|
|
5
|
+
}
|
|
6
|
+
const normalized = value.trim();
|
|
7
|
+
if (!normalized ||
|
|
8
|
+
normalized === 'null' ||
|
|
9
|
+
(options.treatPlaceholdersAsNull === true &&
|
|
10
|
+
PLACEHOLDER_PATTERN.test(normalized))) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
export function parseBoolean(value) {
|
|
16
|
+
if (typeof value === 'boolean') {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
const normalized = value.trim().toLowerCase();
|
|
21
|
+
if (normalized === 'true') {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (normalized === 'false') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../../src/state/artifacts.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAgB,MAAM,UAAU,CAAC;AAW7D,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,cAAc,EAAE,CAAC,CA8B3B"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { detectBoundaryTier } from '../recommender/boundary.js';
|
|
4
|
+
import { isMissingFileError } from '../shared/utils/errors.js';
|
|
5
|
+
import { parseFrontmatterRecord } from '../shared/utils/frontmatter.js';
|
|
6
|
+
import { normalizeNullableString, parseBoolean, } from '../shared/utils/normalize.js';
|
|
7
|
+
const ARTIFACT_TYPES = [
|
|
8
|
+
'discovery',
|
|
9
|
+
'spec',
|
|
10
|
+
'design',
|
|
11
|
+
'plan',
|
|
12
|
+
'implementation',
|
|
13
|
+
'summary',
|
|
14
|
+
];
|
|
15
|
+
export async function scanArtifacts(projectPath) {
|
|
16
|
+
return Promise.all(ARTIFACT_TYPES.map(async (type) => {
|
|
17
|
+
const path = join(projectPath, `${type}.md`);
|
|
18
|
+
const content = await tryReadFile(path);
|
|
19
|
+
if (content == null) {
|
|
20
|
+
return {
|
|
21
|
+
type,
|
|
22
|
+
exists: false,
|
|
23
|
+
path,
|
|
24
|
+
status: null,
|
|
25
|
+
readyFor: null,
|
|
26
|
+
isTemplate: false,
|
|
27
|
+
boundaryTier: 3,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const frontmatter = parseFrontmatterRecord(content);
|
|
31
|
+
return {
|
|
32
|
+
type,
|
|
33
|
+
exists: true,
|
|
34
|
+
path,
|
|
35
|
+
status: normalizeNullableString(frontmatter.oat_status),
|
|
36
|
+
readyFor: normalizeNullableString(frontmatter.oat_ready_for),
|
|
37
|
+
isTemplate: parseBoolean(frontmatter.oat_template),
|
|
38
|
+
boundaryTier: detectBoundaryTier(frontmatter, content),
|
|
39
|
+
};
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
async function tryReadFile(path) {
|
|
43
|
+
try {
|
|
44
|
+
return await readFile(path, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (isMissingFileError(error)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ExecutionMode, Lifecycle, Phase, PhaseStatus, WorkflowMode } from '../types.js';
|
|
2
|
+
export interface ParsedStateFrontmatter {
|
|
3
|
+
currentTask: string | null;
|
|
4
|
+
lastCommit: string | null;
|
|
5
|
+
blockers: string[];
|
|
6
|
+
hillCheckpoints: string[];
|
|
7
|
+
hillCompleted: string[];
|
|
8
|
+
parallelExecution: boolean;
|
|
9
|
+
phase: Phase | null;
|
|
10
|
+
phaseStatus: PhaseStatus | null;
|
|
11
|
+
executionMode: ExecutionMode;
|
|
12
|
+
lifecycle: Lifecycle | null;
|
|
13
|
+
pauseTimestamp: string | null;
|
|
14
|
+
pauseReason: string | null;
|
|
15
|
+
workflowMode: WorkflowMode | null;
|
|
16
|
+
workflowOrigin: string | null;
|
|
17
|
+
docsUpdated: string | null;
|
|
18
|
+
prStatus: string | null;
|
|
19
|
+
prUrl: string | null;
|
|
20
|
+
projectCreated: string | null;
|
|
21
|
+
projectCompleted: string | null;
|
|
22
|
+
projectStateUpdated: string | null;
|
|
23
|
+
generated: boolean;
|
|
24
|
+
template: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function parseStateFrontmatter(content: string): ParsedStateFrontmatter;
|
|
27
|
+
//# sourceMappingURL=parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/state/parser.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EACT,KAAK,EACL,WAAW,EACX,YAAY,EACb,MAAM,UAAU,CAAC;AAQlB,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;IAC5B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;CACnB;AA2BD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,sBAAsB,CAyD7E"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { parseFrontmatterRecord } from '../shared/utils/frontmatter.js';
|
|
2
|
+
import { normalizeNullableString, parseBoolean, } from '../shared/utils/normalize.js';
|
|
3
|
+
const PHASES = ['discovery', 'spec', 'design', 'plan', 'implement'];
|
|
4
|
+
const PHASE_STATUSES = ['in_progress', 'complete', 'pr_open'];
|
|
5
|
+
const EXECUTION_MODES = ['single-thread', 'subagent-driven'];
|
|
6
|
+
const WORKFLOW_MODES = ['spec-driven', 'quick', 'import'];
|
|
7
|
+
const LIFECYCLE_VALUES = ['active', 'paused', 'complete'];
|
|
8
|
+
const EMPTY_PARSED_STATE = {
|
|
9
|
+
currentTask: null,
|
|
10
|
+
lastCommit: null,
|
|
11
|
+
blockers: [],
|
|
12
|
+
hillCheckpoints: [],
|
|
13
|
+
hillCompleted: [],
|
|
14
|
+
parallelExecution: false,
|
|
15
|
+
phase: null,
|
|
16
|
+
phaseStatus: null,
|
|
17
|
+
executionMode: 'single-thread',
|
|
18
|
+
lifecycle: null,
|
|
19
|
+
pauseTimestamp: null,
|
|
20
|
+
pauseReason: null,
|
|
21
|
+
workflowMode: null,
|
|
22
|
+
workflowOrigin: null,
|
|
23
|
+
docsUpdated: null,
|
|
24
|
+
prStatus: null,
|
|
25
|
+
prUrl: null,
|
|
26
|
+
projectCreated: null,
|
|
27
|
+
projectCompleted: null,
|
|
28
|
+
projectStateUpdated: null,
|
|
29
|
+
generated: false,
|
|
30
|
+
template: false,
|
|
31
|
+
};
|
|
32
|
+
export function parseStateFrontmatter(content) {
|
|
33
|
+
const parsed = parseFrontmatterRecord(content);
|
|
34
|
+
if (Object.keys(parsed).length === 0) {
|
|
35
|
+
return EMPTY_PARSED_STATE;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
currentTask: normalizeNullableString(parsed.oat_current_task, {
|
|
39
|
+
treatPlaceholdersAsNull: true,
|
|
40
|
+
}),
|
|
41
|
+
lastCommit: normalizeNullableString(parsed.oat_last_commit, {
|
|
42
|
+
treatPlaceholdersAsNull: true,
|
|
43
|
+
}),
|
|
44
|
+
blockers: parseStringArray(parsed.oat_blockers),
|
|
45
|
+
hillCheckpoints: parseStringArray(parsed.oat_hill_checkpoints),
|
|
46
|
+
hillCompleted: parseStringArray(parsed.oat_hill_completed),
|
|
47
|
+
parallelExecution: parseBoolean(parsed.oat_parallel_execution),
|
|
48
|
+
phase: normalizeEnum(parsed.oat_phase, PHASES),
|
|
49
|
+
phaseStatus: normalizeEnum(parsed.oat_phase_status, PHASE_STATUSES),
|
|
50
|
+
executionMode: normalizeEnum(parsed.oat_execution_mode, EXECUTION_MODES) ??
|
|
51
|
+
'single-thread',
|
|
52
|
+
lifecycle: normalizeEnum(parsed.oat_lifecycle, LIFECYCLE_VALUES),
|
|
53
|
+
pauseTimestamp: normalizeNullableString(parsed.oat_pause_timestamp, {
|
|
54
|
+
treatPlaceholdersAsNull: true,
|
|
55
|
+
}),
|
|
56
|
+
pauseReason: normalizeNullableString(parsed.oat_pause_reason, {
|
|
57
|
+
treatPlaceholdersAsNull: true,
|
|
58
|
+
}),
|
|
59
|
+
workflowMode: normalizeEnum(parsed.oat_workflow_mode, WORKFLOW_MODES),
|
|
60
|
+
workflowOrigin: normalizeNullableString(parsed.oat_workflow_origin, {
|
|
61
|
+
treatPlaceholdersAsNull: true,
|
|
62
|
+
}),
|
|
63
|
+
docsUpdated: normalizeNullableString(parsed.oat_docs_updated, {
|
|
64
|
+
treatPlaceholdersAsNull: true,
|
|
65
|
+
}),
|
|
66
|
+
prStatus: normalizeNullableString(parsed.oat_pr_status, {
|
|
67
|
+
treatPlaceholdersAsNull: true,
|
|
68
|
+
}),
|
|
69
|
+
prUrl: normalizeNullableString(parsed.oat_pr_url, {
|
|
70
|
+
treatPlaceholdersAsNull: true,
|
|
71
|
+
}),
|
|
72
|
+
projectCreated: normalizeNullableString(parsed.oat_project_created, {
|
|
73
|
+
treatPlaceholdersAsNull: true,
|
|
74
|
+
}),
|
|
75
|
+
projectCompleted: normalizeNullableString(parsed.oat_project_completed, {
|
|
76
|
+
treatPlaceholdersAsNull: true,
|
|
77
|
+
}),
|
|
78
|
+
projectStateUpdated: normalizeNullableString(parsed.oat_project_state_updated, {
|
|
79
|
+
treatPlaceholdersAsNull: true,
|
|
80
|
+
}),
|
|
81
|
+
generated: parseBoolean(parsed.oat_generated),
|
|
82
|
+
template: parseBoolean(parsed.oat_template),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function parseStringArray(value) {
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
return value
|
|
88
|
+
.map((item) => normalizeNullableString(item, { treatPlaceholdersAsNull: true }))
|
|
89
|
+
.filter((item) => item !== null);
|
|
90
|
+
}
|
|
91
|
+
if (typeof value !== 'string') {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const normalized = value.trim();
|
|
95
|
+
if (normalizeNullableString(normalized, { treatPlaceholdersAsNull: true }) ==
|
|
96
|
+
null) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(normalized);
|
|
101
|
+
if (Array.isArray(parsed)) {
|
|
102
|
+
return parsed
|
|
103
|
+
.map((item) => normalizeNullableString(item, { treatPlaceholdersAsNull: true }))
|
|
104
|
+
.filter((item) => item !== null);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return [normalized];
|
|
109
|
+
}
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
function normalizeEnum(value, allowedValues) {
|
|
113
|
+
const normalized = normalizeNullableString(value, {
|
|
114
|
+
treatPlaceholdersAsNull: true,
|
|
115
|
+
});
|
|
116
|
+
if (normalized == null) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return allowedValues.includes(normalized) ? normalized : null;
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reviews.d.ts","sourceRoot":"","sources":["../../src/state/reviews.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAI7C,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,EAAE,CAepE;AAED,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CAgBnB"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { isMissingFileError } from '../shared/utils/errors.js';
|
|
4
|
+
const REVIEWS_HEADING = '## Reviews';
|
|
5
|
+
export function parseReviewTable(planContent) {
|
|
6
|
+
const reviewsSection = extractReviewsSection(planContent);
|
|
7
|
+
if (reviewsSection == null) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
const rows = reviewsSection
|
|
11
|
+
.split('\n')
|
|
12
|
+
.map((line) => line.trim())
|
|
13
|
+
.filter((line) => line.startsWith('|'));
|
|
14
|
+
return rows
|
|
15
|
+
.slice(2)
|
|
16
|
+
.map(parseTableRow)
|
|
17
|
+
.filter((row) => row !== null);
|
|
18
|
+
}
|
|
19
|
+
export async function scanUnprocessedReviews(projectPath) {
|
|
20
|
+
const reviewsPath = join(projectPath, 'reviews');
|
|
21
|
+
try {
|
|
22
|
+
const entries = await readdir(reviewsPath, { withFileTypes: true });
|
|
23
|
+
return entries
|
|
24
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
25
|
+
.map((entry) => join(reviewsPath, entry.name))
|
|
26
|
+
.sort();
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (isMissingFileError(error)) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function extractReviewsSection(planContent) {
|
|
36
|
+
const startIndex = planContent.indexOf(REVIEWS_HEADING);
|
|
37
|
+
if (startIndex === -1) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const remaining = planContent.slice(startIndex + REVIEWS_HEADING.length);
|
|
41
|
+
const nextHeadingIndex = remaining.search(/\n## /);
|
|
42
|
+
if (nextHeadingIndex === -1) {
|
|
43
|
+
return remaining.trim();
|
|
44
|
+
}
|
|
45
|
+
return remaining.slice(0, nextHeadingIndex).trim();
|
|
46
|
+
}
|
|
47
|
+
function parseTableRow(line) {
|
|
48
|
+
const cells = line
|
|
49
|
+
.split('|')
|
|
50
|
+
.slice(1, -1)
|
|
51
|
+
.map((cell) => cell.trim());
|
|
52
|
+
if (cells.length !== 5) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const [scope, type, status, date, artifact] = cells;
|
|
56
|
+
if (!scope || !type || !status || !date || !artifact) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return { scope, type, status, date, artifact };
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/state/tasks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAY7C,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,qBAAqB,EAAE,MAAM,GAC5B,YAAY,CAiBd"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { parseFrontmatterRecord } from '../shared/utils/frontmatter.js';
|
|
2
|
+
const PHASE_HEADING_PATTERN = /^## Phase \d+: (.+)$/m;
|
|
3
|
+
const REVISION_PHASE_HEADING_PATTERN = /^## Revision Phase \d+: (.+)$/m;
|
|
4
|
+
export function parseTaskProgress(planContent, implementationContent) {
|
|
5
|
+
const completedTasks = parseCompletedTaskIds(implementationContent);
|
|
6
|
+
const currentTaskId = parseCurrentTaskId(implementationContent);
|
|
7
|
+
const phases = parsePhaseProgress(planContent, completedTasks);
|
|
8
|
+
return {
|
|
9
|
+
total: phases.reduce((sum, phase) => sum + phase.total, 0),
|
|
10
|
+
completed: phases.reduce((sum, phase) => sum + phase.completed, 0),
|
|
11
|
+
currentTaskId,
|
|
12
|
+
phases: phases.map((phase) => ({
|
|
13
|
+
phaseId: phase.phaseId ?? 'unknown',
|
|
14
|
+
name: phase.name,
|
|
15
|
+
total: phase.total,
|
|
16
|
+
completed: phase.completed,
|
|
17
|
+
isRevision: phase.isRevision,
|
|
18
|
+
})),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function parsePhaseProgress(planContent, completedTasks) {
|
|
22
|
+
const phases = [];
|
|
23
|
+
const lines = planContent.split('\n');
|
|
24
|
+
let currentPhase = null;
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
if (PHASE_HEADING_PATTERN.test(line) ||
|
|
27
|
+
REVISION_PHASE_HEADING_PATTERN.test(line)) {
|
|
28
|
+
currentPhase = {
|
|
29
|
+
phaseId: null,
|
|
30
|
+
name: extractPhaseName(line),
|
|
31
|
+
total: 0,
|
|
32
|
+
completed: 0,
|
|
33
|
+
isRevision: line.startsWith('## Revision Phase'),
|
|
34
|
+
};
|
|
35
|
+
phases.push(currentPhase);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const taskMatch = line.match(/^### Task ((?:p\d+|p-rev\d+)-t\d+): (.+)$/);
|
|
39
|
+
if (!taskMatch || currentPhase == null) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const taskId = taskMatch[1];
|
|
43
|
+
if (!taskId) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const phaseId = taskId.replace(/-t\d+$/, '');
|
|
47
|
+
currentPhase.phaseId ??= phaseId;
|
|
48
|
+
currentPhase.total += 1;
|
|
49
|
+
currentPhase.completed += completedTasks.has(taskId) ? 1 : 0;
|
|
50
|
+
}
|
|
51
|
+
return phases.filter((phase) => phase.phaseId !== null);
|
|
52
|
+
}
|
|
53
|
+
function parseCompletedTaskIds(implementationContent) {
|
|
54
|
+
const completedTasks = new Set();
|
|
55
|
+
let currentTaskId = null;
|
|
56
|
+
for (const line of implementationContent.split('\n')) {
|
|
57
|
+
const taskMatch = line.match(/^### Task ((?:p\d+|p-rev\d+)-t\d+): .+$/);
|
|
58
|
+
if (taskMatch?.[1]) {
|
|
59
|
+
currentTaskId = taskMatch[1];
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (currentTaskId && /^\*\*Status:\*\*\s+completed$/.test(line.trim())) {
|
|
63
|
+
completedTasks.add(currentTaskId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return completedTasks;
|
|
67
|
+
}
|
|
68
|
+
function parseCurrentTaskId(implementationContent) {
|
|
69
|
+
const parsed = parseFrontmatterRecord(implementationContent);
|
|
70
|
+
const currentTaskId = parsed.oat_current_task_id;
|
|
71
|
+
return typeof currentTaskId === 'string' && currentTaskId !== 'null'
|
|
72
|
+
? currentTaskId
|
|
73
|
+
: null;
|
|
74
|
+
}
|
|
75
|
+
function extractPhaseName(line) {
|
|
76
|
+
const revisionMatch = line.match(/^## Revision Phase \d+: (.+)$/);
|
|
77
|
+
if (revisionMatch?.[1]) {
|
|
78
|
+
return revisionMatch[1];
|
|
79
|
+
}
|
|
80
|
+
const phaseMatch = line.match(/^## Phase \d+: (.+)$/);
|
|
81
|
+
if (phaseMatch?.[1]) {
|
|
82
|
+
return phaseMatch[1];
|
|
83
|
+
}
|
|
84
|
+
return line.replace(/^## /, '');
|
|
85
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export type Phase = 'discovery' | 'spec' | 'design' | 'plan' | 'implement';
|
|
2
|
+
export type PhaseStatus = 'in_progress' | 'complete' | 'pr_open';
|
|
3
|
+
export type WorkflowMode = 'spec-driven' | 'quick' | 'import';
|
|
4
|
+
export type ExecutionMode = 'single-thread' | 'subagent-driven';
|
|
5
|
+
export type Lifecycle = 'active' | 'paused' | 'complete';
|
|
6
|
+
export type ArtifactType = 'discovery' | 'spec' | 'design' | 'plan' | 'implementation' | 'summary';
|
|
7
|
+
export type BoundaryTier = 1 | 2 | 3;
|
|
8
|
+
export interface ArtifactStatus {
|
|
9
|
+
type: ArtifactType;
|
|
10
|
+
exists: boolean;
|
|
11
|
+
path: string;
|
|
12
|
+
status: string | null;
|
|
13
|
+
readyFor: string | null;
|
|
14
|
+
isTemplate: boolean;
|
|
15
|
+
boundaryTier: BoundaryTier;
|
|
16
|
+
}
|
|
17
|
+
export interface PhaseProgress {
|
|
18
|
+
phaseId: string;
|
|
19
|
+
name: string;
|
|
20
|
+
total: number;
|
|
21
|
+
completed: number;
|
|
22
|
+
isRevision: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface TaskProgress {
|
|
25
|
+
total: number;
|
|
26
|
+
completed: number;
|
|
27
|
+
currentTaskId: string | null;
|
|
28
|
+
phases: PhaseProgress[];
|
|
29
|
+
}
|
|
30
|
+
export interface ReviewStatus {
|
|
31
|
+
scope: string;
|
|
32
|
+
type: string;
|
|
33
|
+
status: string;
|
|
34
|
+
date: string;
|
|
35
|
+
artifact: string;
|
|
36
|
+
}
|
|
37
|
+
export interface SkillRecommendation {
|
|
38
|
+
skill: string;
|
|
39
|
+
reason: string;
|
|
40
|
+
context?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ProjectState {
|
|
43
|
+
name: string;
|
|
44
|
+
path: string;
|
|
45
|
+
phase: Phase;
|
|
46
|
+
phaseStatus: PhaseStatus;
|
|
47
|
+
workflowMode: WorkflowMode;
|
|
48
|
+
executionMode: ExecutionMode;
|
|
49
|
+
lifecycle: Lifecycle;
|
|
50
|
+
pauseTimestamp: string | null;
|
|
51
|
+
pauseReason: string | null;
|
|
52
|
+
progress: TaskProgress;
|
|
53
|
+
artifacts: ArtifactStatus[];
|
|
54
|
+
reviews: ReviewStatus[];
|
|
55
|
+
blockers: string[];
|
|
56
|
+
hillCheckpoints: string[];
|
|
57
|
+
hillCompleted: string[];
|
|
58
|
+
prStatus: string | null;
|
|
59
|
+
prUrl: string | null;
|
|
60
|
+
docsUpdated: string | null;
|
|
61
|
+
lastCommit: string | null;
|
|
62
|
+
timestamps: {
|
|
63
|
+
created: string;
|
|
64
|
+
completed: string | null;
|
|
65
|
+
stateUpdated: string;
|
|
66
|
+
};
|
|
67
|
+
recommendation: SkillRecommendation;
|
|
68
|
+
}
|
|
69
|
+
export interface ProjectSummary {
|
|
70
|
+
name: string;
|
|
71
|
+
path: string;
|
|
72
|
+
phase: Phase;
|
|
73
|
+
phaseStatus: PhaseStatus;
|
|
74
|
+
workflowMode: WorkflowMode;
|
|
75
|
+
lifecycle: Lifecycle;
|
|
76
|
+
progress: {
|
|
77
|
+
completed: number;
|
|
78
|
+
total: number;
|
|
79
|
+
};
|
|
80
|
+
recommendation: {
|
|
81
|
+
skill: string;
|
|
82
|
+
reason: string;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-agent-toolkit/control-plane",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Read-only OAT control-plane library for structured project state",
|
|
6
|
+
"homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/control-plane",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/voxmedia/open-agent-toolkit/issues"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/voxmedia/open-agent-toolkit.git",
|
|
14
|
+
"directory": "packages/control-plane"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"types": "dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"yaml": "2.8.2"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.10.0",
|
|
37
|
+
"tsc-alias": "^1.8.10",
|
|
38
|
+
"typescript": "^5.8.3",
|
|
39
|
+
"vitest": "^4.0.18"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22.17.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc && tsc-alias",
|
|
46
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
47
|
+
"dev": "tsc --watch",
|
|
48
|
+
"lint": "oxlint .",
|
|
49
|
+
"format": "oxfmt --check .",
|
|
50
|
+
"format:fix": "oxfmt .",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"type-check": "tsc --noEmit"
|
|
54
|
+
}
|
|
55
|
+
}
|