@turri-dev/cli 0.1.25-next.d0c7575 → 0.1.25-next.fb3566e
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 +5 -5
- package/dist/extension.d.ts +0 -4
- package/dist/extension.js +13 -55
- package/dist/genome.d.ts +0 -3
- package/dist/genome.js +4 -7
- package/dist/graph.js +5 -2
- package/dist/new-project.d.ts +1 -3
- package/dist/new-project.js +3 -13
- package/dist/plain/parser.d.ts +1 -0
- package/dist/plain/parser.js +1 -1
- package/dist/plain-files.d.ts +21 -0
- package/dist/plain-files.js +156 -0
- package/dist/regenerate/diff-view.d.ts +4 -1
- package/dist/regenerate/diff-view.js +6 -3
- package/dist/regenerate/stop-report.js +1 -1
- package/dist/regenerate/test-scripts.d.ts +2 -0
- package/dist/regenerate/test-scripts.js +1 -1
- package/dist/skill-progress.d.ts +4 -4
- package/dist/skill.d.ts +1 -1
- package/dist/skills.d.ts +4 -4
- package/dist/skills.js +5 -5
- package/docs/authoring-plain.md +7 -23
- package/docs/chats.md +1 -1
- package/docs/config.md +3 -3
- package/docs/development.md +0 -1
- package/docs/headless.md +2 -2
- package/docs/hosting.md +1 -1
- package/docs/regenerating.md +3 -3
- package/docs/skills-and-gates.md +17 -15
- package/docs/using.md +5 -13
- package/genome/rules/concise-specs-examples.md +1 -1
- package/genome/rules/concise-specs-integration-examples.md +1 -1
- package/genome/rules/func-specs.md +10 -2
- package/genome/rules/integration-embedded.md +2 -2
- package/genome/rules/integration-standalone.md +2 -2
- package/genome/skills/break-down-func-spec/SKILL.md +8 -8
- package/genome/skills/consolidate-concepts/SKILL.md +1 -1
- package/genome/skills/resolve-section-ownership/SKILL.md +2 -2
- package/genome/skills/resolve-spec-conflict/SKILL.md +1 -1
- package/package.json +1 -1
- package/dist/mature-tools.d.ts +0 -53
- package/dist/mature-tools.js +0 -193
- package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +0 -206
- package/genome/skills/brood/SKILL.md +0 -129
- package/genome/skills/brood/references/intent.md +0 -27
- package/genome/skills/brood/references/product.md +0 -49
- package/genome/skills/brood/references/stack.md +0 -39
- package/genome/skills/brood/references/testing.md +0 -25
- package/genome/skills/bud/SKILL.md +0 -104
- package/genome/skills/bud/references/intent.md +0 -27
- package/genome/skills/mature/SKILL.md +0 -78
- package/genome/skills/spawn/SKILL.md +0 -85
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
name: break-down-func-spec
|
|
3
3
|
description: >-
|
|
4
4
|
Break down a functional spec that is too complex into smaller specs that
|
|
5
|
-
each imply ≤ 200 lines of code. Use when
|
|
6
|
-
|
|
7
|
-
allowed-tools: [read, write, edit,
|
|
5
|
+
each imply ≤ 200 lines of code. Use when a spec is judged too large against
|
|
6
|
+
the complexity limit (`func-specs.md`), before or after it is written in.
|
|
7
|
+
allowed-tools: [read, write, edit, ls, grep, find]
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Break Down Functional Spec
|
|
11
11
|
|
|
12
12
|
## When to Use
|
|
13
13
|
|
|
14
|
-
- `
|
|
15
|
-
- A spec
|
|
14
|
+
- A spec is judged to exceed the 200 LOC limit (`func-specs.md`, *Complexity limit*) and needs to be split before it is written in.
|
|
15
|
+
- A spec already written in turns out too large - its pulse could not get its unit tests to pass.
|
|
16
16
|
- A spec bundles multiple behaviors, constructs, or concerns that should be separated.
|
|
17
17
|
|
|
18
18
|
## Input
|
|
@@ -23,7 +23,7 @@ The functional spec to break down, plus the full `.plain` file it belongs to.
|
|
|
23
23
|
|
|
24
24
|
1. **Read the full `.plain` file** — definitions, implementation reqs, and all functional specs (including `requires` modules) to understand context.
|
|
25
25
|
2. **Identify the spec to break down** — the one flagged as too complex or pointed to by the user.
|
|
26
|
-
3. **Analyze why it is too complex** — use the
|
|
26
|
+
3. **Analyze why it is too complex** — use the signs in `func-specs.md` (*Complexity limit*):
|
|
27
27
|
- Multiple distinct behaviors bundled together?
|
|
28
28
|
- Too many new constructs introduced at once?
|
|
29
29
|
- Complex branching logic or conditional paths?
|
|
@@ -37,7 +37,7 @@ The functional spec to break down, plus the full `.plain` file it belongs to.
|
|
|
37
37
|
- Within the 200 LOC limit
|
|
38
38
|
5. **Draft the replacement specs** — write the smaller specs in chronological order. The first replacement spec typically sets up the foundation, and subsequent ones layer behavior on top.
|
|
39
39
|
6. **Verify functional completeness** — this is critical. The replacement specs taken together must cover **100% of the functionality** described in the original spec. Walk through every behavior, condition, and detail in the original and confirm it appears in exactly one of the replacement specs. Nothing may be lost, weakened, or left implicit. If any functionality from the original is missing, add it to the appropriate replacement spec or create an additional one.
|
|
40
|
-
7. **Verify each replacement spec** —
|
|
40
|
+
7. **Verify each replacement spec** — judge each against the complexity limit, and split again any that is still too large.
|
|
41
41
|
8. **Check for conflicts** — run `analyze-func-specs` **once** with the full set of replacement specs plus every existing spec in the file and its `requires` chain. The batched analyzer reports every conflicting pair (replacement × existing and replacement × replacement) in one call — do not loop over pairs. Resolve each reported pair with `resolve-spec-conflict`, then re-run `analyze-func-specs` over the touched set until the verdict is `COMPATIBLE`.
|
|
42
42
|
9. **Replace in the file** — remove the original spec and insert the replacement specs in its position, preserving chronological order.
|
|
43
43
|
10. **Read the file again** to confirm correct placement and syntax.
|
|
@@ -192,7 +192,7 @@ If the original spec had acceptance tests, redistribute them to the most appropr
|
|
|
192
192
|
|
|
193
193
|
- [ ] Original spec has been removed from the file
|
|
194
194
|
- [ ] Replacement specs together cover 100% of the original spec's functionality — nothing lost
|
|
195
|
-
- [ ] Each replacement spec implies ≤ 200 LOC (
|
|
195
|
+
- [ ] Each replacement spec implies ≤ 200 LOC (judged against the complexity limit)
|
|
196
196
|
- [ ] No conflicts between replacement specs and existing specs (verified via one batched `analyze-func-specs` call)
|
|
197
197
|
- [ ] No conflicts between the replacement specs themselves (covered by the same batched call)
|
|
198
198
|
- [ ] Replacement specs are in correct chronological order
|
|
@@ -5,7 +5,7 @@ description: >-
|
|
|
5
5
|
shared import module in template/. Removes the moved definitions from the
|
|
6
6
|
original modules and adds the new import to their frontmatter. Use when
|
|
7
7
|
concepts are duplicated or spread across modules and should be centralized.
|
|
8
|
-
allowed-tools: [read, write, edit,
|
|
8
|
+
allowed-tools: [read, write, edit, ls, grep, find]
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Consolidate Concepts
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: resolve-section-ownership
|
|
3
3
|
description: >-
|
|
4
4
|
Route every fact in a .plain bullet, section, or file to the section that owns it — splitting a bullet that carries several kinds of fact, and moving a misplaced one to where the renderer will read it. Use when a bullet answers more than one of what it is / what it does / how it is built / how it is tested, when a requirement looks like it is in the wrong section, or when auditing a file for section ownership. Not for a spec that is merely too large (use break-down-func-spec) or for two specs that contradict each other (use resolve-spec-conflict).
|
|
5
|
-
allowed-tools: [read, write, edit,
|
|
5
|
+
allowed-tools: [read, write, edit, ls, grep, find]
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Resolve Section Ownership
|
|
@@ -19,7 +19,7 @@ This is the ownership counterpart to `break-down-func-spec`. That skill splits o
|
|
|
19
19
|
|
|
20
20
|
## When Not to Use
|
|
21
21
|
|
|
22
|
-
- The spec is correctly placed but too large → `
|
|
22
|
+
- The spec is correctly placed but too large → `break-down-func-spec`.
|
|
23
23
|
- Two specs contradict each other → `analyze-func-specs`, then `resolve-spec-conflict`.
|
|
24
24
|
- The wording is merely verbose → trim per `concise-specs.md`; no fact changes section.
|
|
25
25
|
|
|
@@ -5,7 +5,7 @@ description: >-
|
|
|
5
5
|
when conformance tests for a previously passing spec start failing after a new
|
|
6
6
|
spec is rendered, or when a potential conflict is detected while adding a new
|
|
7
7
|
functional spec (the conflict check of `func-specs.md`).
|
|
8
|
-
allowed-tools: [read, write, edit,
|
|
8
|
+
allowed-tools: [read, write, edit, ls, grep, find]
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Resolve Spec Conflict
|
package/package.json
CHANGED
package/dist/mature-tools.d.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { type Job, type JobRegistry } from "./regenerate/jobs.ts";
|
|
3
|
-
/**
|
|
4
|
-
* The tools a skill that matures a spec works with, besides the conversation's regenerate, drift and halt: wait for
|
|
5
|
-
* the builds, and read back where the spec left the agents to decide.
|
|
6
|
-
*
|
|
7
|
-
* A render that passes can still have been built on a guess: where the spec is silent, the agent
|
|
8
|
-
* decides, and says so in the working notes' assumptions. Those, the pitfalls, a failed job's
|
|
9
|
-
* diagnosis and a spec that contradicts itself are what the spec should have said - and a skill
|
|
10
|
-
* reading them can write it in, so the next build does not have to guess.
|
|
11
|
-
*/
|
|
12
|
-
type AnyTool = ToolDefinition<any, any, any>;
|
|
13
|
-
/** What a maturing skill found: where it came from, and what it says. */
|
|
14
|
-
export interface SpecFinding {
|
|
15
|
-
id: string;
|
|
16
|
-
kind: "assumption" | "pitfall" | "diagnosis" | "conflict";
|
|
17
|
-
/** The module whose codebase it came from, or the job it came from. */
|
|
18
|
-
where: string;
|
|
19
|
-
text: string;
|
|
20
|
-
}
|
|
21
|
-
/** A time as the notes stamp their lines: "2026-09-28 17:40". */
|
|
22
|
-
export declare function noteStamp(date: Date): string;
|
|
23
|
-
/**
|
|
24
|
-
* What the agents and the harness recorded since `since`: the assumptions and pitfalls in every
|
|
25
|
-
* codebase's working notes, the diagnosis of every job that failed for good, and every spec that
|
|
26
|
-
* contradicts itself.
|
|
27
|
-
*/
|
|
28
|
-
export declare function collectFindings(options: {
|
|
29
|
-
project: string;
|
|
30
|
-
jobs: Job[];
|
|
31
|
-
since: Date;
|
|
32
|
-
}): Omit<SpecFinding, "id">[];
|
|
33
|
-
/** Why a build is struggling: a key for what kind of struggle it is - handed out once each - and what it says. */
|
|
34
|
-
export interface Struggle {
|
|
35
|
-
key: string;
|
|
36
|
-
why: string;
|
|
37
|
-
}
|
|
38
|
-
/** How long a build may run before it counts as struggling on its length alone. */
|
|
39
|
-
export declare const LONG_BUILD_MS: number;
|
|
40
|
-
/**
|
|
41
|
-
* Whether a running build is struggling, and how: sent back four times or more in one loop, five
|
|
42
|
-
* setbacks or more, a spec conflict raised, a failure being run again, or running far longer than a
|
|
43
|
-
* build should. Each kind comes once per build - a round count again only every four rounds more.
|
|
44
|
-
*/
|
|
45
|
-
export declare function struggling(job: Job, now?: number): Struggle | undefined;
|
|
46
|
-
export declare function createMatureTools(options: {
|
|
47
|
-
jobs: JobRegistry;
|
|
48
|
-
cwd: string;
|
|
49
|
-
startedAt?: Date;
|
|
50
|
-
}): AnyTool[];
|
|
51
|
-
/** The names of the maturing tools, as a skill's allowed-tools names them: its own - it regenerates, halts and reads drift with the conversation's (loop-tools.ts). */
|
|
52
|
-
export declare const MATURE_TOOLS: string[];
|
|
53
|
-
export {};
|
package/dist/mature-tools.js
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { Type } from "typebox";
|
|
5
|
-
import { formatDuration } from "./regenerate/jobs.js";
|
|
6
|
-
import { readNotes } from "./regenerate/notes.js";
|
|
7
|
-
import { modulesRoot, projectDirOf } from "./regenerate/paths.js";
|
|
8
|
-
/** A time as the notes stamp their lines: "2026-09-28 17:40". */
|
|
9
|
-
export function noteStamp(date) {
|
|
10
|
-
const two = (n) => String(n).padStart(2, "0");
|
|
11
|
-
return `${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())} ${two(date.getHours())}:${two(date.getMinutes())}`;
|
|
12
|
-
}
|
|
13
|
-
/** Every module codebase of a project that has working notes. */
|
|
14
|
-
function codebasesOf(project) {
|
|
15
|
-
const root = modulesRoot(project);
|
|
16
|
-
if (!existsSync(root))
|
|
17
|
-
return [];
|
|
18
|
-
return readdirSync(root, { withFileTypes: true })
|
|
19
|
-
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
20
|
-
.map((entry) => ({ module: entry.name, buildDir: join(root, entry.name, "code") }))
|
|
21
|
-
.filter((codebase) => existsSync(codebase.buildDir));
|
|
22
|
-
}
|
|
23
|
-
/** A notes line's stamp, and what it says: "[2026-09-28 17:40 · app 3] text". */
|
|
24
|
-
function stamped(line) {
|
|
25
|
-
const match = /^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}) · ([^\]]*)\]\s*(.*)$/.exec(line);
|
|
26
|
-
return match === null ? undefined : { stamp: match[1], source: match[2], text: match[3] };
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* What the agents and the harness recorded since `since`: the assumptions and pitfalls in every
|
|
30
|
-
* codebase's working notes, the diagnosis of every job that failed for good, and every spec that
|
|
31
|
-
* contradicts itself.
|
|
32
|
-
*/
|
|
33
|
-
export function collectFindings(options) {
|
|
34
|
-
const since = noteStamp(options.since);
|
|
35
|
-
const found = [];
|
|
36
|
-
for (const { module, buildDir } of codebasesOf(options.project)) {
|
|
37
|
-
// The code's notes, and its conformance tests' beside them.
|
|
38
|
-
for (const notes of [readNotes(buildDir), readNotes(join(dirname(buildDir), "tests"))]) {
|
|
39
|
-
for (const [section, kind] of [["assumptions", "assumption"], ["pitfalls", "pitfall"]]) {
|
|
40
|
-
for (const line of notes[section]) {
|
|
41
|
-
const item = stamped(line);
|
|
42
|
-
if (item === undefined || item.stamp < since)
|
|
43
|
-
continue;
|
|
44
|
-
found.push({ kind, where: `${module} (${item.source})`, text: item.text });
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
for (const job of options.jobs) {
|
|
50
|
-
if (job.queuedAt < options.since.getTime())
|
|
51
|
-
continue;
|
|
52
|
-
if (job.specConflict !== undefined) {
|
|
53
|
-
const specs = job.specConflict.specs.map((spec) => `${spec.module} ${spec.frid}: ${spec.text.trim()}`).join("\n");
|
|
54
|
-
found.push({ kind: "conflict", where: `job ${job.id} (${job.summary})`, text: `${job.specConflict.reasons.at(-1) ?? ""}\n${specs}`.trim() });
|
|
55
|
-
}
|
|
56
|
-
if (job.diagnosis !== undefined)
|
|
57
|
-
found.push({ kind: "diagnosis", where: `job ${job.id} (${job.summary})`, text: job.diagnosis.trim() });
|
|
58
|
-
}
|
|
59
|
-
return found;
|
|
60
|
-
}
|
|
61
|
-
/** How a job stands, in a line: what it is, how it ended, and the first line of what it said. */
|
|
62
|
-
function jobLine(job) {
|
|
63
|
-
const said = (job.status === "succeeded" ? job.report : job.error ?? job.report)?.trim().split("\n")[0] ?? "";
|
|
64
|
-
const flags = [job.diagnosis !== undefined ? "diagnosed" : "", job.specConflict !== undefined ? "spec conflict" : ""].filter(Boolean).join(", ");
|
|
65
|
-
return `- job ${job.id} · ${job.summary} · ${job.status}${job.status === "running" || job.status === "queued" ? "" : ` after ${formatDuration(job)}`}${flags ? ` · ${flags}` : ""}${said ? ` - ${said.slice(0, 200)}` : ""}`;
|
|
66
|
-
}
|
|
67
|
-
/** Setbacks a build reports as it goes: its stages that say a run failed, a review sent tests back, a judge found something wrong. */
|
|
68
|
-
const SETBACK = /not passing|NOT passing|the review found|is wrong|found the|failed|a spec conflict|contradicts|stuck|cannot|still leave|trying it once more/;
|
|
69
|
-
/** How long a build may run before it counts as struggling on its length alone. */
|
|
70
|
-
export const LONG_BUILD_MS = 30 * 60_000;
|
|
71
|
-
/**
|
|
72
|
-
* Whether a running build is struggling, and how: sent back four times or more in one loop, five
|
|
73
|
-
* setbacks or more, a spec conflict raised, a failure being run again, or running far longer than a
|
|
74
|
-
* build should. Each kind comes once per build - a round count again only every four rounds more.
|
|
75
|
-
*/
|
|
76
|
-
export function struggling(job, now = Date.now()) {
|
|
77
|
-
if (job.status !== "running")
|
|
78
|
-
return undefined;
|
|
79
|
-
const stages = job.transcript.filter((entry) => entry.kind === "stage").map((entry) => entry.text);
|
|
80
|
-
if (stages.some((stage) => /a spec conflict|contradicts itself/.test(stage)))
|
|
81
|
-
return { key: "conflict", why: "a spec conflict was raised: two functional specs that may not both hold" };
|
|
82
|
-
if (stages.some((stage) => /trying it once more/.test(stage)))
|
|
83
|
-
return { key: "retry", why: "it failed once and is being run again" };
|
|
84
|
-
const round = /round (\d+) of (\d+)/.exec(job.stage ?? "");
|
|
85
|
-
if (round !== null && Number(round[1]) >= 4) {
|
|
86
|
-
const bucket = Math.floor((Number(round[1]) - 4) / 4);
|
|
87
|
-
return { key: `rounds-${bucket}`, why: `sent back ${round[1]} times of at most ${round[2]}: ${job.stage}` };
|
|
88
|
-
}
|
|
89
|
-
const setbacks = stages.filter((stage) => SETBACK.test(stage));
|
|
90
|
-
if (setbacks.length >= 5)
|
|
91
|
-
return { key: `setbacks-${Math.floor(setbacks.length / 5)}`, why: `${setbacks.length} setbacks so far - the last: ${setbacks.at(-1)}` };
|
|
92
|
-
if (now - job.startedAt > LONG_BUILD_MS)
|
|
93
|
-
return { key: `long-${Math.floor((now - job.startedAt) / LONG_BUILD_MS)}`, why: `running for ${formatDuration(job)}` };
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
/** A struggling build, for the skill to judge: why, its last stages, and the end of what it printed. */
|
|
97
|
-
function describeStruggle(job, struggle) {
|
|
98
|
-
const stages = job.transcript.filter((entry) => entry.kind === "stage").map((entry) => entry.text).slice(-12);
|
|
99
|
-
return [
|
|
100
|
-
`job ${job.id} · ${job.summary} · ${struggle.why}`,
|
|
101
|
-
job.diff !== undefined ? `<its change>\n${job.diff.trim().slice(0, 3000)}\n</its change>` : "",
|
|
102
|
-
`<its last stages>\n${stages.map((stage) => `- ${stage}`).join("\n")}\n</its last stages>`,
|
|
103
|
-
job.tail.trim() !== "" ? `<the end of its output>\n${job.tail.trim().slice(-2500)}\n</the end of its output>` : "",
|
|
104
|
-
]
|
|
105
|
-
.filter((part) => part !== "")
|
|
106
|
-
.join("\n");
|
|
107
|
-
}
|
|
108
|
-
export function createMatureTools(options) {
|
|
109
|
-
const startedAt = options.startedAt ?? new Date();
|
|
110
|
-
const mine = () => options.jobs.list().filter((job) => job.queuedAt >= startedAt.getTime());
|
|
111
|
-
/** Findings already handed out, by what they say: each is reported once. */
|
|
112
|
-
const reported = new Set();
|
|
113
|
-
let counter = 0;
|
|
114
|
-
/** The struggles already handed out, by job and what they were - each reported once, again only when it gets longer. */
|
|
115
|
-
const flagged = new Set();
|
|
116
|
-
const watchJobs = defineTool({
|
|
117
|
-
name: "watch_jobs",
|
|
118
|
-
label: "Watch the builds",
|
|
119
|
-
description: "Watch the builds queued since this run started, and come back as soon as one is struggling - sent back again and " +
|
|
120
|
-
"again, setback after setback, a spec conflict raised, a failure being run again, running far too long - or when " +
|
|
121
|
-
"every one has finished, or when `minutes` have passed. For a struggling build it gives what it has been through: its " +
|
|
122
|
-
"last stages and the end of its output. Says how every build stands.",
|
|
123
|
-
promptSnippet: "watch the builds, and come back when one struggles or all are done",
|
|
124
|
-
parameters: Type.Object({
|
|
125
|
-
minutes: Type.Optional(Type.Number({ description: "The longest to watch, in minutes. Default 20" })),
|
|
126
|
-
}),
|
|
127
|
-
async execute(_id, params, signal) {
|
|
128
|
-
const limit = Math.max(1, Math.min(120, params.minutes ?? 20)) * 60_000;
|
|
129
|
-
const deadline = Date.now() + limit;
|
|
130
|
-
const pending = () => mine().filter((job) => job.status === "running" || job.status === "queued");
|
|
131
|
-
const newStruggles = () => mine()
|
|
132
|
-
.map((job) => ({ job, struggle: struggling(job) }))
|
|
133
|
-
.filter((item) => item.struggle !== undefined && !flagged.has(`${item.job.id}|${item.struggle.key}`));
|
|
134
|
-
let found = newStruggles();
|
|
135
|
-
while (found.length === 0 && pending().length > 0 && Date.now() < deadline && signal?.aborted !== true) {
|
|
136
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
137
|
-
found = newStruggles();
|
|
138
|
-
}
|
|
139
|
-
for (const item of found)
|
|
140
|
-
flagged.add(`${item.job.id}|${item.struggle.key}`);
|
|
141
|
-
const all = mine().sort((a, b) => a.sequence - b.sequence);
|
|
142
|
-
const left = pending();
|
|
143
|
-
const text = [
|
|
144
|
-
found.length > 0
|
|
145
|
-
? `${found.length} build${found.length === 1 ? " is" : "s are"} struggling:`
|
|
146
|
-
: left.length === 0
|
|
147
|
-
? `Every build queued since this run started has finished (${all.length}).`
|
|
148
|
-
: `No build is struggling; ${left.length} of ${all.length} are still running or queued - call watch_jobs again.`,
|
|
149
|
-
...found.flatMap(({ job, struggle }) => ["", describeStruggle(job, struggle)]),
|
|
150
|
-
"",
|
|
151
|
-
"Every build:",
|
|
152
|
-
...all.map(jobLine),
|
|
153
|
-
"",
|
|
154
|
-
"Read what the builds had to decide themselves with spec_findings; see how far the spec is regenerated with drift.",
|
|
155
|
-
].join("\n");
|
|
156
|
-
return { content: [{ type: "text", text }], details: { pending: left.length, struggling: found.map((item) => item.job.id) } };
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
const specFindings = defineTool({
|
|
160
|
-
name: "spec_findings",
|
|
161
|
-
label: "What the spec left open",
|
|
162
|
-
description: "What the builds since this run started recorded where the spec left them to decide or went wrong: every " +
|
|
163
|
-
"assumption and pitfall in the codebases' working notes, the diagnosis of every job that failed for good, and every " +
|
|
164
|
-
"spec that contradicts itself - each numbered, and each given once: a finding already handed out is not repeated.",
|
|
165
|
-
promptSnippet: "where the builds had to guess, or failed on the spec",
|
|
166
|
-
parameters: Type.Object({
|
|
167
|
-
plain_file: Type.Optional(Type.String({ description: "A .plain file of the project to read; default the session's folder" })),
|
|
168
|
-
all: Type.Optional(Type.Boolean({ description: "Every finding since the run started, not only the new ones" })),
|
|
169
|
-
}),
|
|
170
|
-
async execute(_id, params) {
|
|
171
|
-
// The project a spec is built in; with none named, the one the session's folder is.
|
|
172
|
-
const project = projectDirOf(params.plain_file !== undefined ? resolve(options.cwd, params.plain_file) : join(options.cwd, "session.plain"));
|
|
173
|
-
const found = collectFindings({ project, jobs: mine(), since: startedAt });
|
|
174
|
-
const fresh = found.filter((finding) => params.all === true || !reported.has(`${finding.kind}|${finding.where}|${finding.text}`));
|
|
175
|
-
const numbered = fresh.map((finding) => {
|
|
176
|
-
reported.add(`${finding.kind}|${finding.where}|${finding.text}`);
|
|
177
|
-
counter += 1;
|
|
178
|
-
return { ...finding, id: `f${counter}` };
|
|
179
|
-
});
|
|
180
|
-
const text = numbered.length === 0
|
|
181
|
-
? "No new findings: the builds since the last look recorded no assumption, no pitfall, no diagnosis and no spec conflict."
|
|
182
|
-
: [
|
|
183
|
-
`${numbered.length} finding${numbered.length === 1 ? "" : "s"}:`,
|
|
184
|
-
"",
|
|
185
|
-
...numbered.map((finding) => `${finding.id} · ${finding.kind} · ${finding.where}\n${finding.text}`),
|
|
186
|
-
].join("\n\n");
|
|
187
|
-
return { content: [{ type: "text", text }], details: { findings: numbered } };
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
return [watchJobs, specFindings];
|
|
191
|
-
}
|
|
192
|
-
/** The names of the maturing tools, as a skill's allowed-tools names them: its own - it regenerates, halts and reads drift with the conversation's (loop-tools.ts). */
|
|
193
|
-
export const MATURE_TOOLS = ["watch_jobs", "spec_findings"];
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: analyze-if-func-spec-too-complex
|
|
3
|
-
description: >-
|
|
4
|
-
Analyze a functional spec to determine if it is too complex for the renderer.
|
|
5
|
-
A spec is too complex if it would produce more than 200 lines of code changes.
|
|
6
|
-
Use after drafting a new functional spec (per `func-specs.md`, *Adding a
|
|
7
|
-
functional spec*) to verify it fits within the complexity limit before
|
|
8
|
-
inserting it.
|
|
9
|
-
allowed-tools: [read, ls, grep, find]
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Analyze If Functional Spec Is Too Complex
|
|
13
|
-
|
|
14
|
-
## Why This Matters
|
|
15
|
-
|
|
16
|
-
The renderer enforces a hard complexity limit: each functional spec must imply a **maximum of 200 changed lines of code**. If a spec exceeds this, the renderer rejects it with "Functional spec too complex!" and the spec must be broken down. Catching this before insertion saves a failed render cycle.
|
|
17
|
-
|
|
18
|
-
## Input
|
|
19
|
-
|
|
20
|
-
A drafted functional spec (not yet inserted into the file), plus the full context of the `.plain` file it will be added to — definitions, implementation reqs, and existing functional specs.
|
|
21
|
-
|
|
22
|
-
## Workflow
|
|
23
|
-
|
|
24
|
-
1. **Read the full `.plain` file** (and any `requires`/`import` modules) to understand the current codebase context — what already exists and what the new spec builds on.
|
|
25
|
-
2. **Read the drafted functional spec** carefully.
|
|
26
|
-
3. **Run the complexity analysis** using the checklist below.
|
|
27
|
-
4. **Output the verdict and nothing else** — either `ACCEPTABLE` or `TOO COMPLEX`. No reasoning, no LOC estimate, no breakdown.
|
|
28
|
-
|
|
29
|
-
## Complexity Analysis Checklist
|
|
30
|
-
|
|
31
|
-
Work through each indicator. A single "yes" does not automatically mean the spec is too complex, but multiple "yes" answers are a strong signal.
|
|
32
|
-
|
|
33
|
-
### 0. First-Spec Bootstrap Cost
|
|
34
|
-
|
|
35
|
-
Is this the first functional spec in a new root module with no implementation baseline supplied by `requires`? If so, include the implicit cost of creating the project structure, package metadata, framework configuration, entry point, initial source files, and unit-test infrastructure.
|
|
36
|
-
|
|
37
|
-
The first spec must be substantially simpler than a later spec. It should expose one minimal, deterministic capability that can be invoked and tested. Setup without an observable result is not acceptable, but combining bootstrap work with persistence, integrations, multiple interfaces, branching workflows, or cross-cutting behavior is a strong `TOO COMPLEX` signal.
|
|
38
|
-
|
|
39
|
-
### 1. Number of Distinct Behaviors
|
|
40
|
-
|
|
41
|
-
Does the spec describe more than one independently testable behavior?
|
|
42
|
-
|
|
43
|
-
Too complex:
|
|
44
|
-
```plain
|
|
45
|
-
***functional specs***
|
|
46
|
-
|
|
47
|
-
- A :User: can create, edit, delete, and archive :Task: items, with validation on all fields and confirmation dialogs for destructive actions.
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Acceptable (one behavior each):
|
|
51
|
-
```plain
|
|
52
|
-
***functional specs***
|
|
53
|
-
|
|
54
|
-
- A :User: can create :Task:. Only valid :Task: items can be added.
|
|
55
|
-
|
|
56
|
-
- A :User: can edit :Task:.
|
|
57
|
-
|
|
58
|
-
- A :User: can delete :Task:.
|
|
59
|
-
|
|
60
|
-
- A :User: can archive :Task:.
|
|
61
|
-
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### 2. Number of Concepts Introduced or Modified
|
|
65
|
-
|
|
66
|
-
Does the spec require introducing new data structures, UI components, API endpoints, or other constructs that don't already exist? Count them.
|
|
67
|
-
|
|
68
|
-
- 0–1 new constructs → likely fine
|
|
69
|
-
- 2–3 new constructs → borderline, examine closely
|
|
70
|
-
- 4+ new constructs → almost certainly too complex
|
|
71
|
-
|
|
72
|
-
### 3. Branching Logic and Conditions
|
|
73
|
-
|
|
74
|
-
Does the spec describe multiple conditional paths, modes, or special cases?
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
Too complex:
|
|
78
|
-
```plain
|
|
79
|
-
***functional specs***
|
|
80
|
-
|
|
81
|
-
- :Order: processing depends on :OrderType:. Standard orders are validated and stored. Express orders skip validation and are queued for immediate dispatch. Bulk orders are split into sub-orders of 100 items each, validated individually, and processed in parallel with progress tracking.
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Acceptable (separate the modes):
|
|
85
|
-
```plain
|
|
86
|
-
***functional specs***
|
|
87
|
-
|
|
88
|
-
- A standard :Order: is validated and stored.
|
|
89
|
-
|
|
90
|
-
- An express :Order: is queued for immediate dispatch without validation.
|
|
91
|
-
|
|
92
|
-
- A bulk :Order: is split into sub-orders of 100 items each.
|
|
93
|
-
- Each sub-order is processed individually.
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### 4. Cross-Cutting Concerns
|
|
97
|
-
|
|
98
|
-
Does the spec bundle core functionality with cross-cutting concerns like error handling, logging, retry logic, pagination, or caching?
|
|
99
|
-
|
|
100
|
-
Too complex:
|
|
101
|
-
```plain
|
|
102
|
-
***functional specs***
|
|
103
|
-
|
|
104
|
-
- :Resource: items are fetched from the external API with pagination support, retry on transient errors with exponential backoff, caching for 5 minutes, and logging of all API calls.
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Acceptable (separate concerns):
|
|
108
|
-
```plain
|
|
109
|
-
***functional specs***
|
|
110
|
-
- :Resource: items are fetched from the external API.
|
|
111
|
-
|
|
112
|
-
- :Resource: items are fetched from the external API in pages.
|
|
113
|
-
|
|
114
|
-
- Fetching :Resource: items is retried on transient errors using exponential backoff.
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### 5. UI Complexity
|
|
118
|
-
|
|
119
|
-
Does the spec describe a complete screen or page with multiple interactive elements, layouts, and state transitions?
|
|
120
|
-
|
|
121
|
-
Too complex:
|
|
122
|
-
```plain
|
|
123
|
-
***functional specs***
|
|
124
|
-
|
|
125
|
-
- The dashboard shows :User: profile, recent :Task: items in a sortable table, a notification bell with unread count, and a sidebar with navigation links that highlights the active page.
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Acceptable (build incrementally):
|
|
129
|
-
```plain
|
|
130
|
-
***functional specs***
|
|
131
|
-
|
|
132
|
-
- A dashboard page is shown for :User:.
|
|
133
|
-
|
|
134
|
-
- Recent :Task: items are shown in a sortable table on the dashboard.
|
|
135
|
-
|
|
136
|
-
- A notification indicator with the unread count is shown on the dashboard.
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### 6. Data Transformation Complexity
|
|
140
|
-
|
|
141
|
-
Does the spec involve complex data mapping, aggregation, or transformation across multiple entities?
|
|
142
|
-
|
|
143
|
-
- Simple field mapping or filtering → likely fine
|
|
144
|
-
- Multi-step transformations, joins across entities, or aggregations → likely too complex
|
|
145
|
-
|
|
146
|
-
### 7. Implied Technical Component
|
|
147
|
-
|
|
148
|
-
Does the spec imply building a substantial technical component — an engine, parser, scheduler, layout algorithm, state machine, or sync mechanism — **in addition to** the behavior it describes? A component is not a free primitive: code must be generated for it too, so it counts toward the 200-LOC budget and must be evaluated as part of the estimate. A spec that needs a component **and** wires up behavior on top of it is really two bodies of code in one spec — a strong "too complex" signal. Count the component's implementation, not just the glue that calls it.
|
|
149
|
-
|
|
150
|
-
Too complex (builds the component and uses it in one spec):
|
|
151
|
-
```plain
|
|
152
|
-
***functional specs***
|
|
153
|
-
|
|
154
|
-
- A :Report: is exported to PDF, laying out multi-page tables with repeating headers and page numbers, and charts rendered from :Report: data.
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Acceptable (component in its own spec, then referenced):
|
|
158
|
-
```plain
|
|
159
|
-
***functional specs***
|
|
160
|
-
|
|
161
|
-
- :PdfRenderer: lays out multi-page tables from structured content.
|
|
162
|
-
- Table headers repeat at the top of each page.
|
|
163
|
-
- Each page shows its page number.
|
|
164
|
-
|
|
165
|
-
- A :Report: is exported to PDF using :PdfRenderer:, embedding charts rendered from :Report: data.
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
When this indicator fires, the fix is `break-down-func-spec` Strategy 6 — extract the component into its own earlier spec and reference it.
|
|
169
|
-
|
|
170
|
-
### 8. Rough LOC Estimation
|
|
171
|
-
|
|
172
|
-
Mentally estimate the implementation. Consider:
|
|
173
|
-
- New files that need to be created
|
|
174
|
-
- New functions/methods
|
|
175
|
-
- Code for any implied technical component (engine, parser, algorithm), not just the glue that uses it
|
|
176
|
-
- Data model changes (schema, migrations, types)
|
|
177
|
-
- UI components (if applicable)
|
|
178
|
-
- Test setup and assertions (unit tests are auto-generated alongside)
|
|
179
|
-
- For the first spec in a new root module, all project and test scaffold implied by implementation requirements
|
|
180
|
-
- Error handling paths
|
|
181
|
-
- Import statements and boilerplate
|
|
182
|
-
|
|
183
|
-
If the estimate exceeds ~150 LOC, the spec is at high risk of being too complex (leave headroom for the renderer to add necessary boilerplate).
|
|
184
|
-
|
|
185
|
-
## Output Format
|
|
186
|
-
|
|
187
|
-
The skill emits exactly one of these two strings, with no surrounding text, explanation, estimate, or breakdown:
|
|
188
|
-
|
|
189
|
-
```
|
|
190
|
-
ACCEPTABLE
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
or
|
|
194
|
-
|
|
195
|
-
```
|
|
196
|
-
TOO COMPLEX
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
The internal analysis (checklist, LOC estimation, reasoning) informs the verdict but must not appear in the output. The caller decides what to do with the result.
|
|
200
|
-
|
|
201
|
-
## Where it is called
|
|
202
|
-
|
|
203
|
-
This skill is step 2 of adding a functional spec (`func-specs.md`, *Adding a functional spec*) - for each spec of a batch, in order - after drafting a spec but **before** inserting it into the file. The caller acts on the one-word verdict:
|
|
204
|
-
|
|
205
|
-
- **ACCEPTABLE** — proceed to insert the spec.
|
|
206
|
-
- **TOO COMPLEX** — invoke `break-down-func-spec` to produce smaller specs, then insert each one individually (running conflict checks on each).
|