@open-agent-toolkit/cli 0.1.5 → 0.1.7
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/assets/agents/oat-reviewer.md +10 -1
- package/assets/docs/workflows/projects/artifacts.md +17 -0
- package/assets/docs/workflows/projects/index.md +3 -0
- package/assets/docs/workflows/projects/splitting.md +79 -0
- package/assets/docs/workflows/skills/index.md +2 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-brainstorm/SKILL.md +43 -3
- package/assets/skills/oat-project-discover/SKILL.md +72 -8
- package/assets/skills/oat-project-implement/SKILL.md +21 -1
- package/assets/skills/oat-project-quick-start/SKILL.md +14 -5
- package/assets/skills/oat-project-review-provide/SKILL.md +9 -1
- package/assets/skills/oat-project-review-receive/SKILL.md +21 -1
- package/assets/skills/oat-project-split/SKILL.md +82 -0
- package/assets/skills/oat-project-summary/SKILL.md +15 -13
- package/assets/templates/implementation.md +5 -5
- package/assets/templates/state.md +6 -1
- package/assets/templates/summary.md +2 -1
- package/dist/__tests__/skills/split-flow-fixtures.d.ts +50 -0
- package/dist/__tests__/skills/split-flow-fixtures.d.ts.map +1 -0
- package/dist/__tests__/skills/split-flow-fixtures.js +161 -0
- package/dist/commands/init/tools/shared/skill-manifest.d.ts +1 -1
- package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
- package/dist/commands/init/tools/shared/skill-manifest.js +1 -0
- package/dist/commands/project/complete-discovery/index.d.ts +16 -0
- package/dist/commands/project/complete-discovery/index.d.ts.map +1 -0
- package/dist/commands/project/complete-discovery/index.js +123 -0
- package/dist/commands/project/complete-state/index.d.ts.map +1 -1
- package/dist/commands/project/complete-state/index.js +5 -0
- package/dist/commands/project/index.d.ts.map +1 -1
- package/dist/commands/project/index.js +4 -0
- package/dist/commands/project/list.d.ts +6 -0
- package/dist/commands/project/list.d.ts.map +1 -1
- package/dist/commands/project/list.js +37 -4
- package/dist/commands/project/new/scaffold.d.ts.map +1 -1
- package/dist/commands/project/new/scaffold.js +4 -0
- package/dist/commands/project/open/index.d.ts.map +1 -1
- package/dist/commands/project/open/index.js +9 -3
- package/dist/commands/project/pause/index.d.ts.map +1 -1
- package/dist/commands/project/pause/index.js +7 -1
- package/dist/commands/project/split/evaluate-signals.d.ts +8 -0
- package/dist/commands/project/split/evaluate-signals.d.ts.map +1 -0
- package/dist/commands/project/split/evaluate-signals.js +47 -0
- package/dist/commands/project/split/index.d.ts +3 -0
- package/dist/commands/project/split/index.d.ts.map +1 -0
- package/dist/commands/project/split/index.js +11 -0
- package/dist/commands/project/split/run.d.ts +21 -0
- package/dist/commands/project/split/run.d.ts.map +1 -0
- package/dist/commands/project/split/run.js +231 -0
- package/dist/commands/project/split/validate-plan.d.ts +14 -0
- package/dist/commands/project/split/validate-plan.d.ts.map +1 -0
- package/dist/commands/project/split/validate-plan.js +62 -0
- package/dist/commands/shared/frontmatter.d.ts +9 -0
- package/dist/commands/shared/frontmatter.d.ts.map +1 -1
- package/dist/commands/shared/frontmatter.js +46 -0
- package/dist/commands/state/generate.d.ts.map +1 -1
- package/dist/commands/state/generate.js +38 -6
- package/dist/projects/split/child-plan.d.ts +46 -0
- package/dist/projects/split/child-plan.d.ts.map +1 -0
- package/dist/projects/split/child-plan.js +107 -0
- package/dist/projects/split/document-validation.d.ts +14 -0
- package/dist/projects/split/document-validation.d.ts.map +1 -0
- package/dist/projects/split/document-validation.js +106 -0
- package/dist/projects/split/finalize.d.ts +7 -0
- package/dist/projects/split/finalize.d.ts.map +1 -0
- package/dist/projects/split/finalize.js +32 -0
- package/dist/projects/split/resume.d.ts +19 -0
- package/dist/projects/split/resume.d.ts.map +1 -0
- package/dist/projects/split/resume.js +107 -0
- package/dist/projects/split/seed-children.d.ts +9 -0
- package/dist/projects/split/seed-children.d.ts.map +1 -0
- package/dist/projects/split/seed-children.js +122 -0
- package/dist/projects/split/signals.d.ts +10 -0
- package/dist/projects/split/signals.d.ts.map +1 -0
- package/dist/projects/split/signals.js +18 -0
- package/dist/projects/split/validation.d.ts +14 -0
- package/dist/projects/split/validation.d.ts.map +1 -0
- package/dist/projects/split/validation.js +104 -0
- package/dist/projects/split/write-parent.d.ts +16 -0
- package/dist/projects/split/write-parent.d.ts.map +1 -0
- package/dist/projects/split/write-parent.js +176 -0
- package/dist/validation/project-state.d.ts +50 -0
- package/dist/validation/project-state.d.ts.map +1 -0
- package/dist/validation/project-state.js +279 -0
- package/package.json +2 -2
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { readFile, 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 YAML from 'yaml';
|
|
7
|
+
const SEEDED_SECTIONS = [
|
|
8
|
+
'Origin',
|
|
9
|
+
'Inherited Context',
|
|
10
|
+
'Child Scope',
|
|
11
|
+
'Known Dependencies',
|
|
12
|
+
'Assumptions To Revalidate',
|
|
13
|
+
'Likely Workflow Mode',
|
|
14
|
+
'Sibling Projects',
|
|
15
|
+
];
|
|
16
|
+
function readObjectFrontmatter(content, filePath) {
|
|
17
|
+
const block = getFrontmatterBlock(content);
|
|
18
|
+
if (!block) {
|
|
19
|
+
if (content.startsWith('---\n---')) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`${filePath} is missing frontmatter`);
|
|
23
|
+
}
|
|
24
|
+
if (block.trim().length === 0) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
const parsed = YAML.parse(block);
|
|
28
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
29
|
+
throw new Error(`${filePath} frontmatter must be an object`);
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
async function updateFrontmatter(filePath, updates) {
|
|
34
|
+
const content = await readFile(filePath, 'utf8');
|
|
35
|
+
const frontmatter = readObjectFrontmatter(content, filePath);
|
|
36
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
37
|
+
frontmatter[key] = value;
|
|
38
|
+
}
|
|
39
|
+
const nextBlock = YAML.stringify(frontmatter).trimEnd();
|
|
40
|
+
const nextContent = getFrontmatterBlock(content)
|
|
41
|
+
? replaceFrontmatter(content, nextBlock)
|
|
42
|
+
: content.replace(/^---\n---/, `---\n${nextBlock}\n---`);
|
|
43
|
+
await writeFile(filePath, nextContent, 'utf8');
|
|
44
|
+
}
|
|
45
|
+
function bulletList(items) {
|
|
46
|
+
if (items.length === 0) {
|
|
47
|
+
return '- None.';
|
|
48
|
+
}
|
|
49
|
+
return items.map((item) => `- ${item}`).join('\n');
|
|
50
|
+
}
|
|
51
|
+
function renderSeededDiscovery(plan, child) {
|
|
52
|
+
const siblingSlugs = plan.children
|
|
53
|
+
.map((candidate) => candidate.slug)
|
|
54
|
+
.filter((slug) => slug !== child.slug);
|
|
55
|
+
const sections = [
|
|
56
|
+
['Origin', `Split from coordination parent \`${plan.parentSlug}\`.`],
|
|
57
|
+
[
|
|
58
|
+
'Inherited Context',
|
|
59
|
+
child.inheritedContext || 'No inherited context provided.',
|
|
60
|
+
],
|
|
61
|
+
['Child Scope', child.description ?? child.slug],
|
|
62
|
+
['Known Dependencies', bulletList(child.knownDependencies)],
|
|
63
|
+
[
|
|
64
|
+
'Assumptions To Revalidate',
|
|
65
|
+
'- Revalidate inherited context before completing discovery.',
|
|
66
|
+
],
|
|
67
|
+
['Likely Workflow Mode', 'quick'],
|
|
68
|
+
['Sibling Projects', bulletList(siblingSlugs)],
|
|
69
|
+
];
|
|
70
|
+
return [
|
|
71
|
+
'---',
|
|
72
|
+
'oat_status: in_progress',
|
|
73
|
+
'oat_ready_for: null',
|
|
74
|
+
'oat_blockers: []',
|
|
75
|
+
'oat_inherited_context_revalidated: false',
|
|
76
|
+
'oat_generated: false',
|
|
77
|
+
'---',
|
|
78
|
+
'',
|
|
79
|
+
`# Discovery: ${child.slug}`,
|
|
80
|
+
'',
|
|
81
|
+
...sections.flatMap(([heading, body]) => [`## ${heading}`, '', body, '']),
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
export async function seedChildren(plan, context, onlySlugs) {
|
|
85
|
+
const scaffoldProject = context.scaffoldProject ?? defaultScaffoldProject;
|
|
86
|
+
const childProjectPaths = [];
|
|
87
|
+
const orderedChildren = plan.children
|
|
88
|
+
.slice()
|
|
89
|
+
.sort((left, right) => left.order - right.order)
|
|
90
|
+
.filter((child) => !onlySlugs || onlySlugs.has(child.slug));
|
|
91
|
+
for (const child of orderedChildren) {
|
|
92
|
+
const scaffold = await scaffoldProject({
|
|
93
|
+
repoRoot: context.repoRoot,
|
|
94
|
+
projectName: child.slug,
|
|
95
|
+
mode: 'quick',
|
|
96
|
+
setActive: false,
|
|
97
|
+
refreshDashboard: false,
|
|
98
|
+
env: context.env,
|
|
99
|
+
today: context.today,
|
|
100
|
+
nowUtc: context.nowUtc,
|
|
101
|
+
});
|
|
102
|
+
const childRoot = join(context.repoRoot, scaffold.projectPath);
|
|
103
|
+
const siblings = plan.children
|
|
104
|
+
.map((candidate) => candidate.slug)
|
|
105
|
+
.filter((slug) => slug !== child.slug);
|
|
106
|
+
await updateFrontmatter(join(childRoot, 'state.md'), {
|
|
107
|
+
oat_phase: 'discovery',
|
|
108
|
+
oat_workflow_mode: 'quick',
|
|
109
|
+
oat_hill_checkpoints: [],
|
|
110
|
+
oat_parent: plan.parentSlug,
|
|
111
|
+
oat_siblings: siblings,
|
|
112
|
+
oat_depends_on: child.knownDependencies,
|
|
113
|
+
});
|
|
114
|
+
await updateFrontmatter(join(childRoot, 'plan.md'), {
|
|
115
|
+
oat_plan_source: 'quick',
|
|
116
|
+
});
|
|
117
|
+
await writeFile(join(childRoot, 'discovery.md'), renderSeededDiscovery(plan, child), 'utf8');
|
|
118
|
+
childProjectPaths.push(scaffold.projectPath);
|
|
119
|
+
}
|
|
120
|
+
return { childProjectPaths };
|
|
121
|
+
}
|
|
122
|
+
export { SEEDED_SECTIONS };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Signal = 'independently-shippable' | 'no-shared-design-surface' | 'expect-separate-prs' | 'distinct-subsystems';
|
|
2
|
+
export interface SignalEvaluation {
|
|
3
|
+
fired: Signal[];
|
|
4
|
+
triggered: boolean;
|
|
5
|
+
confidence: 'high' | 'soft' | 'below';
|
|
6
|
+
}
|
|
7
|
+
export declare function evaluateSignals(input: {
|
|
8
|
+
fired: Signal[];
|
|
9
|
+
}): SignalEvaluation;
|
|
10
|
+
//# sourceMappingURL=signals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signals.d.ts","sourceRoot":"","sources":["../../../src/projects/split/signals.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GACd,yBAAyB,GACzB,0BAA0B,GAC1B,qBAAqB,GACrB,qBAAqB,CAAC;AAE1B,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CACvC;AAOD,wBAAgB,eAAe,CAAC,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,gBAAgB,CAiB5E"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const LOAD_BEARING = new Set([
|
|
2
|
+
'independently-shippable',
|
|
3
|
+
'no-shared-design-surface',
|
|
4
|
+
]);
|
|
5
|
+
export function evaluateSignals(input) {
|
|
6
|
+
const fired = [...new Set(input.fired)];
|
|
7
|
+
const triggered = fired.length >= 2;
|
|
8
|
+
const loadBearingCount = fired.filter((signal) => LOAD_BEARING.has(signal)).length;
|
|
9
|
+
let confidence = 'below';
|
|
10
|
+
if (triggered) {
|
|
11
|
+
confidence = loadBearingCount === LOAD_BEARING.size ? 'high' : 'soft';
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
fired,
|
|
15
|
+
triggered,
|
|
16
|
+
confidence,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChildPlan } from './child-plan.js';
|
|
2
|
+
export interface ValidationError {
|
|
3
|
+
code: 'slug-collision-existing' | 'duplicate-child-slug' | 'parent-child-slug-collision' | 'unknown-dependency' | 'dependency-cycle' | 'unknown-foundation-child' | 'unknown-initial-active-child';
|
|
4
|
+
message: string;
|
|
5
|
+
slug?: string;
|
|
6
|
+
}
|
|
7
|
+
export type ValidationResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
errors: ValidationError[];
|
|
12
|
+
};
|
|
13
|
+
export declare function validateChildPlan(plan: ChildPlan, existingSlugs: Set<string>): ValidationResult;
|
|
14
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/projects/split/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,WAAW,eAAe;IAC9B,IAAI,EACA,yBAAyB,GACzB,sBAAsB,GACtB,6BAA6B,GAC7B,oBAAoB,GACpB,kBAAkB,GAClB,0BAA0B,GAC1B,8BAA8B,CAAC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,eAAe,EAAE,CAAA;CAAE,CAAC;AAgD7C,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,SAAS,EACf,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GACzB,gBAAgB,CA2ElB"}
|
|
@@ -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"}
|