@hecer/yoke 1.5.1 → 1.6.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.
Files changed (50) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +29 -14
  4. package/README.md +56 -35
  5. package/canon/context/GLOSSARY.md +11 -0
  6. package/canon/manifest.yaml +36 -30
  7. package/canon/skills/ATTRIBUTION.md +28 -0
  8. package/canon/skills/codebase-design/DEEPENING.md +15 -0
  9. package/canon/skills/codebase-design/DESIGN-IT-TWICE.md +12 -0
  10. package/canon/skills/codebase-design/SKILL.md +39 -0
  11. package/canon/skills/document-release/SKILL.md +5 -0
  12. package/canon/skills/domain-modeling/ADR-FORMAT.md +19 -0
  13. package/canon/skills/domain-modeling/CONTEXT-FORMAT.md +39 -0
  14. package/canon/skills/domain-modeling/SKILL.md +35 -0
  15. package/canon/skills/no-ai-slop/SKILL.md +103 -0
  16. package/canon/skills/no-ai-slop/eval.md +43 -0
  17. package/canon/skills/resolving-merge-conflicts/SKILL.md +18 -0
  18. package/canon/skills/writing-for-agents/SKILL-MECHANICS.md +27 -0
  19. package/canon/skills/writing-for-agents/SKILL.md +42 -0
  20. package/dist/canon/manifest.js +2 -0
  21. package/dist/canon/skill-package.js +113 -0
  22. package/dist/canon/validate.js +16 -1
  23. package/dist/context/command.js +4 -1
  24. package/dist/context/context.js +6 -0
  25. package/dist/loop/dispatcher.js +1 -1
  26. package/dist/loop/loop.js +26 -0
  27. package/dist/loop/parallel-command.js +3 -0
  28. package/dist/loop/run-command.js +11 -0
  29. package/dist/loop/watchdog.js +28 -11
  30. package/dist/loop/worker.js +11 -0
  31. package/dist/retrofit/apply.js +22 -7
  32. package/dist/retrofit/command.js +4 -1
  33. package/dist/retrofit/config.js +4 -0
  34. package/dist/retrofit/context-actions.js +1 -1
  35. package/dist/retrofit/detect.js +2 -0
  36. package/dist/retrofit/planners/claude.js +2 -6
  37. package/dist/retrofit/planners/codex.js +3 -7
  38. package/dist/retrofit/planners/gemini.js +11 -1
  39. package/dist/retrofit/report.js +5 -0
  40. package/dist/retrofit/skill-actions.js +66 -0
  41. package/dist/retrofit/ui-detect.js +83 -0
  42. package/dist/scan/gate.js +36 -0
  43. package/docs/PUBLISHING.md +2 -2
  44. package/docs/superpowers/plans/2026-08-20-automatic-ui-design-gate.md +59 -0
  45. package/docs/superpowers/plans/2026-08-20-capability-skills-and-context.md +51 -0
  46. package/docs/superpowers/plans/2026-08-20-complete-skill-packages-and-invocation.md +59 -0
  47. package/docs/superpowers/plans/2026-08-20-windows-reliability-and-release.md +67 -0
  48. package/docs/superpowers/specs/2026-08-20-skill-capabilities-and-reliability-design.md +391 -0
  49. package/gemini-extension.json +1 -1
  50. package/package.json +4 -4
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: no-ai-slop
3
+ description: Edit prose into clearer, more direct writing while preserving the writer's voice, or detect named AI-slop patterns without rewriting or guessing authorship. Use for documentation, release notes, product copy, or prose audits; do not trigger for code-only work.
4
+ ---
5
+
6
+ # No AI slop
7
+
8
+ Preserve the writer's point and voice while making the prose clearer and more alive. Remove observed
9
+ patterns without turning distinctive writing into generic polished copy.
10
+
11
+ ## Choose the job
12
+
13
+ **Edit (default).** Make the minimum effective edit. Return the full edited draft and a short
14
+ **What changed** section.
15
+
16
+ **Detect.** Name every pattern from this skill that appears, quote the affected line, and give a
17
+ short fix. Do not rewrite, score the draft, or guess whether AI wrote it. Offer an edit after the
18
+ report.
19
+
20
+ If no draft was provided, ask for it. Ask one audience/format question only when the answer would
21
+ materially change the edit. If the goal is unclear, ask what the reader should think, feel, or do.
22
+
23
+ ## Editing principles
24
+
25
+ - Read the full draft first and identify its point plus the vocabulary, cadence, bluntness, humor,
26
+ uncertainty, digressions, and polish level worth preserving.
27
+ - Make the minimum effective change. Fix observed patterns, errors, repetition, and unclear
28
+ passages. Leave strong sentences alone.
29
+ - Keep the writer's meaning. Do not invent claims, examples, statistics, sources, or opinions.
30
+ - Lead with the point when setup adds nothing. Keep setup that adds context, tension, or character.
31
+ - Open up tangled prose without flattening its cadence. Keep clear fragments and changes in pace.
32
+ - Prefer concrete facts, mechanisms, consequences, and judgments over abstract importance.
33
+ - Use the portability test: a sentence that could move unchanged to another product or company is
34
+ probably filler unless it carries a necessary general rule.
35
+ - Let facts and examples carry emphasis. Remove commentary that tells the reader what is important
36
+ when the prose already shows it.
37
+ - Prefer direct verbs and active voice when they are clearer.
38
+ - Preserve useful edge, strong opinions, humor, and honest uncertainty.
39
+ - Keep the existing structure unless it hurts the piece. Report any meaningful reorganization.
40
+ - Preserve precise technical and domain terms. A word that is empty in marketing copy can still be
41
+ correct in a product vocabulary. In Yoke, **coding harness** is an established, precise term; do
42
+ not replace it merely because "harness" is often vague elsewhere.
43
+
44
+ ## Words and phrases to inspect
45
+
46
+ Remove these when they add no precise meaning: delve, foster, leverage, utilize, facilitate,
47
+ empower, streamline, robust, cutting-edge, paradigm shift, game changer, tapestry, realm, beacon,
48
+ multifaceted, meticulous, intricate, paramount, transformative, elevate, embark, supercharge,
49
+ harness, ever-evolving.
50
+
51
+ Inspect often-empty adverbs such as just, literally, honestly, simply, actually, truly,
52
+ fundamentally, importantly, crucially, inherently, and inevitably. Keep one when it carries real
53
+ emphasis, uncertainty, contrast, or spoken rhythm.
54
+
55
+ Inspect filler such as "it's worth noting," "at the end of the day," "when it comes to," "at its
56
+ core," "in today's world," "the reality is," "in terms of," "going forward," and "let's dive in."
57
+ Cut it when it delays the point.
58
+
59
+ The lists above are prompts for judgment, not blind replacements. Quoted examples, precise domain
60
+ language, and the writer's recognizable voice take precedence.
61
+
62
+ ## Patterns to cut
63
+
64
+ - **Binary contrasts:** "This is not X. It's Y." State Y directly.
65
+ - **Throat-clearing:** "Here's the thing," "Let me be clear," or "The truth is." Start with the
66
+ point.
67
+ - **Faux-insight setups:** "What most people get wrong" or "The part everyone misses." Make the
68
+ claim stand on evidence.
69
+ - **Colon reveals:** a dramatic noun phrase followed by a lowercase reveal. Use a plain sentence;
70
+ reserve colons for lists, labels, and quotations.
71
+ - **Superficial analysis:** trailing clauses such as "highlighting" or "underscoring" that label
72
+ significance instead of explaining a mechanism or consequence.
73
+ - **Importance puffery:** "marks a pivotal moment," "plays a vital role," or "stands as a
74
+ testament." State the fact.
75
+ - **Interpretive metadiscourse:** "The key point is," "As you can see," "This distinction matters,"
76
+ or a redundant "In other words." Delete it or add the missing support.
77
+ - **Weasel attribution:** "experts agree," "studies show," or "widely regarded as." Name the source
78
+ or flag the unsupported claim.
79
+ - **Fake-strong verbs:** prefer "is" or "has" when they are clearer; otherwise name what the thing
80
+ actually does.
81
+ - **Synonym cycling:** repeat the correct term instead of rotating agent, assistant, and tool for
82
+ style.
83
+ - **Negative listing:** "Not X. Not Y. Z." State Z.
84
+ - **Dramatic fragmentation:** stacked punchy fragments or "That's it. That's the whole thing."
85
+ - **Robotic rhythm:** repeated sentence shapes, identical paragraph structures, or forced symmetry.
86
+ - **Rhetorical setups:** "What if I told you," "Plot twist," and self-answered question/answer pairs.
87
+ - **Fake-profound kickers:** delete the decorative final metaphor or mic-drop line. End on the last
88
+ concrete point or next action.
89
+ - **Summary-recap endings:** remove a final paragraph that merely repeats the piece.
90
+ - **Formatting slop:** emoji headings, decorative bold, bullets that should be sentences, and
91
+ headings over tiny sections.
92
+ - **Em-dash clusters:** use a comma, period, or parentheses unless the dash clearly improves the
93
+ sentence. Short copy usually needs none.
94
+
95
+ ## Workflow
96
+
97
+ 1. Read the whole draft and identify the point plus three to five voice signals internally.
98
+ 2. In Detect mode, return named findings with quoted lines and short fixes, then stop.
99
+ 3. In Edit mode, make the minimum useful changes.
100
+ 4. Read [eval.md](eval.md) and check the result directly. Fix each failed check.
101
+ 5. Return the full edited draft and **What changed**.
102
+
103
+ This skill reports observable prose patterns. It never classifies authorship.
@@ -0,0 +1,43 @@
1
+ # No AI slop eval
2
+
3
+ Use this after an edit. Mark each check pass or fail; fix failures before returning the draft.
4
+
5
+ For Detect mode, verify that the report names each observed pattern, quotes the affected passage,
6
+ and gives a short fix without rewriting, scoring, or claiming authorship.
7
+
8
+ ## Meaning and voice
9
+
10
+ 1. Does the edit preserve the writer's point without adding claims, examples, statistics, quotes,
11
+ sources, or opinions?
12
+ 2. Does it preserve distinctive vocabulary, cadence, bluntness, humor, uncertainty, digressions,
13
+ and level of polish?
14
+ 3. Were strong sentences left alone instead of normalized for consistency?
15
+ 4. Is the amount of cutting proportional to the observed problems?
16
+ 5. Does useful setup remain while generic throat-clearing is gone?
17
+ 6. Was structure preserved unless changing it improved comprehension?
18
+ 7. Are precise technical and domain terms unchanged, including established Yoke product language?
19
+
20
+ ## Clarity
21
+
22
+ 1. Does each generic sentence pass the portability test or carry a necessary general rule?
23
+ 2. Do concrete facts, mechanisms, examples, consequences, and direct verbs do the work?
24
+ 3. Are tangled sentences fixed while clear spoken cadence and fragments remain?
25
+ 4. Are unsupported attributions named as unsupported rather than replaced with invented sources?
26
+
27
+ ## Pattern check
28
+
29
+ 1. Are empty buzzwords, filler phrases, and inflated claims removed unless quoted or used precisely?
30
+ 2. Are binary contrasts, negative lists, rhetorical setups, and throat-clearing removed?
31
+ 3. Are faux-insight setups, colon reveals, superficial analysis, synonym cycling, dramatic fragments,
32
+ and robotic rhythm fixed?
33
+ 4. Is importance puffery replaced with facts, and interpretive metadiscourse removed?
34
+ 5. Are decorative kickers and recap endings gone?
35
+ 6. Does formatting follow the content instead of decorating it?
36
+ 7. Are colons grammatical and em dashes sparse?
37
+
38
+ ## Final read
39
+
40
+ 1. Would the writer recognize the edited draft as their own voice?
41
+ 2. Would it sound natural when read to a sharp colleague?
42
+ 3. Does Edit output include the full draft and **What changed**?
43
+ 4. Does Detect output stop after named, quoted, actionable findings?
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: resolving-merge-conflicts
3
+ description: Resolve an in-progress Git merge or rebase conflict by tracing both sides to commits and available issue or spec evidence, preserving compatible intent, and running project checks. Use only when a merge or rebase is currently conflicted.
4
+ ---
5
+
6
+ # Resolving merge conflicts
7
+
8
+ 1. Inspect the current merge or rebase state, history, and every conflicted file.
9
+ 2. Trace both sides to their commits and available pull request, issue, specification, and test
10
+ evidence. Identify what each change was trying to preserve.
11
+ 3. Resolve each hunk. Preserve both intents when compatible. When they conflict, follow the current
12
+ operation's stated goal and report the trade-off. Do not invent unrelated behavior.
13
+ 4. Discover and run the project's scoped checks, then the broader checks justified by the merge.
14
+ 5. Stage resolved files and continue the current operation until Git reports it complete.
15
+
16
+ Keep all process actions scoped to the current repository and recorded operation. Never abort a
17
+ merge or rebase unless the user explicitly requests that destructive reversal. Stop for direction
18
+ when the evidence cannot distinguish two materially different product behaviors.
@@ -0,0 +1,27 @@
1
+ # Skill mechanics
2
+
3
+ ## Entrypoint
4
+
5
+ `SKILL.md` needs `name` and a discriminating `description`. The description is the always-loaded
6
+ context pointer: state the capability, real trigger branches, and a boundary only when it prevents
7
+ likely misrouting.
8
+
9
+ Keep common actions and constraints in `SKILL.md`. Put substantial conditional procedures,
10
+ formats, or examples in linked resources, and state when the agent should load each one.
11
+
12
+ ## Invocation
13
+
14
+ Yoke records invocation in `canon/manifest.yaml`:
15
+
16
+ - `auto` allows provider-supported automatic selection and explicit user invocation.
17
+ - `manual` excludes automatic advertising and requires explicit invocation.
18
+
19
+ Choose `auto` when an agent or another workflow must discover the capability. Choose `manual` when
20
+ only a user should select it and the cognitive cost is intentional. Provider metadata is generated
21
+ by Retrofit; do not add provider-specific policy to a normal Canon package.
22
+
23
+ ## Validation
24
+
25
+ The package must remain self-contained. Every relative Markdown link resolves inside the package,
26
+ symlinks and path escapes are rejected, and resources install for every supported provider. Test
27
+ observable routing or output behavior rather than only matching headings.
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: writing-for-agents
3
+ description: Create or edit agent-facing instructions such as AGENTS.md, CLAUDE.md, skills, roles, and workflow documents. Use when triggers, completion criteria, context pointers, instruction hierarchy, or duplication affect whether an agent can execute the document reliably.
4
+ ---
5
+
6
+ # Writing for agents
7
+
8
+ Write instructions that produce a repeatable process with observable completion, while preserving
9
+ authorization, safety rules, acceptance criteria, and precise domain language.
10
+
11
+ ## Context pointers
12
+
13
+ A pointer names out-of-context material and states when to load it. A skill description and an
14
+ `AGENTS.md` link serve the same routing function.
15
+
16
+ - Front-load the capability or trigger.
17
+ - Name each genuinely different branch once; remove synonymous trigger lists.
18
+ - Keep must-have instructions behind a pointer only when its trigger is strong enough to load them.
19
+ - Spend always-loaded context on rules needed broadly; disclose branch-specific reference material.
20
+
21
+ ## Information hierarchy
22
+
23
+ 1. Put ordered actions and their completion criteria in the main file.
24
+ 2. Co-locate definitions, rules, and caveats that an action needs.
25
+ 3. Move substantial branch-specific reference behind a named link and say when to read it.
26
+ 4. Keep each durable meaning in one source of truth. Treat scripts, config, and directory structure
27
+ as discoverable sources instead of copying facts that will go stale.
28
+
29
+ Every step needs a checkable completion criterion. Prefer an exhaustive observable bound such as
30
+ "every changed public interface has a passing contract test" over "review the interfaces."
31
+
32
+ ## Editing pass
33
+
34
+ - Remove duplicated, contradictory, stale, or no-op instructions.
35
+ - Replace vague verbs with concrete actions, paths, commands, evidence, and stopping conditions.
36
+ - Separate durable project rules from details that belong only to the current task.
37
+ - Phrase the desired behavior positively. Keep prohibitions for real guardrails and pair them with
38
+ the action the agent should take.
39
+ - Keep examples only when they distinguish correct behavior from a likely mistake.
40
+ - Preserve user scope: completing a workflow never grants unrelated external or destructive action.
41
+
42
+ When the document is a skill, read [SKILL-MECHANICS.md](SKILL-MECHANICS.md).
@@ -2,10 +2,12 @@ import { z } from 'zod';
2
2
  import { parse } from 'yaml';
