@hecer/yoke 1.5.1 → 1.6.1
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 +13 -13
- package/.codex-plugin/plugin.json +7 -7
- package/CHANGELOG.md +280 -259
- package/README.md +855 -834
- package/TODOS.md +5 -5
- package/agents/docs.toml +6 -6
- package/agents/implementer.toml +6 -6
- package/agents/reviewer.toml +6 -6
- package/agents/security.toml +6 -6
- package/bench/README.md +86 -86
- package/bench/RESULTS.md +35 -35
- package/bench/output-compaction.mjs +65 -65
- package/bench/result-schema.mjs +12 -12
- package/bench/results/claude-2026-07-27T18-03-26.json +50 -50
- package/bench/results/codex-unavailable-1785175418318.json +15 -15
- package/bench/results/gemini-2026-07-27T18-03-44.json +46 -46
- package/bench/run-matrix.mjs +26 -26
- package/bench/run.mjs +106 -106
- package/canon/AGENTS.md +30 -30
- package/canon/context/DECISIONS.md +4 -4
- package/canon/context/GLOSSARY.md +11 -0
- package/canon/context/KNOWLEDGE.md +4 -4
- package/canon/context/PROJECT.md +15 -15
- package/canon/loop/loop-spec.md +65 -65
- package/canon/loop/prd.schema.md +43 -43
- package/canon/manifest.yaml +59 -53
- package/canon/policy/gates.md +7 -7
- package/canon/policy/roles.md +9 -9
- package/canon/skills/ATTRIBUTION.md +99 -71
- package/canon/skills/authoring-prd/SKILL.md +58 -58
- package/canon/skills/brainstorming/SKILL.md +164 -164
- 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/dispatching-parallel-agents/SKILL.md +182 -182
- package/canon/skills/document-release/SKILL.md +302 -297
- 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/executing-plans/SKILL.md +70 -70
- package/canon/skills/finishing-a-development-branch/SKILL.md +200 -200
- package/canon/skills/health/SKILL.md +177 -177
- package/canon/skills/maintaining-context/SKILL.md +34 -34
- package/canon/skills/minimal-code/SKILL.md +21 -21
- package/canon/skills/no-ai-slop/SKILL.md +103 -0
- package/canon/skills/no-ai-slop/eval.md +43 -0
- package/canon/skills/plan-ceo-review/SKILL.md +541 -541
- package/canon/skills/plan-eng-review/SKILL.md +362 -362
- package/canon/skills/receiving-code-review/SKILL.md +213 -213
- package/canon/skills/requesting-code-review/SKILL.md +105 -105
- package/canon/skills/resolving-merge-conflicts/SKILL.md +18 -0
- package/canon/skills/retro/SKILL.md +397 -397
- package/canon/skills/review/SKILL.md +246 -246
- package/canon/skills/ship/SKILL.md +691 -691
- package/canon/skills/subagent-driven-development/SKILL.md +277 -277
- package/canon/skills/systematic-debugging/SKILL.md +296 -296
- package/canon/skills/tdd/SKILL.md +371 -371
- package/canon/skills/unslop-ui/SKILL.md +34 -34
- package/canon/skills/using-git-worktrees/SKILL.md +218 -218
- package/canon/skills/verification-before-completion/SKILL.md +139 -139
- package/canon/skills/visual-verification/SKILL.md +54 -54
- package/canon/skills/workflow/SKILL.md +22 -22
- package/canon/skills/writing-for-agents/SKILL-MECHANICS.md +27 -0
- package/canon/skills/writing-for-agents/SKILL.md +42 -0
- package/canon/skills/writing-plans/SKILL.md +152 -152
- package/canon/skills/writing-skills/SKILL.md +655 -655
- package/canon/skills/yoke-retrofit/SKILL.md +26 -26
- package/canon/skills/yoke-workflow/SKILL.md +20 -20
- package/canon/tools/codex-rtk-hook.mjs +35 -35
- package/canon/tools/graphify.md +3 -3
- package/canon/tools/playwright-mcp.md +3 -3
- package/canon/tools/rtk.md +7 -7
- package/canon/tools/serena.md +6 -6
- package/dist/agents/process.js +3 -0
- 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/prd/command.js +17 -17
- 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 +16 -20
- package/dist/retrofit/planners/codex.js +3 -7
- package/dist/retrofit/planners/gemini.js +11 -1
- package/dist/retrofit/preserve.js +2 -2
- package/dist/retrofit/report.js +5 -0
- package/dist/retrofit/skill-actions.js +66 -0
- package/dist/retrofit/ui-detect.js +83 -0
- package/dist/scan/gate.js +36 -0
- package/docs/MIGRATING-TO-1.0.md +33 -33
- package/docs/MIGRATING-TO-1.1.md +27 -27
- package/docs/MIGRATING-TO-1.4.md +70 -70
- package/docs/PUBLISHING.md +91 -91
- package/docs/superpowers/plans/2026-06-28-baustein-e-context-layer.md +981 -981
- package/docs/superpowers/plans/2026-06-29-baustein-f-routing.md +258 -258
- package/docs/superpowers/plans/2026-06-29-baustein-g-loop-observability.md +1006 -1006
- package/docs/superpowers/plans/2026-06-29-baustein-h-loop-robustness.md +374 -374
- package/docs/superpowers/plans/2026-06-30-baustein-i-visual-design-verification.md +450 -450
- package/docs/superpowers/plans/2026-07-02-baustein-k-zero-to-100-bootstrap.md +1024 -1024
- package/docs/superpowers/plans/2026-07-02-baustein-m-flow-smoke-proofs.md +574 -574
- package/docs/superpowers/plans/2026-08-13-gauntlet-quality-loop.md +537 -537
- package/docs/superpowers/plans/2026-08-16-artifact-backed-output-compaction.md +329 -329
- 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-06-28-baustein-e-context-layer-design.md +146 -146
- package/docs/superpowers/specs/2026-06-29-baustein-f-routing-design.md +106 -106
- package/docs/superpowers/specs/2026-06-29-baustein-g-loop-observability-design.md +186 -186
- package/docs/superpowers/specs/2026-06-29-baustein-h-loop-robustness-design.md +113 -113
- package/docs/superpowers/specs/2026-06-30-baustein-i-visual-design-verification-design.md +98 -98
- package/docs/superpowers/specs/2026-07-02-baustein-k-zero-to-100-bootstrap-design.md +200 -200
- package/docs/superpowers/specs/2026-07-02-baustein-m-flow-smoke-proofs-design.md +155 -155
- package/docs/superpowers/specs/2026-08-13-gauntlet-quality-loop-design.md +422 -422
- package/docs/superpowers/specs/2026-08-16-artifact-backed-output-compaction-design.md +166 -166
- package/docs/superpowers/specs/2026-08-20-skill-capabilities-and-reliability-design.md +391 -0
- package/gemini-extension.json +6 -6
- package/hooks/hooks.json +19 -19
- package/package.json +84 -84
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: yoke-retrofit
|
|
3
|
-
description: Use when asked to "retrofit", "yoke this project", or set up the Yoke harness in a project — runs the shared setup wizard and configures the same behavior for Claude, Codex, and Gemini.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Yoke Retrofit
|
|
7
|
-
|
|
8
|
-
Set up or update Yoke through the shared `yoke setup` contract.
|
|
9
|
-
|
|
10
|
-
1. Inspect the project and identify the current host (`claude`, `codex`, or `gemini`).
|
|
11
|
-
2. Ask these setup questions one at a time and give a direct recommendation:
|
|
12
|
-
- target agents (recommend the current host; use `all` for deliberately cross-agent projects),
|
|
13
|
-
- code-graph tool,
|
|
14
|
-
- autonomous loop on/off,
|
|
15
|
-
- default runner (recommend the current host),
|
|
16
|
-
- decision mode: `auto` or `critical`.
|
|
17
|
-
3. Recommend the code graph based on this project:
|
|
18
|
-
- **Serena** is LSP-accurate and best for large typed codebases or systematic symbol refactors where a missed reference is costly. It needs a language server per language.
|
|
19
|
-
- **graphify** is fast and multimodal, and is best for exploration, migration, onboarding, or mixed code and document repositories. Its graph is an index and can become stale.
|
|
20
|
-
4. Apply the answers without a second round of prompts:
|
|
21
|
-
`yoke setup . --yes --host=<host> --agent=<agents> --code-graph=<choice> --runner=<runner> --decision-policy=<auto|critical> --loop|--no-loop`.
|
|
22
|
-
A human who runs `yoke setup .` directly receives the same five terminal questions.
|
|
23
|
-
5. Show the generated report and backup paths. Existing files are backed up under `.yoke/backup/`; settings are merged where supported.
|
|
24
|
-
6. If an old generated `CLAUDE.md` or `GEMINI.md` contained project-specific instructions, restore them inside its `<!-- yoke:preserve:start -->` / `<!-- yoke:preserve:end -->` block. Preserve blocks survive every later retrofit.
|
|
25
|
-
|
|
26
|
-
The generated harness includes the provider-neutral `yoke-workflow` skill. It owns the planning questions, approved-plan handoff, autonomous stories, and critical-decision resume flow.
|
|
1
|
+
---
|
|
2
|
+
name: yoke-retrofit
|
|
3
|
+
description: Use when asked to "retrofit", "yoke this project", or set up the Yoke harness in a project — runs the shared setup wizard and configures the same behavior for Claude, Codex, and Gemini.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Yoke Retrofit
|
|
7
|
+
|
|
8
|
+
Set up or update Yoke through the shared `yoke setup` contract.
|
|
9
|
+
|
|
10
|
+
1. Inspect the project and identify the current host (`claude`, `codex`, or `gemini`).
|
|
11
|
+
2. Ask these setup questions one at a time and give a direct recommendation:
|
|
12
|
+
- target agents (recommend the current host; use `all` for deliberately cross-agent projects),
|
|
13
|
+
- code-graph tool,
|
|
14
|
+
- autonomous loop on/off,
|
|
15
|
+
- default runner (recommend the current host),
|
|
16
|
+
- decision mode: `auto` or `critical`.
|
|
17
|
+
3. Recommend the code graph based on this project:
|
|
18
|
+
- **Serena** is LSP-accurate and best for large typed codebases or systematic symbol refactors where a missed reference is costly. It needs a language server per language.
|
|
19
|
+
- **graphify** is fast and multimodal, and is best for exploration, migration, onboarding, or mixed code and document repositories. Its graph is an index and can become stale.
|
|
20
|
+
4. Apply the answers without a second round of prompts:
|
|
21
|
+
`yoke setup . --yes --host=<host> --agent=<agents> --code-graph=<choice> --runner=<runner> --decision-policy=<auto|critical> --loop|--no-loop`.
|
|
22
|
+
A human who runs `yoke setup .` directly receives the same five terminal questions.
|
|
23
|
+
5. Show the generated report and backup paths. Existing files are backed up under `.yoke/backup/`; settings are merged where supported.
|
|
24
|
+
6. If an old generated `CLAUDE.md` or `GEMINI.md` contained project-specific instructions, restore them inside its `<!-- yoke:preserve:start -->` / `<!-- yoke:preserve:end -->` block. Preserve blocks survive every later retrofit.
|
|
25
|
+
|
|
26
|
+
The generated harness includes the provider-neutral `yoke-workflow` skill. It owns the planning questions, approved-plan handoff, autonomous stories, and critical-decision resume flow.
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: yoke-workflow
|
|
3
|
-
description: Use when the user asks Yoke to plan and build a feature, run stories autonomously, continue a Yoke loop, or only interrupt for major decisions.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Yoke Workflow
|
|
7
|
-
|
|
8
|
-
Provide the same interaction contract in Claude, Codex, and Gemini.
|
|
9
|
-
|
|
10
|
-
1. Read `.yoke/config.yaml`. If it is missing, offer `yoke setup . --host=<current-agent>` and run the setup flow before planning.
|
|
11
|
-
2. Plan before starting the loop. Inspect the project, then ask one focused question at a time only where the answer changes product behavior, scope, architecture, security, data ownership, external cost, or an irreversible choice. Include a recommended answer. Resolve routine implementation details yourself.
|
|
12
|
-
3. Summarize the agreed design in `.yoke/plan.md`, including goals, non-goals, constraints, and decisions. Use the `authoring-prd` skill to turn it into small stories with testable acceptance criteria. Run `yoke prd check .`.
|
|
13
|
-
4. Ask once for approval of the complete plan and story set. Do not begin implementation before that approval.
|
|
14
|
-
5. If `loop.enabled` is true, run the stories without routine follow-up questions using the configured runner. Prefer `yoke loop run . --max=5 --isolate`, report status after each batch, and continue until complete or genuinely blocked.
|
|
15
|
-
6. Respect `loop.decisionPolicy`:
|
|
16
|
-
- `auto`: choose the most suitable option from the plan, current code, and established conventions. Record the interpretation and continue.
|
|
17
|
-
- `critical`: routine ambiguity is still resolved automatically. If the loop reports a pending critical decision, run `yoke loop decision .`, present its options and recommendation to the user, ask exactly that question, then run `yoke loop answer . --choice=<id> --rationale="<answer>"`. The answer command records the decision and resumes the same story.
|
|
18
|
-
7. Never ask whether to run tests, review, commit, or continue to the next approved story. Those are part of the approved workflow.
|
|
19
|
-
|
|
20
|
-
The user's configured commit identity is authoritative. Do not add an AI co-author unless `commit.allowCoAuthors` explicitly permits it.
|
|
1
|
+
---
|
|
2
|
+
name: yoke-workflow
|
|
3
|
+
description: Use when the user asks Yoke to plan and build a feature, run stories autonomously, continue a Yoke loop, or only interrupt for major decisions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Yoke Workflow
|
|
7
|
+
|
|
8
|
+
Provide the same interaction contract in Claude, Codex, and Gemini.
|
|
9
|
+
|
|
10
|
+
1. Read `.yoke/config.yaml`. If it is missing, offer `yoke setup . --host=<current-agent>` and run the setup flow before planning.
|
|
11
|
+
2. Plan before starting the loop. Inspect the project, then ask one focused question at a time only where the answer changes product behavior, scope, architecture, security, data ownership, external cost, or an irreversible choice. Include a recommended answer. Resolve routine implementation details yourself.
|
|
12
|
+
3. Summarize the agreed design in `.yoke/plan.md`, including goals, non-goals, constraints, and decisions. Use the `authoring-prd` skill to turn it into small stories with testable acceptance criteria. Run `yoke prd check .`.
|
|
13
|
+
4. Ask once for approval of the complete plan and story set. Do not begin implementation before that approval.
|
|
14
|
+
5. If `loop.enabled` is true, run the stories without routine follow-up questions using the configured runner. Prefer `yoke loop run . --max=5 --isolate`, report status after each batch, and continue until complete or genuinely blocked.
|
|
15
|
+
6. Respect `loop.decisionPolicy`:
|
|
16
|
+
- `auto`: choose the most suitable option from the plan, current code, and established conventions. Record the interpretation and continue.
|
|
17
|
+
- `critical`: routine ambiguity is still resolved automatically. If the loop reports a pending critical decision, run `yoke loop decision .`, present its options and recommendation to the user, ask exactly that question, then run `yoke loop answer . --choice=<id> --rationale="<answer>"`. The answer command records the decision and resumes the same story.
|
|
18
|
+
7. Never ask whether to run tests, review, commit, or continue to the next approved story. Those are part of the approved workflow.
|
|
19
|
+
|
|
20
|
+
The user's configured commit identity is authoritative. Do not add an AI co-author unless `commit.allowCoAuthors` explicitly permits it.
|
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
import { pathToFileURL } from 'node:url'
|
|
4
|
-
|
|
5
|
-
function rtkCheck(command) {
|
|
6
|
-
const result = spawnSync('rtk', ['hook', 'check', command], { encoding: 'utf8', timeout: 3000 })
|
|
7
|
-
return result.status === 0 ? result.stdout.trim() : ''
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function rewriteHookInput(input, check = rtkCheck) {
|
|
11
|
-
if (input?.tool_name !== 'Bash' && input?.toolName !== 'Bash') return null
|
|
12
|
-
const toolInput = input.tool_input ?? input.toolInput
|
|
13
|
-
const command = toolInput?.command
|
|
14
|
-
if (typeof command !== 'string' || command.trim() === '') return null
|
|
15
|
-
const rewritten = check(command)
|
|
16
|
-
if (!rewritten || rewritten === command) return null
|
|
17
|
-
return {
|
|
18
|
-
hookSpecificOutput: {
|
|
19
|
-
hookEventName: 'PreToolUse',
|
|
20
|
-
updatedInput: { ...toolInput, command: rewritten },
|
|
21
|
-
},
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function main() {
|
|
26
|
-
let raw = ''
|
|
27
|
-
for await (const chunk of process.stdin) raw += chunk
|
|
28
|
-
try {
|
|
29
|
-
const output = rewriteHookInput(JSON.parse(raw))
|
|
30
|
-
if (output) process.stdout.write(JSON.stringify(output))
|
|
31
|
-
} catch {
|
|
32
|
-
// Compression is an optimization. Malformed input must never block Codex.
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) await main()
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
function rtkCheck(command) {
|
|
6
|
+
const result = spawnSync('rtk', ['hook', 'check', command], { encoding: 'utf8', timeout: 3000 })
|
|
7
|
+
return result.status === 0 ? result.stdout.trim() : ''
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function rewriteHookInput(input, check = rtkCheck) {
|
|
11
|
+
if (input?.tool_name !== 'Bash' && input?.toolName !== 'Bash') return null
|
|
12
|
+
const toolInput = input.tool_input ?? input.toolInput
|
|
13
|
+
const command = toolInput?.command
|
|
14
|
+
if (typeof command !== 'string' || command.trim() === '') return null
|
|
15
|
+
const rewritten = check(command)
|
|
16
|
+
if (!rewritten || rewritten === command) return null
|
|
17
|
+
return {
|
|
18
|
+
hookSpecificOutput: {
|
|
19
|
+
hookEventName: 'PreToolUse',
|
|
20
|
+
updatedInput: { ...toolInput, command: rewritten },
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
let raw = ''
|
|
27
|
+
for await (const chunk of process.stdin) raw += chunk
|
|
28
|
+
try {
|
|
29
|
+
const output = rewriteHookInput(JSON.parse(raw))
|
|
30
|
+
if (output) process.stdout.write(JSON.stringify(output))
|
|
31
|
+
} catch {
|
|
32
|
+
// Compression is an optimization. Malformed input must never block Codex.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) await main()
|
package/canon/tools/graphify.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# Tool: graphify (code-graph)
|
|
2
|
-
|
|
3
|
-
MIT, multimodal code/doc graph. Wired as an MCP server for all three agents (stdio). Prefer symbol/graph lookups over reading whole files. Caveat: heuristic edges (INFERRED/AMBIGUOUS) and a static index that can go stale — rebuild on significant changes.
|
|
1
|
+
# Tool: graphify (code-graph)
|
|
2
|
+
|
|
3
|
+
MIT, multimodal code/doc graph. Wired as an MCP server for all three agents (stdio). Prefer symbol/graph lookups over reading whole files. Caveat: heuristic edges (INFERRED/AMBIGUOUS) and a static index that can go stale — rebuild on significant changes.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# Tool: Playwright MCP (browser / dogfooding)
|
|
2
|
-
|
|
3
|
-
Microsoft Playwright MCP, Apache-2.0. Wired as an MCP server for all three agents — the only browser tool with native MCP parity across Claude/Codex/Gemini. Used for QA, dogfooding user flows, screenshots, and deploy verification.
|
|
1
|
+
# Tool: Playwright MCP (browser / dogfooding)
|
|
2
|
+
|
|
3
|
+
Microsoft Playwright MCP, Apache-2.0. Wired as an MCP server for all three agents — the only browser tool with native MCP parity across Claude/Codex/Gemini. Used for QA, dogfooding user flows, screenshots, and deploy verification.
|
package/canon/tools/rtk.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Tool: rtk (token compression)
|
|
2
|
-
|
|
3
|
-
Per-agent wiring (generated by Baustein B):
|
|
4
|
-
|
|
5
|
-
- **Claude Code:** PreToolUse hook auto-rewrites commands (`git status` → `rtk git status`). On Windows this needs WSL; otherwise fall back to instruction mode (this file injected into CLAUDE.md telling the agent to prefix commands with `rtk`).
|
|
6
|
-
- **Codex CLI:** inject `RTK.md` / AGENTS.md instruction.
|
|
7
|
-
- **Gemini CLI:** no hook system — register rtk as an MCP tool or inject the GEMINI.md instruction.
|
|
1
|
+
# Tool: rtk (token compression)
|
|
2
|
+
|
|
3
|
+
Per-agent wiring (generated by Baustein B):
|
|
4
|
+
|
|
5
|
+
- **Claude Code:** PreToolUse hook auto-rewrites commands (`git status` → `rtk git status`). On Windows this needs WSL; otherwise fall back to instruction mode (this file injected into CLAUDE.md telling the agent to prefix commands with `rtk`).
|
|
6
|
+
- **Codex CLI:** inject `RTK.md` / AGENTS.md instruction.
|
|
7
|
+
- **Gemini CLI:** no hook system — register rtk as an MCP tool or inject the GEMINI.md instruction.
|
package/canon/tools/serena.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# Tool: Serena (code-graph, LSP-accurate)
|
|
2
|
-
|
|
3
|
-
MIT, MCP-first. The alternative to graphify, selected via `yoke retrofit --code-graph=serena`. Serena uses real language servers (LSP) for symbol-accurate, cross-file retrieval and refactoring (`find_symbol`, `find_referencing_symbols`, rename/move) — no static index that goes stale, so it will not miss a reference.
|
|
4
|
-
|
|
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
|
-
|
|
1
|
+
# Tool: Serena (code-graph, LSP-accurate)
|
|
2
|
+
|
|
3
|
+
MIT, MCP-first. The alternative to graphify, selected via `yoke retrofit --code-graph=serena`. Serena uses real language servers (LSP) for symbol-accurate, cross-file retrieval and refactoring (`find_symbol`, `find_referencing_symbols`, rename/move) — no static index that goes stale, so it will not miss a reference.
|
|
4
|
+
|
|
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
|
+
|
|
7
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
8
|
|
|
9
9
|
Yoke disables Serena's automatic web-dashboard launch in generated MCP configurations. The
|
package/dist/agents/process.js
CHANGED
|
@@ -81,6 +81,9 @@ export function startProviderProcess(agent, invocation, options = {}) {
|
|
|
81
81
|
telemetry: telemetry.finish(),
|
|
82
82
|
});
|
|
83
83
|
const finalize = (exitCode) => {
|
|
84
|
+
if (termination && pid !== undefined && !terminationConfirmed) {
|
|
85
|
+
terminationConfirmed = terminateProcessTree(pid, true);
|
|
86
|
+
}
|
|
84
87
|
const details = evidence();
|
|
85
88
|
if (recordFailure) {
|
|
86
89
|
finish({ ...details, kind: 'spawn-failed', error: recordFailure });
|
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 !isProcessAlive(pid);
|
|
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);
|