@open-agent-toolkit/cli 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/assets/docs/workflows/projects/artifacts.md +17 -0
  2. package/assets/docs/workflows/projects/index.md +3 -0
  3. package/assets/docs/workflows/projects/splitting.md +79 -0
  4. package/assets/docs/workflows/skills/index.md +2 -0
  5. package/assets/public-package-versions.json +4 -4
  6. package/assets/skills/oat-brainstorm/SKILL.md +43 -3
  7. package/assets/skills/oat-project-discover/SKILL.md +72 -8
  8. package/assets/skills/oat-project-quick-start/SKILL.md +14 -5
  9. package/assets/skills/oat-project-split/SKILL.md +82 -0
  10. package/assets/templates/state.md +6 -1
  11. package/dist/__tests__/skills/split-flow-fixtures.d.ts +50 -0
  12. package/dist/__tests__/skills/split-flow-fixtures.d.ts.map +1 -0
  13. package/dist/__tests__/skills/split-flow-fixtures.js +161 -0
  14. package/dist/commands/init/tools/shared/skill-manifest.d.ts +1 -1
  15. package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
  16. package/dist/commands/init/tools/shared/skill-manifest.js +1 -0
  17. package/dist/commands/project/complete-discovery/index.d.ts +16 -0
  18. package/dist/commands/project/complete-discovery/index.d.ts.map +1 -0
  19. package/dist/commands/project/complete-discovery/index.js +123 -0
  20. package/dist/commands/project/complete-state/index.d.ts.map +1 -1
  21. package/dist/commands/project/complete-state/index.js +5 -0
  22. package/dist/commands/project/index.d.ts.map +1 -1
  23. package/dist/commands/project/index.js +4 -0
  24. package/dist/commands/project/list.d.ts +6 -0
  25. package/dist/commands/project/list.d.ts.map +1 -1
  26. package/dist/commands/project/list.js +37 -4
  27. package/dist/commands/project/new/scaffold.d.ts.map +1 -1
  28. package/dist/commands/project/new/scaffold.js +4 -0
  29. package/dist/commands/project/open/index.d.ts.map +1 -1
  30. package/dist/commands/project/open/index.js +9 -3
  31. package/dist/commands/project/pause/index.d.ts.map +1 -1
  32. package/dist/commands/project/pause/index.js +7 -1
  33. package/dist/commands/project/split/evaluate-signals.d.ts +8 -0
  34. package/dist/commands/project/split/evaluate-signals.d.ts.map +1 -0
  35. package/dist/commands/project/split/evaluate-signals.js +47 -0
  36. package/dist/commands/project/split/index.d.ts +3 -0
  37. package/dist/commands/project/split/index.d.ts.map +1 -0
  38. package/dist/commands/project/split/index.js +11 -0
  39. package/dist/commands/project/split/run.d.ts +21 -0
  40. package/dist/commands/project/split/run.d.ts.map +1 -0
  41. package/dist/commands/project/split/run.js +231 -0
  42. package/dist/commands/project/split/validate-plan.d.ts +14 -0
  43. package/dist/commands/project/split/validate-plan.d.ts.map +1 -0
  44. package/dist/commands/project/split/validate-plan.js +62 -0
  45. package/dist/commands/shared/frontmatter.d.ts +9 -0
  46. package/dist/commands/shared/frontmatter.d.ts.map +1 -1
  47. package/dist/commands/shared/frontmatter.js +46 -0
  48. package/dist/commands/state/generate.d.ts.map +1 -1
  49. package/dist/commands/state/generate.js +38 -6
  50. package/dist/projects/split/child-plan.d.ts +46 -0
  51. package/dist/projects/split/child-plan.d.ts.map +1 -0
  52. package/dist/projects/split/child-plan.js +107 -0
  53. package/dist/projects/split/document-validation.d.ts +14 -0
  54. package/dist/projects/split/document-validation.d.ts.map +1 -0
  55. package/dist/projects/split/document-validation.js +106 -0
  56. package/dist/projects/split/finalize.d.ts +7 -0
  57. package/dist/projects/split/finalize.d.ts.map +1 -0
  58. package/dist/projects/split/finalize.js +32 -0
  59. package/dist/projects/split/resume.d.ts +19 -0
  60. package/dist/projects/split/resume.d.ts.map +1 -0
  61. package/dist/projects/split/resume.js +107 -0
  62. package/dist/projects/split/seed-children.d.ts +9 -0
  63. package/dist/projects/split/seed-children.d.ts.map +1 -0
  64. package/dist/projects/split/seed-children.js +122 -0
  65. package/dist/projects/split/signals.d.ts +10 -0
  66. package/dist/projects/split/signals.d.ts.map +1 -0
  67. package/dist/projects/split/signals.js +18 -0
  68. package/dist/projects/split/validation.d.ts +14 -0
  69. package/dist/projects/split/validation.d.ts.map +1 -0
  70. package/dist/projects/split/validation.js +104 -0
  71. package/dist/projects/split/write-parent.d.ts +16 -0
  72. package/dist/projects/split/write-parent.d.ts.map +1 -0
  73. package/dist/projects/split/write-parent.js +176 -0
  74. package/dist/validation/project-state.d.ts +50 -0
  75. package/dist/validation/project-state.d.ts.map +1 -0
  76. package/dist/validation/project-state.js +279 -0
  77. package/package.json +2 -2
