@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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/index.d.ts +4 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +3 -0
  6. package/dist/project.d.ts +4 -0
  7. package/dist/project.d.ts.map +1 -0
  8. package/dist/project.js +191 -0
  9. package/dist/recommender/boundary.d.ts +3 -0
  10. package/dist/recommender/boundary.d.ts.map +1 -0
  11. package/dist/recommender/boundary.js +22 -0
  12. package/dist/recommender/router.d.ts +3 -0
  13. package/dist/recommender/router.d.ts.map +1 -0
  14. package/dist/recommender/router.js +182 -0
  15. package/dist/shared/utils/errors.d.ts +2 -0
  16. package/dist/shared/utils/errors.d.ts.map +1 -0
  17. package/dist/shared/utils/errors.js +6 -0
  18. package/dist/shared/utils/frontmatter.d.ts +3 -0
  19. package/dist/shared/utils/frontmatter.d.ts.map +1 -0
  20. package/dist/shared/utils/frontmatter.js +22 -0
  21. package/dist/shared/utils/normalize.d.ts +5 -0
  22. package/dist/shared/utils/normalize.d.ts.map +1 -0
  23. package/dist/shared/utils/normalize.js +29 -0
  24. package/dist/state/artifacts.d.ts +3 -0
  25. package/dist/state/artifacts.d.ts.map +1 -0
  26. package/dist/state/artifacts.js +52 -0
  27. package/dist/state/parser.d.ts +27 -0
  28. package/dist/state/parser.d.ts.map +1 -0
  29. package/dist/state/parser.js +120 -0
  30. package/dist/state/reviews.d.ts +4 -0
  31. package/dist/state/reviews.d.ts.map +1 -0
  32. package/dist/state/reviews.js +60 -0
  33. package/dist/state/tasks.d.ts +3 -0
  34. package/dist/state/tasks.d.ts.map +1 -0
  35. package/dist/state/tasks.js +85 -0
  36. package/dist/types.d.ts +85 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +1 -0
  39. 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
+ ```
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export { getProjectState, listProjects } from './project.js';
3
+ export { recommendSkill } from './recommender/router.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ export * from './types.js';
2
+ export { getProjectState, listProjects } from './project.js';
3
+ export { recommendSkill } from './recommender/router.js';
@@ -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"}
@@ -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,3 @@
1
+ import type { BoundaryTier } from '../types.js';
2
+ export declare function detectBoundaryTier(frontmatter: Record<string, unknown>, content: string): BoundaryTier;
3
+ //# sourceMappingURL=boundary.d.ts.map
@@ -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,3 @@
1
+ import type { ProjectState, SkillRecommendation } from '../types.js';
2
+ export declare function recommendSkill(state: Omit<ProjectState, 'recommendation'>): SkillRecommendation;
3
+ //# sourceMappingURL=router.d.ts.map
@@ -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,2 @@
1
+ export declare function isMissingFileError(error: unknown): boolean;
2
+ //# sourceMappingURL=errors.d.ts.map
@@ -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,6 @@
1
+ export function isMissingFileError(error) {
2
+ return (typeof error === 'object' &&
3
+ error !== null &&
4
+ 'code' in error &&
5
+ error.code === 'ENOENT');
6
+ }
@@ -0,0 +1,3 @@
1
+ export declare function extractFrontmatter(content: string): string | null;
2
+ export declare function parseFrontmatterRecord(content: string): Record<string, unknown>;
3
+ //# sourceMappingURL=frontmatter.d.ts.map
@@ -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,5 @@
1
+ export declare function normalizeNullableString(value: unknown, options?: {
2
+ treatPlaceholdersAsNull?: boolean;
3
+ }): string | null;
4
+ export declare function parseBoolean(value: unknown): boolean;
5
+ //# sourceMappingURL=normalize.d.ts.map
@@ -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,3 @@
1
+ import type { ArtifactStatus } from '../types.js';
2
+ export declare function scanArtifacts(projectPath: string): Promise<ArtifactStatus[]>;
3
+ //# sourceMappingURL=artifacts.d.ts.map
@@ -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,4 @@
1
+ import type { ReviewStatus } from '../types.js';
2
+ export declare function parseReviewTable(planContent: string): ReviewStatus[];
3
+ export declare function scanUnprocessedReviews(projectPath: string): Promise<string[]>;
4
+ //# sourceMappingURL=reviews.d.ts.map
@@ -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,3 @@
1
+ import type { TaskProgress } from '../types.js';
2
+ export declare function parseTaskProgress(planContent: string, implementationContent: string): TaskProgress;
3
+ //# sourceMappingURL=tasks.d.ts.map
@@ -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
+ }
@@ -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
+ }