@opencode_weave/weave 0.4.2 → 0.5.0
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.
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export type { WorkState, PlanProgress } from "./types";
|
|
2
|
+
export type { ValidationResult, ValidationIssue, ValidationSeverity, ValidationCategory } from "./validation-types";
|
|
2
3
|
export { WEAVE_DIR, WORK_STATE_FILE, WORK_STATE_PATH, PLANS_DIR } from "./constants";
|
|
3
4
|
export { readWorkState, writeWorkState, clearWorkState, appendSessionId, createWorkState, findPlans, getPlanProgress, getPlanName, } from "./storage";
|
|
5
|
+
export { validatePlan } from "./validation";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 6 validation categories checked by validatePlan().
|
|
3
|
+
*/
|
|
4
|
+
export type ValidationCategory = "structure" | "checkboxes" | "file-references" | "numbering" | "effort-estimate" | "verification";
|
|
5
|
+
/** Severity level for a validation issue. */
|
|
6
|
+
export type ValidationSeverity = "error" | "warning";
|
|
7
|
+
/**
|
|
8
|
+
* A single validation issue found in a plan file.
|
|
9
|
+
*/
|
|
10
|
+
export interface ValidationIssue {
|
|
11
|
+
severity: ValidationSeverity;
|
|
12
|
+
category: ValidationCategory;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The result of validating a plan file.
|
|
17
|
+
* `valid` is false if there are any blocking errors.
|
|
18
|
+
*/
|
|
19
|
+
export interface ValidationResult {
|
|
20
|
+
/** False when there is at least one error (blocking). True when only warnings or clean. */
|
|
21
|
+
valid: boolean;
|
|
22
|
+
/** Blocking issues — prevent /start-work from proceeding */
|
|
23
|
+
errors: ValidationIssue[];
|
|
24
|
+
/** Non-blocking issues — surfaced to user but don't block execution */
|
|
25
|
+
warnings: ValidationIssue[];
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ValidationResult } from "./validation-types";
|
|
2
|
+
/**
|
|
3
|
+
* Validates a plan file's structure and content before /start-work execution.
|
|
4
|
+
*
|
|
5
|
+
* @param planPath Absolute path to the plan markdown file
|
|
6
|
+
* @param projectDir Absolute path to the project root (used for file-reference checks)
|
|
7
|
+
* @returns ValidationResult with errors (blocking) and warnings (non-blocking)
|
|
8
|
+
*/
|
|
9
|
+
export declare function validatePlan(planPath: string, projectDir: string): ValidationResult;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Start-work hook: detects the /start-work command, resolves the target plan,
|
|
3
3
|
* creates/updates work state, and returns context for injection into the prompt.
|
|
4
4
|
*/
|
|
5
|
+
import type { ValidationResult } from "../features/work-state";
|
|
5
6
|
export interface StartWorkInput {
|
|
6
7
|
promptText: string;
|
|
7
8
|
sessionId: string;
|
|
@@ -18,3 +19,7 @@ export interface StartWorkResult {
|
|
|
18
19
|
* Returns null contextInjection if this message is not a /start-work command.
|
|
19
20
|
*/
|
|
20
21
|
export declare function handleStartWork(input: StartWorkInput): StartWorkResult;
|
|
22
|
+
/**
|
|
23
|
+
* Format validation errors and warnings as a markdown string.
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatValidationResults(result: ValidationResult): string;
|
package/dist/index.js
CHANGED
|
@@ -1877,6 +1877,286 @@ function getPlanProgress(planPath) {
|
|
|
1877
1877
|
function getPlanName(planPath) {
|
|
1878
1878
|
return basename(planPath, ".md");
|
|
1879
1879
|
}
|
|
1880
|
+
// src/features/work-state/validation.ts
|
|
1881
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
|
|
1882
|
+
import { resolve as resolve2 } from "path";
|
|
1883
|
+
function validatePlan(planPath, projectDir) {
|
|
1884
|
+
const errors = [];
|
|
1885
|
+
const warnings = [];
|
|
1886
|
+
const resolvedPlanPath = resolve2(planPath);
|
|
1887
|
+
const allowedDir = resolve2(projectDir, PLANS_DIR);
|
|
1888
|
+
if (!resolvedPlanPath.startsWith(allowedDir + "/") && resolvedPlanPath !== allowedDir) {
|
|
1889
|
+
errors.push({
|
|
1890
|
+
severity: "error",
|
|
1891
|
+
category: "structure",
|
|
1892
|
+
message: `Plan path is outside the allowed directory (${PLANS_DIR}/): ${planPath}`
|
|
1893
|
+
});
|
|
1894
|
+
return { valid: false, errors, warnings };
|
|
1895
|
+
}
|
|
1896
|
+
if (!existsSync6(resolvedPlanPath)) {
|
|
1897
|
+
errors.push({
|
|
1898
|
+
severity: "error",
|
|
1899
|
+
category: "structure",
|
|
1900
|
+
message: `Plan file not found: ${planPath}`
|
|
1901
|
+
});
|
|
1902
|
+
return { valid: false, errors, warnings };
|
|
1903
|
+
}
|
|
1904
|
+
const content = readFileSync5(resolvedPlanPath, "utf-8");
|
|
1905
|
+
validateStructure(content, errors, warnings);
|
|
1906
|
+
validateCheckboxes(content, errors, warnings);
|
|
1907
|
+
validateFileReferences(content, projectDir, warnings);
|
|
1908
|
+
validateNumbering(content, errors, warnings);
|
|
1909
|
+
validateEffortEstimate(content, warnings);
|
|
1910
|
+
validateVerificationSection(content, errors);
|
|
1911
|
+
return {
|
|
1912
|
+
valid: errors.length === 0,
|
|
1913
|
+
errors,
|
|
1914
|
+
warnings
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
function extractSection(content, heading) {
|
|
1918
|
+
const lines = content.split(`
|
|
1919
|
+
`);
|
|
1920
|
+
let startIdx = -1;
|
|
1921
|
+
for (let i = 0;i < lines.length; i++) {
|
|
1922
|
+
if (lines[i].trim() === heading) {
|
|
1923
|
+
startIdx = i + 1;
|
|
1924
|
+
break;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
if (startIdx === -1)
|
|
1928
|
+
return null;
|
|
1929
|
+
const sectionLines = [];
|
|
1930
|
+
for (let i = startIdx;i < lines.length; i++) {
|
|
1931
|
+
if (/^## /.test(lines[i]))
|
|
1932
|
+
break;
|
|
1933
|
+
sectionLines.push(lines[i]);
|
|
1934
|
+
}
|
|
1935
|
+
return sectionLines.join(`
|
|
1936
|
+
`);
|
|
1937
|
+
}
|
|
1938
|
+
function hasSection(content, heading) {
|
|
1939
|
+
return content.split(`
|
|
1940
|
+
`).some((line) => line.trim() === heading);
|
|
1941
|
+
}
|
|
1942
|
+
function validateStructure(content, errors, warnings) {
|
|
1943
|
+
const requiredSections = [
|
|
1944
|
+
["## TL;DR", "Missing required section: ## TL;DR"],
|
|
1945
|
+
["## TODOs", "Missing required section: ## TODOs"],
|
|
1946
|
+
["## Verification", "Missing required section: ## Verification"]
|
|
1947
|
+
];
|
|
1948
|
+
for (const [heading, message] of requiredSections) {
|
|
1949
|
+
if (!hasSection(content, heading)) {
|
|
1950
|
+
errors.push({ severity: "error", category: "structure", message });
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
const optionalSections = [
|
|
1954
|
+
["## Context", "Missing optional section: ## Context"],
|
|
1955
|
+
["## Objectives", "Missing optional section: ## Objectives"]
|
|
1956
|
+
];
|
|
1957
|
+
for (const [heading, message] of optionalSections) {
|
|
1958
|
+
if (!hasSection(content, heading)) {
|
|
1959
|
+
warnings.push({ severity: "warning", category: "structure", message });
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
function validateCheckboxes(content, errors, warnings) {
|
|
1964
|
+
const todosSection = extractSection(content, "## TODOs");
|
|
1965
|
+
if (todosSection === null) {
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
const checkboxPattern = /^- \[[ x]\] /m;
|
|
1969
|
+
if (!checkboxPattern.test(todosSection)) {
|
|
1970
|
+
errors.push({
|
|
1971
|
+
severity: "error",
|
|
1972
|
+
category: "checkboxes",
|
|
1973
|
+
message: "## TODOs section contains no checkboxes (- [ ] or - [x])"
|
|
1974
|
+
});
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
const lines = todosSection.split(`
|
|
1978
|
+
`);
|
|
1979
|
+
let taskIndex = 0;
|
|
1980
|
+
for (let i = 0;i < lines.length; i++) {
|
|
1981
|
+
const line = lines[i];
|
|
1982
|
+
if (/^- \[[ x]\] /.test(line)) {
|
|
1983
|
+
taskIndex++;
|
|
1984
|
+
const taskLabel = `Task ${taskIndex}`;
|
|
1985
|
+
const bodyLines = [];
|
|
1986
|
+
let j = i + 1;
|
|
1987
|
+
while (j < lines.length && !/^- \[[ x]\] /.test(lines[j])) {
|
|
1988
|
+
bodyLines.push(lines[j]);
|
|
1989
|
+
j++;
|
|
1990
|
+
}
|
|
1991
|
+
const body = bodyLines.join(`
|
|
1992
|
+
`);
|
|
1993
|
+
if (!/\*\*What\*\*/.test(body)) {
|
|
1994
|
+
warnings.push({
|
|
1995
|
+
severity: "warning",
|
|
1996
|
+
category: "checkboxes",
|
|
1997
|
+
message: `${taskLabel} is missing **What** sub-field`
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
if (!/\*\*Files\*\*/.test(body)) {
|
|
2001
|
+
warnings.push({
|
|
2002
|
+
severity: "warning",
|
|
2003
|
+
category: "checkboxes",
|
|
2004
|
+
message: `${taskLabel} is missing **Files** sub-field`
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
if (!/\*\*Acceptance\*\*/.test(body)) {
|
|
2008
|
+
warnings.push({
|
|
2009
|
+
severity: "warning",
|
|
2010
|
+
category: "checkboxes",
|
|
2011
|
+
message: `${taskLabel} is missing **Acceptance** sub-field`
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
var NEW_FILE_INDICATORS = [
|
|
2018
|
+
/^\s*create\s+/i,
|
|
2019
|
+
/^\s*new:\s*/i,
|
|
2020
|
+
/\(new\)/i,
|
|
2021
|
+
/^\s*add\s+/i
|
|
2022
|
+
];
|
|
2023
|
+
function isNewFile(rawPath) {
|
|
2024
|
+
return NEW_FILE_INDICATORS.some((re) => re.test(rawPath));
|
|
2025
|
+
}
|
|
2026
|
+
function extractFilePath(raw) {
|
|
2027
|
+
let cleaned = raw.replace(/^\s*(create|modify|new:|add)\s+/i, "").replace(/\(new\)/gi, "").trim();
|
|
2028
|
+
const firstToken = cleaned.split(/\s+/)[0];
|
|
2029
|
+
if (firstToken && (firstToken.includes("/") || firstToken.endsWith(".ts") || firstToken.endsWith(".js") || firstToken.endsWith(".json") || firstToken.endsWith(".md"))) {
|
|
2030
|
+
cleaned = firstToken;
|
|
2031
|
+
} else if (!cleaned.includes("/")) {
|
|
2032
|
+
return null;
|
|
2033
|
+
}
|
|
2034
|
+
return cleaned || null;
|
|
2035
|
+
}
|
|
2036
|
+
function validateFileReferences(content, projectDir, warnings) {
|
|
2037
|
+
const todosSection = extractSection(content, "## TODOs");
|
|
2038
|
+
if (todosSection === null)
|
|
2039
|
+
return;
|
|
2040
|
+
const lines = todosSection.split(`
|
|
2041
|
+
`);
|
|
2042
|
+
for (const line of lines) {
|
|
2043
|
+
const filesMatch = /^\s*\*\*Files\*\*:?\s*(.+)$/.exec(line);
|
|
2044
|
+
if (!filesMatch)
|
|
2045
|
+
continue;
|
|
2046
|
+
const rawValue = filesMatch[1].trim();
|
|
2047
|
+
const parts = rawValue.split(",");
|
|
2048
|
+
for (const part of parts) {
|
|
2049
|
+
const trimmed = part.trim();
|
|
2050
|
+
if (!trimmed)
|
|
2051
|
+
continue;
|
|
2052
|
+
const newFile = isNewFile(trimmed);
|
|
2053
|
+
const filePath = extractFilePath(trimmed);
|
|
2054
|
+
if (!filePath)
|
|
2055
|
+
continue;
|
|
2056
|
+
if (newFile)
|
|
2057
|
+
continue;
|
|
2058
|
+
if (filePath.startsWith("/")) {
|
|
2059
|
+
warnings.push({
|
|
2060
|
+
severity: "warning",
|
|
2061
|
+
category: "file-references",
|
|
2062
|
+
message: `Absolute file path not allowed in plan references: ${filePath}`
|
|
2063
|
+
});
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
const resolvedProject = resolve2(projectDir);
|
|
2067
|
+
const absolutePath = resolve2(projectDir, filePath);
|
|
2068
|
+
if (!absolutePath.startsWith(resolvedProject + "/") && absolutePath !== resolvedProject) {
|
|
2069
|
+
warnings.push({
|
|
2070
|
+
severity: "warning",
|
|
2071
|
+
category: "file-references",
|
|
2072
|
+
message: `File reference escapes project directory (path traversal): ${filePath}`
|
|
2073
|
+
});
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
if (!existsSync6(absolutePath)) {
|
|
2077
|
+
warnings.push({
|
|
2078
|
+
severity: "warning",
|
|
2079
|
+
category: "file-references",
|
|
2080
|
+
message: `Referenced file does not exist (may be created by an earlier task): ${filePath}`
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
function validateNumbering(content, errors, warnings) {
|
|
2087
|
+
const todosSection = extractSection(content, "## TODOs");
|
|
2088
|
+
if (todosSection === null)
|
|
2089
|
+
return;
|
|
2090
|
+
const numbers = [];
|
|
2091
|
+
const seen = new Set;
|
|
2092
|
+
for (const line of todosSection.split(`
|
|
2093
|
+
`)) {
|
|
2094
|
+
const match = /^- \[[ x]\] (\d+)\./.exec(line);
|
|
2095
|
+
if (!match)
|
|
2096
|
+
continue;
|
|
2097
|
+
const n = parseInt(match[1], 10);
|
|
2098
|
+
if (seen.has(n)) {
|
|
2099
|
+
errors.push({
|
|
2100
|
+
severity: "error",
|
|
2101
|
+
category: "numbering",
|
|
2102
|
+
message: `Duplicate task number: ${n}`
|
|
2103
|
+
});
|
|
2104
|
+
} else {
|
|
2105
|
+
seen.add(n);
|
|
2106
|
+
numbers.push(n);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
if (numbers.length < 2)
|
|
2110
|
+
return;
|
|
2111
|
+
const sorted = [...numbers].sort((a, b) => a - b);
|
|
2112
|
+
for (let i = 1;i < sorted.length; i++) {
|
|
2113
|
+
if (sorted[i] !== sorted[i - 1] + 1) {
|
|
2114
|
+
warnings.push({
|
|
2115
|
+
severity: "warning",
|
|
2116
|
+
category: "numbering",
|
|
2117
|
+
message: `Gap in task numbering: expected ${sorted[i - 1] + 1} but found ${sorted[i]}`
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
var VALID_EFFORT_VALUES = ["quick", "short", "medium", "large", "xl"];
|
|
2123
|
+
function validateEffortEstimate(content, warnings) {
|
|
2124
|
+
const tldrSection = extractSection(content, "## TL;DR");
|
|
2125
|
+
if (tldrSection === null) {
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
const effortMatch = /\*\*Estimated Effort\*\*:?\s*(.+)/i.exec(tldrSection);
|
|
2129
|
+
if (!effortMatch) {
|
|
2130
|
+
warnings.push({
|
|
2131
|
+
severity: "warning",
|
|
2132
|
+
category: "effort-estimate",
|
|
2133
|
+
message: "Missing **Estimated Effort** in ## TL;DR section"
|
|
2134
|
+
});
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
const value = effortMatch[1].trim().toLowerCase();
|
|
2138
|
+
if (!VALID_EFFORT_VALUES.includes(value)) {
|
|
2139
|
+
warnings.push({
|
|
2140
|
+
severity: "warning",
|
|
2141
|
+
category: "effort-estimate",
|
|
2142
|
+
message: `Invalid effort estimate value: "${effortMatch[1].trim()}". Expected one of: Quick, Short, Medium, Large, XL`
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
function validateVerificationSection(content, errors) {
|
|
2147
|
+
const verificationSection = extractSection(content, "## Verification");
|
|
2148
|
+
if (verificationSection === null) {
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
const hasCheckbox = /^- \[[ x]\] /m.test(verificationSection);
|
|
2152
|
+
if (!hasCheckbox) {
|
|
2153
|
+
errors.push({
|
|
2154
|
+
severity: "error",
|
|
2155
|
+
category: "verification",
|
|
2156
|
+
message: "## Verification section contains no checkboxes — at least one verifiable condition is required"
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
1880
2160
|
// src/hooks/start-work-hook.ts
|
|
1881
2161
|
function handleStartWork(input) {
|
|
1882
2162
|
const { promptText, sessionId, directory } = input;
|
|
@@ -1892,10 +2172,33 @@ function handleStartWork(input) {
|
|
|
1892
2172
|
if (existingState) {
|
|
1893
2173
|
const progress = getPlanProgress(existingState.active_plan);
|
|
1894
2174
|
if (!progress.isComplete) {
|
|
2175
|
+
const validation = validatePlan(existingState.active_plan, directory);
|
|
2176
|
+
if (!validation.valid) {
|
|
2177
|
+
clearWorkState(directory);
|
|
2178
|
+
return {
|
|
2179
|
+
switchAgent: "tapestry",
|
|
2180
|
+
contextInjection: `## Plan Validation Failed
|
|
2181
|
+
The active plan "${existingState.plan_name}" has structural issues. Work state has been cleared.
|
|
2182
|
+
|
|
2183
|
+
${formatValidationResults(validation)}
|
|
2184
|
+
|
|
2185
|
+
Tell the user to fix the plan file and run /start-work again.`
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
1895
2188
|
appendSessionId(directory, sessionId);
|
|
2189
|
+
const resumeContext = buildResumeContext(existingState.active_plan, existingState.plan_name, progress);
|
|
2190
|
+
if (validation.warnings.length > 0) {
|
|
2191
|
+
return {
|
|
2192
|
+
switchAgent: "tapestry",
|
|
2193
|
+
contextInjection: `${resumeContext}
|
|
2194
|
+
|
|
2195
|
+
### Validation Warnings
|
|
2196
|
+
${formatValidationResults(validation)}`
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
1896
2199
|
return {
|
|
1897
2200
|
switchAgent: "tapestry",
|
|
1898
|
-
contextInjection:
|
|
2201
|
+
contextInjection: resumeContext
|
|
1899
2202
|
};
|
|
1900
2203
|
}
|
|
1901
2204
|
}
|
|
@@ -1934,12 +2237,34 @@ The plan "${getPlanName(matched)}" has all ${progress.total} tasks completed.
|
|
|
1934
2237
|
Tell the user this plan is already done and suggest creating a new one with Pattern.`
|
|
1935
2238
|
};
|
|
1936
2239
|
}
|
|
2240
|
+
const validation = validatePlan(matched, directory);
|
|
2241
|
+
if (!validation.valid) {
|
|
2242
|
+
return {
|
|
2243
|
+
switchAgent: "tapestry",
|
|
2244
|
+
contextInjection: `## Plan Validation Failed
|
|
2245
|
+
The plan "${getPlanName(matched)}" has structural issues that must be fixed before execution can begin.
|
|
2246
|
+
|
|
2247
|
+
${formatValidationResults(validation)}
|
|
2248
|
+
|
|
2249
|
+
Tell the user to fix these issues in the plan file and try again.`
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
1937
2252
|
clearWorkState(directory);
|
|
1938
2253
|
const state = createWorkState(matched, sessionId, "tapestry");
|
|
1939
2254
|
writeWorkState(directory, state);
|
|
2255
|
+
const freshContext = buildFreshContext(matched, getPlanName(matched), progress);
|
|
2256
|
+
if (validation.warnings.length > 0) {
|
|
2257
|
+
return {
|
|
2258
|
+
switchAgent: "tapestry",
|
|
2259
|
+
contextInjection: `${freshContext}
|
|
2260
|
+
|
|
2261
|
+
### Validation Warnings
|
|
2262
|
+
${formatValidationResults(validation)}`
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
1940
2265
|
return {
|
|
1941
2266
|
switchAgent: "tapestry",
|
|
1942
|
-
contextInjection:
|
|
2267
|
+
contextInjection: freshContext
|
|
1943
2268
|
};
|
|
1944
2269
|
}
|
|
1945
2270
|
function handlePlanDiscovery(allPlans, sessionId, directory) {
|
|
@@ -1961,11 +2286,33 @@ Tell the user to switch to Pattern agent to create a new plan.`
|
|
|
1961
2286
|
if (incompletePlans.length === 1) {
|
|
1962
2287
|
const plan = incompletePlans[0];
|
|
1963
2288
|
const progress = getPlanProgress(plan);
|
|
2289
|
+
const validation = validatePlan(plan, directory);
|
|
2290
|
+
if (!validation.valid) {
|
|
2291
|
+
return {
|
|
2292
|
+
switchAgent: "tapestry",
|
|
2293
|
+
contextInjection: `## Plan Validation Failed
|
|
2294
|
+
The plan "${getPlanName(plan)}" has structural issues that must be fixed before execution can begin.
|
|
2295
|
+
|
|
2296
|
+
${formatValidationResults(validation)}
|
|
2297
|
+
|
|
2298
|
+
Tell the user to fix these issues in the plan file and try again.`
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
1964
2301
|
const state = createWorkState(plan, sessionId, "tapestry");
|
|
1965
2302
|
writeWorkState(directory, state);
|
|
2303
|
+
const freshContext = buildFreshContext(plan, getPlanName(plan), progress);
|
|
2304
|
+
if (validation.warnings.length > 0) {
|
|
2305
|
+
return {
|
|
2306
|
+
switchAgent: "tapestry",
|
|
2307
|
+
contextInjection: `${freshContext}
|
|
2308
|
+
|
|
2309
|
+
### Validation Warnings
|
|
2310
|
+
${formatValidationResults(validation)}`
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
1966
2313
|
return {
|
|
1967
2314
|
switchAgent: "tapestry",
|
|
1968
|
-
contextInjection:
|
|
2315
|
+
contextInjection: freshContext
|
|
1969
2316
|
};
|
|
1970
2317
|
}
|
|
1971
2318
|
const listing = incompletePlans.map((p) => {
|
|
@@ -1990,6 +2337,25 @@ function findPlanByName(plans, requestedName) {
|
|
|
1990
2337
|
const partial = plans.find((p) => getPlanName(p).toLowerCase().includes(lower));
|
|
1991
2338
|
return partial || null;
|
|
1992
2339
|
}
|
|
2340
|
+
function formatValidationResults(result) {
|
|
2341
|
+
const lines = [];
|
|
2342
|
+
if (result.errors.length > 0) {
|
|
2343
|
+
lines.push("**Errors (blocking):**");
|
|
2344
|
+
for (const err of result.errors) {
|
|
2345
|
+
lines.push(`- [${err.category}] ${err.message}`);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
if (result.warnings.length > 0) {
|
|
2349
|
+
if (result.errors.length > 0)
|
|
2350
|
+
lines.push("");
|
|
2351
|
+
lines.push("**Warnings:**");
|
|
2352
|
+
for (const warn of result.warnings) {
|
|
2353
|
+
lines.push(`- [${warn.category}] ${warn.message}`);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return lines.join(`
|
|
2357
|
+
`);
|
|
2358
|
+
}
|
|
1993
2359
|
function buildFreshContext(planPath, planName, progress) {
|
|
1994
2360
|
return `## Starting Plan: ${planName}
|
|
1995
2361
|
**Plan file**: ${planPath}
|