@open-agent-toolkit/cli 0.3.18 → 0.3.20

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 (132) hide show
  1. package/README.md +2 -1
  2. package/assets/agents/oat-phase-implementer.md +104 -3
  3. package/assets/agents/oat-reviewer.md +20 -5
  4. package/assets/bundle-metadata.json +1 -1
  5. package/assets/docs/reference/cli-reference.md +15 -7
  6. package/assets/docs/reference/oat-directory-structure.md +2 -0
  7. package/assets/docs/reference/project-artifacts.md +72 -1
  8. package/assets/docs/reference/project-state-machine.md +55 -6
  9. package/assets/docs/skills/brainstorm.md +3 -1
  10. package/assets/docs/skills/index.md +13 -9
  11. package/assets/docs/workflows/advanced/autonomy.md +12 -2
  12. package/assets/docs/workflows/advanced/dispatch-ceiling.md +2 -2
  13. package/assets/docs/workflows/advanced/workflow-gates.md +25 -7
  14. package/assets/docs/workflows/approvals-and-automation.md +25 -9
  15. package/assets/docs/workflows/choose-workflow.md +24 -8
  16. package/assets/docs/workflows/ideas/index.md +1 -1
  17. package/assets/docs/workflows/ideas/lifecycle.md +3 -2
  18. package/assets/docs/workflows/projects/closeout/closeout-skills.md +83 -5
  19. package/assets/docs/workflows/projects/closeout/pr-flow.md +9 -1
  20. package/assets/docs/workflows/projects/execution/execution-skills.md +27 -2
  21. package/assets/docs/workflows/projects/execution/implementation-execution.md +15 -0
  22. package/assets/docs/workflows/projects/execution/picking-up-projects.md +20 -1
  23. package/assets/docs/workflows/projects/lifecycle.md +172 -9
  24. package/assets/docs/workflows/projects/planning/hill-checkpoints.md +18 -1
  25. package/assets/docs/workflows/projects/planning/planning-skills.md +13 -10
  26. package/assets/docs/workflows/projects/planning/starting-projects.md +138 -20
  27. package/assets/docs/workflows/projects/reviews/index.md +36 -2
  28. package/assets/docs/workflows/projects/reviews/review-flavors.md +7 -3
  29. package/assets/public-package-versions.json +4 -4
  30. package/assets/skills/oat-brainstorm/SKILL.md +58 -9
  31. package/assets/skills/oat-brainstorm/references/destinations.md +12 -4
  32. package/assets/skills/oat-docs/SKILL.md +13 -4
  33. package/assets/skills/oat-pjm-review-backlog/SKILL.md +2 -2
  34. package/assets/skills/oat-project-autonomous/SKILL.md +20 -8
  35. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +156 -122
  36. package/assets/skills/oat-project-brief/SKILL.md +592 -0
  37. package/assets/skills/oat-project-brief/references/brief-contract.md +743 -0
  38. package/assets/skills/oat-project-brief/references/docs/autonomy-contract.md +412 -0
  39. package/assets/skills/oat-project-brief/references/docs/gate-approval-record.md +93 -0
  40. package/assets/skills/oat-project-capture/SKILL.md +2 -2
  41. package/assets/skills/oat-project-complete/SKILL.md +16 -2
  42. package/assets/skills/oat-project-discover/SKILL.md +4 -2
  43. package/assets/skills/oat-project-dispatch-subagents/SKILL.md +9 -1
  44. package/assets/skills/oat-project-document/SKILL.md +17 -1
  45. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +156 -122
  46. package/assets/skills/oat-project-goal/SKILL.md +697 -0
  47. package/assets/skills/oat-project-goal/references/docs/autonomy-contract.md +412 -0
  48. package/assets/skills/oat-project-goal/references/docs/gate-approval-record.md +93 -0
  49. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  50. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +107 -0
  51. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +156 -122
  52. package/assets/skills/oat-project-implement/references/docs/gate-approval-record.md +13 -0
  53. package/assets/skills/oat-project-implement/references/phase-execution.md +173 -1
  54. package/assets/skills/oat-project-implement/references/plan-and-resume.md +124 -3
  55. package/assets/skills/oat-project-import-plan/SKILL.md +5 -1
  56. package/assets/skills/oat-project-lite/SKILL.md +4 -1
  57. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +156 -122
  58. package/assets/skills/oat-project-new/SKILL.md +5 -1
  59. package/assets/skills/oat-project-next/SKILL.md +71 -14
  60. package/assets/skills/oat-project-next/references/docs/gate-approval-record.md +13 -0
  61. package/assets/skills/oat-project-plan/SKILL.md +24 -3
  62. package/assets/skills/oat-project-plan-writing/SKILL.md +8 -4
  63. package/assets/skills/oat-project-pr-final/SKILL.md +65 -3
  64. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +156 -122
  65. package/assets/skills/oat-project-pr-progress/SKILL.md +125 -4
  66. package/assets/skills/oat-project-progress/SKILL.md +63 -8
  67. package/assets/skills/oat-project-progress/references/docs/gate-approval-record.md +13 -0
  68. package/assets/skills/oat-project-promote-spec-driven/SKILL.md +2 -2
  69. package/assets/skills/oat-project-quick-start/SKILL.md +65 -1
  70. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +156 -122
  71. package/assets/skills/oat-project-quick-start/references/docs/gate-approval-record.md +13 -0
  72. package/assets/skills/oat-project-reconcile/SKILL.md +94 -3
  73. package/assets/skills/oat-project-review-provide/SKILL.md +397 -12
  74. package/assets/skills/oat-project-review-receive/SKILL.md +155 -1
  75. package/assets/skills/oat-project-revise/SKILL.md +51 -4
  76. package/assets/skills/oat-project-summary/SKILL.md +150 -2
  77. package/assets/skills/oat-wave-execute/SKILL.md +3 -2
  78. package/assets/templates/implementation-brief.md +165 -0
  79. package/assets/templates/plan-brief.md +140 -0
  80. package/assets/templates/plan.md +1 -1
  81. package/assets/templates/state.md +8 -2
  82. package/dist/commands/project/closeout-check/closeout-invariant.d.ts +119 -3
  83. package/dist/commands/project/closeout-check/closeout-invariant.d.ts.map +1 -1
  84. package/dist/commands/project/closeout-check/closeout-invariant.js +454 -2
  85. package/dist/commands/project/closeout-check/index.d.ts +21 -3
  86. package/dist/commands/project/closeout-check/index.d.ts.map +1 -1
  87. package/dist/commands/project/closeout-check/index.js +190 -4
  88. package/dist/commands/project/complete-state/index.d.ts +2 -1
  89. package/dist/commands/project/complete-state/index.d.ts.map +1 -1
  90. package/dist/commands/project/complete-state/index.js +7 -1
  91. package/dist/commands/project/delivery/check.d.ts +66 -0
  92. package/dist/commands/project/delivery/check.d.ts.map +1 -0
  93. package/dist/commands/project/delivery/check.js +232 -0
  94. package/dist/commands/project/delivery/evidence.d.ts +171 -0
  95. package/dist/commands/project/delivery/evidence.d.ts.map +1 -0
  96. package/dist/commands/project/delivery/evidence.js +902 -0
  97. package/dist/commands/project/delivery/index.d.ts +7 -0
  98. package/dist/commands/project/delivery/index.d.ts.map +1 -0
  99. package/dist/commands/project/delivery/index.js +13 -0
  100. package/dist/commands/project/delivery/refresh.d.ts +110 -0
  101. package/dist/commands/project/delivery/refresh.d.ts.map +1 -0
  102. package/dist/commands/project/delivery/refresh.js +441 -0
  103. package/dist/commands/project/index.d.ts.map +1 -1
  104. package/dist/commands/project/index.js +2 -0
  105. package/dist/commands/project/log/append.d.ts +6 -0
  106. package/dist/commands/project/log/append.d.ts.map +1 -1
  107. package/dist/commands/project/log/append.js +6 -1
  108. package/dist/commands/project/new/scaffold.d.ts +1 -1
  109. package/dist/commands/project/new/scaffold.d.ts.map +1 -1
  110. package/dist/commands/project/new/scaffold.js +81 -7
  111. package/dist/commands/project/promote/promote.d.ts +46 -1
  112. package/dist/commands/project/promote/promote.d.ts.map +1 -1
  113. package/dist/commands/project/promote/promote.js +646 -5
  114. package/dist/commands/project/validate-plan/frontmatter.d.ts +12 -0
  115. package/dist/commands/project/validate-plan/frontmatter.d.ts.map +1 -0
  116. package/dist/commands/project/validate-plan/frontmatter.js +27 -0
  117. package/dist/commands/project/validate-plan/index.d.ts.map +1 -1
  118. package/dist/commands/project/validate-plan/index.js +62 -18
  119. package/dist/commands/project/validate-plan/profiles.d.ts +69 -0
  120. package/dist/commands/project/validate-plan/profiles.d.ts.map +1 -0
  121. package/dist/commands/project/validate-plan/profiles.js +420 -0
  122. package/dist/commands/project/validate-plan/validate-plan.d.ts +3 -19
  123. package/dist/commands/project/validate-plan/validate-plan.d.ts.map +1 -1
  124. package/dist/commands/project/validate-plan/validate-plan.js +5 -82
  125. package/dist/commands/shared/frontmatter.d.ts +1 -1
  126. package/dist/commands/shared/frontmatter.d.ts.map +1 -1
  127. package/dist/commands/shared/frontmatter.js +2 -0
  128. package/dist/commands/state/generate.d.ts.map +1 -1
  129. package/dist/commands/state/generate.js +82 -2
  130. package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
  131. package/dist/commands/tools/shared/pack-manifest.js +4 -0
  132. package/package.json +2 -2