@@ -0,0 +1,104 @@
1
+ function findDependencyCycle(plan) {
2
+ const childSlugs = new Set(plan.children.map((child) => child.slug));
3
+ const graph = new Map(plan.children.map((child) => [
4
+ child.slug,
5
+ child.knownDependencies.filter((dependency) => childSlugs.has(dependency)),
6
+ ]));
7
+ const visiting = new Set();
8
+ const visited = new Set();
9
+ const stack = [];
10
+ function visit(slug) {
11
+ if (visiting.has(slug)) {
12
+ return [...stack.slice(stack.indexOf(slug)), slug];
13
+ }
14
+ if (visited.has(slug)) {
15
+ return null;
16
+ }
17
+ visiting.add(slug);
18
+ stack.push(slug);
19
+ for (const dependency of graph.get(slug) ?? []) {
20
+ const cycle = visit(dependency);
21
+ if (cycle) {
22
+ return cycle;
23
+ }
24
+ }
25
+ stack.pop();
26
+ visiting.delete(slug);
27
+ visited.add(slug);
28
+ return null;
29
+ }
30
+ for (const child of plan.children) {
31
+ const cycle = visit(child.slug);
32
+ if (cycle) {
33
+ return cycle;
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ export function validateChildPlan(plan, existingSlugs) {
39
+ const errors = [];
40
+ const seenChildSlugs = new Set();
41
+ const childSlugs = new Set(plan.children.map((child) => child.slug));
42
+ if (existingSlugs.has(plan.parentSlug)) {
43
+ errors.push({
44
+ code: 'slug-collision-existing',
45
+ message: `Parent slug already exists: ${plan.parentSlug}`,
46
+ slug: plan.parentSlug,
47
+ });
48
+ }
49
+ for (const child of plan.children) {
50
+ if (existingSlugs.has(child.slug)) {
51
+ errors.push({
52
+ code: 'slug-collision-existing',
53
+ message: `Child slug already exists: ${child.slug}`,
54
+ slug: child.slug,
55
+ });
56
+ }
57
+ if (seenChildSlugs.has(child.slug)) {
58
+ errors.push({
59
+ code: 'duplicate-child-slug',
60
+ message: `Duplicate child slug: ${child.slug}`,
61
+ slug: child.slug,
62
+ });
63
+ }
64
+ seenChildSlugs.add(child.slug);
65
+ if (child.slug === plan.parentSlug) {
66
+ errors.push({
67
+ code: 'parent-child-slug-collision',
68
+ message: `Parent slug collides with child slug: ${child.slug}`,
69
+ slug: child.slug,
70
+ });
71
+ }
72
+ for (const dependency of child.knownDependencies) {
73
+ if (dependency === child.slug || !childSlugs.has(dependency)) {
74
+ errors.push({
75
+ code: 'unknown-dependency',
76
+ message: `Dependency ${dependency} is not a sibling of ${child.slug}`,
77
+ slug: child.slug,
78
+ });
79
+ }
80
+ }
81
+ }
82
+ if (plan.foundationChild && !childSlugs.has(plan.foundationChild)) {
83
+ errors.push({
84
+ code: 'unknown-foundation-child',
85
+ message: `foundationChild is not in children: ${plan.foundationChild}`,
86
+ slug: plan.foundationChild,
87
+ });
88
+ }
89
+ if (!childSlugs.has(plan.initialActiveChild)) {
90
+ errors.push({
91
+ code: 'unknown-initial-active-child',
92
+ message: `initialActiveChild is not in children: ${plan.initialActiveChild}`,
93
+ slug: plan.initialActiveChild,
94
+ });
95
+ }
96
+ const cycle = findDependencyCycle(plan);
97
+ if (cycle) {
98
+ errors.push({
99
+ code: 'dependency-cycle',
100
+ message: `Child dependencies contain a cycle: ${cycle.join(' -> ')}`,
101
+ });
102
+ }
103
+ return errors.length === 0 ? { ok: true } : { ok: false, errors };
104
+ }
@@ -0,0 +1,16 @@
1
+ import { scaffoldProject as defaultScaffoldProject, type ScaffoldProjectResult } from '../../commands/project/new/scaffold.js';
2
+ import type { SplitPlanDocument } from './child-plan.js';
3
+ export interface SplitProjectContext {
4
+ repoRoot: string;
5
+ projectsRoot?: string;
6
+ today?: string;
7
+ nowUtc?: string;
8
+ env?: NodeJS.ProcessEnv;
9
+ scaffoldProject?: typeof defaultScaffoldProject;
10
+ }
11
+ export interface WriteCoordinationParentResult {
12
+ parentProjectPath: string;
13
+ scaffold: ScaffoldProjectResult;
14
+ }
15
+ export declare function writeCoordinationParent(document: SplitPlanDocument, context: SplitProjectContext): Promise<WriteCoordinationParentResult>;
16
+ //# sourceMappingURL=write-parent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-parent.d.ts","sourceRoot":"","sources":["../../../src/projects/split/write-parent.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,eAAe,IAAI,sBAAsB,EACzC,KAAK,qBAAqB,EAC3B,MAAM,gCAAgC,CAAC;AAMxC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,sBAAsB,CAAC;CACjD;AA6JD,MAAM,WAAW,6BAA6B;IAC5C,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,qBAAqB,CAAC;CACjC;AAED,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,6BAA6B,CAAC,CA4DxC"}
@@ -0,0 +1,176 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { scaffoldProject as defaultScaffoldProject, } from '../../commands/project/new/scaffold.js';
4
+ import { getFrontmatterBlock } from '../../commands/shared/frontmatter.js';
5
+ import { replaceFrontmatter } from '../../commands/shared/frontmatter-write.js';
6
+ import { assertValidProjectStateFilesystemContent } from '../../validation/project-state.js';
7
+ import YAML from 'yaml';
8
+ function readObjectFrontmatter(content, filePath) {
9
+ const block = getFrontmatterBlock(content);
10
+ if (!block) {
11
+ throw new Error(`${filePath} is missing frontmatter`);
12
+ }
13
+ const parsed = YAML.parse(block);
14
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
15
+ throw new Error(`${filePath} frontmatter must be an object`);
16
+ }
17
+ return parsed;
18
+ }
19
+ async function writeObjectFrontmatter(filePath, updates) {
20
+ const content = await readFile(filePath, 'utf8');
21
+ const frontmatter = readObjectFrontmatter(content, filePath);
22
+ updates(frontmatter);
23
+ await writeFile(filePath, replaceFrontmatter(content, YAML.stringify(frontmatter).trimEnd()), 'utf8');
24
+ }
25
+ function projectPathFor(projectsRoot, slug) {
26
+ return join(projectsRoot, slug).split('\\').join('/');
27
+ }
28
+ function renderParentDiscovery(document) {
29
+ const { plan } = document;
30
+ const orderedChildren = plan.children
31
+ .slice()
32
+ .sort((left, right) => left.order - right.order);
33
+ const childSlugs = orderedChildren.map((child) => child.slug);
34
+ const childSummary = orderedChildren
35
+ .map((child) => {
36
+ const siblings = childSlugs.filter((slug) => slug !== child.slug);
37
+ return [
38
+ `${child.order}. ${child.slug}: ${child.description ?? 'No description provided.'}`,
39
+ ` - Dependencies: ${child.knownDependencies.length > 0 ? child.knownDependencies.join(', ') : 'None'}`,
40
+ ` - Siblings: ${siblings.length > 0 ? siblings.join(', ') : 'None'}`,
41
+ ` - Inherited context: ${child.inheritedContext || 'None provided.'}`,
42
+ ].join('\n');
43
+ })
44
+ .join('\n');
45
+ const inheritedContext = orderedChildren
46
+ .map((child) => `- ${child.slug}: ${child.inheritedContext || 'None provided.'}`)
47
+ .join('\n');
48
+ const sharedConstraints = [
49
+ plan.foundationChild ? `- Foundation child: ${plan.foundationChild}` : null,
50
+ `- Initial active child: ${plan.initialActiveChild}`,
51
+ ]
52
+ .filter((line) => Boolean(line))
53
+ .join('\n');
54
+ return [
55
+ '---',
56
+ 'oat_status: complete',
57
+ 'oat_ready_for: null',
58
+ 'oat_blockers: []',
59
+ 'oat_generated: false',
60
+ '---',
61
+ '',
62
+ `# Discovery: ${plan.parentSlug}`,
63
+ '',
64
+ '## Split Rationale',
65
+ '',
66
+ `Origin: ${document.origin}`,
67
+ `Interactive: ${document.interactive ? 'true' : 'false'}`,
68
+ `Why: ${plan.integrationSketch ?? 'Split child scopes were captured in a coordination parent for tracked sequencing and integration.'}`,
69
+ '',
70
+ '## Ordered Children',
71
+ '',
72
+ childSummary,
73
+ '',
74
+ '## Inherited Broad Context',
75
+ '',
76
+ inheritedContext || 'No inherited broad context provided.',
77
+ '',
78
+ '## Shared Constraints',
79
+ '',
80
+ sharedConstraints || 'No shared constraints provided.',
81
+ '',
82
+ '## Integration Sketch',
83
+ '',
84
+ plan.integrationSketch ?? 'No integration sketch provided.',
85
+ '',
86
+ ].join('\n');
87
+ }
88
+ function renderParentStateBody(document) {
89
+ const childList = document.plan.children
90
+ .slice()
91
+ .sort((left, right) => left.order - right.order)
92
+ .map((child) => `- ${child.slug}`)
93
+ .join('\n');
94
+ return [
95
+ `# Project State: ${document.plan.parentSlug}`,
96
+ '',
97
+ '**Status:** Coordination',
98
+ '',
99
+ '## Current Phase',
100
+ '',
101
+ 'Decomposition coordination - split children are tracked as implementation projects.',
102
+ '',
103
+ '## Artifacts',
104
+ '',
105
+ '- **Discovery:** `discovery.md` (coordination summary)',
106
+ '- **Split Plan:** `references/split-plan.json` (persisted resume source)',
107
+ '- **Spec:** N/A (coordination parent)',
108
+ '- **Design:** N/A (coordination parent)',
109
+ '- **Plan:** N/A (coordination parent)',
110
+ '- **Implementation:** N/A (coordination parent)',
111
+ '',
112
+ '## Children',
113
+ '',
114
+ childList || '- None.',
115
+ '',
116
+ '## Progress',
117
+ '',
118
+ '- Split plan persisted',
119
+ '- Child projects selected for implementation tracking',
120
+ '',
121
+ '## Blockers',
122
+ '',
123
+ 'None',
124
+ '',
125
+ '## Next Milestone',
126
+ '',
127
+ `Continue through active child \`${document.plan.initialActiveChild}\`.`,
128
+ '',
129
+ ].join('\n');
130
+ }
131
+ async function replaceMarkdownBody(filePath, body) {
132
+ const content = await readFile(filePath, 'utf8');
133
+ const frontmatter = getFrontmatterBlock(content);
134
+ if (!frontmatter) {
135
+ throw new Error(`${filePath} is missing frontmatter`);
136
+ }
137
+ await writeFile(filePath, `---\n${frontmatter}\n---\n\n${body}`, 'utf8');
138
+ }
139
+ export async function writeCoordinationParent(document, context) {
140
+ const scaffoldProject = context.scaffoldProject ?? defaultScaffoldProject;
141
+ const scaffold = await scaffoldProject({
142
+ repoRoot: context.repoRoot,
143
+ projectName: document.plan.parentSlug,
144
+ mode: 'quick',
145
+ setActive: false,
146
+ refreshDashboard: false,
147
+ env: context.env,
148
+ today: context.today,
149
+ nowUtc: context.nowUtc,
150
+ });
151
+ const parentProjectPath = scaffold.projectPath;
152
+ const parentRoot = join(context.repoRoot, parentProjectPath);
153
+ const statePath = join(parentRoot, 'state.md');
154
+ await writeObjectFrontmatter(statePath, (frontmatter) => {
155
+ frontmatter['oat_kind'] = 'coordination';
156
+ frontmatter['oat_workflow_mode'] = 'quick';
157
+ frontmatter['oat_hill_checkpoints'] = [];
158
+ frontmatter['oat_children'] = document.plan.children
159
+ .slice()
160
+ .sort((left, right) => left.order - right.order)
161
+ .map((child) => child.slug);
162
+ });
163
+ await replaceMarkdownBody(statePath, renderParentStateBody(document));
164
+ await mkdir(join(parentRoot, 'references'), { recursive: true });
165
+ await writeFile(join(parentRoot, 'references', 'split-plan.json'), `${JSON.stringify(document, null, 2)}\n`, 'utf8');
166
+ await writeFile(join(parentRoot, 'discovery.md'), renderParentDiscovery(document), 'utf8');
167
+ await Promise.all(['spec.md', 'design.md', 'plan.md', 'implementation.md'].map((file) => rm(join(parentRoot, file), { force: true })));
168
+ await assertValidProjectStateFilesystemContent(await readFile(statePath, 'utf8'), {
169
+ filePath: statePath,
170
+ projectPath: parentRoot,
171
+ });
172
+ return {
173
+ parentProjectPath: projectPathFor(context.projectsRoot ?? scaffold.projectsRoot, document.plan.parentSlug),
174
+ scaffold,
175
+ };
176
+ }
@@ -0,0 +1,50 @@
1
+ import { readdir as defaultReaddir, readFile as defaultReadFile } from 'node:fs/promises';
2
+ import { type ProjectStateKind, type ProjectStatePhase } from '../commands/shared/frontmatter.js';
3
+ export interface NormalizedProjectState {
4
+ oat_kind: ProjectStateKind;
5
+ oat_phase?: ProjectStatePhase;
6
+ oat_phase_status?: string;
7
+ oat_parent?: string;
8
+ oat_siblings: string[];
9
+ oat_depends_on: string[];
10
+ oat_children: string[];
11
+ }
12
+ export interface ProjectStateValidationError {
13
+ code: string;
14
+ message: string;
15
+ }
16
+ export interface ProjectStateValidationInput {
17
+ slug?: string;
18
+ frontmatter: Record<string, unknown>;
19
+ relatedProjects?: ProjectStateSnapshot[];
20
+ }
21
+ export interface ProjectStateSnapshot {
22
+ slug: string;
23
+ frontmatter: Record<string, unknown>;
24
+ }
25
+ export interface ProjectStateValidationResult {
26
+ ok: boolean;
27
+ state: NormalizedProjectState;
28
+ errors: ProjectStateValidationError[];
29
+ }
30
+ export interface AssertProjectStateContentOptions {
31
+ filePath?: string;
32
+ slug?: string;
33
+ relatedProjects?: ProjectStateSnapshot[];
34
+ }
35
+ export interface AssertProjectStateFilesystemContentOptions extends Omit<AssertProjectStateContentOptions, 'relatedProjects'> {
36
+ projectPath: string;
37
+ projectsRoot?: string;
38
+ readdir?: typeof defaultReaddir;
39
+ readFile?: typeof defaultReadFile;
40
+ }
41
+ export declare function validateProjectState(input: ProjectStateValidationInput): ProjectStateValidationResult;
42
+ export declare function assertValidProjectStateContent(content: string, options?: AssertProjectStateContentOptions): void;
43
+ export declare function readRelatedProjectStateSnapshots(options: {
44
+ projectsRoot: string;
45
+ currentProjectSlug: string;
46
+ readdir?: typeof defaultReaddir;
47
+ readFile?: typeof defaultReadFile;
48
+ }): Promise<ProjectStateSnapshot[]>;
49
+ export declare function assertValidProjectStateFilesystemContent(content: string, options: AssertProjectStateFilesystemContentOptions): Promise<void>;
50
+ //# sourceMappingURL=project-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-state.d.ts","sourceRoot":"","sources":["../../src/validation/project-state.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,IAAI,cAAc,EACzB,QAAQ,IAAI,eAAe,EAC5B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAIL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,8BAA8B,CAAC;AAGtC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,eAAe,CAAC,EAAE,oBAAoB,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,sBAAsB,CAAC;IAC9B,MAAM,EAAE,2BAA2B,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,oBAAoB,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,0CAA2C,SAAQ,IAAI,CACtE,gCAAgC,EAChC,iBAAiB,CAClB;IACC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,cAAc,CAAC;IAChC,QAAQ,CAAC,EAAE,OAAO,eAAe,CAAC;CACnC;AAmND,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,2BAA2B,GACjC,4BAA4B,CAqE9B;AAED,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,gCAAqC,GAC7C,IAAI,CAYN;AAED,wBAAsB,gCAAgC,CAAC,OAAO,EAAE;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,cAAc,CAAC;IAChC,QAAQ,CAAC,EAAE,OAAO,eAAe,CAAC;CACnC,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAwBlC;AAED,wBAAsB,wCAAwC,CAC5D,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,0CAA0C,GAClD,OAAO,CAAC,IAAI,CAAC,CAkDf"}
@@ -0,0 +1,279 @@
1
+ import { readdir as defaultReaddir, readFile as defaultReadFile, } from 'node:fs/promises';
2
+ import { basename, join } from 'node:path';
3
+ import { getFrontmatterBlock, isProjectStateKind, isProjectStatePhase, } from '../commands/shared/frontmatter.js';
4
+ import YAML from 'yaml';
5
+ function readStringField(frontmatter, key) {
6
+ const value = frontmatter[key];
7
+ return typeof value === 'string' ? value : undefined;
8
+ }
9
+ function readStringArrayField(frontmatter, key, errors) {
10
+ const value = frontmatter[key];
11
+ if (value === undefined || value === null) {
12
+ return [];
13
+ }
14
+ if (Array.isArray(value) && value.every((item) => typeof item === 'string')) {
15
+ return [...value];
16
+ }
17
+ errors.push({
18
+ code: 'invalid-string-array',
19
+ message: `${key} must be an array of strings`,
20
+ });
21
+ return [];
22
+ }
23
+ function readKind(frontmatter) {
24
+ const rawKind = readStringField(frontmatter, 'oat_kind');
25
+ return rawKind && isProjectStateKind(rawKind) ? rawKind : 'implementation';
26
+ }
27
+ function sameStringSet(left, right) {
28
+ if (left.length !== right.length) {
29
+ return false;
30
+ }
31
+ const sortedLeft = [...left].sort();
32
+ const sortedRight = [...right].sort();
33
+ return sortedLeft.every((value, index) => value === sortedRight[index]);
34
+ }
35
+ function graphHasCycle(graph) {
36
+ const visiting = new Set();
37
+ const visited = new Set();
38
+ function visit(slug) {
39
+ if (visiting.has(slug)) {
40
+ return true;
41
+ }
42
+ if (visited.has(slug)) {
43
+ return false;
44
+ }
45
+ visiting.add(slug);
46
+ for (const dependency of graph.get(slug) ?? []) {
47
+ if (graph.has(dependency) && visit(dependency)) {
48
+ return true;
49
+ }
50
+ }
51
+ visiting.delete(slug);
52
+ visited.add(slug);
53
+ return false;
54
+ }
55
+ return [...graph.keys()].some((slug) => visit(slug));
56
+ }
57
+ function validateChildLinkage(input, state, errors) {
58
+ if (!state.oat_parent) {
59
+ return;
60
+ }
61
+ const relatedProjects = input.relatedProjects ?? [];
62
+ const parent = relatedProjects.find((project) => project.slug === state.oat_parent);
63
+ if (!parent && input.relatedProjects !== undefined) {
64
+ errors.push({
65
+ code: 'parent-missing',
66
+ message: `oat_parent ${state.oat_parent} must reference an existing project`,
67
+ });
68
+ }
69
+ if (parent && readKind(parent.frontmatter) !== 'coordination') {
70
+ errors.push({
71
+ code: 'parent-not-coordination',
72
+ message: `oat_parent ${state.oat_parent} must reference a coordination project`,
73
+ });
74
+ }
75
+ for (const dependency of state.oat_depends_on) {
76
+ if (!state.oat_siblings.includes(dependency)) {
77
+ errors.push({
78
+ code: 'depends-on-non-sibling',
79
+ message: `oat_depends_on entry ${dependency} must be listed in oat_siblings`,
80
+ });
81
+ }
82
+ }
83
+ if (input.slug && parent) {
84
+ const parentChildren = readStringArrayField(parent.frontmatter, 'oat_children', errors);
85
+ if (parentChildren.length > 0) {
86
+ const expectedSiblings = parentChildren.filter((childSlug) => childSlug !== input.slug);
87
+ if (!sameStringSet(state.oat_siblings, expectedSiblings)) {
88
+ errors.push({
89
+ code: 'siblings-must-match-parent-children',
90
+ message: 'oat_siblings must equal parent oat_children minus the current child',
91
+ });
92
+ }
93
+ }
94
+ }
95
+ const graph = new Map();
96
+ if (input.slug) {
97
+ graph.set(input.slug, state.oat_depends_on);
98
+ }
99
+ for (const project of relatedProjects) {
100
+ if (project.slug === input.slug || project.slug === state.oat_parent) {
101
+ continue;
102
+ }
103
+ if (readStringField(project.frontmatter, 'oat_parent') !== state.oat_parent) {
104
+ continue;
105
+ }
106
+ graph.set(project.slug, readStringArrayField(project.frontmatter, 'oat_depends_on', errors));
107
+ }
108
+ if (graphHasCycle(graph)) {
109
+ errors.push({
110
+ code: 'sibling-dependency-cycle',
111
+ message: 'oat_depends_on across sibling projects must be acyclic',
112
+ });
113
+ }
114
+ }
115
+ function validateInheritedContextGate(frontmatter, state, errors) {
116
+ if (!state.oat_parent) {
117
+ return;
118
+ }
119
+ if (readStringField(frontmatter, 'oat_status') === 'complete' &&
120
+ frontmatter['oat_inherited_context_revalidated'] === false) {
121
+ errors.push({
122
+ code: 'inherited-context-revalidation-required',
123
+ message: 'child discovery cannot complete until oat_inherited_context_revalidated is true',
124
+ });
125
+ }
126
+ }
127
+ function validateCoordinationParent(state, errors) {
128
+ if (state.oat_phase !== 'decomposition') {
129
+ return;
130
+ }
131
+ if (state.oat_children.length === 0) {
132
+ errors.push({
133
+ code: 'decomposition-requires-children',
134
+ message: 'oat_phase: decomposition requires non-empty oat_children',
135
+ });
136
+ }
137
+ }
138
+ function parseFrontmatterObject(content, filePath) {
139
+ const frontmatter = getFrontmatterBlock(content);
140
+ if (!frontmatter) {
141
+ throw new Error(`${filePath} is missing frontmatter`);
142
+ }
143
+ const parsed = YAML.parse(frontmatter);
144
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
145
+ throw new Error(`${filePath} frontmatter must be a YAML object`);
146
+ }
147
+ return parsed;
148
+ }
149
+ export function validateProjectState(input) {
150
+ const errors = [];
151
+ const rawKind = readStringField(input.frontmatter, 'oat_kind');
152
+ const rawPhase = readStringField(input.frontmatter, 'oat_phase');
153
+ const rawPhaseStatus = readStringField(input.frontmatter, 'oat_phase_status');
154
+ const rawParent = readStringField(input.frontmatter, 'oat_parent');
155
+ let kind = 'implementation';
156
+ if (rawKind) {
157
+ if (isProjectStateKind(rawKind)) {
158
+ kind = rawKind;
159
+ }
160
+ else {
161
+ errors.push({
162
+ code: 'invalid-oat-kind',
163
+ message: `Invalid oat_kind: ${rawKind}`,
164
+ });
165
+ }
166
+ }
167
+ let phase;
168
+ if (rawPhase) {
169
+ if (isProjectStatePhase(rawPhase)) {
170
+ phase = rawPhase;
171
+ }
172
+ else {
173
+ errors.push({
174
+ code: 'invalid-oat-phase',
175
+ message: `Invalid oat_phase: ${rawPhase}`,
176
+ });
177
+ }
178
+ }
179
+ if (phase === 'decomposition' && kind !== 'coordination') {
180
+ errors.push({
181
+ code: 'decomposition-requires-coordination',
182
+ message: 'oat_phase: decomposition requires oat_kind: coordination',
183
+ });
184
+ }
185
+ const state = {
186
+ oat_kind: kind,
187
+ oat_phase: phase,
188
+ oat_phase_status: rawPhaseStatus,
189
+ oat_parent: rawParent,
190
+ oat_siblings: readStringArrayField(input.frontmatter, 'oat_siblings', errors),
191
+ oat_depends_on: readStringArrayField(input.frontmatter, 'oat_depends_on', errors),
192
+ oat_children: readStringArrayField(input.frontmatter, 'oat_children', errors),
193
+ };
194
+ validateChildLinkage(input, state, errors);
195
+ validateInheritedContextGate(input.frontmatter, state, errors);
196
+ validateCoordinationParent(state, errors);
197
+ return {
198
+ ok: errors.length === 0,
199
+ state,
200
+ errors,
201
+ };
202
+ }
203
+ export function assertValidProjectStateContent(content, options = {}) {
204
+ const filePath = options.filePath ?? 'state.md';
205
+ const frontmatter = parseFrontmatterObject(content, filePath);
206
+ const result = validateProjectState({
207
+ frontmatter,
208
+ slug: options.slug,
209
+ relatedProjects: options.relatedProjects,
210
+ });
211
+ if (!result.ok) {
212
+ throw new Error(result.errors.map((error) => error.message).join('; '));
213
+ }
214
+ }
215
+ export async function readRelatedProjectStateSnapshots(options) {
216
+ const readdir = options.readdir ?? defaultReaddir;
217
+ const readFile = options.readFile ?? defaultReadFile;
218
+ const snapshots = [];
219
+ const entries = await readdir(options.projectsRoot, { withFileTypes: true });
220
+ for (const entry of entries) {
221
+ if (!entry.isDirectory() || entry.name === options.currentProjectSlug) {
222
+ continue;
223
+ }
224
+ const statePath = join(options.projectsRoot, entry.name, 'state.md');
225
+ try {
226
+ const content = await readFile(statePath, 'utf8');
227
+ snapshots.push({
228
+ slug: entry.name,
229
+ frontmatter: parseFrontmatterObject(content, statePath),
230
+ });
231
+ }
232
+ catch {
233
+ continue;
234
+ }
235
+ }
236
+ return snapshots;
237
+ }
238
+ export async function assertValidProjectStateFilesystemContent(content, options) {
239
+ const slug = options.slug ?? basename(options.projectPath);
240
+ const relatedProjects = await readRelatedProjectStateSnapshots({
241
+ projectsRoot: options.projectsRoot ?? join(options.projectPath, '..'),
242
+ currentProjectSlug: slug,
243
+ readdir: options.readdir,
244
+ readFile: options.readFile,
245
+ });
246
+ const frontmatter = parseFrontmatterObject(content, options.filePath ?? 'state.md');
247
+ const result = validateProjectState({
248
+ frontmatter,
249
+ slug,
250
+ relatedProjects,
251
+ });
252
+ const errors = [...result.errors];
253
+ if (result.state.oat_kind === 'coordination' ||
254
+ result.state.oat_phase === 'decomposition') {
255
+ const readdir = options.readdir ?? defaultReaddir;
256
+ const entries = await readdir(options.projectPath, {
257
+ withFileTypes: true,
258
+ });
259
+ const executableArtifacts = new Set([
260
+ 'spec.md',
261
+ 'design.md',
262
+ 'plan.md',
263
+ 'implementation.md',
264
+ ]);
265
+ const presentExecutableArtifacts = entries
266
+ .filter((entry) => entry.isFile() && executableArtifacts.has(entry.name))
267
+ .map((entry) => entry.name)
268
+ .sort();
269
+ if (presentExecutableArtifacts.length > 0) {
270
+ errors.push({
271
+ code: 'coordination-parent-no-executable-artifacts',
272
+ message: `coordination projects must not contain executable phase artifacts: ${presentExecutableArtifacts.join(', ')}`,
273
+ });
274
+ }
275
+ }
276
+ if (errors.length > 0) {
277
+ throw new Error(errors.map((error) => error.message).join('; '));
278
+ }
279
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "description": "Open Agent Toolkit CLI",
6
6
  "homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
@@ -33,7 +33,7 @@
33
33
  "ora": "^9.0.0",
34
34
  "yaml": "2.8.2",
35
35
  "zod": "^3.25.76",
36
- "@open-agent-toolkit/control-plane": "0.1.5"
36
+ "@open-agent-toolkit/control-plane": "0.1.6"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.10.0",