@open-agent-toolkit/cli 0.3.17 → 0.3.19
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-phase-implementer.md +81 -3
- package/assets/agents/oat-reviewer.md +15 -5
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/reference/cli-reference.md +8 -5
- package/assets/docs/reference/oat-directory-structure.md +1 -0
- package/assets/docs/reference/project-artifacts.md +17 -1
- package/assets/docs/reference/project-state-machine.md +19 -4
- package/assets/docs/skills/brainstorm.md +2 -1
- package/assets/docs/skills/index.md +1 -0
- package/assets/docs/workflows/advanced/autonomy.md +9 -1
- package/assets/docs/workflows/advanced/workflow-gates.md +11 -6
- package/assets/docs/workflows/approvals-and-automation.md +13 -8
- package/assets/docs/workflows/choose-workflow.md +16 -8
- package/assets/docs/workflows/projects/closeout/closeout-skills.md +8 -3
- package/assets/docs/workflows/projects/closeout/pr-flow.md +2 -0
- package/assets/docs/workflows/projects/execution/execution-skills.md +14 -2
- package/assets/docs/workflows/projects/execution/implementation-execution.md +15 -0
- package/assets/docs/workflows/projects/execution/picking-up-projects.md +10 -1
- package/assets/docs/workflows/projects/execution/project-log.md +22 -0
- package/assets/docs/workflows/projects/lifecycle.md +73 -9
- package/assets/docs/workflows/projects/planning/hill-checkpoints.md +18 -1
- package/assets/docs/workflows/projects/planning/planning-skills.md +11 -10
- package/assets/docs/workflows/projects/planning/starting-projects.md +92 -20
- package/assets/docs/workflows/projects/reviews/index.md +15 -2
- package/assets/docs/workflows/projects/reviews/review-flavors.md +5 -3
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-brainstorm/SKILL.md +33 -9
- package/assets/skills/oat-brainstorm/references/destinations.md +9 -4
- package/assets/skills/oat-docs/SKILL.md +9 -4
- package/assets/skills/oat-pjm-review-backlog/SKILL.md +2 -2
- package/assets/skills/oat-project-autonomous/SKILL.md +14 -8
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +17 -2
- package/assets/skills/oat-project-brief/SKILL.md +592 -0
- package/assets/skills/oat-project-brief/references/brief-contract.md +294 -0
- package/assets/skills/oat-project-brief/references/docs/autonomy-contract.md +393 -0
- package/assets/skills/oat-project-brief/references/docs/gate-approval-record.md +84 -0
- package/assets/skills/oat-project-capture/SKILL.md +2 -2
- package/assets/skills/oat-project-complete/SKILL.md +7 -2
- package/assets/skills/oat-project-discover/SKILL.md +3 -2
- package/assets/skills/oat-project-dispatch-subagents/SKILL.md +9 -1
- package/assets/skills/oat-project-document/SKILL.md +11 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +17 -2
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +40 -0
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +17 -2
- package/assets/skills/oat-project-implement/references/docs/gate-approval-record.md +4 -0
- package/assets/skills/oat-project-implement/references/phase-execution.md +166 -1
- package/assets/skills/oat-project-implement/references/plan-and-resume.md +97 -3
- package/assets/skills/oat-project-import-plan/SKILL.md +5 -1
- package/assets/skills/oat-project-lite/SKILL.md +4 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +17 -2
- package/assets/skills/oat-project-new/SKILL.md +5 -1
- package/assets/skills/oat-project-next/SKILL.md +31 -4
- package/assets/skills/oat-project-next/references/docs/gate-approval-record.md +4 -0
- package/assets/skills/oat-project-plan/SKILL.md +12 -2
- package/assets/skills/oat-project-plan-writing/SKILL.md +6 -4
- package/assets/skills/oat-project-pr-final/SKILL.md +15 -3
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +17 -2
- package/assets/skills/oat-project-pr-progress/SKILL.md +31 -4
- package/assets/skills/oat-project-progress/SKILL.md +30 -8
- package/assets/skills/oat-project-progress/references/docs/gate-approval-record.md +4 -0
- package/assets/skills/oat-project-promote-spec-driven/SKILL.md +2 -2
- package/assets/skills/oat-project-quick-start/SKILL.md +60 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +17 -2
- package/assets/skills/oat-project-quick-start/references/docs/gate-approval-record.md +4 -0
- package/assets/skills/oat-project-reconcile/SKILL.md +65 -3
- package/assets/skills/oat-project-review-provide/SKILL.md +331 -12
- package/assets/skills/oat-project-review-receive/SKILL.md +86 -1
- package/assets/skills/oat-project-revise/SKILL.md +27 -4
- package/assets/skills/oat-project-summary/SKILL.md +95 -2
- package/assets/skills/oat-wave-execute/SKILL.md +3 -2
- package/assets/templates/implementation-brief.md +165 -0
- package/assets/templates/plan-brief.md +140 -0
- package/assets/templates/plan.md +1 -1
- package/assets/templates/state.md +8 -2
- package/dist/commands/init/tools/shared/skill-script-references.d.ts.map +1 -1
- package/dist/commands/init/tools/shared/skill-script-references.js +8 -1
- package/dist/commands/project/log/append.d.ts.map +1 -1
- package/dist/commands/project/log/append.js +70 -10
- package/dist/commands/project/new/scaffold.d.ts +1 -1
- package/dist/commands/project/new/scaffold.d.ts.map +1 -1
- package/dist/commands/project/new/scaffold.js +35 -7
- package/dist/commands/project/promote/promote.d.ts +32 -1
- package/dist/commands/project/promote/promote.d.ts.map +1 -1
- package/dist/commands/project/promote/promote.js +562 -5
- package/dist/commands/project/validate-plan/frontmatter.d.ts +12 -0
- package/dist/commands/project/validate-plan/frontmatter.d.ts.map +1 -0
- package/dist/commands/project/validate-plan/frontmatter.js +27 -0
- package/dist/commands/project/validate-plan/index.d.ts.map +1 -1
- package/dist/commands/project/validate-plan/index.js +52 -18
- package/dist/commands/project/validate-plan/profiles.d.ts +54 -0
- package/dist/commands/project/validate-plan/profiles.d.ts.map +1 -0
- package/dist/commands/project/validate-plan/profiles.js +269 -0
- package/dist/commands/project/validate-plan/validate-plan.d.ts +3 -19
- package/dist/commands/project/validate-plan/validate-plan.d.ts.map +1 -1
- package/dist/commands/project/validate-plan/validate-plan.js +5 -82
- package/dist/commands/shared/agents-md-lock.d.ts +3 -0
- package/dist/commands/shared/agents-md-lock.d.ts.map +1 -0
- package/dist/commands/shared/agents-md-lock.js +130 -0
- package/dist/commands/shared/agents-md.d.ts.map +1 -1
- package/dist/commands/shared/agents-md.js +26 -8
- package/dist/commands/shared/frontmatter.d.ts +1 -1
- package/dist/commands/shared/frontmatter.d.ts.map +1 -1
- package/dist/commands/shared/frontmatter.js +1 -0
- package/dist/commands/state/generate.d.ts.map +1 -1
- package/dist/commands/state/generate.js +51 -2
- package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-manifest.js +3 -0
- package/package.json +2 -2
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
3
|
import { buildCommandContext, } from '../../../app/command-context.js';
|
|
4
4
|
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
5
|
+
import { WORKFLOW_MODES, } from '@open-agent-toolkit/control-plane';
|
|
5
6
|
import { Command } from 'commander';
|
|
6
|
-
import {
|
|
7
|
+
import { extractPhaseIds, parseFrontmatterFromContent, resolvePlanProfile, validateParallelGroups, validatePlanForProfile, } from './validate-plan.js';
|
|
8
|
+
function toWorkflowMode(value) {
|
|
9
|
+
return typeof value === 'string' &&
|
|
10
|
+
WORKFLOW_MODES.includes(value)
|
|
11
|
+
? value
|
|
12
|
+
: null;
|
|
13
|
+
}
|
|
7
14
|
const DEFAULT_DEPENDENCIES = {
|
|
8
15
|
buildCommandContext,
|
|
9
16
|
};
|
|
@@ -15,7 +22,8 @@ export function createProjectValidatePlanCommand(overrides = {}) {
|
|
|
15
22
|
.option('--project-path <path>', 'project directory containing plan.md', process.cwd())
|
|
16
23
|
.action((options, command) => {
|
|
17
24
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
18
|
-
const
|
|
25
|
+
const projectPath = resolve(context.cwd, options.projectPath);
|
|
26
|
+
const planPath = join(projectPath, 'plan.md');
|
|
19
27
|
let content;
|
|
20
28
|
try {
|
|
21
29
|
content = readFileSync(planPath, 'utf-8');
|
|
@@ -46,27 +54,49 @@ export function createProjectValidatePlanCommand(overrides = {}) {
|
|
|
46
54
|
return;
|
|
47
55
|
}
|
|
48
56
|
const groups = frontmatterResult.data['oat_plan_parallel_groups'];
|
|
49
|
-
|
|
50
|
-
let workflowMode = null;
|
|
57
|
+
let stateFrontmatter = null;
|
|
51
58
|
try {
|
|
52
|
-
const stateContent = readFileSync(join(
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
? null
|
|
56
|
-
: stateFrontmatter.data['oat_workflow_mode'];
|
|
57
|
-
workflowMode = typeof mode === 'string' ? mode : null;
|
|
59
|
+
const stateContent = readFileSync(join(projectPath, 'state.md'), 'utf-8');
|
|
60
|
+
const parsedState = parseFrontmatterFromContent(stateContent);
|
|
61
|
+
stateFrontmatter =
|
|
62
|
+
parsedState.kind === 'invalid' ? null : parsedState.data;
|
|
58
63
|
}
|
|
59
64
|
catch {
|
|
60
|
-
|
|
65
|
+
stateFrontmatter = null;
|
|
66
|
+
}
|
|
67
|
+
const workflowMode = toWorkflowMode(stateFrontmatter?.['oat_workflow_mode']);
|
|
68
|
+
const profile = resolvePlanProfile(workflowMode);
|
|
69
|
+
const phaseIds = extractPhaseIds(content, profile);
|
|
70
|
+
const issues = validatePlanForProfile(content, {
|
|
71
|
+
profile,
|
|
72
|
+
workflowMode,
|
|
73
|
+
stateFrontmatter,
|
|
74
|
+
});
|
|
75
|
+
const profileErrors = issues
|
|
76
|
+
.filter((issue) => issue.severity === 'error')
|
|
77
|
+
.map((issue) => issue.message);
|
|
78
|
+
const warnings = issues
|
|
79
|
+
.filter((issue) => issue.severity === 'warning')
|
|
80
|
+
.map((issue) => issue.message);
|
|
81
|
+
const warningPayload = warnings.length > 0 ? { warnings } : {};
|
|
82
|
+
if (!context.json) {
|
|
83
|
+
for (const warning of warnings) {
|
|
84
|
+
context.logger.warn(warning);
|
|
85
|
+
}
|
|
61
86
|
}
|
|
62
|
-
|
|
63
|
-
if (!liteResult.ok) {
|
|
87
|
+
if (profileErrors.length > 0) {
|
|
64
88
|
if (context.json) {
|
|
65
|
-
context.logger.json({
|
|
89
|
+
context.logger.json({
|
|
90
|
+
valid: false,
|
|
91
|
+
errors: profileErrors,
|
|
92
|
+
...warningPayload,
|
|
93
|
+
});
|
|
66
94
|
}
|
|
67
95
|
else {
|
|
68
96
|
context.logger.error('Plan validation failed:');
|
|
69
|
-
|
|
97
|
+
for (const err of profileErrors) {
|
|
98
|
+
context.logger.error(` - ${err}`);
|
|
99
|
+
}
|
|
70
100
|
}
|
|
71
101
|
process.exitCode = 1;
|
|
72
102
|
return;
|
|
@@ -74,7 +104,7 @@ export function createProjectValidatePlanCommand(overrides = {}) {
|
|
|
74
104
|
const result = validateParallelGroups(groups, phaseIds);
|
|
75
105
|
if (result.valid) {
|
|
76
106
|
if (context.json) {
|
|
77
|
-
context.logger.json({ valid: true });
|
|
107
|
+
context.logger.json({ valid: true, ...warningPayload });
|
|
78
108
|
}
|
|
79
109
|
else {
|
|
80
110
|
context.logger.success('Plan validation passed.');
|
|
@@ -83,7 +113,11 @@ export function createProjectValidatePlanCommand(overrides = {}) {
|
|
|
83
113
|
return;
|
|
84
114
|
}
|
|
85
115
|
if (context.json) {
|
|
86
|
-
context.logger.json({
|
|
116
|
+
context.logger.json({
|
|
117
|
+
valid: false,
|
|
118
|
+
errors: result.errors,
|
|
119
|
+
...warningPayload,
|
|
120
|
+
});
|
|
87
121
|
}
|
|
88
122
|
else {
|
|
89
123
|
context.logger.error('Plan validation failed:');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type WorkflowMode } from '@open-agent-toolkit/control-plane';
|
|
2
|
+
/**
|
|
3
|
+
* Plan validation profiles. Each workflow mode validates `plan.md` against
|
|
4
|
+
* one profile:
|
|
5
|
+
*
|
|
6
|
+
* - `task`: spec-driven, quick and import plans. Phase ids come from task
|
|
7
|
+
* headings; no profile-specific body rules.
|
|
8
|
+
* - `lite`: the lite single-phase rules.
|
|
9
|
+
* - `brief`: taskless phase blocks (`**Intent:**`, `**Exit evidence:**`,
|
|
10
|
+
* optional `**Provisional:**`). Phase ids come from `## Phase` headings.
|
|
11
|
+
* - `goal`: reserved for the goal mode; until its milestone rules land it
|
|
12
|
+
* validates exactly like `brief`.
|
|
13
|
+
*/
|
|
14
|
+
export type PlanProfile = 'task' | 'lite' | 'brief' | 'goal';
|
|
15
|
+
export interface ValidationContext {
|
|
16
|
+
profile: PlanProfile;
|
|
17
|
+
workflowMode: WorkflowMode | null;
|
|
18
|
+
stateFrontmatter: Record<string, unknown> | null;
|
|
19
|
+
}
|
|
20
|
+
export type BriefValidationErrorCode = 'brief-no-phases' | 'brief-duplicate-phase' | 'brief-phase-missing-intent' | 'brief-phase-missing-exit-evidence' | 'brief-provisional-invalid' | 'brief-first-phase-provisional' | 'brief-malformed-phase-heading';
|
|
21
|
+
export type BriefValidationWarningCode = 'brief-task-headings-ignored';
|
|
22
|
+
export type LiteValidationErrorCode = 'lite-multi-phase' | 'lite-parallel-groups' | 'lite-criterion-without-command';
|
|
23
|
+
export type ValidationIssue = {
|
|
24
|
+
severity: 'error';
|
|
25
|
+
code: BriefValidationErrorCode | LiteValidationErrorCode;
|
|
26
|
+
message: string;
|
|
27
|
+
} | {
|
|
28
|
+
severity: 'warning';
|
|
29
|
+
code: BriefValidationWarningCode;
|
|
30
|
+
message: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function resolvePlanProfile(mode: WorkflowMode | null): PlanProfile;
|
|
33
|
+
export type LitePlanValidationResult = {
|
|
34
|
+
ok: true;
|
|
35
|
+
} | {
|
|
36
|
+
ok: false;
|
|
37
|
+
code: 'lite-multi-phase' | 'lite-parallel-groups' | 'lite-criterion-without-command';
|
|
38
|
+
message: string;
|
|
39
|
+
};
|
|
40
|
+
export declare function validateLitePlan(planContent: string, workflowMode: string | null | undefined): LitePlanValidationResult;
|
|
41
|
+
/**
|
|
42
|
+
* The normalized id and name of a `## Phase` heading line, or `null` when the
|
|
43
|
+
* line is not a well-formed phase heading. Delegates to the control-plane
|
|
44
|
+
* phase-unit parser so heading spellings (`## Phase 1:`, `## Phase p01:`,
|
|
45
|
+
* `## Phase p-rev1:`, `## Revision Phase 1:`) normalize exactly as progress
|
|
46
|
+
* reads them.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parsePhaseHeadingLine(line: string): {
|
|
49
|
+
phaseId: string;
|
|
50
|
+
name: string;
|
|
51
|
+
} | null;
|
|
52
|
+
export declare function extractPhaseIds(content: string, profile: PlanProfile): string[];
|
|
53
|
+
export declare function validatePlanForProfile(content: string, ctx: ValidationContext): ValidationIssue[];
|
|
54
|
+
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/validate-plan/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,mCAAmC,CAAC;AAI3C;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,WAAW,CAAC;IACrB,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAClD;AAED,MAAM,MAAM,wBAAwB,GAChC,iBAAiB,GACjB,uBAAuB,GACvB,4BAA4B,GAC5B,mCAAmC,GACnC,2BAA2B,GAC3B,+BAA+B,GAC/B,+BAA+B,CAAC;AAEpC,MAAM,MAAM,0BAA0B,GAAG,6BAA6B,CAAC;AAEvE,MAAM,MAAM,uBAAuB,GAC/B,kBAAkB,GAClB,sBAAsB,GACtB,gCAAgC,CAAC;AAErC,MAAM,MAAM,eAAe,GACvB;IACE,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,wBAAwB,GAAG,uBAAuB,CAAC;IACzD,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IACE,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,EAAE,0BAA0B,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEN,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,GAAG,WAAW,CAIzE;AAMD,MAAM,MAAM,wBAAwB,GAChC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IACE,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EACA,kBAAkB,GAClB,sBAAsB,GACtB,gCAAgC,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAeN,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACtC,wBAAwB,CA8C1B;AAeD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,GACX;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAG1C;AAiBD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,WAAW,GACnB,MAAM,EAAE,CAIV;AA0LD,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,iBAAiB,GACrB,eAAe,EAAE,CAcnB"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { isBriefMode, parseProvisionalPhaseIds, parseTaskProgress, recordLineMask, } from '@open-agent-toolkit/control-plane';
|
|
2
|
+
import { parseFrontmatterFromContent } from './frontmatter.js';
|
|
3
|
+
export function resolvePlanProfile(mode) {
|
|
4
|
+
if (mode === 'lite')
|
|
5
|
+
return 'lite';
|
|
6
|
+
if (isBriefMode(mode))
|
|
7
|
+
return mode;
|
|
8
|
+
return 'task';
|
|
9
|
+
}
|
|
10
|
+
function validationCriteria(planContent) {
|
|
11
|
+
const heading = /^## Validation Criteria[ \t]*$/m.exec(planContent);
|
|
12
|
+
if (!heading)
|
|
13
|
+
return null;
|
|
14
|
+
const bodyStart = heading.index + heading[0].length;
|
|
15
|
+
const remaining = planContent.slice(bodyStart);
|
|
16
|
+
const nextHeading = /^##\s+/m.exec(remaining);
|
|
17
|
+
const body = remaining.slice(0, nextHeading?.index ?? remaining.length);
|
|
18
|
+
return body
|
|
19
|
+
.split('\n')
|
|
20
|
+
.filter((line) => /^\s*-\s+/.test(line))
|
|
21
|
+
.map((line) => line.replace(/^\s*-\s+(?:\[[ xX]\]\s*)?/, '').trim());
|
|
22
|
+
}
|
|
23
|
+
export function validateLitePlan(planContent, workflowMode) {
|
|
24
|
+
if (workflowMode !== 'lite') {
|
|
25
|
+
return { ok: true };
|
|
26
|
+
}
|
|
27
|
+
const phaseCount = planContent.match(/^## Phase\b.*$/gm)?.length ?? 0;
|
|
28
|
+
if (phaseCount !== 1) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
code: 'lite-multi-phase',
|
|
32
|
+
message: `Lite plans must contain exactly one phase; found ${phaseCount}.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const frontmatter = parseFrontmatterFromContent(planContent);
|
|
36
|
+
const groups = frontmatter.kind === 'invalid'
|
|
37
|
+
? undefined
|
|
38
|
+
: frontmatter.data['oat_plan_parallel_groups'];
|
|
39
|
+
if (groups !== undefined &&
|
|
40
|
+
groups !== null &&
|
|
41
|
+
!(Array.isArray(groups) && groups.length === 0)) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
code: 'lite-parallel-groups',
|
|
45
|
+
message: 'Lite plans must declare no parallel groups.',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const criteria = validationCriteria(planContent);
|
|
49
|
+
const invalidCriterion = criteria?.find((criterion) => !criterion.startsWith('manual:') && !/`[^`\n]+`/.test(criterion));
|
|
50
|
+
if (!criteria || criteria.length === 0 || invalidCriterion !== undefined) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
code: 'lite-criterion-without-command',
|
|
54
|
+
message: 'Every lite Validation Criteria bullet must name a command in backticks or start with manual: after the bullet marker.',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { ok: true };
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// phase id extraction
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
function extractTaskHeadingPhaseIds(planContent) {
|
|
63
|
+
const matches = planContent.match(/### Task (p\d+)-t\d+/g) ?? [];
|
|
64
|
+
const ids = matches.map((m) => {
|
|
65
|
+
const parts = m.split(' ');
|
|
66
|
+
return parts[2].split('-')[0];
|
|
67
|
+
});
|
|
68
|
+
return Array.from(new Set(ids)).sort();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The normalized id and name of a `## Phase` heading line, or `null` when the
|
|
72
|
+
* line is not a well-formed phase heading. Delegates to the control-plane
|
|
73
|
+
* phase-unit parser so heading spellings (`## Phase 1:`, `## Phase p01:`,
|
|
74
|
+
* `## Phase p-rev1:`, `## Revision Phase 1:`) normalize exactly as progress
|
|
75
|
+
* reads them.
|
|
76
|
+
*/
|
|
77
|
+
export function parsePhaseHeadingLine(line) {
|
|
78
|
+
const phase = parseTaskProgress(line, '', { phaseUnits: true }).phases[0];
|
|
79
|
+
return phase ? { phaseId: phase.phaseId, name: phase.name } : null;
|
|
80
|
+
}
|
|
81
|
+
function phaseIdOfHeading(line) {
|
|
82
|
+
return parsePhaseHeadingLine(line)?.phaseId ?? null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Normalized `## Phase` heading ids in document order, outside fenced and
|
|
86
|
+
* indented code, each listed once.
|
|
87
|
+
*/
|
|
88
|
+
function extractBriefPhaseIds(planContent) {
|
|
89
|
+
const ids = parseTaskProgress(planContent, '', {
|
|
90
|
+
phaseUnits: true,
|
|
91
|
+
}).phases.map((phase) => phase.phaseId);
|
|
92
|
+
return Array.from(new Set(ids));
|
|
93
|
+
}
|
|
94
|
+
export function extractPhaseIds(content, profile) {
|
|
95
|
+
return profile === 'brief' || profile === 'goal'
|
|
96
|
+
? extractBriefPhaseIds(content)
|
|
97
|
+
: extractTaskHeadingPhaseIds(content);
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// brief profile
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/**
|
|
103
|
+
* Plan record lines, through the control-plane reader that phase-unit
|
|
104
|
+
* progress and quick plan readiness use, so a fenced example phase block
|
|
105
|
+
* never validates or fails a real plan.
|
|
106
|
+
*/
|
|
107
|
+
function recordLines(content) {
|
|
108
|
+
const lines = content.split('\n');
|
|
109
|
+
const mask = recordLineMask(lines);
|
|
110
|
+
return lines.filter((_, index) => mask[index]);
|
|
111
|
+
}
|
|
112
|
+
const PHASE_HEADING_CANDIDATE = /^##[ \t]+(?:Revision[ \t]+)?Phase\b/;
|
|
113
|
+
const SECTION_HEADING = /^#{1,2}[ \t]/;
|
|
114
|
+
const ANY_HEADING = /^#/;
|
|
115
|
+
const TASK_HEADING = /^###[ \t]+Task\b/;
|
|
116
|
+
const FIELD_MARKER = /^\*\*[^*\n]+:\*\*/;
|
|
117
|
+
const INTENT_MARKER = /^\*\*Intent:\*\*(.*)$/;
|
|
118
|
+
const EXIT_EVIDENCE_MARKER = /^\*\*Exit evidence:\*\*(.*)$/;
|
|
119
|
+
const PROVISIONAL_LINE = /^\*\*Provisional:\*\*/;
|
|
120
|
+
// The control-plane parser's grammar: a space after the marker, then the value.
|
|
121
|
+
const PROVISIONAL_VALID = /^\*\*Provisional:\*\*\s+(?:yes|no)$/;
|
|
122
|
+
/**
|
|
123
|
+
* Whether a phase block carries `marker` with content: a non-empty value on
|
|
124
|
+
* the marker line, or at least one continuation line before the next blank
|
|
125
|
+
* line or field marker.
|
|
126
|
+
*/
|
|
127
|
+
function hasFilledField(lines, marker) {
|
|
128
|
+
for (const [index, line] of lines.entries()) {
|
|
129
|
+
const match = line.trim().match(marker);
|
|
130
|
+
if (!match)
|
|
131
|
+
continue;
|
|
132
|
+
if ((match[1] ?? '').trim().length > 0)
|
|
133
|
+
return true;
|
|
134
|
+
const next = (lines[index + 1] ?? '').trim();
|
|
135
|
+
if (next.length > 0 && !FIELD_MARKER.test(next))
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
function readBriefPhaseBlocks(content) {
|
|
141
|
+
const blocks = [];
|
|
142
|
+
const malformed = [];
|
|
143
|
+
// Marker ownership ends at the next line starting with `#` (the phase block
|
|
144
|
+
// grammar); task-heading attribution lasts until the next `#`/`##` heading.
|
|
145
|
+
let owner = null;
|
|
146
|
+
let section = null;
|
|
147
|
+
for (const line of recordLines(content)) {
|
|
148
|
+
if (PHASE_HEADING_CANDIDATE.test(line)) {
|
|
149
|
+
const phaseId = phaseIdOfHeading(line);
|
|
150
|
+
if (phaseId == null) {
|
|
151
|
+
malformed.push(line.trim());
|
|
152
|
+
owner = null;
|
|
153
|
+
section = null;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const block = { phaseId, lines: [], taskHeadings: 0 };
|
|
157
|
+
blocks.push(block);
|
|
158
|
+
owner = block;
|
|
159
|
+
section = block;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (SECTION_HEADING.test(line)) {
|
|
163
|
+
owner = null;
|
|
164
|
+
section = null;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (TASK_HEADING.test(line) && section != null) {
|
|
168
|
+
section.taskHeadings += 1;
|
|
169
|
+
}
|
|
170
|
+
if (ANY_HEADING.test(line)) {
|
|
171
|
+
owner = null;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
owner?.lines.push(line);
|
|
175
|
+
}
|
|
176
|
+
return { blocks, malformed };
|
|
177
|
+
}
|
|
178
|
+
function validateBriefPlan(content) {
|
|
179
|
+
const issues = [];
|
|
180
|
+
const { blocks, malformed } = readBriefPhaseBlocks(content);
|
|
181
|
+
for (const heading of malformed) {
|
|
182
|
+
issues.push({
|
|
183
|
+
severity: 'error',
|
|
184
|
+
code: 'brief-malformed-phase-heading',
|
|
185
|
+
message: `brief-malformed-phase-heading: "${heading}" is not a phase heading; use "## Phase pNN: <name>".`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (blocks.length === 0) {
|
|
189
|
+
issues.push({
|
|
190
|
+
severity: 'error',
|
|
191
|
+
code: 'brief-no-phases',
|
|
192
|
+
message: 'brief-no-phases: Brief plans must declare at least one "## Phase pNN: <name>" block.',
|
|
193
|
+
});
|
|
194
|
+
return issues;
|
|
195
|
+
}
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
const reported = new Set();
|
|
198
|
+
for (const { phaseId } of blocks) {
|
|
199
|
+
if (seen.has(phaseId) && !reported.has(phaseId)) {
|
|
200
|
+
reported.add(phaseId);
|
|
201
|
+
issues.push({
|
|
202
|
+
severity: 'error',
|
|
203
|
+
code: 'brief-duplicate-phase',
|
|
204
|
+
message: `brief-duplicate-phase: Phase ${phaseId} is declared more than once.`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
seen.add(phaseId);
|
|
208
|
+
}
|
|
209
|
+
for (const block of blocks) {
|
|
210
|
+
if (!hasFilledField(block.lines, INTENT_MARKER)) {
|
|
211
|
+
issues.push({
|
|
212
|
+
severity: 'error',
|
|
213
|
+
code: 'brief-phase-missing-intent',
|
|
214
|
+
message: `brief-phase-missing-intent: Phase ${block.phaseId} has no non-empty **Intent:**.`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (!hasFilledField(block.lines, EXIT_EVIDENCE_MARKER)) {
|
|
218
|
+
issues.push({
|
|
219
|
+
severity: 'error',
|
|
220
|
+
code: 'brief-phase-missing-exit-evidence',
|
|
221
|
+
message: `brief-phase-missing-exit-evidence: Phase ${block.phaseId} has no non-empty **Exit evidence:**.`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
for (const line of block.lines) {
|
|
225
|
+
const trimmed = line.trim();
|
|
226
|
+
if (PROVISIONAL_LINE.test(trimmed) && !PROVISIONAL_VALID.test(trimmed)) {
|
|
227
|
+
issues.push({
|
|
228
|
+
severity: 'error',
|
|
229
|
+
code: 'brief-provisional-invalid',
|
|
230
|
+
message: `brief-provisional-invalid: Phase ${block.phaseId} has "${trimmed}"; use "**Provisional:** yes" or "**Provisional:** no".`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const firstPhaseId = blocks[0].phaseId;
|
|
236
|
+
if (parseProvisionalPhaseIds(content).has(firstPhaseId)) {
|
|
237
|
+
issues.push({
|
|
238
|
+
severity: 'error',
|
|
239
|
+
code: 'brief-first-phase-provisional',
|
|
240
|
+
message: `brief-first-phase-provisional: The first phase (${firstPhaseId}) cannot be provisional.`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
const withTasks = blocks.filter((block) => block.taskHeadings > 0);
|
|
244
|
+
if (withTasks.length > 0) {
|
|
245
|
+
issues.push({
|
|
246
|
+
severity: 'warning',
|
|
247
|
+
code: 'brief-task-headings-ignored',
|
|
248
|
+
message: `brief-task-headings-ignored: Task headings in plan.md under ${withTasks
|
|
249
|
+
.map((block) => block.phaseId)
|
|
250
|
+
.join(', ')} do not count toward brief progress; record execution tasks in implementation.md.`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return issues;
|
|
254
|
+
}
|
|
255
|
+
export function validatePlanForProfile(content, ctx) {
|
|
256
|
+
switch (ctx.profile) {
|
|
257
|
+
case 'lite': {
|
|
258
|
+
const result = validateLitePlan(content, 'lite');
|
|
259
|
+
return result.ok
|
|
260
|
+
? []
|
|
261
|
+
: [{ severity: 'error', code: result.code, message: result.message }];
|
|
262
|
+
}
|
|
263
|
+
case 'brief':
|
|
264
|
+
case 'goal':
|
|
265
|
+
return validateBriefPlan(content);
|
|
266
|
+
case 'task':
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -1,28 +1,12 @@
|
|
|
1
|
-
export type FrontmatterResult
|
|
2
|
-
|
|
3
|
-
data: Record<string, unknown>;
|
|
4
|
-
} | {
|
|
5
|
-
kind: 'no-frontmatter';
|
|
6
|
-
data: Record<string, unknown>;
|
|
7
|
-
} | {
|
|
8
|
-
kind: 'invalid';
|
|
9
|
-
message: string;
|
|
10
|
-
};
|
|
11
|
-
export declare function parseFrontmatterFromContent(content: string): FrontmatterResult;
|
|
1
|
+
export { parseFrontmatterFromContent, type FrontmatterResult, } from './frontmatter.js';
|
|
2
|
+
export { extractPhaseIds, resolvePlanProfile, validateLitePlan, validatePlanForProfile, type LitePlanValidationResult, type PlanProfile, type ValidationContext, type ValidationIssue, } from './profiles.js';
|
|
12
3
|
export type ValidationResult = {
|
|
13
4
|
valid: true;
|
|
14
5
|
} | {
|
|
15
6
|
valid: false;
|
|
16
7
|
errors: string[];
|
|
17
8
|
};
|
|
18
|
-
export type LitePlanValidationResult = {
|
|
19
|
-
ok: true;
|
|
20
|
-
} | {
|
|
21
|
-
ok: false;
|
|
22
|
-
code: 'lite-multi-phase' | 'lite-parallel-groups' | 'lite-criterion-without-command';
|
|
23
|
-
message: string;
|
|
24
|
-
};
|
|
25
|
-
export declare function validateLitePlan(planContent: string, workflowMode: string | null | undefined): LitePlanValidationResult;
|
|
26
9
|
export declare function validateParallelGroups(groups: unknown, phaseIds: readonly string[]): ValidationResult;
|
|
10
|
+
/** Task-profile phase ids: unique task-heading phase ids, sorted. */
|
|
27
11
|
export declare function extractPhaseIdsFromPlan(planContent: string): string[];
|
|
28
12
|
//# sourceMappingURL=validate-plan.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-plan.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/validate-plan/validate-plan.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate-plan.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/validate-plan/validate-plan.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,2BAA2B,EAC3B,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,gBAAgB,GACxB;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GACf;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAEvC,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,gBAAgB,CAgDlB;AAED,qEAAqE;AACrE,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAErE"}
|
|
@@ -1,79 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
const match = content.match(FRONTMATTER_PATTERN);
|
|
5
|
-
if (!match?.[1]) {
|
|
6
|
-
return { kind: 'no-frontmatter', data: {} };
|
|
7
|
-
}
|
|
8
|
-
try {
|
|
9
|
-
const parsed = YAML.parse(match[1]);
|
|
10
|
-
if (parsed !== null &&
|
|
11
|
-
typeof parsed === 'object' &&
|
|
12
|
-
!Array.isArray(parsed)) {
|
|
13
|
-
return { kind: 'ok', data: parsed };
|
|
14
|
-
}
|
|
15
|
-
return {
|
|
16
|
-
kind: 'invalid',
|
|
17
|
-
message: 'Frontmatter did not parse as an object',
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
catch (err) {
|
|
21
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
22
|
-
return {
|
|
23
|
-
kind: 'invalid',
|
|
24
|
-
message: `Malformed YAML in frontmatter: ${message}`,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function validationCriteria(planContent) {
|
|
29
|
-
const heading = /^## Validation Criteria[ \t]*$/m.exec(planContent);
|
|
30
|
-
if (!heading)
|
|
31
|
-
return null;
|
|
32
|
-
const bodyStart = heading.index + heading[0].length;
|
|
33
|
-
const remaining = planContent.slice(bodyStart);
|
|
34
|
-
const nextHeading = /^##\s+/m.exec(remaining);
|
|
35
|
-
const body = remaining.slice(0, nextHeading?.index ?? remaining.length);
|
|
36
|
-
return body
|
|
37
|
-
.split('\n')
|
|
38
|
-
.filter((line) => /^\s*-\s+/.test(line))
|
|
39
|
-
.map((line) => line.replace(/^\s*-\s+(?:\[[ xX]\]\s*)?/, '').trim());
|
|
40
|
-
}
|
|
41
|
-
export function validateLitePlan(planContent, workflowMode) {
|
|
42
|
-
if (workflowMode !== 'lite') {
|
|
43
|
-
return { ok: true };
|
|
44
|
-
}
|
|
45
|
-
const phaseCount = planContent.match(/^## Phase\b.*$/gm)?.length ?? 0;
|
|
46
|
-
if (phaseCount !== 1) {
|
|
47
|
-
return {
|
|
48
|
-
ok: false,
|
|
49
|
-
code: 'lite-multi-phase',
|
|
50
|
-
message: `Lite plans must contain exactly one phase; found ${phaseCount}.`,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
const frontmatter = parseFrontmatterFromContent(planContent);
|
|
54
|
-
const groups = frontmatter.kind === 'invalid'
|
|
55
|
-
? undefined
|
|
56
|
-
: frontmatter.data['oat_plan_parallel_groups'];
|
|
57
|
-
if (groups !== undefined &&
|
|
58
|
-
groups !== null &&
|
|
59
|
-
!(Array.isArray(groups) && groups.length === 0)) {
|
|
60
|
-
return {
|
|
61
|
-
ok: false,
|
|
62
|
-
code: 'lite-parallel-groups',
|
|
63
|
-
message: 'Lite plans must declare no parallel groups.',
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
const criteria = validationCriteria(planContent);
|
|
67
|
-
const invalidCriterion = criteria?.find((criterion) => !criterion.startsWith('manual:') && !/`[^`\n]+`/.test(criterion));
|
|
68
|
-
if (!criteria || criteria.length === 0 || invalidCriterion !== undefined) {
|
|
69
|
-
return {
|
|
70
|
-
ok: false,
|
|
71
|
-
code: 'lite-criterion-without-command',
|
|
72
|
-
message: 'Every lite Validation Criteria bullet must name a command in backticks or start with manual: after the bullet marker.',
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
return { ok: true };
|
|
76
|
-
}
|
|
1
|
+
import { extractPhaseIds } from './profiles.js';
|
|
2
|
+
export { parseFrontmatterFromContent, } from './frontmatter.js';
|
|
3
|
+
export { extractPhaseIds, resolvePlanProfile, validateLitePlan, validatePlanForProfile, } from './profiles.js';
|
|
77
4
|
export function validateParallelGroups(groups, phaseIds) {
|
|
78
5
|
// Missing or empty → valid (sequential execution, no parallelism declared)
|
|
79
6
|
if (groups === undefined || groups === null) {
|
|
@@ -116,11 +43,7 @@ export function validateParallelGroups(groups, phaseIds) {
|
|
|
116
43
|
}
|
|
117
44
|
return errors.length > 0 ? { valid: false, errors } : { valid: true };
|
|
118
45
|
}
|
|
46
|
+
/** Task-profile phase ids: unique task-heading phase ids, sorted. */
|
|
119
47
|
export function extractPhaseIdsFromPlan(planContent) {
|
|
120
|
-
|
|
121
|
-
const ids = matches.map((m) => {
|
|
122
|
-
const parts = m.split(' ');
|
|
123
|
-
return parts[2].split('-')[0];
|
|
124
|
-
});
|
|
125
|
-
return Array.from(new Set(ids)).sort();
|
|
48
|
+
return extractPhaseIds(planContent, 'task');
|
|
126
49
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents-md-lock.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/agents-md-lock.ts"],"names":[],"mappings":"AAOA,2EAA2E;AAC3E,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CA6I9B"}
|