@@ -0,0 +1,12 @@
1
+ export type FrontmatterResult = {
2
+ kind: 'ok';
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;
12
+ //# sourceMappingURL=frontmatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/validate-plan/frontmatter.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,GACd,iBAAiB,CAyBnB"}
@@ -0,0 +1,27 @@
1
+ import YAML from 'yaml';
2
+ const FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/;
3
+ export function parseFrontmatterFromContent(content) {
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
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/validate-plan/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,UAAU,+BAA+B;IACvC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;CACjE;AAMD,wBAAgB,gCAAgC,CAC9C,SAAS,GAAE,OAAO,CAAC,+BAA+B,CAAM,GACvD,OAAO,CAsGT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/validate-plan/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAM9B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiBpC,UAAU,+BAA+B;IACvC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;CACjE;AAMD,wBAAgB,gCAAgC,CAC9C,SAAS,GAAE,OAAO,CAAC,+BAA+B,CAAM,GACvD,OAAO,CAgJT"}
@@ -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 { extractPhaseIdsFromPlan, parseFrontmatterFromContent, validateLitePlan, validateParallelGroups, } from './validate-plan.js';
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 planPath = join(options.projectPath, 'plan.md');
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,59 @@ export function createProjectValidatePlanCommand(overrides = {}) {
46
54
  return;
47
55
  }
48
56
  const groups = frontmatterResult.data['oat_plan_parallel_groups'];
49
- const phaseIds = extractPhaseIdsFromPlan(content);
50
- let workflowMode = null;
57
+ let stateFrontmatter = null;
51
58
  try {
52
- const stateContent = readFileSync(join(options.projectPath, 'state.md'), 'utf-8');
53
- const stateFrontmatter = parseFrontmatterFromContent(stateContent);
54
- const mode = stateFrontmatter.kind === 'invalid'
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
- workflowMode = null;
65
+ stateFrontmatter = null;
61
66
  }
62
- const liteResult = validateLitePlan(content, workflowMode);
63
- if (!liteResult.ok) {
67
+ const workflowMode = toWorkflowMode(stateFrontmatter?.['oat_workflow_mode']);
68
+ const profile = resolvePlanProfile(workflowMode);
69
+ const phaseIds = extractPhaseIds(content, profile);
70
+ let implementationContent = null;
71
+ if (profile === 'goal') {
72
+ try {
73
+ implementationContent = readFileSync(join(projectPath, 'implementation.md'), 'utf-8');
74
+ }
75
+ catch {
76
+ implementationContent = null;
77
+ }
78
+ }
79
+ const issues = validatePlanForProfile(content, {
80
+ profile,
81
+ workflowMode,
82
+ stateFrontmatter,
83
+ implementationContent,
84
+ });
85
+ const profileErrors = issues
86
+ .filter((issue) => issue.severity === 'error')
87
+ .map((issue) => issue.message);
88
+ const warnings = issues
89
+ .filter((issue) => issue.severity === 'warning')
90
+ .map((issue) => issue.message);
91
+ const warningPayload = warnings.length > 0 ? { warnings } : {};
92
+ if (!context.json) {
93
+ for (const warning of warnings) {
94
+ context.logger.warn(warning);
95
+ }
96
+ }
97
+ if (profileErrors.length > 0) {
64
98
  if (context.json) {
65
- context.logger.json({ valid: false, errors: [liteResult.message] });
99
+ context.logger.json({
100
+ valid: false,
101
+ errors: profileErrors,
102
+ ...warningPayload,
103
+ });
66
104
  }
67
105
  else {
68
106
  context.logger.error('Plan validation failed:');
69
- context.logger.error(` - ${liteResult.message}`);
107
+ for (const err of profileErrors) {
108
+ context.logger.error(` - ${err}`);
109
+ }
70
110
  }
71
111
  process.exitCode = 1;
72
112
  return;
@@ -74,7 +114,7 @@ export function createProjectValidatePlanCommand(overrides = {}) {
74
114
  const result = validateParallelGroups(groups, phaseIds);
75
115
  if (result.valid) {
76
116
  if (context.json) {
77
- context.logger.json({ valid: true });
117
+ context.logger.json({ valid: true, ...warningPayload });
78
118
  }
79
119
  else {
80
120
  context.logger.success('Plan validation passed.');
@@ -83,7 +123,11 @@ export function createProjectValidatePlanCommand(overrides = {}) {
83
123
  return;
84
124
  }
85
125
  if (context.json) {
86
- context.logger.json({ valid: false, errors: result.errors });
126
+ context.logger.json({
127
+ valid: false,
128
+ errors: result.errors,
129
+ ...warningPayload,
130
+ });
87
131
  }
88
132
  else {
89
133
  context.logger.error('Plan validation failed:');
@@ -0,0 +1,69 @@
1
+ import { type MilestoneMembershipErrorCode, 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`: every `brief` rule plus the goal milestone rules: membership
12
+ * read through the control-plane Milestones reader (its error codes map
13
+ * onto `goal-*` codes), frozen delivery scopes from `oat_deliveries`,
14
+ * parallel groups inside one milestone, `oat_plan_source: goal`, and
15
+ * canonical phase ids in persisted registry lists.
16
+ */
17
+ export type PlanProfile = 'task' | 'lite' | 'brief' | 'goal';
18
+ export interface ValidationContext {
19
+ profile: PlanProfile;
20
+ workflowMode: WorkflowMode | null;
21
+ stateFrontmatter: Record<string, unknown> | null;
22
+ /**
23
+ * The journal (`implementation.md`), when readable. Goal membership reads it
24
+ * so a phase the journal marks extracted need not belong to a milestone.
25
+ */
26
+ implementationContent?: string | null;
27
+ }
28
+ 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';
29
+ export type BriefValidationWarningCode = 'brief-task-headings-ignored';
30
+ export type GoalValidationErrorCode = 'goal-milestones-missing' | 'goal-milestones-malformed' | 'goal-duplicate-milestone' | 'goal-milestone-empty' | 'goal-milestone-unknown-phase' | 'goal-phase-in-multiple-milestones' | 'goal-phase-without-milestone' | 'goal-shipped-milestone-changed' | 'goal-delivered-scope-changed' | 'goal-group-crosses-milestone' | 'goal-plan-source' | 'goal-noncanonical-phase-id';
31
+ export type LiteValidationErrorCode = 'lite-multi-phase' | 'lite-parallel-groups' | 'lite-criterion-without-command';
32
+ export type ValidationIssue = {
33
+ severity: 'error';
34
+ code: BriefValidationErrorCode | GoalValidationErrorCode | LiteValidationErrorCode;
35
+ message: string;
36
+ } | {
37
+ severity: 'warning';
38
+ code: BriefValidationWarningCode;
39
+ message: string;
40
+ };
41
+ export declare function resolvePlanProfile(mode: WorkflowMode | null): PlanProfile;
42
+ export type LitePlanValidationResult = {
43
+ ok: true;
44
+ } | {
45
+ ok: false;
46
+ code: 'lite-multi-phase' | 'lite-parallel-groups' | 'lite-criterion-without-command';
47
+ message: string;
48
+ };
49
+ export declare function validateLitePlan(planContent: string, workflowMode: string | null | undefined): LitePlanValidationResult;
50
+ /**
51
+ * The normalized id and name of a `## Phase` heading line, or `null` when the
52
+ * line is not a well-formed phase heading. Delegates to the control-plane
53
+ * phase-unit parser so heading spellings (`## Phase 1:`, `## Phase p01:`,
54
+ * `## Phase p-rev1:`, `## Revision Phase 1:`) normalize exactly as progress
55
+ * reads them.
56
+ */
57
+ export declare function parsePhaseHeadingLine(line: string): {
58
+ phaseId: string;
59
+ name: string;
60
+ } | null;
61
+ export declare function extractPhaseIds(content: string, profile: PlanProfile): string[];
62
+ export declare function validatePlanForProfile(content: string, ctx: ValidationContext): ValidationIssue[];
63
+ /**
64
+ * Each Milestones reader error code maps onto exactly one goal rule code, so
65
+ * the reader stays the one membership predicate and validation never
66
+ * re-parses the table.
67
+ */
68
+ export declare const GOAL_MEMBERSHIP_RULE_CODES: Readonly<Record<MilestoneMembershipErrorCode, GoalValidationErrorCode>>;
69
+ //# 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,EAQL,KAAK,4BAA4B,EACjC,KAAK,YAAY,EAClB,MAAM,mCAAmC,CAAC;AAI3C;;;;;;;;;;;;;;GAcG;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;IACjD;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;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,yBAAyB,GACzB,2BAA2B,GAC3B,0BAA0B,GAC1B,sBAAsB,GACtB,8BAA8B,GAC9B,mCAAmC,GACnC,8BAA8B,GAC9B,gCAAgC,GAChC,8BAA8B,GAC9B,8BAA8B,GAC9B,kBAAkB,GAClB,4BAA4B,CAAC;AAEjC,MAAM,MAAM,uBAAuB,GAC/B,kBAAkB,GAClB,sBAAsB,GACtB,gCAAgC,CAAC;AAErC,MAAM,MAAM,eAAe,GACvB;IACE,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EACA,wBAAwB,GACxB,uBAAuB,GACvB,uBAAuB,CAAC;IAC5B,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,CAenB;AAMD;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAC/C,MAAM,CAAC,4BAA4B,EAAE,uBAAuB,CAAC,CAS9D,CAAC"}
@@ -0,0 +1,420 @@
1
+ import { isBriefMode, normalizeMilestoneId, normalizePhaseId, parseProvisionalPhaseIds, parseTaskProgress, readMilestoneMembership, 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
+ return validateBriefPlan(content);
265
+ case 'goal':
266
+ return [...validateBriefPlan(content), ...validateGoalPlan(content, ctx)];
267
+ case 'task':
268
+ return [];
269
+ }
270
+ }
271
+ // ---------------------------------------------------------------------------
272
+ // goal profile
273
+ // ---------------------------------------------------------------------------
274
+ /**
275
+ * Each Milestones reader error code maps onto exactly one goal rule code, so
276
+ * the reader stays the one membership predicate and validation never
277
+ * re-parses the table.
278
+ */
279
+ export const GOAL_MEMBERSHIP_RULE_CODES = {
280
+ 'milestones-missing': 'goal-milestones-missing',
281
+ 'milestones-malformed': 'goal-milestones-malformed',
282
+ 'milestone-duplicate': 'goal-duplicate-milestone',
283
+ 'milestone-empty': 'goal-milestone-empty',
284
+ 'milestone-unknown-phase': 'goal-milestone-unknown-phase',
285
+ 'phase-in-multiple-milestones': 'goal-phase-in-multiple-milestones',
286
+ 'phase-without-milestone': 'goal-phase-without-milestone',
287
+ };
288
+ function goalError(code, message) {
289
+ return { severity: 'error', code, message: `${code}: ${message}` };
290
+ }
291
+ function isRecord(value) {
292
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
293
+ }
294
+ function rawList(value) {
295
+ return Array.isArray(value) ? value : [];
296
+ }
297
+ function readRawDeliveries(state) {
298
+ return rawList(state?.['oat_deliveries'])
299
+ .filter(isRecord)
300
+ .map((entry, index) => ({
301
+ label: typeof entry['delivery_id'] === 'string'
302
+ ? entry['delivery_id']
303
+ : `oat_deliveries[${index}]`,
304
+ milestone: normalizeMilestoneId(entry['milestone']),
305
+ scope: rawList(entry['scope'])
306
+ .map((token) => normalizePhaseId(String(token)))
307
+ .filter((token) => token !== null),
308
+ }));
309
+ }
310
+ /**
311
+ * Persisted phase-id lists in the goal registry. The state reader normalizes
312
+ * these and drops tokens that are not phase ids; validation reports every
313
+ * token that is not already canonical so the drift is repaired at its source.
314
+ */
315
+ function persistedPhaseIdLists(state) {
316
+ if (state === null)
317
+ return [];
318
+ const lists = [];
319
+ const carrierLists = (prefix, carrier) => {
320
+ if (isRecord(carrier) && isRecord(carrier['subject'])) {
321
+ lists.push({
322
+ field: `${prefix}.subject.scope`,
323
+ values: rawList(carrier['subject']['scope']),
324
+ });
325
+ }
326
+ };
327
+ const recovery = state['oat_goal_recovery'];
328
+ if (isRecord(recovery)) {
329
+ lists.push({
330
+ field: 'oat_goal_recovery.phase_ids',
331
+ values: rawList(recovery['phase_ids']),
332
+ });
333
+ }
334
+ for (const [index, entry] of rawList(state['oat_deliveries']).entries()) {
335
+ if (!isRecord(entry))
336
+ continue;
337
+ const prefix = `oat_deliveries[${index}]`;
338
+ lists.push({ field: `${prefix}.scope`, values: rawList(entry['scope']) });
339
+ if (isRecord(entry['integrated_review'])) {
340
+ lists.push({
341
+ field: `${prefix}.integrated_review.scope`,
342
+ values: rawList(entry['integrated_review']['scope']),
343
+ });
344
+ }
345
+ carrierLists(`${prefix}.exit_gate`, entry['exit_gate']);
346
+ }
347
+ const gates = state['oat_milestone_gates'];
348
+ if (isRecord(gates)) {
349
+ for (const [milestone, carriers] of Object.entries(gates)) {
350
+ if (!isRecord(carriers))
351
+ continue;
352
+ carrierLists(`oat_milestone_gates.${milestone}.plan`, carriers['plan']);
353
+ carrierLists(`oat_milestone_gates.${milestone}.exit`, carriers['exit']);
354
+ }
355
+ }
356
+ return lists;
357
+ }
358
+ function validateGoalPlan(content, ctx) {
359
+ const issues = [];
360
+ const frontmatter = parseFrontmatterFromContent(content);
361
+ const planData = frontmatter.kind === 'invalid' ? {} : frontmatter.data;
362
+ if (planData['oat_plan_source'] !== 'goal') {
363
+ issues.push(goalError('goal-plan-source', `Goal plans must declare "oat_plan_source: goal"; found ${JSON.stringify(planData['oat_plan_source'] ?? null)}.`));
364
+ }
365
+ const membership = readMilestoneMembership(content, ctx.implementationContent ?? '');
366
+ for (const error of membership.errors) {
367
+ issues.push(goalError(GOAL_MEMBERSHIP_RULE_CODES[error.code], error.message));
368
+ }
369
+ const owners = new Map();
370
+ for (const row of membership.milestones) {
371
+ for (const phaseId of row.phaseIds) {
372
+ if (!owners.has(phaseId))
373
+ owners.set(phaseId, row.milestoneId);
374
+ }
375
+ }
376
+ const declaredMilestones = new Set(membership.milestones.map((row) => row.milestoneId));
377
+ const declaredPhases = new Set(extractBriefPhaseIds(content));
378
+ for (const delivery of readRawDeliveries(ctx.stateFrontmatter)) {
379
+ if (!membership.present || delivery.milestone === null)
380
+ continue;
381
+ if (!declaredMilestones.has(delivery.milestone)) {
382
+ issues.push(goalError('goal-shipped-milestone-changed', `Delivery ${delivery.label} shipped milestone ${delivery.milestone}, which the Milestones table no longer declares.`));
383
+ continue;
384
+ }
385
+ for (const phaseId of delivery.scope) {
386
+ if (!declaredPhases.has(phaseId)) {
387
+ issues.push(goalError('goal-delivered-scope-changed', `Delivery ${delivery.label} froze ${phaseId} in its scope, but the brief no longer declares it.`));
388
+ continue;
389
+ }
390
+ const owner = owners.get(phaseId);
391
+ if (owner !== undefined && owner !== delivery.milestone) {
392
+ issues.push(goalError('goal-delivered-scope-changed', `Delivery ${delivery.label} froze ${phaseId} under ${delivery.milestone}, but the Milestones table now assigns it to ${owner}.`));
393
+ }
394
+ else if (owner === undefined) {
395
+ issues.push(goalError('goal-delivered-scope-changed', `Delivery ${delivery.label} froze ${phaseId} under ${delivery.milestone}, but the Milestones table no longer lists it there.`));
396
+ }
397
+ }
398
+ }
399
+ for (const [index, group] of rawList(planData['oat_plan_parallel_groups']).entries()) {
400
+ const groupOwners = new Set();
401
+ for (const member of rawList(group)) {
402
+ const phaseId = normalizePhaseId(String(member));
403
+ const owner = phaseId === null ? undefined : owners.get(phaseId);
404
+ if (owner !== undefined)
405
+ groupOwners.add(owner);
406
+ }
407
+ if (groupOwners.size > 1) {
408
+ issues.push(goalError('goal-group-crosses-milestone', `Parallel group ${index} spans milestones ${[...groupOwners].join(', ')}; a group must sit inside one milestone.`));
409
+ }
410
+ }
411
+ for (const { field, values } of persistedPhaseIdLists(ctx.stateFrontmatter)) {
412
+ for (const value of values) {
413
+ const token = typeof value === 'string' ? value : String(value);
414
+ if (normalizePhaseId(token) !== token) {
415
+ issues.push(goalError('goal-noncanonical-phase-id', `${field} holds ${JSON.stringify(value)}, which is not a canonical phase id like "p01" or "p-rev1".`));
416
+ }
417
+ }
418
+ }
419
+ return issues;
420
+ }