@hecer/yoke 1.5.0 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +29 -7
- package/README.md +56 -35
- package/canon/context/GLOSSARY.md +11 -0
- package/canon/manifest.yaml +36 -30
- package/canon/skills/ATTRIBUTION.md +28 -0
- package/canon/skills/codebase-design/DEEPENING.md +15 -0
- package/canon/skills/codebase-design/DESIGN-IT-TWICE.md +12 -0
- package/canon/skills/codebase-design/SKILL.md +39 -0
- package/canon/skills/document-release/SKILL.md +5 -0
- package/canon/skills/domain-modeling/ADR-FORMAT.md +19 -0
- package/canon/skills/domain-modeling/CONTEXT-FORMAT.md +39 -0
- package/canon/skills/domain-modeling/SKILL.md +35 -0
- package/canon/skills/no-ai-slop/SKILL.md +103 -0
- package/canon/skills/no-ai-slop/eval.md +43 -0
- package/canon/skills/resolving-merge-conflicts/SKILL.md +18 -0
- package/canon/skills/writing-for-agents/SKILL-MECHANICS.md +27 -0
- package/canon/skills/writing-for-agents/SKILL.md +42 -0
- package/canon/tools/serena.md +5 -1
- package/dist/canon/manifest.js +2 -0
- package/dist/canon/skill-package.js +113 -0
- package/dist/canon/validate.js +16 -1
- package/dist/context/command.js +4 -1
- package/dist/context/context.js +6 -0
- package/dist/loop/dispatcher.js +1 -1
- package/dist/loop/loop.js +26 -0
- package/dist/loop/parallel-command.js +3 -0
- package/dist/loop/run-command.js +11 -0
- package/dist/loop/watchdog.js +28 -11
- package/dist/loop/worker.js +11 -0
- package/dist/retrofit/apply.js +22 -7
- package/dist/retrofit/command.js +4 -1
- package/dist/retrofit/config.js +4 -0
- package/dist/retrofit/context-actions.js +1 -1
- package/dist/retrofit/detect.js +2 -0
- package/dist/retrofit/planners/claude.js +2 -6
- package/dist/retrofit/planners/codex.js +3 -7
- package/dist/retrofit/planners/gemini.js +11 -1
- package/dist/retrofit/report.js +5 -0
- package/dist/retrofit/skill-actions.js +66 -0
- package/dist/retrofit/tools.js +4 -1
- package/dist/retrofit/ui-detect.js +83 -0
- package/dist/scan/gate.js +36 -0
- package/docs/PUBLISHING.md +2 -2
- package/docs/superpowers/plans/2026-08-20-automatic-ui-design-gate.md +59 -0
- package/docs/superpowers/plans/2026-08-20-capability-skills-and-context.md +51 -0
- package/docs/superpowers/plans/2026-08-20-complete-skill-packages-and-invocation.md +59 -0
- package/docs/superpowers/plans/2026-08-20-windows-reliability-and-release.md +67 -0
- package/docs/superpowers/specs/2026-08-20-skill-capabilities-and-reliability-design.md +391 -0
- package/gemini-extension.json +1 -1
- 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).
|
package/canon/tools/serena.md
CHANGED
|
@@ -4,4 +4,8 @@ MIT, MCP-first. The alternative to graphify, selected via `yoke retrofit --code-
|
|
|
4
4
|
|
|
5
5
|
Wired as an MCP server for all three agents. Best for large, strongly-typed codebases (TypeScript, Python, Go) doing systematic refactoring, where missing a caller is costly.
|
|
6
6
|
|
|
7
|
-
Caveat: needs one language server per language (can be fiddly on Windows for exotic languages) and requires `uv`. The launch command is a best-effort template — adjust to your install, e.g. `uvx --from git+https://github.com/oraios/serena serena-mcp-server`.
|
|
7
|
+
Caveat: needs one language server per language (can be fiddly on Windows for exotic languages) and requires `uv`. The launch command is a best-effort template — adjust to your install, e.g. `uvx --from git+https://github.com/oraios/serena serena-mcp-server`.
|
|
8
|
+
|
|
9
|
+
Yoke disables Serena's automatic web-dashboard launch in generated MCP configurations. The
|
|
10
|
+
dashboard server remains available for manual inspection, but selecting Serena no longer opens a
|
|
11
|
+
browser window on every MCP startup.
|
package/dist/canon/manifest.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/canon/validate.js
CHANGED
|
@@ -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
|
}
|
package/dist/context/command.js
CHANGED
|
@@ -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();
|
package/dist/context/context.js
CHANGED
|
@@ -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)',
|
package/dist/loop/dispatcher.js
CHANGED
|
@@ -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,
|
package/dist/loop/run-command.js
CHANGED
|
@@ -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,
|
package/dist/loop/watchdog.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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.
|
package/dist/loop/worker.js
CHANGED
|
@@ -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);
|