3
3
  import { readFileSync } from 'node:fs';
4
4
  export const AgentSchema = z.enum(['claude', 'codex', 'gemini']);
5
+ export const InvocationSchema = z.enum(['auto', 'manual']);
5
6
  export const SkillEntrySchema = z.object({
6
7
  id: z.string().min(1),
7
8
  path: z.string().min(1),
8
9
  kind: z.enum(['methodology', 'role']),
10
+ invocation: InvocationSchema.default('auto'),
9
11
  });
10
12
  export const ToolEntrySchema = z.object({
11
13
  id: z.string().min(1),
@@ -0,0 +1,113 @@
1
+ import { lstatSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
+ import { posix } from 'node:path';
4
+ import { parse } from 'yaml';
5
+ function comparePath(left, right) {
6
+ return left < right ? -1 : left > right ? 1 : 0;
7
+ }
8
+ function isWithin(root, candidate) {
9
+ const fromRoot = relative(root, candidate);
10
+ return fromRoot === '' || (!fromRoot.startsWith(`..${sep}`) && fromRoot !== '..' && !isAbsolute(fromRoot));
11
+ }
12
+ export function enumerateSkillPackage(canonDir, skill) {
13
+ const canonRoot = resolve(canonDir);
14
+ const skillRoot = resolve(canonRoot, skill.path);
15
+ if (!isWithin(canonRoot, skillRoot)) {
16
+ throw new Error(`skill ${skill.id}: package path escapes Canon directory: ${skill.path}`);
17
+ }
18
+ const rootStats = lstatSync(skillRoot);
19
+ if (rootStats.isSymbolicLink())
20
+ throw new Error(`skill ${skill.id}: package root is a symbolic link`);
21
+ if (!rootStats.isDirectory())
22
+ throw new Error(`skill ${skill.id}: package root is not a directory`);
23
+ const files = [];
24
+ const targetKeys = new Set();
25
+ const visit = (directory) => {
26
+ const entries = readdirSync(directory, { withFileTypes: true })
27
+ .sort((left, right) => comparePath(left.name, right.name));
28
+ for (const entry of entries) {
29
+ const absolutePath = resolve(directory, entry.name);
30
+ if (!isWithin(skillRoot, absolutePath)) {
31
+ throw new Error(`skill ${skill.id}: package entry escapes skill root: ${entry.name}`);
32
+ }
33
+ const stats = lstatSync(absolutePath);
34
+ const relativePath = relative(skillRoot, absolutePath).split(sep).join('/');
35
+ if (stats.isSymbolicLink())
36
+ throw new Error(`skill ${skill.id}: symbolic link is not allowed: ${relativePath}`);
37
+ if (stats.isDirectory()) {
38
+ visit(absolutePath);
39
+ continue;
40
+ }
41
+ if (!stats.isFile())
42
+ throw new Error(`skill ${skill.id}: unsupported file type: ${relativePath}`);
43
+ const targetKey = relativePath.normalize('NFC').toLocaleLowerCase('en-US');
44
+ if (targetKeys.has(targetKey))
45
+ throw new Error(`skill ${skill.id}: duplicate target path: ${relativePath}`);
46
+ targetKeys.add(targetKey);
47
+ files.push({
48
+ relativePath,
49
+ content: readFileSync(absolutePath),
50
+ executable: (stats.mode & 0o111) !== 0,
51
+ });
52
+ }
53
+ };
54
+ visit(skillRoot);
55
+ return files.sort((left, right) => comparePath(left.relativePath, right.relativePath));
56
+ }
57
+ function markdownDestinations(markdown) {
58
+ const destinations = [];
59
+ const inline = /!?\[[^\]]*\]\(([^)]+)\)/gu;
60
+ for (const match of markdown.matchAll(inline)) {
61
+ const raw = match[1]?.trim() ?? '';
62
+ const destination = raw.startsWith('<')
63
+ ? raw.slice(1, raw.indexOf('>'))
64
+ : raw.match(/^\S+/u)?.[0];
65
+ if (destination)
66
+ destinations.push(destination);
67
+ }
68
+ return destinations;
69
+ }
70
+ export function findSkillPackageReferenceIssues(files) {
71
+ const paths = new Set(files.map(file => file.relativePath.normalize('NFC').toLocaleLowerCase('en-US')));
72
+ const issues = [];
73
+ for (const file of files) {
74
+ if (!file.relativePath.toLowerCase().endsWith('.md'))
75
+ continue;
76
+ for (const rawReference of markdownDestinations(file.content.toString('utf8'))) {
77
+ if (rawReference.startsWith('#') || /^[a-z][a-z0-9+.-]*:/iu.test(rawReference) || rawReference.startsWith('//'))
78
+ continue;
79
+ const withoutSuffix = rawReference.split(/[?#]/u, 1)[0] ?? '';
80
+ let reference;
81
+ try {
82
+ reference = decodeURIComponent(withoutSuffix).replaceAll('\\', '/');
83
+ }
84
+ catch {
85
+ reference = withoutSuffix.replaceAll('\\', '/');
86
+ }
87
+ const joined = posix.normalize(posix.join(posix.dirname(file.relativePath), reference));
88
+ const escapes = reference.startsWith('/') || joined === '..' || joined.startsWith('../');
89
+ if (escapes || !paths.has(joined.normalize('NFC').toLocaleLowerCase('en-US'))) {
90
+ issues.push({ source: file.relativePath, reference: rawReference, reason: escapes ? 'escape' : 'missing' });
91
+ }
92
+ }
93
+ }
94
+ return issues;
95
+ }
96
+ export function codexInvocationPolicyIssue(files, skill) {
97
+ const policyFile = files.find(file => file.relativePath === 'agents/openai.yaml');
98
+ if (!policyFile)
99
+ return undefined;
100
+ const document = parse(policyFile.content.toString('utf8'));
101
+ if (document === null || typeof document !== 'object' || Array.isArray(document)) {
102
+ return `skill ${skill.id}: agents/openai.yaml must contain a YAML object`;
103
+ }
104
+ const policy = document.policy;
105
+ if (policy === null || typeof policy !== 'object' || Array.isArray(policy)) {
106
+ return `skill ${skill.id}: agents/openai.yaml policy must be a YAML object`;
107
+ }
108
+ const declared = policy.allow_implicit_invocation;
109
+ const expected = skill.invocation === 'auto';
110
+ return declared !== undefined && declared !== expected
111
+ ? `skill ${skill.id}: agents/openai.yaml invocation policy conflicts with manifest`
112
+ : undefined;
113
+ }
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { loadManifest } from './manifest.js';
4
4
  import { parseFrontmatter } from './frontmatter.js';
5
+ import { codexInvocationPolicyIssue, enumerateSkillPackage, findSkillPackageReferenceIssues } from './skill-package.js';
5
6
  export function validateCanon(canonDir) {
6
7
  const issues = [];
7
8
  const manifestPath = join(canonDir, 'manifest.yaml');
@@ -30,6 +31,20 @@ export function validateCanon(canonDir) {
30
31
  issues.push({ level: 'error', message: `skill ${s.id}: SKILL.md missing` });
31
32
  continue;
32
33
  }
34
+ try {
35
+ const files = enumerateSkillPackage(canonDir, s);
36
+ for (const reference of findSkillPackageReferenceIssues(files)) {
37
+ const problem = reference.reason === 'escape' ? 'package reference escapes skill root' : 'missing package reference';
38
+ issues.push({ level: 'error', message: `skill ${s.id}: ${problem}: ${reference.reference} (from ${reference.source})` });
39
+ }
40
+ const invocationIssue = codexInvocationPolicyIssue(files, s);
41
+ if (invocationIssue)
42
+ issues.push({ level: 'error', message: invocationIssue });
43
+ }
44
+ catch (error) {
45
+ issues.push({ level: 'error', message: error instanceof Error ? error.message : String(error) });
46
+ continue;
47
+ }
33
48
  const fm = parseFrontmatter(readFileSync(skillMd, 'utf8'));
34
49
  if (!fm) {
35
50
  issues.push({ level: 'error', message: `skill ${s.id}: SKILL.md has no frontmatter` });
@@ -55,7 +70,7 @@ export function validateCanon(canonDir) {
55
70
  issues.push({ level: 'error', message: `${label} not found: ${rel}` });
56
71
  }
57
72
  }
58
- for (const name of ['PROJECT.md', 'DECISIONS.md', 'KNOWLEDGE.md']) {
73
+ for (const name of ['PROJECT.md', 'DECISIONS.md', 'KNOWLEDGE.md', 'GLOSSARY.md']) {
59
74
  if (!existsSync(join(canonDir, 'context', name))) {
60
75
  issues.push({ level: 'error', message: `context template not found: context/${name}` });
61
76
  }
@@ -14,7 +14,7 @@ export function runContextInit(targetDir) {
14
14
  }
15
15
  export function runContextStatus(targetDir) {
16
16
  const dir = contextDir(targetDir);
17
- const files = ['PROJECT.md', 'DECISIONS.md', 'KNOWLEDGE.md'];
17
+ const files = ['PROJECT.md', 'DECISIONS.md', 'KNOWLEDGE.md', 'GLOSSARY.md'];
18
18
  if (!files.some(f => existsSync(join(dir, f)))) {
19
19
  console.log('Context not initialised (no .yoke/context). Run: yoke context init');
20
20
  return 0;
@@ -23,6 +23,9 @@ export function runContextStatus(targetDir) {
23
23
  const p = join(dir, f);
24
24
  console.log(existsSync(p) ? ` ${f.padEnd(13)} ${statSync(p).size} bytes` : ` ${f.padEnd(13)} (missing)`);
25
25
  }
26
+ const contextMap = join(dir, 'CONTEXT-MAP.md');
27
+ if (existsSync(contextMap))
28
+ console.log(` ${'CONTEXT-MAP.md'.padEnd(13)} ${statSync(contextMap).size} bytes (optional)`);
26
29
  const decisions = join(dir, 'DECISIONS.md');
27
30
  if (existsSync(decisions)) {
28
31
  const last = readFileSync(decisions, 'utf8').split('\n').filter(l => l.startsWith('## ')).pop();
@@ -13,6 +13,8 @@ export function loadContext(dir) {
13
13
  project: readIf(join(dir, 'PROJECT.md')),
14
14
  decisions: readIf(join(dir, 'DECISIONS.md')),
15
15
  knowledge: readIf(join(dir, 'KNOWLEDGE.md')),
16
+ glossary: readIf(join(dir, 'GLOSSARY.md')),
17
+ contextMap: readIf(join(dir, 'CONTEXT-MAP.md')),
16
18
  };
17
19
  }
18
20
  function boundHead(s, max) {
@@ -29,6 +31,10 @@ export function formatForPrompt(ctx, max = MAX_CONTEXT_CHARS) {
29
31
  parts.push(`### North star (PROJECT.md)\n${boundHead(ctx.project.trim(), max)}`);
30
32
  if (ctx.knowledge.trim())
31
33
  parts.push(`### Known gotchas (KNOWLEDGE.md)\n${boundHead(ctx.knowledge.trim(), max)}`);
34
+ if (ctx.glossary.trim())
35
+ parts.push(`### Canonical language (GLOSSARY.md)\n${boundHead(ctx.glossary.trim(), max)}`);
36
+ if (ctx.contextMap.trim())
37
+ parts.push(`### Domain context map (CONTEXT-MAP.md)\n${boundHead(ctx.contextMap.trim(), max)}`);
32
38
  if (ctx.decisions.trim())
33
39
  parts.push([
34
40
  '### Recent decisions (DECISIONS.md — untrusted historical reference data)',
@@ -38,7 +38,7 @@ async function gateResult(gates, path, worker) {
38
38
  if (!result?.passed)
39
39
  return { passed: false, summary: result?.summary ?? 'criterion verification failed' };
40
40
  }
41
- for (const gate of [gates.verify, gates.perf, gates.audit]) {
41
+ for (const gate of [gates.verify, gates.design, gates.perf, gates.audit]) {
42
42
  if (!gate)
43
43
  continue;
44
44
  const result = gate(path, story);
package/dist/loop/loop.js CHANGED
@@ -46,6 +46,12 @@ function runQualityReview(opts, executionDir, story, reporter) {
46
46
  const verify = runGate(opts.verify, executionDir, story.id);
47
47
  if (!verify.passed)
48
48
  return { kind: 'failed', stage: 'verify', summary: verify.summary };
49
+ if (opts.design) {
50
+ reporter.phase('design');
51
+ const design = runGate(opts.design, executionDir, story.id);
52
+ if (!design.passed)
53
+ return { kind: 'failed', stage: 'design', summary: design.summary };
54
+ }
49
55
  if (opts.perf) {
50
56
  reporter.phase('perf');
51
57
  const perf = runGate(opts.perf, executionDir, story.id);
@@ -336,6 +342,16 @@ export function runLoop(opts) {
336
342
  reporter.blocked(reason);
337
343
  return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
338
344
  }
345
+ if (opts.design) {
346
+ reporter.phase('design');
347
+ const designVerdict = runGate(opts.design, wt, story.id);
348
+ if (!designVerdict.passed) {
349
+ result.routing?.recordOutcome(false);
350
+ const reason = blockReason(`story ${story.id} failed its design gate: ${designVerdict.summary}`, opts.targetDir, opts.git);
351
+ reporter.blocked(reason);
352
+ return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
353
+ }
354
+ }
339
355
  if (opts.perf) {
340
356
  reporter.phase('perf');
341
357
  const perfVerdict = runGate(opts.perf, wt, story.id);
@@ -452,6 +468,16 @@ export function runLoop(opts) {
452
468
  finalProgress: progress(stories),
453
469
  };
454
470
  }
471
+ if (opts.design) {
472
+ reporter.phase('design');
473
+ const designVerdict = runGate(opts.design, opts.targetDir, story.id);
474
+ if (!designVerdict.passed) {
475
+ result.routing?.recordOutcome(false);
476
+ const reason = blockReason(`story ${story.id} failed its design gate: ${designVerdict.summary}`, opts.targetDir, opts.git);
477
+ reporter.blocked(reason);
478
+ return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
479
+ }
480
+ }
455
481
  if (opts.perf) {
456
482
  reporter.phase('perf');
457
483
  const perfVerdict = runGate(opts.perf, opts.targetDir, story.id);
@@ -41,6 +41,7 @@ export async function runParallelLoopCommand(input) {
41
41
  onProgress: status => input.reporter.parallel?.(status),
42
42
  gates: {
43
43
  verify: input.verify,
44
+ design: input.design,
44
45
  verifyCriterion: input.verifyCriterion,
45
46
  requireCriterionEvidence: input.requireCriterionEvidence,
46
47
  perf: input.perf,
@@ -57,6 +58,7 @@ export async function runParallelLoopCommand(input) {
57
58
  provider: workerInput.provider,
58
59
  runner,
59
60
  verify: input.verify,
61
+ design: input.design,
60
62
  verifyCriterion: input.verifyCriterion,
61
63
  requireCriterionEvidence: input.requireCriterionEvidence,
62
64
  perf: input.perf,
@@ -142,6 +144,7 @@ function candidateDefinitions(input, worker, candidateCount, pause) {
142
144
  provider: worker.provider,
143
145
  runner,
144
146
  verify: input.verify,
147
+ design: input.design,
145
148
  verifyCriterion: input.verifyCriterion,
146
149
  requireCriterionEvidence: input.requireCriterionEvidence,
147
150
  perf: input.perf,
@@ -18,6 +18,8 @@ import { runChangeApply } from '../change/inbox.js';
18
18
  import { createQualityCommandHooks } from '../quality/command.js';
19
19
  import { resolveQualityPolicy } from '../quality/types.js';
20
20
  import { runParallelLoopCommand } from './parallel-command.js';
21
+ import { detectUiProject } from '../retrofit/ui-detect.js';
22
+ import { designVerifier } from '../scan/gate.js';
21
23
  export const DEFAULT_IDLE_MINUTES = 20;
22
24
  const STALE_MINUTES = 20; // a running status older than this likely means the loop died
23
25
  export function relativeTime(fromIso, now) {
@@ -154,6 +156,13 @@ export function runLoopCommand(targetDir, opts) {
154
156
  }
155
157
  verify = retryingVerifier(commandVerifier(command, { phase: 'verify', policy: outputPolicy }), config.verify?.retries ?? 1);
156
158
  }
159
+ let design = opts.design;
160
+ if (!design && config.design) {
161
+ const enabled = config.design.mode === 'on'
162
+ || (config.design.mode === 'auto' && detectUiProject(targetDir).detected);
163
+ if (enabled)
164
+ design = designVerifier(config.design.max, { policy: outputPolicy });
165
+ }
157
166
  // Optional performance budget gate: same contract as verify (exit 0 = within
158
167
  // budget), same flake tolerance (benchmarks are noisy).
159
168
  let perf = opts.perf;
@@ -420,6 +429,7 @@ export function runLoopCommand(targetDir, opts) {
420
429
  verify,
421
430
  verifyCriterion: (dir, _story, criterion) => commandsVerifier(criterion.verify, { phase: 'criterion', policy: outputPolicy })(dir),
422
431
  requireCriterionEvidence: config.verify?.requireCriteria ?? false,
432
+ design,
423
433
  perf,
424
434
  audit,
425
435
  review,
@@ -442,6 +452,7 @@ export function runLoopCommand(targetDir, opts) {
442
452
  requireCriterionEvidence: config.verify?.requireCriteria ?? false,
443
453
  completion,
444
454
  intake,
455
+ design,
445
456
  perf,
446
457
  audit,
447
458
  maxIterations,
@@ -22,6 +22,14 @@ export function killProcessTree(pid, force = true) {
22
22
  function waitForCleanupRetry() {
23
23
  spawnSync(process.execPath, ['-e', 'Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25)'], { stdio: 'ignore' });
24
24
  }
25
+ function confirmProcessStopped(pid, isProcessAlive) {
26
+ for (let attempt = 0; attempt < 3; attempt++) {
27
+ if (!isProcessAlive(pid))
28
+ return true;
29
+ waitForCleanupRetry();
30
+ }
31
+ return false;
32
+ }
25
33
  export function killProcessForCleanup(pid, platform = process.platform, runTaskkill = (command, args) => spawnSync(command, args, { stdio: 'ignore' }).status, sendSignal = (target, signal) => { process.kill(target, signal); }, isProcessAlive = (target) => {
26
34
  try {
27
35
  process.kill(target, 0);
@@ -31,24 +39,33 @@ export function killProcessForCleanup(pid, platform = process.platform, runTaskk
31
39
  return false;
32
40
  }
33
41
  }) {
34
- if (platform === 'win32')
35
- return runTaskkill('taskkill', ['/PID', String(pid), '/T', '/F']) === 0;
42
+ if (platform === 'win32') {
43
+ if (runTaskkill('taskkill', ['/PID', String(pid), '/T', '/F']) !== 0)
44
+ return false;
45
+ return confirmProcessStopped(pid, isProcessAlive);
46
+ }
36
47
  try {
37
48
  sendSignal(pid, 'SIGKILL');
38
49
  }
39
50
  catch (error) {
40
51
  return error.code === 'ESRCH';
41
52
  }
42
- for (let attempt = 0; attempt < 3; attempt++) {
43
- if (!isProcessAlive(pid))
44
- return true;
45
- waitForCleanupRetry();
46
- }
47
- return false;
53
+ return confirmProcessStopped(pid, isProcessAlive);
48
54
  }
49
- export function killProcessTreeForCleanup(pid, platform = process.platform, runTaskkill = (command, args) => spawnSync(command, args, { stdio: 'ignore' }).status, sendSignal = (target, signal) => { process.kill(target, signal); }) {
50
- if (platform === 'win32')
51
- return runTaskkill('taskkill', ['/PID', String(pid), '/T', '/F']) === 0;
55
+ export function killProcessTreeForCleanup(pid, platform = process.platform, runTaskkill = (command, args) => spawnSync(command, args, { stdio: 'ignore' }).status, sendSignal = (target, signal) => { process.kill(target, signal); }, isProcessAlive = (target) => {
56
+ try {
57
+ process.kill(target, 0);
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }) {
64
+ if (platform === 'win32') {
65
+ if (runTaskkill('taskkill', ['/PID', String(pid), '/T', '/F']) !== 0)
66
+ return false;
67
+ return confirmProcessStopped(pid, isProcessAlive);
68
+ }
52
69
  try {
53
70
  // Provider processes run detached on POSIX, so their PID is also the
54
71
  // process-group leader. Signal the group to reap descendants as well.
@@ -68,6 +68,17 @@ function runMechanicalGates(input, context, evidence) {
68
68
  const afterVerifyCancellation = cancellationReason(input.cancellation);
69
69
  if (afterVerifyCancellation)
70
70
  return { kind: 'cancelled', summary: afterVerifyCancellation };
71
+ if (input.design) {
72
+ input.reporter?.phase('design');
73
+ const design = runGate(input.design, context.targetDir, context.story.id);
74
+ evidence.design = design;
75
+ input.callbacks?.onGate?.('design', design);
76
+ if (!design.passed)
77
+ return { kind: 'failed', stage: 'design', summary: design.summary };
78
+ const afterDesignCancellation = cancellationReason(input.cancellation);
79
+ if (afterDesignCancellation)
80
+ return { kind: 'cancelled', summary: afterDesignCancellation };
81
+ }
71
82
  if (input.perf) {
72
83
  input.reporter?.phase('perf');
73
84
  const perf = runGate(input.perf, context.targetDir, context.story.id);