cclaw-cli 6.5.0 → 6.7.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/dist/artifact-linter/brainstorm.js +2 -1
- package/dist/artifact-linter/design.js +2 -1
- package/dist/artifact-linter/findings-dedup.d.ts +56 -0
- package/dist/artifact-linter/findings-dedup.js +232 -0
- package/dist/artifact-linter/plan.js +4 -2
- package/dist/artifact-linter/review.js +2 -1
- package/dist/artifact-linter/scope.js +2 -1
- package/dist/artifact-linter/shared.d.ts +103 -0
- package/dist/artifact-linter/shared.js +177 -0
- package/dist/artifact-linter/tdd.js +2 -1
- package/dist/artifact-linter.d.ts +1 -1
- package/dist/artifact-linter.js +45 -3
- package/dist/content/examples.d.ts +32 -0
- package/dist/content/examples.js +74 -0
- package/dist/content/hooks.js +36 -1
- package/dist/content/node-hooks.js +43 -0
- package/dist/content/skills-elicitation.js +3 -6
- package/dist/content/skills.d.ts +10 -0
- package/dist/content/skills.js +44 -2
- package/dist/content/stages/brainstorm.js +7 -5
- package/dist/content/stages/design.js +3 -1
- package/dist/content/stages/plan.js +3 -1
- package/dist/content/stages/review.js +3 -1
- package/dist/content/stages/scope.js +5 -3
- package/dist/content/stages/ship.js +2 -1
- package/dist/content/stages/spec.js +3 -1
- package/dist/content/stages/tdd.js +3 -1
- package/dist/content/templates.d.ts +9 -0
- package/dist/content/templates.js +45 -2
- package/dist/delegation.d.ts +9 -0
- package/dist/delegation.js +3 -0
- package/dist/internal/advance-stage/advance.js +23 -1
- package/dist/internal/advance-stage/parsers.d.ts +8 -0
- package/dist/internal/advance-stage/parsers.js +7 -0
- package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +3 -0
- package/dist/internal/advance-stage/proactive-delegation-trace.js +8 -1
- package/dist/internal/advance-stage/rewind.js +2 -2
- package/dist/internal/advance-stage/start-flow.js +4 -1
- package/dist/internal/advance-stage.js +32 -2
- package/dist/internal/flow-state-repair.d.ts +13 -0
- package/dist/internal/flow-state-repair.js +65 -0
- package/dist/internal/waiver-grant.d.ts +62 -0
- package/dist/internal/waiver-grant.js +294 -0
- package/dist/run-persistence.d.ts +70 -0
- package/dist/run-persistence.js +215 -3
- package/dist/runs.d.ts +1 -1
- package/dist/runs.js +1 -1
- package/dist/runtime/run-hook.mjs +43 -0
- package/package.json +1 -1
|
@@ -384,6 +384,41 @@ export function duplicateH2Headings(markdown) {
|
|
|
384
384
|
.filter(([, count]) => count > 1)
|
|
385
385
|
.map(([key]) => displayHeading.get(key) ?? key);
|
|
386
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Return the author-authored prose of an artifact, stripping linter meta
|
|
389
|
+
* regions so free-text scans (placeholder tokens, scope-reduction phrases,
|
|
390
|
+
* investigation trigger words) don't self-cannibalize by matching the
|
|
391
|
+
* linter's own templated meta-phrases.
|
|
392
|
+
*
|
|
393
|
+
* Stripping rules (in order):
|
|
394
|
+
* 1. `<!-- linter-meta --> ... <!-- /linter-meta -->` paired blocks.
|
|
395
|
+
* Both markers must appear on their own line; unterminated openings
|
|
396
|
+
* are left as-is so a malformed artifact cannot hide arbitrary
|
|
397
|
+
* content by omitting the closing marker.
|
|
398
|
+
* 2. Every other HTML comment (`<!-- ... -->`, possibly multi-line).
|
|
399
|
+
* 3. Fenced code blocks that are tagged `linter-rule` (e.g.
|
|
400
|
+
* ```` ```linter-rule ````). Plain fenced code blocks are preserved
|
|
401
|
+
* because many stages quote code samples that the linter should
|
|
402
|
+
* still see.
|
|
403
|
+
*
|
|
404
|
+
* The function guarantees the returned string is a strict subset of the
|
|
405
|
+
* original: no characters are synthesized, and line offsets are
|
|
406
|
+
* preserved for any surviving line (blank lines stand in for stripped
|
|
407
|
+
* regions). This keeps regex-based linter checks stable when authors
|
|
408
|
+
* add or remove linter-meta blocks between runs.
|
|
409
|
+
*/
|
|
410
|
+
export function extractAuthoredBody(rawArtifact) {
|
|
411
|
+
if (typeof rawArtifact !== "string" || rawArtifact.length === 0) {
|
|
412
|
+
return "";
|
|
413
|
+
}
|
|
414
|
+
const linterMetaBlock = /^[ \t]*<!--\s*linter-meta\s*-->[\s\S]*?^[ \t]*<!--\s*\/linter-meta\s*-->[ \t]*$/gmu;
|
|
415
|
+
let body = rawArtifact.replace(linterMetaBlock, (match) => match.replace(/[^\n]/gu, ""));
|
|
416
|
+
const htmlComment = /<!--[\s\S]*?-->/gu;
|
|
417
|
+
body = body.replace(htmlComment, (match) => match.replace(/[^\n]/gu, ""));
|
|
418
|
+
const linterRuleFence = /^([ \t]*)(`{3,}|~{3,})\s*linter-rule\b[^\n]*\n[\s\S]*?\n\1\2[ \t]*$/gmu;
|
|
419
|
+
body = body.replace(linterRuleFence, (match) => match.replace(/[^\n]/gu, ""));
|
|
420
|
+
return body;
|
|
421
|
+
}
|
|
387
422
|
export function headingPresent(sections, section) {
|
|
388
423
|
const want = normalizeHeadingTitle(section).toLowerCase();
|
|
389
424
|
for (const h of sections.keys()) {
|
|
@@ -1715,6 +1750,148 @@ export function parseLearningsSection(sectionBody) {
|
|
|
1715
1750
|
details: `Parsed ${entries.length} learning bullet(s) as knowledge-compatible JSON entries.`
|
|
1716
1751
|
};
|
|
1717
1752
|
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Round 5 (v6.6.0) — file-path / reference detector for the
|
|
1755
|
+
* `investigation_path_first_missing` advisory rule.
|
|
1756
|
+
*
|
|
1757
|
+
* The detector is intentionally permissive: it only needs to recognize
|
|
1758
|
+
* "the author wrote down a path or ref" — the linter does NOT validate
|
|
1759
|
+
* the path resolves on disk. Patterns matched (any one is enough):
|
|
1760
|
+
* - TS/JS/MD/JSON/YAML path with extension
|
|
1761
|
+
* (`src/foo/bar.ts`, `tests/spec.test.ts`, `docs/quality-gates.md`).
|
|
1762
|
+
* - Slash-bearing path under a known repo root prefix
|
|
1763
|
+
* (`src/...`, `tests/...`, `docs/...`, `scripts/...`,
|
|
1764
|
+
* `.cclaw/...`, `.cursor/...`, `node_modules/...`,
|
|
1765
|
+
* `examples/...`, `e2e/...`).
|
|
1766
|
+
* - GitHub-style ref (`owner/repo#123`, `org/repo@sha`,
|
|
1767
|
+
* `path:line`, `path:line-line`).
|
|
1768
|
+
* - Explicit `path:` / `paths:` / `ref:` / `refs:` marker.
|
|
1769
|
+
* - Stable cclaw IDs (`R1`, `D-12`, `AC-3`, `T-4`, `S-2`, `DD-5`,
|
|
1770
|
+
* `ADR-1`, `R-1`, `F-1`, `CR-1`, `I-1`, `QS-1`).
|
|
1771
|
+
* - Backticked path-like token containing a slash.
|
|
1772
|
+
*
|
|
1773
|
+
* Exposed for unit tests (`tests/unit/investigation-trace-evaluator.test.ts`).
|
|
1774
|
+
*/
|
|
1775
|
+
export const INVESTIGATION_TRACE_PATH_PATTERNS = [
|
|
1776
|
+
/(?:^|[\s`(\[])(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\.(?:ts|tsx|js|jsx|mjs|cjs|md|mdx|json|yaml|yml|toml|sh|py|rs|go|java|kt|swift|rb|css|scss|html)\b/iu,
|
|
1777
|
+
/(?:^|[\s`(\[])(?:src|tests?|docs?|scripts?|e2e|examples?|packages?|apps?|cmd|internal|pkg|lib|app|server|client|backend|frontend|\.cclaw|\.cursor|\.github|node_modules)\/[A-Za-z0-9_./-]+/iu,
|
|
1778
|
+
/\b[A-Za-z0-9_./-]+(?:\.[A-Za-z0-9]+)?:\d+(?:[-:]\d+)?\b/u,
|
|
1779
|
+
/\b[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#\d+|@[0-9a-f]{6,40})\b/iu,
|
|
1780
|
+
/(?:^|\s)(?:paths?|refs?|file|files|cite|citation)\s*:\s*\S/iu,
|
|
1781
|
+
/\b(?:R|D|AC|T|S|DD|ADR|F|CR|I|QS)-?\d+\b/u,
|
|
1782
|
+
/`[^`]*\/[^`]+`/u
|
|
1783
|
+
];
|
|
1784
|
+
const INVESTIGATION_TRACE_PLACEHOLDER_PATTERN = /^(?:none|none\.|n\/a|tbd|todo|fixme|placeholder|optional|fill[\s-]?in)\b/u;
|
|
1785
|
+
const INVESTIGATION_TRACE_ID_ONLY_CELL = /^[A-Z]{1,4}-?\d+$/u;
|
|
1786
|
+
function isInvestigationTracePlaceholderCell(cell) {
|
|
1787
|
+
const stripped = cell.replace(/[`*_>#]/gu, "").trim();
|
|
1788
|
+
if (stripped.length === 0)
|
|
1789
|
+
return true;
|
|
1790
|
+
if (INVESTIGATION_TRACE_PLACEHOLDER_PATTERN.test(stripped.toLowerCase()))
|
|
1791
|
+
return true;
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
function isInvestigationTracePlaceholderProseLine(line) {
|
|
1795
|
+
const stripped = line.replace(/[`*_>#-]/gu, "").trim();
|
|
1796
|
+
if (stripped.length === 0)
|
|
1797
|
+
return true;
|
|
1798
|
+
const lower = stripped.toLowerCase();
|
|
1799
|
+
if (INVESTIGATION_TRACE_PLACEHOLDER_PATTERN.test(lower))
|
|
1800
|
+
return true;
|
|
1801
|
+
if (/^\(\s*(?:none|n\/a|tbd|todo|fixme|placeholder|optional|fill[\s-]?in)\b/u.test(lower)) {
|
|
1802
|
+
return true;
|
|
1803
|
+
}
|
|
1804
|
+
return false;
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Internal core that does NOT depend on `StageLintContext`. Returned
|
|
1808
|
+
* shape is consumed by `evaluateInvestigationTrace` (which pushes a
|
|
1809
|
+
* finding into the context) and by unit tests that exercise the
|
|
1810
|
+
* detector directly.
|
|
1811
|
+
*
|
|
1812
|
+
* Returns `null` for sections that are missing, empty, or contain only
|
|
1813
|
+
* template scaffolding (table headers, separators, placeholder rows
|
|
1814
|
+
* with empty cells, lone `- None.` lines). Callers treat `null` as
|
|
1815
|
+
* silent — no finding is emitted.
|
|
1816
|
+
*/
|
|
1817
|
+
export function checkInvestigationTrace(sectionBody) {
|
|
1818
|
+
if (sectionBody === null)
|
|
1819
|
+
return null;
|
|
1820
|
+
const lines = sectionBody.split(/\r?\n/u);
|
|
1821
|
+
const candidates = [];
|
|
1822
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1823
|
+
const raw = lines[index] ?? "";
|
|
1824
|
+
const trimmed = raw.trim();
|
|
1825
|
+
if (trimmed.length === 0)
|
|
1826
|
+
continue;
|
|
1827
|
+
if (trimmed.startsWith("<!--"))
|
|
1828
|
+
continue;
|
|
1829
|
+
const isTableLine = /^\|.*\|$/u.test(trimmed);
|
|
1830
|
+
if (isTableLine) {
|
|
1831
|
+
if (/^\|[-:| ]+\|$/u.test(trimmed))
|
|
1832
|
+
continue; // separator row
|
|
1833
|
+
const next = (lines[index + 1] ?? "").trim();
|
|
1834
|
+
if (/^\|[-:| ]+\|$/u.test(next))
|
|
1835
|
+
continue; // header row (followed by separator)
|
|
1836
|
+
const cells = trimmed
|
|
1837
|
+
.split("|")
|
|
1838
|
+
.slice(1, -1)
|
|
1839
|
+
.map((cell) => cell.trim());
|
|
1840
|
+
const substantive = cells.filter((cell) => !isInvestigationTracePlaceholderCell(cell));
|
|
1841
|
+
if (substantive.length === 0)
|
|
1842
|
+
continue;
|
|
1843
|
+
if (substantive.length === 1 && INVESTIGATION_TRACE_ID_ONLY_CELL.test(substantive[0])) {
|
|
1844
|
+
continue;
|
|
1845
|
+
}
|
|
1846
|
+
candidates.push(substantive.join(" "));
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1849
|
+
if (isInvestigationTracePlaceholderProseLine(trimmed))
|
|
1850
|
+
continue;
|
|
1851
|
+
candidates.push(trimmed);
|
|
1852
|
+
}
|
|
1853
|
+
if (candidates.length === 0)
|
|
1854
|
+
return null;
|
|
1855
|
+
const sample = candidates.slice(0, Math.min(5, candidates.length));
|
|
1856
|
+
const detectorMatched = sample.some((line) => INVESTIGATION_TRACE_PATH_PATTERNS.some((pattern) => pattern.test(line)));
|
|
1857
|
+
if (detectorMatched) {
|
|
1858
|
+
return {
|
|
1859
|
+
ok: true,
|
|
1860
|
+
details: "Investigation trace cites file paths or refs in the first non-empty row(s)."
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
return {
|
|
1864
|
+
ok: false,
|
|
1865
|
+
details: "Investigation trace has prose-only content in its first row(s). Pass paths and refs, not pasted file contents (e.g. `src/foo/bar.ts:42`, `D-12`, `AC-3`)."
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Round 5 (v6.6.0) — advisory rule wired into the brainstorm / scope /
|
|
1870
|
+
* design / tdd / plan / review linters.
|
|
1871
|
+
*
|
|
1872
|
+
* Behavior contract:
|
|
1873
|
+
* - Section missing or empty / placeholder-only: silent (no finding).
|
|
1874
|
+
* - Section has substantive content with a recognizable file path /
|
|
1875
|
+
* ref / explicit `path:`-style marker in the first non-empty rows:
|
|
1876
|
+
* advisory pass (no finding).
|
|
1877
|
+
* - Section has substantive content but no path/ref signal: advisory
|
|
1878
|
+
* FAIL finding with ruleId `investigation_path_first_missing`.
|
|
1879
|
+
*
|
|
1880
|
+
* The rule is `required: false` so it never blocks `stage-complete`.
|
|
1881
|
+
*/
|
|
1882
|
+
export function evaluateInvestigationTrace(ctx, sectionName) {
|
|
1883
|
+
const body = sectionBodyByName(ctx.sections, sectionName);
|
|
1884
|
+
const result = checkInvestigationTrace(body);
|
|
1885
|
+
if (result === null)
|
|
1886
|
+
return;
|
|
1887
|
+
ctx.findings.push({
|
|
1888
|
+
section: "investigation_path_first_missing",
|
|
1889
|
+
required: false,
|
|
1890
|
+
rule: `[P3] investigation_path_first_missing — \`## ${sectionName}\` should cite paths and refs in the first non-empty row(s); pass paths and refs, not content.`,
|
|
1891
|
+
found: result.ok,
|
|
1892
|
+
details: result.details
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1718
1895
|
export function lineContainsVagueAdjective(text) {
|
|
1719
1896
|
const lower = text.toLowerCase();
|
|
1720
1897
|
for (const adjective of VAGUE_AC_ADJECTIVES) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { readDelegationLedger } from "../delegation.js";
|
|
4
|
-
import { sectionBodyByName } from "./shared.js";
|
|
4
|
+
import { evaluateInvestigationTrace, sectionBodyByName } from "./shared.js";
|
|
5
5
|
export async function lintTddStage(ctx) {
|
|
6
6
|
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
|
|
7
|
+
evaluateInvestigationTrace(ctx, "Watched-RED Proof");
|
|
7
8
|
// Universal Layer 2.6 structural checks (superpowers TDD + evanflow vertical slices).
|
|
8
9
|
const ironLawBody = sectionBodyByName(sections, "Iron Law Acknowledgement");
|
|
9
10
|
if (ironLawBody === null) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FlowStage, FlowTrack } from "./types.js";
|
|
2
2
|
import { type LintResult } from "./artifact-linter/shared.js";
|
|
3
3
|
export { validateReviewArmy, checkReviewVerdictConsistency, checkReviewSecurityNoChangeAttestation, checkReviewTddNoCrossArtifactDuplication, type ReviewVerdictConsistencyResult, type ReviewSecurityNoChangeAttestationResult, type ReviewTddDuplicationConflict, type ReviewTddDuplicationResult } from "./artifact-linter/review-army.js";
|
|
4
|
-
export { type LintFinding, type LintResult, type LearningEntryType, type LearningConfidence, type LearningSeverity, type LearningSource, type LearningSeedEntry, type LearningsParseResult, formatLearningsErrorsBullets, learningsParseFailureHumanSummary, extractMarkdownSectionBody, parseLearningsSection } from "./artifact-linter/shared.js";
|
|
4
|
+
export { type LintFinding, type LintResult, type LearningEntryType, type LearningConfidence, type LearningSeverity, type LearningSource, type LearningSeedEntry, type LearningsParseResult, extractAuthoredBody, formatLearningsErrorsBullets, learningsParseFailureHumanSummary, extractMarkdownSectionBody, parseLearningsSection } from "./artifact-linter/shared.js";
|
|
5
5
|
export interface LintArtifactOptions {
|
|
6
6
|
/**
|
|
7
7
|
* Stage-level flags supplied by the caller (typically `advance-stage`)
|
package/dist/artifact-linter.js
CHANGED
|
@@ -5,7 +5,8 @@ import { stageSchema } from "./content/stage-schema.js";
|
|
|
5
5
|
import { readFlowState } from "./run-persistence.js";
|
|
6
6
|
import { duplicateH2Headings, extractH2Sections, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody, formatLearningsErrorsBullets } from "./artifact-linter/shared.js";
|
|
7
7
|
import { shouldDemoteArtifactValidationByTrack } from "./content/stage-schema.js";
|
|
8
|
-
import { recordArtifactValidationDemotedByTrack } from "./delegation.js";
|
|
8
|
+
import { readDelegationLedger, recordArtifactValidationDemotedByTrack } from "./delegation.js";
|
|
9
|
+
import { classifyAndPersistFindings } from "./artifact-linter/findings-dedup.js";
|
|
9
10
|
import { lintBrainstormStage } from "./artifact-linter/brainstorm.js";
|
|
10
11
|
import { lintDesignStage } from "./artifact-linter/design.js";
|
|
11
12
|
import { lintPlanStage } from "./artifact-linter/plan.js";
|
|
@@ -15,7 +16,7 @@ import { lintTddStage } from "./artifact-linter/tdd.js";
|
|
|
15
16
|
import { lintReviewStage } from "./artifact-linter/review.js";
|
|
16
17
|
import { lintShipStage } from "./artifact-linter/ship.js";
|
|
17
18
|
export { validateReviewArmy, checkReviewVerdictConsistency, checkReviewSecurityNoChangeAttestation, checkReviewTddNoCrossArtifactDuplication } from "./artifact-linter/review-army.js";
|
|
18
|
-
export { formatLearningsErrorsBullets, learningsParseFailureHumanSummary, extractMarkdownSectionBody, parseLearningsSection } from "./artifact-linter/shared.js";
|
|
19
|
+
export { extractAuthoredBody, formatLearningsErrorsBullets, learningsParseFailureHumanSummary, extractMarkdownSectionBody, parseLearningsSection } from "./artifact-linter/shared.js";
|
|
19
20
|
const FRONTMATTER_REQUIRED_KEYS = [
|
|
20
21
|
"stage",
|
|
21
22
|
"schema_version",
|
|
@@ -328,6 +329,30 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
328
329
|
});
|
|
329
330
|
}
|
|
330
331
|
}
|
|
332
|
+
try {
|
|
333
|
+
const delegationLedger = await readDelegationLedger(projectRoot);
|
|
334
|
+
const legacyWaivers = delegationLedger.entries.filter((entry) => entry.status === "waived" &&
|
|
335
|
+
entry.mode === "proactive" &&
|
|
336
|
+
entry.stage === stage &&
|
|
337
|
+
(typeof entry.approvalToken !== "string" || entry.approvalToken.trim().length === 0));
|
|
338
|
+
if (legacyWaivers.length > 0) {
|
|
339
|
+
const descriptors = legacyWaivers
|
|
340
|
+
.map((entry) => [entry.agent, entry.spanId].filter((value) => typeof value === "string").join("@"))
|
|
341
|
+
.filter((value) => value.length > 0);
|
|
342
|
+
findings.push({
|
|
343
|
+
section: "waiver_legacy_provenance",
|
|
344
|
+
required: false,
|
|
345
|
+
rule: "waiver_legacy_provenance — proactive waiver(s) without approvalToken. Issue new waivers via `cclaw-cli internal waiver-grant --stage <stage> --reason <slug>` so the provenance trail is signed. Legacy waivers remain valid (advisory).",
|
|
346
|
+
found: false,
|
|
347
|
+
details: `Found ${legacyWaivers.length} proactive waiver(s) on stage="${stage}" without approvalToken` +
|
|
348
|
+
(descriptors.length > 0 ? ` (${descriptors.join(", ")})` : "") +
|
|
349
|
+
". Next waiver should be issued with `cclaw-cli internal waiver-grant` and consumed via `--accept-proactive-waiver=<token>`."
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
// Ledger absent or unreadable: no advisory to emit.
|
|
355
|
+
}
|
|
331
356
|
const demote = shouldDemoteArtifactValidationByTrack(track, taskClass);
|
|
332
357
|
const demotedSections = [];
|
|
333
358
|
if (demote) {
|
|
@@ -356,7 +381,24 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
356
381
|
}
|
|
357
382
|
}
|
|
358
383
|
const passed = findings.every((f) => !f.required || f.found);
|
|
359
|
-
|
|
384
|
+
let dedup;
|
|
385
|
+
try {
|
|
386
|
+
const dedupResult = await classifyAndPersistFindings(projectRoot, stage, findings);
|
|
387
|
+
const statusByFingerprint = new Map(dedupResult.classified.map(({ fingerprint, status }) => [fingerprint, status]));
|
|
388
|
+
const statuses = dedupResult.classified.map(({ status }) => status);
|
|
389
|
+
void statusByFingerprint;
|
|
390
|
+
dedup = {
|
|
391
|
+
newCount: dedupResult.summary.newCount,
|
|
392
|
+
repeatCount: dedupResult.summary.repeatCount,
|
|
393
|
+
resolvedCount: dedupResult.summary.resolvedCount,
|
|
394
|
+
header: dedupResult.header,
|
|
395
|
+
statuses
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
dedup = undefined;
|
|
400
|
+
}
|
|
401
|
+
return { stage, file: relFile, passed, findings, ...(dedup ? { dedup } : {}) };
|
|
360
402
|
}
|
|
361
403
|
/**
|
|
362
404
|
* Wave 25 (v6.1.0) — section names whose required-finding outcome is
|
|
@@ -1,4 +1,36 @@
|
|
|
1
1
|
import type { FlowStage } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Round 5 (v6.6.0) — short bad → good behavior anchor per stage.
|
|
4
|
+
*
|
|
5
|
+
* Each entry is rendered exactly once in the corresponding stage skill md
|
|
6
|
+
* (via `behaviorAnchorBlock` in `skills.ts`) and exactly once in the stage's
|
|
7
|
+
* artifact template (via `renderBehaviorAnchorTemplateLine`). Anchors are
|
|
8
|
+
* deliberately attached to a real artifact section name so the cross-check
|
|
9
|
+
* test in `tests/unit/behavior-anchors.test.ts` can verify the section
|
|
10
|
+
* exists in the stage's schema.
|
|
11
|
+
*
|
|
12
|
+
* Constraints enforced by the unit test:
|
|
13
|
+
* - Exactly one entry per FlowStage (8 total).
|
|
14
|
+
* - `bad` and `good` must be distinct across stages and ≤ 40 words each.
|
|
15
|
+
* - `section` must match a section name present in
|
|
16
|
+
* `stageSchema(stage).artifactRules.artifactValidation`.
|
|
17
|
+
*/
|
|
18
|
+
export interface BehaviorAnchor {
|
|
19
|
+
stage: FlowStage;
|
|
20
|
+
section: string;
|
|
21
|
+
bad: string;
|
|
22
|
+
good: string;
|
|
23
|
+
ruleHint?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare const BEHAVIOR_ANCHORS: ReadonlyArray<BehaviorAnchor>;
|
|
26
|
+
export declare function behaviorAnchorFor(stage: FlowStage): BehaviorAnchor | null;
|
|
27
|
+
/**
|
|
28
|
+
* Render the one-line "Behavior anchor (bad → good)" pointer used at the top
|
|
29
|
+
* of each artifact template (01..08). Templates carry the anchor inline so
|
|
30
|
+
* agents see it before they start filling sections; the prose itself lives
|
|
31
|
+
* only in `BEHAVIOR_ANCHORS` to avoid duplication.
|
|
32
|
+
*/
|
|
33
|
+
export declare function renderBehaviorAnchorTemplateLine(stage: FlowStage): string;
|
|
2
34
|
export declare function stageGoodBadExamples(stage: FlowStage): string;
|
|
3
35
|
/**
|
|
4
36
|
* Returns the full example artifact body for tests and internal quality checks.
|
package/dist/content/examples.js
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
+
export const BEHAVIOR_ANCHORS = [
|
|
2
|
+
{
|
|
3
|
+
stage: "brainstorm",
|
|
4
|
+
section: "Problem Decision Record",
|
|
5
|
+
bad: "Frame the problem broadly and quietly add a second outcome (\"and while we're at it, refresh the dashboard\") that no Q&A row sanctioned.",
|
|
6
|
+
good: "Name one affected user, one current failure mode, and one observable outcome; record any extra outcome as a separate row in `## Not Doing`.",
|
|
7
|
+
ruleHint: "Scope creep starts in framing — keep the Problem Decision Record single-target."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
stage: "scope",
|
|
11
|
+
section: "Scope Contract",
|
|
12
|
+
bad: "Invent a contract from a hunch: \"I'll let the user choose 3 templates\" with no Q&A row, no user feedback citation, no upstream decision.",
|
|
13
|
+
good: "Cite the Q&A row or upstream decision (`brainstorm > Selected Direction`) that produced each in/out boundary; refuse to lock without that citation.",
|
|
14
|
+
ruleHint: "Every scope contract row must trace to a recorded user signal or carried-forward decision."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
stage: "design",
|
|
18
|
+
section: "Codebase Investigation",
|
|
19
|
+
bad: "Open with \"Use a queue + worker pool\" before reading any file; the architecture choice precedes the trace and the diagram has no concrete node.",
|
|
20
|
+
good: "List 1-3 blast-radius files in `Codebase Investigation` with current responsibility and reuse candidate first; only then propose architecture in `ADR`.",
|
|
21
|
+
ruleHint: "Trace before lock — no architecture decision lands without a codebase citation."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
stage: "spec",
|
|
25
|
+
section: "Acceptance Criteria",
|
|
26
|
+
bad: "AC: \"System should be fast and reliable\" — no measurable predicate, no verification approach, no design-decision ref.",
|
|
27
|
+
good: "AC: \"GET /feed returns ≤ 50 items in < 200 ms p95; verified via integration test `tests/feed.spec.ts` against scope `R-2`.\"",
|
|
28
|
+
ruleHint: "Every AC carries an observable predicate plus the exact evidence command or path that proves it."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
stage: "plan",
|
|
32
|
+
section: "Execution Posture",
|
|
33
|
+
bad: "Posture: \"parallel-safe\" with three units that all edit the same `src/api/router.ts`; no shared interface contract, no boundary map.",
|
|
34
|
+
good: "Posture: \"parallel-safe\" only when each Implementation Unit owns disjoint files and the shared types live in one cited interface contract entry.",
|
|
35
|
+
ruleHint: "Parallelization needs disjoint units AND a single shared interface contract — claim otherwise and the next batch deadlocks."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
stage: "tdd",
|
|
39
|
+
section: "RED Evidence",
|
|
40
|
+
bad: "RED: `expect(true).toBe(true)` then \"failing test observed\" — the assertion can never have caught the bug it claims to prove.",
|
|
41
|
+
good: "RED: `expect(api.fetchFeed()).rejects.toThrow(AuthError)`; the failure output names the missing guard and ties to AC-3.",
|
|
42
|
+
ruleHint: "Mental mutation test: name a plausible bug that would still pass the assertion. If you can, the assertion is too coarse."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
stage: "review",
|
|
46
|
+
section: "Layer 2 Findings",
|
|
47
|
+
bad: "Slip in a rename of `userSvc` → `userService` and a folder reorg under \"Layer 2: cleanup\"; no acceptance criterion or finding ID demanded the change.",
|
|
48
|
+
good: "Findings name observed defects with `file:line`; refactors land as a separate slice with their own RED/GREEN, not bundled into the review pass.",
|
|
49
|
+
ruleHint: "Review surfaces findings; it does not refactor. Drive-by edits go back through TDD."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
stage: "ship",
|
|
53
|
+
section: "Preflight Results",
|
|
54
|
+
bad: "Preflight: \"Looks good, tests passed last night\"; no fresh command output, no commit SHA, no exit code.",
|
|
55
|
+
good: "Preflight: paste the command, the exit code, and the commit SHA from this turn; if the suite was not re-run after the last edit, mark BLOCKED.",
|
|
56
|
+
ruleHint: "Victory-by-confidence is not a preflight. Re-run, capture, cite SHA — or stay BLOCKED."
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
const BEHAVIOR_ANCHOR_BY_STAGE = new Map(BEHAVIOR_ANCHORS.map((entry) => [entry.stage, entry]));
|
|
60
|
+
export function behaviorAnchorFor(stage) {
|
|
61
|
+
return BEHAVIOR_ANCHOR_BY_STAGE.get(stage) ?? null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Render the one-line "Behavior anchor (bad → good)" pointer used at the top
|
|
65
|
+
* of each artifact template (01..08). Templates carry the anchor inline so
|
|
66
|
+
* agents see it before they start filling sections; the prose itself lives
|
|
67
|
+
* only in `BEHAVIOR_ANCHORS` to avoid duplication.
|
|
68
|
+
*/
|
|
69
|
+
export function renderBehaviorAnchorTemplateLine(stage) {
|
|
70
|
+
const anchor = behaviorAnchorFor(stage);
|
|
71
|
+
if (!anchor)
|
|
72
|
+
return "";
|
|
73
|
+
return `> Behavior anchor (bad -> good) — ${anchor.section}: bad: ${anchor.bad} good: ${anchor.good}`;
|
|
74
|
+
}
|
|
1
75
|
const STAGE_EXAMPLES = {
|
|
2
76
|
brainstorm: `## Context
|
|
3
77
|
|
package/dist/content/hooks.js
CHANGED
|
@@ -191,7 +191,7 @@ export function cancelRunScript() {
|
|
|
191
191
|
return internalHelperScript("cancel-run", "cancel-run", "Usage: node " + RUNTIME_ROOT + "/hooks/cancel-run.mjs --reason=<text> [--disposition=<cancelled|abandoned>] [--name=<slug>]");
|
|
192
192
|
}
|
|
193
193
|
export function stageCompleteScript() {
|
|
194
|
-
return internalHelperScript("stage-complete", "advance-stage", "Usage: node " + RUNTIME_ROOT + "/hooks/stage-complete.mjs <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...] [--accept-proactive-waiver] [--accept-proactive-waiver-reason=\"<why safe>\"] [--skip-questions] [--json]", {
|
|
194
|
+
return internalHelperScript("stage-complete", "advance-stage", "Usage: node " + RUNTIME_ROOT + "/hooks/stage-complete.mjs <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...] [--accept-proactive-waiver=<token>] [--accept-proactive-waiver-reason=\"<why safe>\"] [--skip-questions] [--json]", {
|
|
195
195
|
positionalArgName: "stage",
|
|
196
196
|
positionalArgRequired: true,
|
|
197
197
|
defaultQuietEnvVar: "CCLAW_STAGE_COMPLETE_QUIET"
|
|
@@ -199,6 +199,7 @@ export function stageCompleteScript() {
|
|
|
199
199
|
}
|
|
200
200
|
export function delegationRecordScript() {
|
|
201
201
|
return `#!/usr/bin/env node
|
|
202
|
+
import { createHash } from "node:crypto";
|
|
202
203
|
import fs from "node:fs/promises";
|
|
203
204
|
import path from "node:path";
|
|
204
205
|
import process from "node:process";
|
|
@@ -210,6 +211,37 @@ const VALID_DISPATCH_SURFACES = ${JSON.stringify([...DELEGATION_DISPATCH_SURFACE
|
|
|
210
211
|
const VALID_DISPATCH_SURFACES_SET = new Set(VALID_DISPATCH_SURFACES);
|
|
211
212
|
const SURFACE_PATH_PREFIXES = ${JSON.stringify(DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES)};
|
|
212
213
|
const LEDGER_SCHEMA_VERSION = 3;
|
|
214
|
+
const FLOW_STATE_GUARD_REL_PATH = RUNTIME_ROOT + "/.flow-state.guard.json";
|
|
215
|
+
|
|
216
|
+
async function verifyFlowStateGuardInline(root) {
|
|
217
|
+
const statePath = path.join(root, RUNTIME_ROOT, "state", "flow-state.json");
|
|
218
|
+
const guardPath = path.join(root, FLOW_STATE_GUARD_REL_PATH);
|
|
219
|
+
let raw;
|
|
220
|
+
try {
|
|
221
|
+
raw = await fs.readFile(statePath, "utf8");
|
|
222
|
+
} catch {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
let guard;
|
|
226
|
+
try {
|
|
227
|
+
const guardRaw = await fs.readFile(guardPath, "utf8");
|
|
228
|
+
guard = JSON.parse(guardRaw);
|
|
229
|
+
} catch {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!guard || typeof guard !== "object" || typeof guard.sha256 !== "string") return;
|
|
233
|
+
const actual = createHash("sha256").update(raw, "utf8").digest("hex");
|
|
234
|
+
if (actual === guard.sha256) return;
|
|
235
|
+
process.stderr.write(
|
|
236
|
+
"[cclaw] delegation-record: flow-state guard mismatch: " + (guard.runId || "unknown-run") + "\\n" +
|
|
237
|
+
"expected sha: " + guard.sha256 + "\\n" +
|
|
238
|
+
"actual sha: " + actual + "\\n" +
|
|
239
|
+
"last writer: " + (guard.writerSubsystem || "unknown") + "@" + (guard.writtenAt || "unknown") + "\\n" +
|
|
240
|
+
"do not edit flow-state.json by hand. To recover, run:\\n" +
|
|
241
|
+
" cclaw-cli internal flow-state-repair --reason \\"manual_edit_recovery\\"\\n"
|
|
242
|
+
);
|
|
243
|
+
process.exit(2);
|
|
244
|
+
}
|
|
213
245
|
|
|
214
246
|
function parseArgs(argv) {
|
|
215
247
|
const args = {};
|
|
@@ -693,6 +725,9 @@ async function main() {
|
|
|
693
725
|
const args = parseArgs(process.argv.slice(2));
|
|
694
726
|
const json = args.json !== undefined;
|
|
695
727
|
|
|
728
|
+
const guardRoot = await detectRoot();
|
|
729
|
+
await verifyFlowStateGuardInline(guardRoot);
|
|
730
|
+
|
|
696
731
|
if (args.repair) {
|
|
697
732
|
await runRepair(args, json);
|
|
698
733
|
return;
|
|
@@ -49,12 +49,14 @@ export function nodeHookRuntimeScript(options = {}) {
|
|
|
49
49
|
const defaultDisabledHooks = [];
|
|
50
50
|
const cliRuntime = resolveCliRuntimeForGeneratedHook();
|
|
51
51
|
return `#!/usr/bin/env node
|
|
52
|
+
import { createHash } from "node:crypto";
|
|
52
53
|
import fs from "node:fs/promises";
|
|
53
54
|
import path from "node:path";
|
|
54
55
|
import process from "node:process";
|
|
55
56
|
import { spawn } from "node:child_process";
|
|
56
57
|
|
|
57
58
|
const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
|
|
59
|
+
const FLOW_STATE_GUARD_REL_PATH = RUNTIME_ROOT + "/.flow-state.guard.json";
|
|
58
60
|
// Single strictness default, derived from config.strictness at install time.
|
|
59
61
|
// \`CCLAW_STRICTNESS\` env var overrides for the current process. All guards
|
|
60
62
|
// (prompt, workflow, TDD, iron-laws) route through \`resolveStrictness()\`.
|
|
@@ -1017,6 +1019,40 @@ function extractCodePathsFromText(value) {
|
|
|
1017
1019
|
return out;
|
|
1018
1020
|
}
|
|
1019
1021
|
|
|
1022
|
+
async function verifyFlowStateGuardInline(root, hookName) {
|
|
1023
|
+
const statePath = path.join(root, RUNTIME_ROOT, "state", "flow-state.json");
|
|
1024
|
+
const guardPath = path.join(root, FLOW_STATE_GUARD_REL_PATH);
|
|
1025
|
+
let raw;
|
|
1026
|
+
try {
|
|
1027
|
+
raw = await fs.readFile(statePath, "utf8");
|
|
1028
|
+
} catch {
|
|
1029
|
+
return true;
|
|
1030
|
+
}
|
|
1031
|
+
let guard;
|
|
1032
|
+
try {
|
|
1033
|
+
const guardRaw = await fs.readFile(guardPath, "utf8");
|
|
1034
|
+
guard = JSON.parse(guardRaw);
|
|
1035
|
+
} catch {
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
if (!guard || typeof guard !== "object" || typeof guard.sha256 !== "string") {
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
const actual = createHash("sha256").update(raw, "utf8").digest("hex");
|
|
1042
|
+
if (actual === guard.sha256) return true;
|
|
1043
|
+
const hookLabel = typeof hookName === "string" && hookName.length > 0 ? hookName : "hook";
|
|
1044
|
+
process.stderr.write(
|
|
1045
|
+
"[cclaw] " + hookLabel + ": flow-state guard mismatch: " + (guard.runId || "unknown-run") + "\\n" +
|
|
1046
|
+
"expected sha: " + guard.sha256 + "\\n" +
|
|
1047
|
+
"actual sha: " + actual + "\\n" +
|
|
1048
|
+
"last writer: " + (guard.writerSubsystem || "unknown") + "@" + (guard.writtenAt || "unknown") + "\\n" +
|
|
1049
|
+
"do not edit flow-state.json by hand. To recover, run:\\n" +
|
|
1050
|
+
" cclaw-cli internal flow-state-repair --reason \\"manual_edit_recovery\\"\\n"
|
|
1051
|
+
);
|
|
1052
|
+
await recordHookError(root, hookLabel, "flow-state guard mismatch actual=" + actual + " expected=" + guard.sha256).catch(() => undefined);
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1020
1056
|
async function readFlowState(root) {
|
|
1021
1057
|
const statePath = path.join(root, RUNTIME_ROOT, "state", "flow-state.json");
|
|
1022
1058
|
// Loud-on-corrupt: if flow-state.json exists but fails JSON.parse, log
|
|
@@ -2110,6 +2146,13 @@ async function main() {
|
|
|
2110
2146
|
};
|
|
2111
2147
|
|
|
2112
2148
|
try {
|
|
2149
|
+
if (hookName === "session-start" || hookName === "stop-handoff") {
|
|
2150
|
+
const guardOk = await verifyFlowStateGuardInline(runtime.root, hookName);
|
|
2151
|
+
if (!guardOk) {
|
|
2152
|
+
process.exitCode = 2;
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2113
2156
|
if (hookName === "session-start") {
|
|
2114
2157
|
process.exitCode = await handleSessionStart(runtime);
|
|
2115
2158
|
return;
|
|
@@ -29,7 +29,7 @@ Pinned anchor: "Don't tell it what to do, give it success criteria and watch it
|
|
|
29
29
|
These behaviors are the exact reason this skill exists. The linter will block your stage-complete if you do them.
|
|
30
30
|
|
|
31
31
|
- **Bad**: User asks for a "simple web app" -> agent asks 1 question about stack -> 1 question about auth -> drafts the brainstorm artifact and asks for approval.
|
|
32
|
-
- **Good**: User asks for a "simple web app" -> agent asks Q1 (what pain) -> Q2 (direct path) -> Q3 (
|
|
32
|
+
- **Good**: User asks for a "simple web app" -> agent asks Q1 (what pain) -> Q2 (direct path) -> Q3 (first operator/user) -> Q4 (no-go boundaries) -> self-eval: clear -> drafts the brainstorm artifact.
|
|
33
33
|
|
|
34
34
|
- **Bad**: Agent immediately dispatches a subagent (\`product-discovery\`, \`critic\`, \`planner\`) at the start of brainstorm/scope/design to "gather context" before any user dialogue.
|
|
35
35
|
- **Good**: Agent walks the Q&A loop with the user first; subagent dispatch happens only after the user approves the elicitation outcome.
|
|
@@ -121,7 +121,7 @@ Default mapping note: \`lean\` maps to a lightweight specialist tier on early st
|
|
|
121
121
|
|
|
122
122
|
### Topic tagging (MANDATORY for forcing-question rows)
|
|
123
123
|
|
|
124
|
-
Each forcing question has a stable topic id (kebab-case ASCII, e.g. \`pain\`, \`
|
|
124
|
+
Each forcing question has a stable topic id (kebab-case ASCII, e.g. \`pain\`, \`direct-path\`, \`data-flow\`). Tag the matching Q&A Log row's \`Decision impact\` cell with \`[topic:<id>]\` so the linter can verify coverage in any natural language. This is a **HARD requirement** in Wave 24 (v6.0.0): the linter no longer keyword-matches English question prose, so an un-tagged row does NOT count toward coverage even if the answer fully addresses the topic.
|
|
125
125
|
|
|
126
126
|
RU example (after asking \`pain\` in Russian):
|
|
127
127
|
|
|
@@ -131,21 +131,18 @@ RU example (after asking \`pain\` in Russian):
|
|
|
131
131
|
| 1 | Какую боль мы решаем? | Регистрация занимает 30 минут. | scope-shaping [topic:pain] |
|
|
132
132
|
\`\`\`
|
|
133
133
|
|
|
134
|
-
Multiple tags in one row are allowed when one answer covers several topics: \`[topic:pain] [topic:
|
|
134
|
+
Multiple tags in one row are allowed when one answer covers several topics: \`[topic:pain] [topic:direct-path]\`. Stop-signal rows do NOT need a tag.
|
|
135
135
|
|
|
136
136
|
Stage forcing question lists (id → topic):
|
|
137
137
|
|
|
138
138
|
- **Brainstorm**:
|
|
139
139
|
- \`pain\` — What pain are we solving?
|
|
140
140
|
- \`direct-path\` — What is the most direct path?
|
|
141
|
-
- \`do-nothing\` — What happens if we do nothing?
|
|
142
141
|
- \`operator\` — Who is the operator/user impacted first?
|
|
143
142
|
- \`no-go\` — What are non-negotiable no-go boundaries?
|
|
144
143
|
- **Scope**:
|
|
145
144
|
- \`in-out\` — What is definitely in and definitely out?
|
|
146
145
|
- \`locked-upstream\` — Which decisions are already locked upstream?
|
|
147
|
-
- \`rollback\` — What is the rollback path if this fails?
|
|
148
|
-
- \`failure-modes\` — What are the top failure modes we must design for?
|
|
149
146
|
- **Design**:
|
|
150
147
|
- \`data-flow\` — What is the data flow end-to-end?
|
|
151
148
|
- \`seams\` — Where are the seams/interfaces and ownership boundaries?
|
package/dist/content/skills.d.ts
CHANGED
|
@@ -8,6 +8,16 @@ export declare function outsideVoiceSlotBlock(): string;
|
|
|
8
8
|
export declare function antiSycophancyBlock(): string;
|
|
9
9
|
export declare function noPlaceholdersBlock(): string;
|
|
10
10
|
export declare function watchedFailProofBlock(): string;
|
|
11
|
+
/**
|
|
12
|
+
* Stages that perform real investigation work. The shared
|
|
13
|
+
* `INVESTIGATION_DISCIPLINE_BLOCK` is rendered once per stage skill in this
|
|
14
|
+
* set so the search → graph → narrow-read → draft ladder appears verbatim
|
|
15
|
+
* across the elicitation/spec/plan/tdd/review pipeline. `ship` is excluded:
|
|
16
|
+
* it consumes the upstream trace rather than producing one.
|
|
17
|
+
*/
|
|
18
|
+
export declare const INVESTIGATION_DISCIPLINE_STAGES: ReadonlySet<FlowStage>;
|
|
19
|
+
export declare function investigationDisciplineBlock(): string;
|
|
20
|
+
export declare function behaviorAnchorBlock(stage: FlowStage): string;
|
|
11
21
|
export declare function stageSkillFolder(stage: FlowStage): string;
|
|
12
22
|
export declare function stageSkillMarkdown(stage: FlowStage, track?: FlowTrack): string;
|
|
13
23
|
export declare function executingWavesSkillMarkdown(): string;
|