flanders 0.7.0 → 0.9.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.
- package/README.md +91 -79
- package/lib/ai/AiRunner.d.ts +3 -1
- package/lib/ai/AiRunner.js +2 -2
- package/lib/ai/AiSession.d.ts +3 -1
- package/lib/ai/AiSession.js +2 -0
- package/lib/ai/ClaudeAdapter.d.ts +2 -1
- package/lib/ai/ClaudeAdapter.js +7 -3
- package/lib/ai/CodexAdapter.js +9 -80
- package/lib/ai/ToolAdapter.d.ts +7 -3
- package/lib/ai/toolErrorClassification.d.ts +7 -0
- package/lib/ai/toolErrorClassification.js +78 -0
- package/lib/cli.js +10 -4
- package/lib/commands/Implement.d.ts +3 -0
- package/lib/commands/Implement.js +60 -16
- package/lib/commands/Install.d.ts +9 -4
- package/lib/commands/Install.js +250 -105
- package/lib/commands/skillArtifacts.js +40 -36
- package/lib/contexts.d.ts +1 -0
- package/lib/fastMode.d.ts +2 -0
- package/lib/fastMode.js +15 -0
- package/lib/plan/PlanFile.d.ts +3 -1
- package/lib/plan/PlanFile.js +8 -1
- package/lib/prompts/prompts.js +6 -6
- package/lib/prompts/skills.js +13 -7
- package/lib/toolNames.d.ts +1 -0
- package/lib/toolNames.js +4 -0
- package/lib/ui/BottomBlock.d.ts +11 -8
- package/lib/ui/BottomBlock.js +24 -3
- package/lib/ui/PromptHelper.d.ts +7 -0
- package/lib/ui/PromptHelper.js +22 -0
- package/lib/ui/formatters.d.ts +3 -2
- package/lib/ui/formatters.js +7 -2
- package/lib/workspace/FlandersConfig.d.ts +3 -1
- package/lib/workspace/FlandersConfig.js +5 -1
- package/package.json +3 -2
|
@@ -15,10 +15,10 @@ exports.SKILLS = [
|
|
|
15
15
|
const CLAUDE_SKILLS_SUBDIR = ".claude/skills";
|
|
16
16
|
const CODEX_PROMPTS_SUBDIR = ".codex/prompts";
|
|
17
17
|
function skillArtifactPath(scopeRoot, tool, skillName) {
|
|
18
|
-
if (tool === "
|
|
19
|
-
return (0, fsUtils_1.joinPath)(scopeRoot,
|
|
18
|
+
if (tool === "codex") {
|
|
19
|
+
return (0, fsUtils_1.joinPath)(scopeRoot, CODEX_PROMPTS_SUBDIR, `${skillName}.md`);
|
|
20
20
|
}
|
|
21
|
-
return (0, fsUtils_1.joinPath)(scopeRoot,
|
|
21
|
+
return (0, fsUtils_1.joinPath)(scopeRoot, CLAUDE_SKILLS_SUBDIR, skillName, "SKILL.md");
|
|
22
22
|
}
|
|
23
23
|
function skillArtifactPaths(scopeRoot, tool) {
|
|
24
24
|
return exports.SKILLS.map(skill => skillArtifactPath(scopeRoot, tool, skill.name));
|
|
@@ -38,29 +38,53 @@ function stripYamlFrontmatter(body) {
|
|
|
38
38
|
}
|
|
39
39
|
return body.slice(closerIndex + "\n---\n".length);
|
|
40
40
|
}
|
|
41
|
+
async function writeDirectorySkillTrio(fs, scopeRoot, tool, isDisposed) {
|
|
42
|
+
const skillsRoot = (0, fsUtils_1.joinPath)(scopeRoot, CLAUDE_SKILLS_SUBDIR);
|
|
43
|
+
const writtenPaths = [];
|
|
44
|
+
for (const skill of exports.SKILLS) {
|
|
45
|
+
if (isDisposed()) {
|
|
46
|
+
return { ok: false, diagnostic: null };
|
|
47
|
+
}
|
|
48
|
+
const skillFolder = (0, fsUtils_1.joinPath)(skillsRoot, skill.name);
|
|
49
|
+
try {
|
|
50
|
+
await fs.mkdir(skillFolder, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { ok: false, diagnostic: `Cannot create destination: ${skillFolder}\n` };
|
|
54
|
+
}
|
|
55
|
+
const filePath = skillArtifactPath(scopeRoot, tool, skill.name);
|
|
56
|
+
try {
|
|
57
|
+
await fs.writeFile(filePath, skill.body);
|
|
58
|
+
writtenPaths.push(filePath);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { ok: false, diagnostic: `Cannot write file: ${filePath}\n` };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, writtenPaths };
|
|
65
|
+
}
|
|
41
66
|
async function writeSkillArtifacts(fs, scopeRoot, tool, isDisposed) {
|
|
42
67
|
for (const skill of exports.SKILLS) {
|
|
43
68
|
if (!skill.body) {
|
|
44
69
|
return { ok: false, diagnostic: `Skill "${skill.name}" has no content.\n` };
|
|
45
70
|
}
|
|
46
71
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
if (tool === "codex") {
|
|
73
|
+
const codexPromptsRoot = (0, fsUtils_1.joinPath)(scopeRoot, CODEX_PROMPTS_SUBDIR);
|
|
74
|
+
try {
|
|
75
|
+
await fs.mkdir(codexPromptsRoot, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return { ok: false, diagnostic: `Cannot create destination: ${codexPromptsRoot}\n` };
|
|
79
|
+
}
|
|
80
|
+
const writtenPaths = [];
|
|
50
81
|
for (const skill of exports.SKILLS) {
|
|
51
82
|
if (isDisposed()) {
|
|
52
83
|
return { ok: false, diagnostic: null };
|
|
53
84
|
}
|
|
54
|
-
const
|
|
55
|
-
try {
|
|
56
|
-
await fs.mkdir(skillFolder, { recursive: true });
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return { ok: false, diagnostic: `Cannot create destination: ${skillFolder}\n` };
|
|
60
|
-
}
|
|
61
|
-
const filePath = skillArtifactPath(scopeRoot, "claude", skill.name);
|
|
85
|
+
const filePath = skillArtifactPath(scopeRoot, "codex", skill.name);
|
|
62
86
|
try {
|
|
63
|
-
await fs.writeFile(filePath, skill.body);
|
|
87
|
+
await fs.writeFile(filePath, stripYamlFrontmatter(skill.body));
|
|
64
88
|
writtenPaths.push(filePath);
|
|
65
89
|
}
|
|
66
90
|
catch {
|
|
@@ -69,25 +93,5 @@ async function writeSkillArtifacts(fs, scopeRoot, tool, isDisposed) {
|
|
|
69
93
|
}
|
|
70
94
|
return { ok: true, writtenPaths };
|
|
71
95
|
}
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
await fs.mkdir(codexPromptsRoot, { recursive: true });
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return { ok: false, diagnostic: `Cannot create destination: ${codexPromptsRoot}\n` };
|
|
78
|
-
}
|
|
79
|
-
for (const skill of exports.SKILLS) {
|
|
80
|
-
if (isDisposed()) {
|
|
81
|
-
return { ok: false, diagnostic: null };
|
|
82
|
-
}
|
|
83
|
-
const filePath = skillArtifactPath(scopeRoot, "codex", skill.name);
|
|
84
|
-
try {
|
|
85
|
-
await fs.writeFile(filePath, stripYamlFrontmatter(skill.body));
|
|
86
|
-
writtenPaths.push(filePath);
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
return { ok: false, diagnostic: `Cannot write file: ${filePath}\n` };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return { ok: true, writtenPaths };
|
|
96
|
+
return writeDirectorySkillTrio(fs, scopeRoot, tool, isDisposed);
|
|
93
97
|
}
|
package/lib/contexts.d.ts
CHANGED
package/lib/fastMode.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FAST_CAPABLE_MODELS = void 0;
|
|
4
|
+
exports.modelSupportsFastMode = modelSupportsFastMode;
|
|
5
|
+
exports.FAST_CAPABLE_MODELS = [
|
|
6
|
+
"opus",
|
|
7
|
+
"opus[1m]",
|
|
8
|
+
"claude-opus-4-8",
|
|
9
|
+
"claude-opus-4-8[1m]",
|
|
10
|
+
"claude-opus-4-7",
|
|
11
|
+
"claude-opus-4-7[1m]"
|
|
12
|
+
];
|
|
13
|
+
function modelSupportsFastMode(model) {
|
|
14
|
+
return exports.FAST_CAPABLE_MODELS.includes(model);
|
|
15
|
+
}
|
package/lib/plan/PlanFile.d.ts
CHANGED
|
@@ -33,11 +33,13 @@ export declare function headingAnchor(headingText: string): string;
|
|
|
33
33
|
export declare function extractHeadingSection(content: string, anchor: string): string | null;
|
|
34
34
|
export declare function buildSpecFileContent(references: readonly LinkedReference[], fileContents: ReadonlyMap<string, string>): string;
|
|
35
35
|
export declare class PlanFile {
|
|
36
|
-
readonly path: string;
|
|
37
36
|
private _content;
|
|
38
37
|
private _fs;
|
|
38
|
+
private _path;
|
|
39
39
|
private constructor();
|
|
40
|
+
get path(): string;
|
|
40
41
|
static load(path: string, fs: FsContext): Promise<PlanFile>;
|
|
42
|
+
rename(newPath: string): Promise<void>;
|
|
41
43
|
parse(): PlanParseResult;
|
|
42
44
|
nextOpenTask(): PlanTask | null;
|
|
43
45
|
updateMetrics(lineNumber: number, metrics: TaskMetrics): Promise<void>;
|
package/lib/plan/PlanFile.js
CHANGED
|
@@ -206,14 +206,21 @@ function buildSpecFileContent(references, fileContents) {
|
|
|
206
206
|
}
|
|
207
207
|
class PlanFile {
|
|
208
208
|
constructor(path, _content, _fs) {
|
|
209
|
-
this.path = path;
|
|
210
209
|
this._content = _content;
|
|
211
210
|
this._fs = _fs;
|
|
211
|
+
this._path = path;
|
|
212
|
+
}
|
|
213
|
+
get path() {
|
|
214
|
+
return this._path;
|
|
212
215
|
}
|
|
213
216
|
static async load(path, fs) {
|
|
214
217
|
const content = await fs.readFile(path);
|
|
215
218
|
return new PlanFile(path, content, fs);
|
|
216
219
|
}
|
|
220
|
+
async rename(newPath) {
|
|
221
|
+
await this._fs.rename(this._path, newPath);
|
|
222
|
+
this._path = newPath;
|
|
223
|
+
}
|
|
217
224
|
parse() {
|
|
218
225
|
return parsePlan(this._content);
|
|
219
226
|
}
|
package/lib/prompts/prompts.js
CHANGED
|
@@ -23,7 +23,7 @@ const workerToolchainRerunStep = `When a test-guarded regression argument cannot
|
|
|
23
23
|
const claimClassification = `${claimClassificationCore}
|
|
24
24
|
|
|
25
25
|
${workerToolchainRerunStep}`;
|
|
26
|
-
const foregroundBoundary = `Foreground execution boundary: you run every command you execute in the foreground and keep your turn active until that command finishes and its result is in hand.
|
|
26
|
+
const foregroundBoundary = `Foreground execution boundary: you run every command you execute in the foreground and keep your turn active until that command finishes and its result is in hand. This binds every command without exception — build scripts, test scripts, linters, and any other shell command; give a long-running command a tool timeout large enough to finish in the foreground rather than detaching it. Forbidden mechanisms include a tool call made with a background flag (for example \`run_in_background: true\`), shell-level detachment (a trailing \`&\`, \`nohup\`, \`setsid\`, \`disown\`, \`start\`, \`Start-Process\`, \`Start-Job\`), converting a timed-out foreground command into a background task, and ending your turn with a message that a spawned command is still running. The full obligation lives in rules/ai/agents/no-background-commands.md.`;
|
|
27
27
|
const specFolderWriteBoundary = `Spec-folder write boundary: you must not create, modify, delete, or rename any file inside any \`.spec/contracts\` folder, any \`.spec/rules\` folder, any \`.spec/flanders\` folder, or the \`plans/\` folder. These folders are governed by dedicated skills and the implement command's bounded checkpoint updates; no other agent may write to them. See shared/spec-folder-write-authority.md for the full obligation.`;
|
|
28
28
|
const claimClassificationCitationFree = claimClassificationCore.replace(" per `rules/testing/assert-via-public-surface.md`", "");
|
|
29
29
|
function buildReviewerMethodology(s) {
|
|
@@ -144,18 +144,18 @@ const citationFreeReviewerMethodology = buildReviewerMethodology(citationFreeRev
|
|
|
144
144
|
exports.reviewerMethodologyCore = `${citationFreeReviewerMethodology.changeSet}
|
|
145
145
|
|
|
146
146
|
${citationFreeReviewerMethodology.audit}`;
|
|
147
|
-
const
|
|
147
|
+
const voiceEnglishOnlyDirective = "is English, the character's original language; in any other language, apply no flavor and deliver the message plainly.";
|
|
148
148
|
const voiceExclusionLead = "The flavor lives only in flowing prose: it never appears in code, file paths, directory names, command lines, flag or option tokens, the factual content of a diagnostic or error message (the problem described, the path, the line number, and every other datum needed to act on it), any token another part of the tool reads programmatically, git commit messages";
|
|
149
149
|
const voiceTail = " — all of which stay exact and as actionable as before.";
|
|
150
150
|
function buildFlandersVoiceSection(parts) {
|
|
151
151
|
return `## Voice
|
|
152
152
|
|
|
153
|
-
Season ${parts.subject} — with a soft Ned-Flanders touch in every message: a gentle note of the character's warm, folksy, good-natured manner, so the voice is a steady, recognizable presence across the whole run rather than a rare flourish, the one exception being a message
|
|
153
|
+
Season ${parts.subject} — with a soft Ned-Flanders touch in every message: a gentle note of the character's warm, folksy, good-natured manner, so the voice is a steady, recognizable presence across the whole run rather than a rare flourish, the one exception being a message you address to the user in a language other than English, which is delivered plainly with no touch. Keep it light — typically a single touch per message, never on every line and never exaggerated — and never let the flavor change the substance, structure, or accuracy of anything you say. Apply the flavor only while ${parts.languageFraming} ${voiceEnglishOnlyDirective} ${voiceExclusionLead}${parts.finalExclusion}${voiceTail}${parts.trailer}`;
|
|
154
154
|
}
|
|
155
155
|
function flandersToneInstruction(reviewer) {
|
|
156
156
|
return buildFlandersVoiceSection({
|
|
157
157
|
subject: "your user-facing narration — the prose you stream as you work",
|
|
158
|
-
languageFraming: "
|
|
158
|
+
languageFraming: "the language you are narrating in",
|
|
159
159
|
finalExclusion: reviewer
|
|
160
160
|
? ", or the violation entries you record in your error-log file"
|
|
161
161
|
: "",
|
|
@@ -207,7 +207,7 @@ Your output will be inspected by an adversarial reviewer immediately after you f
|
|
|
207
207
|
4. A contract or rule from the global lists below that the reviewer determines should have been applied but was not — even if the task did not reference it.
|
|
208
208
|
5. A behavior rule from the behavior-rule list below whose \`.spec/flanders\` scope encloses the files your changes touch is not honored by the changes — in-scope behavior rules are mandatory whether or not the task links them.
|
|
209
209
|
|
|
210
|
-
Condition 4 causes most rejections in practice.
|
|
210
|
+
Condition 4 causes most rejections in practice. The reviewer will also enumerate every occurrence of a pattern violation, not just the first one, so partial compliance within a file is itself a FAIL.
|
|
211
211
|
|
|
212
212
|
Procedure:
|
|
213
213
|
1. Read the task shown above and respect the obligations of every contract and rule it references exactly. You may consult those files, or the plan file for broader context, at your discretion.
|
|
@@ -272,7 +272,7 @@ The plan file is at ${"<PLAN_PATH>"}; you may open it for broader context, but y
|
|
|
272
272
|
|
|
273
273
|
${"<TASK_TEXT>"}
|
|
274
274
|
|
|
275
|
-
|
|
275
|
+
Inspect the working-tree changes that the worker just produced.
|
|
276
276
|
|
|
277
277
|
${linkedReferenceDirective("<SPEC_PATH>")}
|
|
278
278
|
|
package/lib/prompts/skills.js
CHANGED
|
@@ -6,7 +6,7 @@ const prompts_1 = require("./prompts");
|
|
|
6
6
|
function skillVoiceSection(authoredArtifactExclusion) {
|
|
7
7
|
return (0, prompts_1.buildFlandersVoiceSection)({
|
|
8
8
|
subject: "the messages you address to the user — your questions, summaries, warnings, recommendations, and every other text you print in the conversation",
|
|
9
|
-
languageFraming: "
|
|
9
|
+
languageFraming: "the resolved interaction language you are addressing the user in",
|
|
10
10
|
finalExclusion: `, or ${authoredArtifactExclusion}`,
|
|
11
11
|
trailer: ""
|
|
12
12
|
});
|
|
@@ -31,7 +31,7 @@ The user invokes you as: /flanders-plan [<data>]
|
|
|
31
31
|
2. Discover every directory named \`.spec\` across the whole project tree at every depth, excluding every path the project's git ignore rules exclude (for example by enumerating with \`git ls-files --cached --others --exclude-standard\` — which lists tracked files plus untracked-but-not-ignored files — and dropping any candidate that sits under a git-ignored path, for example via \`git check-ignore\`); the files under each \`.spec/contracts\` subfolder form the canonical contracts listing and the files under each \`.spec/rules\` subfolder form the canonical rules listing; the files under each \`.spec/flanders\` subfolder form the behavior-rule listing, treating every file inside a \`.spec/flanders\` folder at any depth as a behavior rule; each file is identified by its namespace — its path relative to the project root, which for nested \`.spec\` folders includes the directories above the \`.spec\` folder, so files sharing a leaf filename in different \`.spec\` folders stay distinct.
|
|
32
32
|
|
|
33
33
|
**Behavior rules.** Before persisting the plan file, read every behavior rule whose \`.spec/flanders\` scope encloses the plan file you are about to write — the project-root \`.spec\` folder and any other \`.spec\` folder whose scope encloses the \`plans/\` target — and honor all of them. Behavior rules govern how you name and organize the plan file you author; an in-scope behavior rule is binding on that work, not advisory, and applies whether or not the request mentions it. This adds no new task-line link obligation: the plan-file format and its contract and rule links are unchanged.
|
|
34
|
-
3. **Clarification phase.** Ask the user clarifying questions only when the question targets one of three things: an implementation choice that shapes a task's observable outcome and that the request does not specify, or a task-scope ambiguity you cannot reasonably infer from the request or from the canonical contracts and rules, or a load-bearing runtime-behavior premise the plan would otherwise have to assert without backing (see the plan content rules below). A choice that affects only how a task's work is carried out internally, with no effect on any observable outcome its acceptance criteria pin, is not asked about: it is left for the implementer to resolve against the real code. Any other doubt is resolved silently: pick the most reasonable default and proceed, documenting the choice in the relevant task's description when it is plan-local and load-bearing.
|
|
34
|
+
3. **Clarification phase.** Ask the user clarifying questions only when the question targets one of three things: an implementation choice that shapes a task's observable outcome and that the request does not specify, or a task-scope ambiguity you cannot reasonably infer from the request or from the canonical contracts and rules, or a load-bearing runtime-behavior premise the plan would otherwise have to assert without backing (see the plan content rules below). A choice that affects only how a task's work is carried out internally, with no effect on any observable outcome its acceptance criteria pin, is not asked about: it is left for the implementer to resolve against the real code. Any other doubt is resolved silently: pick the most reasonable default and proceed, documenting the choice in the relevant task's description when it is plan-local and load-bearing. Before you start asking, accumulate every permitted question whose content does not depend on another question's answer and ask that whole set together in a single interaction rather than one question at a time; a question is held back only when it genuinely depends on an earlier answer, and is then asked in a later round that again batches the questions that have become independent. When your AI tool provides a facility for asking several questions at once, present the accumulated batch through that facility in a single interaction; when it provides no such facility, ask one question per turn. Multiple-choice questions are preferred when the answer space is bounded.
|
|
35
35
|
|
|
36
36
|
When the doubt is about how the code should be implemented, resolve it through one of two outcomes:
|
|
37
37
|
- **Cross-cutting convention** — the answer would apply to all future code of the same kind in the project and belongs in a \`.spec/rules\` folder. Surface the gap to the user and recommend creating the rule via /flanders-spec before the plan is drafted, instead of silently baking the decision into the plan. The user may explicitly elect to treat the decision as plan-local for this run; in that case it follows the plan-local outcome below.
|
|
@@ -94,8 +94,9 @@ Tasks are written in the order they must be implemented, accounting for dependen
|
|
|
94
94
|
|
|
95
95
|
- The persisted plan is free of placeholders, contradictions with existing contracts or rules, acceptance criteria that leave a leaf task's observable outcome ambiguous, missing acceptance criteria on leaf tasks, and missing contract or rule links on leaf tasks.
|
|
96
96
|
- The persisted plan is internally self-consistent: its narrative — context, rationale, and any explanatory prose — does not contradict the obligations, verification approach, or any other statement made in its task bodies, and no task contradicts that narrative. Where the prose describes how something is tested or built, it matches what the tasks prescribe.
|
|
97
|
-
- Each leaf task's acceptance criteria pin the task's observable outcome — the behavior the result must exhibit through the surface a reader or a test can inspect — so that any two implementations satisfying them are observably equivalent. The plan does not dictate a task's internal mechanism beyond what an observable acceptance criterion or an explicitly required architectural property demands: a choice that affects only how the work is carried out internally, changing no observable outcome the acceptance criteria pin, is left for the implementer to resolve against the real code rather than fixed by the planner. When the planner needs a structural property for an architectural reason — a single source for some logic, the absence of duplication, a module boundary — it states that property as a required outcome the acceptance criteria assert rather than
|
|
97
|
+
- Each leaf task's acceptance criteria pin the task's observable outcome — the behavior the result must exhibit through the surface a reader or a test can inspect — so that any two implementations satisfying them are observably equivalent. The plan does not dictate a task's internal mechanism beyond what an observable acceptance criterion or an explicitly required architectural property demands: a choice that affects only how the work is carried out internally — which mechanism a task uses, and how its code and tests are organized across files and modules — changing no observable outcome the acceptance criteria pin, is left for the implementer to resolve against the real code rather than fixed by the planner. When the planner needs a structural property for an architectural reason — a single source for some logic, the absence of duplication, a module boundary — it states that property as a required outcome the acceptance criteria assert rather than fixing a specific internal mechanism, whether a code element to reuse or leave untouched or the files and modules its code and tests are placed in.
|
|
98
98
|
- Write each paragraph of prose in the plan as a single continuous line, however long; insert a line break only where markdown structure requires it — a blank line between paragraphs, a list item, a heading, a table row, or inside a fenced code block (whose contents you reproduce verbatim). Never break a paragraph across multiple lines to keep it within a maximum column width; the reader's editor wraps long lines for display.
|
|
99
|
+
- Use the fewest words that state each task, obligation, and explanation unambiguously, and write content — a sentence, a section, a cross-reference — only when it carries something not already carried by another task, an earlier sentence, or the reader's ordinary competence. Reach for more words only when fewer would leave an observable outcome ambiguous; task granularity itself follows the granularity rule below.
|
|
99
100
|
- Every task that creates, modifies, or removes code is grounded in the real state of the code it builds on — the current source, plus the changes any earlier task it depends on prescribes. Before writing the task, establish that state: read the current source for code that already exists, and consult the producing earlier task for code an earlier task in the plan creates or changes. Changing what the code does is the task's purpose and is allowed; what a task may not do is misstate the code it builds on — naming structure or behavior that code does not and will not have, or removing or rewriting code on a mistaken account of what it is for.
|
|
100
101
|
- No task asserts, as settled fact, a runtime- or observable-behavior premise that its approach depends on and that cannot be confirmed by reading the source. Such a premise is either backed — by an existing contract or rule, an existing test, or a preceding task in the plan that establishes it executably — or escalated to the user during the clarification phase. A task does not remove, weaken, or replace existing code on the strength of an unbacked, unescalated runtime-behavior premise.
|
|
101
102
|
- The plan only references contracts and rules that exist in the canonical state captured at invocation.
|
|
@@ -162,7 +163,7 @@ Five categories, all mandatory; failure in any one is a FAIL. Each category is a
|
|
|
162
163
|
- Free of placeholders. No \`<TBD>\` or analogous task markers, no template-style blanks, no parenthetical "(to be decided)" deferrals.
|
|
163
164
|
- Free of contradictions with existing contracts or rules. No task pins behavior the canonical listings forbid.
|
|
164
165
|
- Internally self-consistent — no contradiction between the plan's narrative and its tasks. The plan's context, rationale, and explanatory prose do not contradict the obligations, verification approach, or any other statement in its task bodies, and no task contradicts that narrative. Where the prose describes how something is tested or built, it matches what the tasks prescribe.
|
|
165
|
-
- Acceptance criteria pin the observable outcome; the internal mechanism may be left to the implementer. Each leaf task's acceptance criteria pin the task's observable outcome precisely, such that two implementations satisfying them are observably equivalent. Acceptance criteria that leave the observable outcome open — satisfiable by implementations that differ in observable behavior — or hedge wording that defers the outcome are FAIL. This includes, non-exhaustively, hedge phrases such as: \`(or class)\`, \`(or function)\`, \`(or refactor in place if preferred)\`, \`pick the lower-friction option\`, \`pick the X that minimizes Y\`, \`suggested location\`, \`or — alternatively —\`, \`or — equivalently —\`, \`or equivalent\`, \`at the time of implementation\`, \`if the X exists, do Y; otherwise Z\`, \`either A or B — pick one\`, \`A or B (or some hybrid)\`, \`or, more strongly\`, \`or X if Y\`. An outcome-affecting choice that the request did not specify must be either closed to a single observable commitment in the task's acceptance criteria, or escalated by the skill to the user before the plan was drafted. A choice that affects only the task's internal mechanism — which helper to reuse, which internal structure, which implementation approach — and that changes no observable outcome the acceptance criteria pin is NOT a violation when left for the implementer to resolve; conversely, a task that freezes an internal mechanism that no observable acceptance criterion and no explicitly required architectural property needs is FAIL.
|
|
166
|
+
- Acceptance criteria pin the observable outcome; the internal mechanism may be left to the implementer. Each leaf task's acceptance criteria pin the task's observable outcome precisely, such that two implementations satisfying them are observably equivalent. Acceptance criteria that leave the observable outcome open — satisfiable by implementations that differ in observable behavior — or hedge wording that defers the outcome are FAIL. This includes, non-exhaustively, hedge phrases such as: \`(or class)\`, \`(or function)\`, \`(or refactor in place if preferred)\`, \`pick the lower-friction option\`, \`pick the X that minimizes Y\`, \`suggested location\`, \`or — alternatively —\`, \`or — equivalently —\`, \`or equivalent\`, \`at the time of implementation\`, \`if the X exists, do Y; otherwise Z\`, \`either A or B — pick one\`, \`A or B (or some hybrid)\`, \`or, more strongly\`, \`or X if Y\`. An outcome-affecting choice that the request did not specify must be either closed to a single observable commitment in the task's acceptance criteria, or escalated by the skill to the user before the plan was drafted. A choice that affects only the task's internal mechanism — which helper to reuse, which internal structure, how its code and tests are organized across files and modules, which implementation approach — and that changes no observable outcome the acceptance criteria pin is NOT a violation when left for the implementer to resolve; conversely, a task that freezes an internal mechanism that no observable acceptance criterion and no explicitly required architectural property needs is FAIL.
|
|
166
167
|
- Every leaf task carries an explicit acceptance-criteria section.
|
|
167
168
|
- Every leaf task carries the relevant contract link(s) as markdown links in project-root-relative namespace form — the link text is the file's listed namespace with no leading slash, and the target is that same namespace prefixed with a single leading slash.
|
|
168
169
|
- Every leaf task carries the relevant rule link(s) as markdown links in project-root-relative namespace form — the link text is the file's listed namespace with no leading slash, and the target is that same namespace prefixed with a single leading slash. When a rule's enforcement is bound to a specific scope, that scope is referenced alongside the file path.
|
|
@@ -190,7 +191,7 @@ If the validator wants to show its work, it does so in the body of its response
|
|
|
190
191
|
When the validator returns FAIL, enter the triage-then-fix loop:
|
|
191
192
|
|
|
192
193
|
1. Triage each issue. For every issue enumerated in the FAIL report, classify it against the clarification-scope criteria of this skill's clarification phase — the same criteria that govern the initial clarification phase above: an implementation choice that shapes a task's observable outcome and that the request does not specify, a task-scope ambiguity the planner cannot reasonably infer from the request or the canonical contracts and rules, or a load-bearing runtime-behavior premise the plan would otherwise have to assert without backing. A validator FAIL never broadens what the skill may ask the user about; an unbacked runtime-behavior premise the validator flags is escalated to the user, never silently rewritten.
|
|
193
|
-
2. For issues whose fix would commit the skill to an answer that, per the clarification phase, the user is the one who must give and that the user did not give in the initial clarification phase of this invocation: re-enter the clarification phase for that specific ambiguity before any rewrite. Re-entered clarification follows the same
|
|
194
|
+
2. For issues whose fix would commit the skill to an answer that, per the clarification phase, the user is the one who must give and that the user did not give in the initial clarification phase of this invocation: re-enter the clarification phase for that specific ambiguity before any rewrite. Re-entered clarification follows the same cadence — accumulate every question whose content does not depend on another's answer and ask that whole set together in a single interaction, using your AI tool's facility for asking several questions at once when it provides one and otherwise asking one question per turn, multiple-choice preferred when the answer space is bounded. The re-entered phase is scoped to the specific ambiguity at hand and never re-asks decisions the user has already given in this invocation.
|
|
194
195
|
3. For every other issue — placeholders, missing acceptance criteria, missing contract or rule links on a leaf task, hedge phrasing the planner can resolve by picking a concrete value, task ordering, hierarchical numbering, format-shape violations, and any other fix the skill is authorized to resolve on its own — apply in place without asking.
|
|
195
196
|
4. Rewrite the plan file in place, addressing every enumerated issue.
|
|
196
197
|
5. Re-launch the validator (a new subagent in a fresh session when the subagent host is available) over the rewritten file.
|
|
@@ -208,6 +209,10 @@ After the final validator returns PASS, print a summary in chat containing:
|
|
|
208
209
|
- The plan file's total line count.
|
|
209
210
|
- The total number of detected tasks.
|
|
210
211
|
|
|
212
|
+
## After completion: implementing the plan
|
|
213
|
+
|
|
214
|
+
After the final validator returns PASS and you print the summary above, tell the user in chat that the plan you produced is implemented from the command line by running \`flanders implement\` against it. This message is informational and final: you state that implementation path and end the run. You do not ask the user whether to proceed, and you do not offer to launch, nor launch, any AI-tool skill — including /flanders-work — to implement the plan; carrying out a plan is the job of the \`flanders implement\` command, not of an in-session skill.
|
|
215
|
+
|
|
211
216
|
## Output language
|
|
212
217
|
|
|
213
218
|
Write the plan file in the same natural language as the input request, unless the user says otherwise.
|
|
@@ -270,11 +275,12 @@ For every obligation in the request, the skill decides whether it is a contract,
|
|
|
270
275
|
**Behavior rules.** Before persisting any file, read every behavior rule whose \`.spec/flanders\` scope encloses each file you are about to write — the \`.spec\` folder you write the file into and every parent \`.spec\` folder up to the project root — and honor all of them. Behavior rules govern how you name, place, and organize the files you author; an in-scope behavior rule is binding on that work, not advisory, and applies whether or not the request mentions it.
|
|
271
276
|
|
|
272
277
|
**Rename sweep.** When the run renames, relocates, or removes a term that can recur across the corpus beyond the files it is editing — a folder name, a path segment, a flag, an identifier, a fixed string, or a namespace convention — establish the full set of files to touch by searching the whole corpus (every contract and every rule) for the old term and inspecting every occurrence the search returns. The search is exhaustive over the corpus; it is not narrowed to the files you already planned to edit. Triage each occurrence individually into exactly one of two dispositions: an occurrence the rename must update, which the run edits; or an occurrence that is an intentional reference the rename leaves alone (for example a cross-reference to an unrelated file, or a deliberately unchanged example). An occurrence is never left unexamined on the grounds that its file looked irrelevant. Coverage is driven by the token, not by a judgment of which files are relevant: the set of files the run edits is the union of the occurrences the sweep shows must be updated, and a file the sweep surfaces that you had not planned to touch is added to the run.
|
|
273
|
-
4. **Clarification phase.** Whenever the request leaves an obligation ambiguous, leaves a UI or logic decision unspecified, leaves a rule or its scope of enforcement unspecified, or admits multiple valid interpretations, ask the user clarifying questions
|
|
278
|
+
4. **Clarification phase.** Whenever the request leaves an obligation ambiguous, leaves a UI or logic decision unspecified, leaves a rule or its scope of enforcement unspecified, or admits multiple valid interpretations, ask the user clarifying questions in batches: before you start asking, accumulate every question whose content does not depend on another question's answer and ask that whole set together in a single interaction rather than one question at a time, holding a question back only when it genuinely depends on an earlier answer and asking it in a later round that again batches the questions that have become independent. When your AI tool provides a facility for asking several questions at once, present the accumulated batch through that facility in a single interaction; when it provides no such facility, ask one question per turn. Prefer multiple-choice questions when the answer space is bounded. Use open-ended questions only when multiple-choice would force a false dichotomy. When two or three substantially different approaches would all satisfy the request, present those approaches with a short trade-off summary for each and ask the user to pick or redirect, instead of silently choosing one. The clarification phase ends only when you have enough information to draft files that contain no placeholders, no contradictions, and no scope ambiguity.
|
|
274
279
|
5. **Drafting phase.** Before persisting any file:
|
|
275
280
|
- Present the planned file layout — which files will exist, which \`.spec\` folder each falls in, which are contracts and which are rules (the classification and placement made visible), and the key obligations of each file — as a structured summary, and wait for user approval or redirection.
|
|
276
281
|
- Once the layout is approved, persist every resulting file in a single batch without any further per-file or per-section confirmation step.
|
|
277
282
|
- Update related existing files in place when the request affects obligations they already cover, and create new files only for obligations not already covered. Do not duplicate an obligation across files, whether within a folder or across the two folders.
|
|
283
|
+
- Use the fewest files and the fewest words that state each obligation unambiguously: write a file, a section, a sentence, or a cross-reference only when it carries something not already carried elsewhere — by another file, by a sentence already written, or by the reader's ordinary competence — and reach for more files or more words only when fewer would leave an obligation ambiguous or would fuse genuinely separable concerns into one place.
|
|
278
284
|
- Do not write historical, transitional, or migration content into the contracts and rules you produce. A spec file states only the present spec — what the software does now and what the code must do now. Content recording what the spec used to be, what it replaces, what changed in this run, or any transitional framing (for example, "replaces the former X", "previously Y", a changelog of what this run changed) belongs in the commit message or pull-request description, not in a permanent spec file.
|
|
279
285
|
- State each obligation as the behavior the code performs — what the software does and what the code must do. The set of things the code does not do is unbounded, so do not enumerate non-actions: satisfy a request to remove or stop a behavior by describing the resulting positive behavior, letting the removed behavior vanish by omission. Write an explicit prohibition — "does not…", "never…", "must not…" — only when it is load-bearing, namely when BOTH conditions hold: (1) its absence is not already entailed by a positive obligation (a positive obligation stated exclusively, such as "the only X is Y", already excludes every alternative, so a prohibition restating that exclusion adds nothing); and (2) it guards a behavior a competent implementer reading only the positive spec would plausibly introduce — an attractive default they would reach for, or a behavior that falls inside a responsibility the component otherwise has. A prohibition that fails either condition is redundant and is not written.
|
|
280
286
|
- Write every cross-reference to another spec file as a markdown link in project-root-relative namespace form: the link text is the referenced file's namespace — its path relative to the project root, with no leading slash — and the target is that same namespace prefixed with a single leading slash, so the link resolves against the project root from a referencing file at any depth and never as a path computed relative to the referencing file's own location. When the relevant obligation lives in a specific section or line range, name that section or line range in the link text and point the target's fragment at it.
|
|
@@ -360,7 +366,7 @@ If the validator wants to show its work, it does so in the body of its response
|
|
|
360
366
|
When the validator returns FAIL, enter the triage-then-fix loop:
|
|
361
367
|
|
|
362
368
|
1. Triage each issue. For every issue enumerated in the FAIL report, classify it against the clarification-scope criteria of this skill's clarification phase — the same criteria that govern the initial clarification phase above: obligation ambiguous, UI or logic decision unspecified, rule or scope of enforcement unspecified, or multiple valid interpretations.
|
|
363
|
-
2. For issues whose fix would commit the skill to an answer that, per the clarification phase, the user is the one who must give and that the user did not give in the initial clarification phase of this invocation: re-enter the clarification phase for that specific ambiguity before any rewrite. Re-entered clarification follows the same
|
|
369
|
+
2. For issues whose fix would commit the skill to an answer that, per the clarification phase, the user is the one who must give and that the user did not give in the initial clarification phase of this invocation: re-enter the clarification phase for that specific ambiguity before any rewrite. Re-entered clarification follows the same cadence — accumulate every question whose content does not depend on another's answer and ask that whole set together in a single interaction, using your AI tool's facility for asking several questions at once when it provides one and otherwise asking one question per turn, multiple-choice preferred when the answer space is bounded. The re-entered phase is scoped to the specific ambiguity at hand and never re-asks decisions the user has already given in this invocation.
|
|
364
370
|
3. For every other issue — formatting, naming, descriptive-filename violations, placeholders that do not require a user-level decision, and any other fix the skill is authorized to resolve on its own — apply in place without asking.
|
|
365
371
|
4. Rewrite the affected file(s) in place, addressing every enumerated issue.
|
|
366
372
|
5. Re-launch the validator (a new subagent in a fresh session when the subagent host is available) over the rewritten file(s).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const TOOL_NAMES: readonly ["claude", "codex"];
|
package/lib/toolNames.js
ADDED
package/lib/ui/BottomBlock.d.ts
CHANGED
|
@@ -9,20 +9,20 @@ export type BottomBlockIO = {
|
|
|
9
9
|
export type Activity = "implementing" | "reviewing" | "building" | "testing";
|
|
10
10
|
export type HeaderFields = {
|
|
11
11
|
indexLabel?: string | null;
|
|
12
|
+
phaseMessage?: string | null;
|
|
12
13
|
iteration?: number | null;
|
|
13
14
|
activity?: Activity | null;
|
|
14
15
|
taskNumber?: string | null;
|
|
15
16
|
title?: string | null;
|
|
16
17
|
};
|
|
18
|
+
export type MetricsPairFields = {
|
|
19
|
+
tokens: number;
|
|
20
|
+
anchorMs?: number;
|
|
21
|
+
baseSeconds: number;
|
|
22
|
+
};
|
|
17
23
|
export type MetricsFields = {
|
|
18
|
-
task?:
|
|
19
|
-
|
|
20
|
-
seconds: number;
|
|
21
|
-
};
|
|
22
|
-
plan?: {
|
|
23
|
-
tokens: number;
|
|
24
|
-
seconds: number;
|
|
25
|
-
};
|
|
24
|
+
task?: MetricsPairFields;
|
|
25
|
+
plan?: MetricsPairFields;
|
|
26
26
|
};
|
|
27
27
|
export type WaitKind = "rate-limit";
|
|
28
28
|
export type FooterState = {
|
|
@@ -58,6 +58,8 @@ export declare class BottomBlock {
|
|
|
58
58
|
private _countdownTimer;
|
|
59
59
|
private _unsubResize;
|
|
60
60
|
private _prevLineWidths;
|
|
61
|
+
private _metricsPausedAtMs;
|
|
62
|
+
private _metricsPauseAccumMs;
|
|
61
63
|
constructor(_io: BottomBlockIO, _time: TimeContext, _random: RandomContext);
|
|
62
64
|
mount(): void;
|
|
63
65
|
isFinalized(): boolean;
|
|
@@ -76,5 +78,6 @@ export declare class BottomBlock {
|
|
|
76
78
|
private _drawBlock;
|
|
77
79
|
private _renderHeader;
|
|
78
80
|
private _renderMetrics;
|
|
81
|
+
private _resolvePair;
|
|
79
82
|
private _renderFooter;
|
|
80
83
|
}
|
package/lib/ui/BottomBlock.js
CHANGED
|
@@ -31,6 +31,8 @@ class BottomBlock {
|
|
|
31
31
|
this._countdownTimer = null;
|
|
32
32
|
this._unsubResize = null;
|
|
33
33
|
this._prevLineWidths = null;
|
|
34
|
+
this._metricsPausedAtMs = null;
|
|
35
|
+
this._metricsPauseAccumMs = 0;
|
|
34
36
|
}
|
|
35
37
|
mount() {
|
|
36
38
|
if (this._disposed)
|
|
@@ -64,6 +66,7 @@ class BottomBlock {
|
|
|
64
66
|
if (this._disposed || this._finalized)
|
|
65
67
|
return;
|
|
66
68
|
this._metrics = fields;
|
|
69
|
+
this._metricsPauseAccumMs = 0;
|
|
67
70
|
if (this._mounted) {
|
|
68
71
|
this._clearBlock();
|
|
69
72
|
this._drawBlock();
|
|
@@ -73,6 +76,13 @@ class BottomBlock {
|
|
|
73
76
|
if (this._disposed || this._finalized)
|
|
74
77
|
return;
|
|
75
78
|
this._cancelTimers();
|
|
79
|
+
if (state.kind === "waiting") {
|
|
80
|
+
this._metricsPausedAtMs = this._time.now();
|
|
81
|
+
}
|
|
82
|
+
else if (this._metricsPausedAtMs !== null) {
|
|
83
|
+
this._metricsPauseAccumMs += this._time.now() - this._metricsPausedAtMs;
|
|
84
|
+
this._metricsPausedAtMs = null;
|
|
85
|
+
}
|
|
76
86
|
this._footer = state;
|
|
77
87
|
if (state.kind === "working") {
|
|
78
88
|
this._animFrame = 0;
|
|
@@ -213,11 +223,22 @@ class BottomBlock {
|
|
|
213
223
|
this._prevLineWidths = [cols, (0, formatters_1.stripAnsi)(header).length, (0, formatters_1.stripAnsi)(metrics).length, (0, formatters_1.stripAnsi)(footer).length];
|
|
214
224
|
}
|
|
215
225
|
_renderHeader(cols) {
|
|
216
|
-
var _a, _b, _c, _d, _e;
|
|
217
|
-
return (0, formatters_1.formatHeaderLine)((_a = this._header.indexLabel) !== null && _a !== void 0 ? _a : null, (_b = this._header.
|
|
226
|
+
var _a, _b, _c, _d, _e, _f;
|
|
227
|
+
return (0, formatters_1.formatHeaderLine)((_a = this._header.indexLabel) !== null && _a !== void 0 ? _a : null, (_b = this._header.phaseMessage) !== null && _b !== void 0 ? _b : null, (_c = this._header.iteration) !== null && _c !== void 0 ? _c : null, (_d = this._header.activity) !== null && _d !== void 0 ? _d : null, (_e = this._header.taskNumber) !== null && _e !== void 0 ? _e : null, (_f = this._header.title) !== null && _f !== void 0 ? _f : null, cols);
|
|
218
228
|
}
|
|
219
229
|
_renderMetrics(cols) {
|
|
220
|
-
return (0, formatters_1.formatMetricsLine)(this._metrics.task, this._metrics.plan, cols);
|
|
230
|
+
return (0, formatters_1.formatMetricsLine)(this._resolvePair(this._metrics.task), this._resolvePair(this._metrics.plan), cols);
|
|
231
|
+
}
|
|
232
|
+
_resolvePair(pair) {
|
|
233
|
+
var _a;
|
|
234
|
+
if (!pair)
|
|
235
|
+
return undefined;
|
|
236
|
+
if (pair.anchorMs === undefined) {
|
|
237
|
+
return { tokens: pair.tokens, seconds: pair.baseSeconds };
|
|
238
|
+
}
|
|
239
|
+
const evalNow = (_a = this._metricsPausedAtMs) !== null && _a !== void 0 ? _a : this._time.now();
|
|
240
|
+
const activeMs = Math.max(0, evalNow - pair.anchorMs - this._metricsPauseAccumMs);
|
|
241
|
+
return { tokens: pair.tokens, seconds: Math.floor(activeMs / 1000) + pair.baseSeconds };
|
|
221
242
|
}
|
|
222
243
|
_renderFooter(cols) {
|
|
223
244
|
switch (this._footer.kind) {
|
package/lib/ui/PromptHelper.d.ts
CHANGED
|
@@ -10,5 +10,12 @@ export type AskTextArgs = Readonly<{
|
|
|
10
10
|
placeholder?: string;
|
|
11
11
|
default?: string;
|
|
12
12
|
}>;
|
|
13
|
+
export type AskMultiChoiceArgs = Readonly<{
|
|
14
|
+
header: string;
|
|
15
|
+
question: string;
|
|
16
|
+
options: readonly ChoiceOption[];
|
|
17
|
+
selected?: readonly ChoiceOption[];
|
|
18
|
+
}>;
|
|
13
19
|
export declare function askChoice(ask: AskContext, args: AskChoiceArgs, output?: OutputContext): Promise<ChoiceOption>;
|
|
20
|
+
export declare function askMultiChoice(ask: AskContext, args: AskMultiChoiceArgs, output?: OutputContext): Promise<readonly ChoiceOption[]>;
|
|
14
21
|
export declare function askText(ask: AskContext, args: AskTextArgs): Promise<string>;
|
package/lib/ui/PromptHelper.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.askChoice = askChoice;
|
|
4
|
+
exports.askMultiChoice = askMultiChoice;
|
|
4
5
|
exports.askText = askText;
|
|
5
6
|
function abortError() {
|
|
6
7
|
const e = new Error("Aborted");
|
|
@@ -23,6 +24,27 @@ async function askChoice(ask, args, output) {
|
|
|
23
24
|
}
|
|
24
25
|
return answer.picked[0];
|
|
25
26
|
}
|
|
27
|
+
async function askMultiChoice(ask, args, output) {
|
|
28
|
+
var _a;
|
|
29
|
+
const selectedLabels = new Set(((_a = args.selected) !== null && _a !== void 0 ? _a : []).map(o => o.label));
|
|
30
|
+
const defaultIndexes = [];
|
|
31
|
+
for (let i = 0; i < args.options.length; i++) {
|
|
32
|
+
if (selectedLabels.has(args.options[i].label)) {
|
|
33
|
+
defaultIndexes.push(i);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const [answer] = await ask.askChoices([{
|
|
37
|
+
header: args.header,
|
|
38
|
+
question: args.question,
|
|
39
|
+
options: args.options,
|
|
40
|
+
multiSelect: true,
|
|
41
|
+
defaultIndexes: defaultIndexes.length > 0 ? defaultIndexes : undefined
|
|
42
|
+
}], output);
|
|
43
|
+
if (!answer || answer.picked.length === 0) {
|
|
44
|
+
throw abortError();
|
|
45
|
+
}
|
|
46
|
+
return answer.picked;
|
|
47
|
+
}
|
|
26
48
|
async function askText(ask, args) {
|
|
27
49
|
const prompt = args.placeholder
|
|
28
50
|
? `${args.question} (${args.placeholder}): `
|
package/lib/ui/formatters.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ToolName } from "../ai/ToolAdapter";
|
|
1
2
|
export declare const CYAN = "\u001B[36m";
|
|
2
3
|
export declare const YELLOW = "\u001B[33m";
|
|
3
4
|
export declare const MAGENTA = "\u001B[35m";
|
|
@@ -23,7 +24,7 @@ export declare function formatDateTime(date: Date): string;
|
|
|
23
24
|
export declare function truncateToWidth(text: string, cols: number): string;
|
|
24
25
|
export declare function formatTokens(n: number): string;
|
|
25
26
|
export declare function formatActiveTime(seconds: number): string;
|
|
26
|
-
export declare function formatHeaderLine(indexLabel: string | null, iteration: number | null, activity: string | null, taskNumber: string | null | undefined, title: string | null, cols: number): string;
|
|
27
|
+
export declare function formatHeaderLine(indexLabel: string | null, phaseMessage: string | null, iteration: number | null, activity: string | null, taskNumber: string | null | undefined, title: string | null, cols: number): string;
|
|
27
28
|
export type MetricsPair = {
|
|
28
29
|
tokens: number;
|
|
29
30
|
seconds: number;
|
|
@@ -32,7 +33,7 @@ export declare function formatMetricsLine(task: MetricsPair | undefined, plan: M
|
|
|
32
33
|
export declare function formatSnapshotHeader(indexLabel: string, iteration: number, taskNumber: string | undefined, title: string): string;
|
|
33
34
|
export declare function formatSnapshotMetrics(taskTokens: number, taskSeconds: number, planTokens: number, planSeconds: number): string;
|
|
34
35
|
export declare function formatSnapshotBlock(indexLabel: string, iteration: number, taskNumber: string | undefined, title: string, taskTokens: number, taskSeconds: number, planTokens: number, planSeconds: number, cols: number): string;
|
|
35
|
-
export type ReviewerTool =
|
|
36
|
+
export type ReviewerTool = ToolName;
|
|
36
37
|
export type ReviewerState = "running" | "waiting" | "pass" | "fail";
|
|
37
38
|
export type ReviewerEntry = {
|
|
38
39
|
tool: ReviewerTool;
|
package/lib/ui/formatters.js
CHANGED
|
@@ -141,11 +141,16 @@ function formatActiveTime(seconds) {
|
|
|
141
141
|
return `${h}h${pad2(m)}m${pad2(s % 60)}s`;
|
|
142
142
|
}
|
|
143
143
|
const LIVE_ACTIVITIES = new Set(["implementing", "reviewing", "building", "testing"]);
|
|
144
|
-
function formatHeaderLine(indexLabel, iteration, activity, taskNumber, title, cols) {
|
|
144
|
+
function formatHeaderLine(indexLabel, phaseMessage, iteration, activity, taskNumber, title, cols) {
|
|
145
145
|
const segments = [];
|
|
146
146
|
if (indexLabel != null) {
|
|
147
147
|
segments.push({ text: indexLabel, color: exports.CYAN });
|
|
148
148
|
}
|
|
149
|
+
if (phaseMessage != null) {
|
|
150
|
+
if (segments.length > 0)
|
|
151
|
+
segments.push({ text: " " });
|
|
152
|
+
segments.push({ text: phaseMessage, color: exports.MAGENTA });
|
|
153
|
+
}
|
|
149
154
|
if (iteration != null) {
|
|
150
155
|
if (segments.length > 0)
|
|
151
156
|
segments.push({ text: " " });
|
|
@@ -233,7 +238,7 @@ function formatMetricsLine(task, plan, cols) {
|
|
|
233
238
|
return renderSegmentsToWidth(compactSegments, cols);
|
|
234
239
|
}
|
|
235
240
|
function formatSnapshotHeader(indexLabel, iteration, taskNumber, title) {
|
|
236
|
-
return formatHeaderLine(indexLabel, iteration, "done", taskNumber, title, Number.MAX_SAFE_INTEGER);
|
|
241
|
+
return formatHeaderLine(indexLabel, null, iteration, "done", taskNumber, title, Number.MAX_SAFE_INTEGER);
|
|
237
242
|
}
|
|
238
243
|
function formatSnapshotMetrics(taskTokens, taskSeconds, planTokens, planSeconds) {
|
|
239
244
|
const tTok = formatTokens(taskTokens);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { FsContext } from "../contexts";
|
|
2
|
+
import type { ToolName } from "../ai/ToolAdapter";
|
|
2
3
|
export type FlandersRole = Readonly<{
|
|
3
|
-
tool:
|
|
4
|
+
tool: ToolName;
|
|
4
5
|
model: string;
|
|
5
6
|
effort: string;
|
|
7
|
+
fast: boolean;
|
|
6
8
|
}>;
|
|
7
9
|
export type FlandersReviewer = FlandersRole & Readonly<{
|
|
8
10
|
optional: boolean;
|
|
@@ -4,6 +4,7 @@ exports.read = read;
|
|
|
4
4
|
exports.readScope = readScope;
|
|
5
5
|
exports.write = write;
|
|
6
6
|
const fsUtils_1 = require("../system/fsUtils");
|
|
7
|
+
const toolNames_1 = require("../toolNames");
|
|
7
8
|
const CONFIG_PATH = ".flanders/config.json";
|
|
8
9
|
const CONFIG_DIR = ".flanders";
|
|
9
10
|
const ALLOWED_TOP_LEVEL_KEYS = ["worker", "reviewers", "minimumReviews"];
|
|
@@ -55,7 +56,7 @@ function validateRole(role, name, filePath) {
|
|
|
55
56
|
if (!("tool" in role) || typeof role["tool"] !== "string") {
|
|
56
57
|
throw new Error(`Malformed config at ${filePath}: missing or invalid field "${name}.tool"`);
|
|
57
58
|
}
|
|
58
|
-
if (role["tool"]
|
|
59
|
+
if (!toolNames_1.TOOL_NAMES.includes(role["tool"])) {
|
|
59
60
|
throw new Error(`Malformed config at ${filePath}: invalid value for "${name}.tool": "${role["tool"]}"`);
|
|
60
61
|
}
|
|
61
62
|
if (!("model" in role) || typeof role["model"] !== "string") {
|
|
@@ -64,6 +65,9 @@ function validateRole(role, name, filePath) {
|
|
|
64
65
|
if (!("effort" in role) || typeof role["effort"] !== "string") {
|
|
65
66
|
throw new Error(`Malformed config at ${filePath}: missing or invalid field "${name}.effort"`);
|
|
66
67
|
}
|
|
68
|
+
if (!("fast" in role) || typeof role["fast"] !== "boolean") {
|
|
69
|
+
throw new Error(`Malformed config at ${filePath}: missing or invalid field "${name}.fast"`);
|
|
70
|
+
}
|
|
67
71
|
}
|
|
68
72
|
async function read(fs, args) {
|
|
69
73
|
const projectPath = configPath(args.projectRoot);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flanders",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Flanders never breaks a rule",
|
|
5
5
|
"main": "lib/",
|
|
6
6
|
"types": "lib/types",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"watch": "npm run clean && npx tsc -p tsconfig.json -watch",
|
|
23
23
|
"build": "npm run clean && npx tsc -p tsconfig.build.json",
|
|
24
24
|
"build-debug": "npm run clean && npx tsc -p tsconfig.json",
|
|
25
|
-
"test": "npm run build-debug && npx aaa --folder lib --include-files \"\\.test\\.js$\" --exclude-files node_modules --coverage-exclude node_modules"
|
|
25
|
+
"test": "npm run build-debug && npx aaa --folder lib --include-files \"\\.test\\.js$\" --exclude-files node_modules --coverage-exclude node_modules",
|
|
26
|
+
"test-with-coverage": "npm test -- --coverage-target 100 --summary"
|
|
26
27
|
},
|
|
27
28
|
"author": "Llorx",
|
|
28
29
|
"license": "MIT",
|