@planu/cli 4.0.0 → 4.1.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/CHANGELOG.md +22 -1
- package/dist/engine/ai-integration/agents-md/generator.js +4 -1
- package/dist/engine/ai-integration/cline/clinerules-generator.js +7 -2
- package/dist/engine/ai-integration/codex/agents-md-generator.js +2 -0
- package/dist/engine/ai-integration/cursor/cursorrules-generator.js +7 -2
- package/dist/engine/ai-integration/windsurf/windsurfrules-generator.js +7 -2
- package/dist/engine/hooks/git-hook-generator.js +31 -0
- package/dist/engine/marketplace-fetcher/anthropic-source.js +2 -0
- package/dist/engine/opencode/config-scaffold.js +4 -0
- package/dist/engine/rules-generator/index.js +2 -0
- package/dist/engine/rules-reconciler.js +2 -0
- package/dist/engine/skill-bootstrap/skill-writer.js +2 -0
- package/dist/engine/skill-generation/multi-agent-writer.js +2 -0
- package/dist/engine/spec-language/english-only.d.ts +8 -7
- package/dist/engine/spec-language/english-only.js +27 -3
- package/dist/engine/universal-rules/host-writer.js +8 -2
- package/dist/engine/universal-rules/rules/planu-english-specs.js +9 -5
- package/dist/hosts/claude-code/ux/skills-writer.js +2 -0
- package/dist/hosts/codex/config-scaffold.js +5 -0
- package/dist/hosts/gemini/config-scaffold.js +4 -0
- package/dist/tools/create-skill.js +21 -0
- package/dist/tools/git/hook-ops.js +30 -0
- package/dist/tools/init-project/agents-md-writer.js +2 -0
- package/dist/tools/init-project/conventions-writer.js +2 -0
- package/dist/tools/init-project/find-skills-writer.js +2 -0
- package/dist/tools/init-project/helpers.js +5 -0
- package/dist/tools/init-project/per-client-files-writer.js +2 -0
- package/dist/tools/init-project/planu-workflow-generator.js +2 -0
- package/dist/tools/init-project/rules-generator.js +7 -1
- package/dist/tools/init-project/rules-writer.js +3 -0
- package/dist/tools/init-project/skills-multi-teammate-review-writer.js +2 -0
- package/dist/tools/init-project/skills-writer.js +2 -0
- package/dist/types/spec-language.d.ts +8 -0
- package/dist/types/spec-language.js +2 -0
- package/package.json +21 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## [4.1.1] - 2026-05-21
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
- Enforce dependency freshness as a blocking pre-push gate for lockfile drift, high/critical vulnerabilities, and outdated direct dependencies.
|
|
5
|
+
- Add the same pnpm dependency freshness guard to Planu-generated pre-push hooks.
|
|
6
|
+
|
|
7
|
+
### Tests
|
|
8
|
+
- Add coverage for the dependency freshness script and generated hook contents.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [4.1.0] - 2026-05-21
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
- Enforce English-only persisted Planu artifacts across specs, skills, agent instructions, and rules.
|
|
15
|
+
- Gate host-aware init scaffolding for `AGENTS.md`, `CLAUDE.md`, Cursor, Windsurf, Cline, Gemini, Codex, and OpenCode generated AI docs.
|
|
16
|
+
- Keep user-authored host file content intact while validating only Planu-owned generated blocks.
|
|
17
|
+
|
|
18
|
+
### Tests
|
|
19
|
+
- Add coverage for skill, rule, and agent instruction language gates across core writers and host generators.
|
|
20
|
+
|
|
21
|
+
|
|
1
22
|
## [4.0.0] - 2026-05-20
|
|
2
23
|
|
|
3
24
|
**Tarball SHA-256:** `8c00d74f48ed5614197000a967b103cc17653150aadf876fcfd18d0174263017`
|
|
@@ -3792,4 +3813,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
3792
3813
|
- Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
|
|
3793
3814
|
- Multi-language i18n (EN/ES/PT) for generated specs
|
|
3794
3815
|
- Clean Architecture (hexagonal) — engine, tools, storage, types layers
|
|
3795
|
-
- 10,857 tests with ≥95% coverage
|
|
3816
|
+
- 10,857 tests with ≥95% coverage
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// engine/ai-integration/agents-md/generator.ts — Universal AGENTS.md generator (SPEC-269)
|
|
2
2
|
import { writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../../spec-language/english-only.js';
|
|
4
5
|
const PLANU_SECTION_START = '<!-- planu-sdd:start -->';
|
|
5
6
|
const PLANU_SECTION_END = '<!-- planu-sdd:end -->';
|
|
6
7
|
function buildActiveSpecBlock(activeSpecId, activeSpecTitle) {
|
|
@@ -105,7 +106,9 @@ function buildSddSection(options) {
|
|
|
105
106
|
parts.push('', approvedQueueBlock);
|
|
106
107
|
}
|
|
107
108
|
parts.push('', toolsBlock, '', branchBlock, '', architectureBlock, '', PLANU_SECTION_END);
|
|
108
|
-
|
|
109
|
+
const content = parts.join('\n');
|
|
110
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
111
|
+
return content;
|
|
109
112
|
}
|
|
110
113
|
/** Generates the full universal AGENTS.md content for a project using Planu SDD. */
|
|
111
114
|
export function generateUniversalAgentsMd(options) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// engine/ai-integration/cline/clinerules-generator.ts — .clinerules generator (SPEC-271)
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../../spec-language/english-only.js';
|
|
4
5
|
const SECTION_START = '<!-- planu-sdd:start -->';
|
|
5
6
|
const SECTION_END = '<!-- planu-sdd:end -->';
|
|
6
7
|
function buildPlanuSection(options) {
|
|
@@ -52,12 +53,16 @@ function buildPlanuSection(options) {
|
|
|
52
53
|
lines.push('Place in `.clinerules` at project root or in `.clinerules/` directory.');
|
|
53
54
|
lines.push('');
|
|
54
55
|
lines.push(SECTION_END);
|
|
55
|
-
|
|
56
|
+
const content = lines.join('\n');
|
|
57
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
58
|
+
return content;
|
|
56
59
|
}
|
|
57
60
|
export function generateClineRules(options) {
|
|
58
61
|
const stackStr = options.stack.length > 0 ? options.stack.join(', ') : 'Not specified';
|
|
59
62
|
const header = [`# Cline Rules — ${options.projectName}`, '', `Stack: ${stackStr}`, ''].join('\n');
|
|
60
|
-
|
|
63
|
+
const content = header + buildPlanuSection(options);
|
|
64
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
65
|
+
return content;
|
|
61
66
|
}
|
|
62
67
|
export function writeClineRules(projectPath, options) {
|
|
63
68
|
const filePath = join(projectPath, '.clinerules');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { assertEnglishOnlyArtifactText } from '../../spec-language/english-only.js';
|
|
3
4
|
const PLANU_SECTION_START = '<!-- planu:start -->';
|
|
4
5
|
const PLANU_SECTION_END = '<!-- planu:end -->';
|
|
5
6
|
function buildWorkflowSection() {
|
|
@@ -91,6 +92,7 @@ export function generateAgentsMd(options) {
|
|
|
91
92
|
architectureSection,
|
|
92
93
|
PLANU_SECTION_END,
|
|
93
94
|
].join('\n');
|
|
95
|
+
assertEnglishOnlyArtifactText(planuBlock, 'agent');
|
|
94
96
|
return planuBlock;
|
|
95
97
|
}
|
|
96
98
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// engine/ai-integration/cursor/cursorrules-generator.ts — .cursorrules generator (SPEC-268)
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../../spec-language/english-only.js';
|
|
4
5
|
const SECTION_START = '<!-- planu-sdd:start -->';
|
|
5
6
|
const SECTION_END = '<!-- planu-sdd:end -->';
|
|
6
7
|
function buildPlanuSection(options) {
|
|
@@ -46,12 +47,16 @@ function buildPlanuSection(options) {
|
|
|
46
47
|
lines.push('');
|
|
47
48
|
}
|
|
48
49
|
lines.push(SECTION_END);
|
|
49
|
-
|
|
50
|
+
const content = lines.join('\n');
|
|
51
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
52
|
+
return content;
|
|
50
53
|
}
|
|
51
54
|
export function generateCursorRules(options) {
|
|
52
55
|
const stackStr = options.stack.length > 0 ? options.stack.join(', ') : 'Not specified';
|
|
53
56
|
const header = [`# Cursor Rules — ${options.projectName}`, '', `Stack: ${stackStr}`, ''].join('\n');
|
|
54
|
-
|
|
57
|
+
const content = header + buildPlanuSection(options);
|
|
58
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
59
|
+
return content;
|
|
55
60
|
}
|
|
56
61
|
export function writeCursorRules(projectPath, options) {
|
|
57
62
|
const filePath = join(projectPath, '.cursorrules');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// engine/ai-integration/windsurf/windsurfrules-generator.ts — .windsurfrules generator (SPEC-268)
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../../spec-language/english-only.js';
|
|
4
5
|
const SECTION_START = '<!-- planu-sdd:start -->';
|
|
5
6
|
const SECTION_END = '<!-- planu-sdd:end -->';
|
|
6
7
|
function buildPlanuSection(options) {
|
|
@@ -46,12 +47,16 @@ function buildPlanuSection(options) {
|
|
|
46
47
|
lines.push('');
|
|
47
48
|
}
|
|
48
49
|
lines.push(SECTION_END);
|
|
49
|
-
|
|
50
|
+
const content = lines.join('\n');
|
|
51
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
52
|
+
return content;
|
|
50
53
|
}
|
|
51
54
|
export function generateWindsurfRules(options) {
|
|
52
55
|
const stackStr = options.stack.length > 0 ? options.stack.join(', ') : 'Not specified';
|
|
53
56
|
const header = [`# Windsurf Rules — ${options.projectName}`, '', `Stack: ${stackStr}`, ''].join('\n');
|
|
54
|
-
|
|
57
|
+
const content = header + buildPlanuSection(options);
|
|
58
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
59
|
+
return content;
|
|
55
60
|
}
|
|
56
61
|
export function writeWindsurfRules(projectPath, options) {
|
|
57
62
|
const filePath = join(projectPath, '.windsurfrules');
|
|
@@ -66,6 +66,37 @@ if command -v planu >/dev/null 2>&1; then
|
|
|
66
66
|
planu detect-drift --project-id "${projectId}" --mode quick 2>/dev/null || true
|
|
67
67
|
fi
|
|
68
68
|
|
|
69
|
+
if command -v pnpm >/dev/null 2>&1 && [ -f "package.json" ] && [ -f "pnpm-lock.yaml" ]; then
|
|
70
|
+
echo "[Planu] Checking dependency freshness..."
|
|
71
|
+
pnpm install --frozen-lockfile --ignore-scripts >/dev/null || {
|
|
72
|
+
echo "ERROR: package.json and pnpm-lock.yaml are not synchronized."
|
|
73
|
+
echo "Run: pnpm install --lockfile-only --ignore-scripts"
|
|
74
|
+
exit 1
|
|
75
|
+
}
|
|
76
|
+
pnpm audit --audit-level=high || {
|
|
77
|
+
echo "ERROR: High or critical dependency vulnerabilities found."
|
|
78
|
+
echo "Run: pnpm audit and update the affected packages before pushing."
|
|
79
|
+
exit 1
|
|
80
|
+
}
|
|
81
|
+
OUTDATED_JSON="$(mktemp)"
|
|
82
|
+
OUTDATED_ERR="$(mktemp)"
|
|
83
|
+
set +e
|
|
84
|
+
pnpm outdated --format=json >"\${OUTDATED_JSON}" 2>"\${OUTDATED_ERR}"
|
|
85
|
+
OUTDATED_STATUS=$?
|
|
86
|
+
set -e
|
|
87
|
+
if [ "\${OUTDATED_STATUS}" -gt 1 ]; then
|
|
88
|
+
echo "ERROR: Failed to query dependency freshness from the registry."
|
|
89
|
+
cat "\${OUTDATED_ERR}"
|
|
90
|
+
rm -f "\${OUTDATED_JSON}" "\${OUTDATED_ERR}"
|
|
91
|
+
exit 1
|
|
92
|
+
fi
|
|
93
|
+
node -e "const fs=require('fs');const raw=fs.readFileSync(process.argv[1],'utf8').trim();const data=raw?JSON.parse(raw):{};const count=Array.isArray(data)?data.length:Object.keys(data).length;if(count>0){console.error('ERROR: Outdated dependencies found. Run: bash scripts/check-updates.sh --apply, pnpm update, or update intentionally before pushing.');process.exit(1)}" "\${OUTDATED_JSON}" || {
|
|
94
|
+
rm -f "\${OUTDATED_JSON}" "\${OUTDATED_ERR}"
|
|
95
|
+
exit 1
|
|
96
|
+
}
|
|
97
|
+
rm -f "\${OUTDATED_JSON}" "\${OUTDATED_ERR}"
|
|
98
|
+
fi
|
|
99
|
+
|
|
69
100
|
echo "[Planu] Pre-push checks complete."
|
|
70
101
|
`;
|
|
71
102
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Wraps fetchAnthropicSkillContent from anthropic-adapter.ts and converts to FetchedSkill[].
|
|
3
3
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from '../spec-language/english-only.js';
|
|
5
6
|
import { fetchAnthropicSkillContent } from '../skill-registry/anthropic-adapter.js';
|
|
6
7
|
import { readCache, writeCache } from './cache.js';
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -107,6 +108,7 @@ async function writeSkillFile(projectPath, skillName, content) {
|
|
|
107
108
|
const skillsDir = join(projectPath, '.claude', 'skills');
|
|
108
109
|
await mkdir(skillsDir, { recursive: true });
|
|
109
110
|
const filePath = join(skillsDir, `${skillName}.md`);
|
|
111
|
+
assertEnglishOnlyArtifactText(`${skillName}\n\n${content}`, 'skill');
|
|
110
112
|
await writeFile(filePath, content, 'utf-8');
|
|
111
113
|
return filePath;
|
|
112
114
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/engine/opencode/config-scaffold.ts — SPEC-966: Generate OpenCode config files
|
|
2
2
|
import { mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../spec-language/english-only.js';
|
|
4
5
|
const PLANU_OPENCODE_RULES = `# Planu Rules for OpenCode
|
|
5
6
|
|
|
6
7
|
## SDD Workflow
|
|
@@ -73,16 +74,19 @@ export function scaffoldOpenCodeConfig(projectPath) {
|
|
|
73
74
|
const rulesDir = join(projectPath, '.opencode', 'rules');
|
|
74
75
|
mkdirSync(rulesDir, { recursive: true });
|
|
75
76
|
const rulesPath = join(rulesDir, 'planu-workflow.md');
|
|
77
|
+
assertEnglishOnlyArtifactText(PLANU_OPENCODE_RULES, 'rule');
|
|
76
78
|
writeFileSync(rulesPath, PLANU_OPENCODE_RULES, 'utf-8');
|
|
77
79
|
files.push(rulesPath);
|
|
78
80
|
// .opencode/skills/planu-sdd.md
|
|
79
81
|
const skillsDir = join(projectPath, '.opencode', 'skills');
|
|
80
82
|
mkdirSync(skillsDir, { recursive: true });
|
|
81
83
|
const skillsPath = join(skillsDir, 'planu-sdd.md');
|
|
84
|
+
assertEnglishOnlyArtifactText(PLANU_OPENCODE_SKILLS, 'skill');
|
|
82
85
|
writeFileSync(skillsPath, PLANU_OPENCODE_SKILLS, 'utf-8');
|
|
83
86
|
files.push(skillsPath);
|
|
84
87
|
// AGENTS.md append (or create)
|
|
85
88
|
const agentsMdPath = join(projectPath, 'AGENTS.md');
|
|
89
|
+
assertEnglishOnlyArtifactText(PLANU_AGENTS_MD_BLOCK, 'agent');
|
|
86
90
|
if (existsSync(agentsMdPath)) {
|
|
87
91
|
writeFileSync(agentsMdPath, '\n' + PLANU_AGENTS_MD_BLOCK + '\n', { flag: 'a' });
|
|
88
92
|
files.push(agentsMdPath + ' (appended)');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { detectAiTools, getPrimaryAiTool } from '../ai-tool-detector/index.js';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../spec-language/english-only.js';
|
|
4
5
|
// Stack-specific architecture rules catalog
|
|
5
6
|
export const ARCHITECTURE_RULES = {
|
|
6
7
|
nextjs: {
|
|
@@ -300,6 +301,7 @@ export function writeRules(rules) {
|
|
|
300
301
|
if (dir) {
|
|
301
302
|
mkdirSync(dir, { recursive: true });
|
|
302
303
|
}
|
|
304
|
+
assertEnglishOnlyArtifactText(rule.content, 'rule');
|
|
303
305
|
writeFileSync(rule.filePath, rule.content, 'utf-8');
|
|
304
306
|
written.push(rule.filePath);
|
|
305
307
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { readFileSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { join, basename } from 'node:path';
|
|
4
4
|
import { parseConventions } from './convention-scanner/convention-parser.js';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from './spec-language/english-only.js';
|
|
5
6
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
6
7
|
function listRulesFiles(projectPath) {
|
|
7
8
|
const rulesDir = join(projectPath, '.claude', 'rules');
|
|
@@ -122,6 +123,7 @@ function writeNewRulesFile(projectPath, category, conventions) {
|
|
|
122
123
|
const fileName = `${category}.md`;
|
|
123
124
|
const filePath = join(rulesDir, fileName);
|
|
124
125
|
const content = generateRuleFileContent(category, conventions);
|
|
126
|
+
assertEnglishOnlyArtifactText(content, 'rule');
|
|
125
127
|
writeFileSync(filePath, content, 'utf-8');
|
|
126
128
|
return fileName;
|
|
127
129
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// engine/skill-bootstrap/skill-writer.ts — SPEC-439: Write skills to target AI tool files
|
|
2
2
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../spec-language/english-only.js';
|
|
4
5
|
const AUTO_SECTION_HEADER = '## Auto-installed Skills (Planu)';
|
|
5
6
|
function targetToRelativePath(target) {
|
|
6
7
|
switch (target) {
|
|
@@ -42,6 +43,7 @@ export function formatSkillsForTarget(skills, target) {
|
|
|
42
43
|
return parts.join('\n\n');
|
|
43
44
|
}
|
|
44
45
|
export async function writeSkillFile(projectPath, target, content, autoMerge) {
|
|
46
|
+
assertEnglishOnlyArtifactText(content, 'skill');
|
|
45
47
|
const relativePath = targetToRelativePath(target);
|
|
46
48
|
const filePath = join(projectPath, relativePath);
|
|
47
49
|
await mkdir(dirname(filePath), { recursive: true });
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Detects which AI agents are present in a project and writes adapted skill files.
|
|
3
3
|
import { access, mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from '../spec-language/english-only.js';
|
|
5
6
|
import { adaptSkillForAgent } from './multi-agent-adapter.js';
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
8
|
// Agent detection signals
|
|
@@ -92,6 +93,7 @@ async function writeNewFile(filePath, content) {
|
|
|
92
93
|
* @param targets - Optional explicit list of targets (default: auto-detect)
|
|
93
94
|
*/
|
|
94
95
|
export async function writeSkillToAllAgents(skillContent, metadata, projectPath, targets) {
|
|
96
|
+
assertEnglishOnlyArtifactText(`${metadata.name}\n\n${metadata.description}\n\n${skillContent}`, 'skill');
|
|
95
97
|
const resolvedTargets = targets ?? (await detectPresentAgents(projectPath));
|
|
96
98
|
const results = [];
|
|
97
99
|
await Promise.all(resolvedTargets.map(async (target) => {
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import type { EnglishOnlyArtifactKind, EnglishOnlyValidationResult } from '../../types/spec-language.js';
|
|
1
2
|
/**
|
|
2
|
-
* Validate that persisted
|
|
3
|
+
* Validate that a Planu-owned persisted artifact is authored in English.
|
|
3
4
|
*
|
|
4
5
|
* The detector is intentionally conservative: it ignores code fences, inline code,
|
|
5
6
|
* file paths, markdown headings, and BDD keywords before looking for common
|
|
6
7
|
* Spanish/Portuguese function words. It should block obvious non-English prose
|
|
7
8
|
* without punishing short technical descriptions.
|
|
8
9
|
*/
|
|
9
|
-
export declare function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
export declare function validateEnglishOnlyArtifactText(text: string, kind?: EnglishOnlyArtifactKind): EnglishOnlyValidationResult;
|
|
11
|
+
/**
|
|
12
|
+
* Backward-compatible spec-specific entrypoint used by create_spec/update_status gates.
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateEnglishOnlySpecText(text: string): EnglishOnlyValidationResult;
|
|
15
|
+
export declare function assertEnglishOnlyArtifactText(text: string, kind: EnglishOnlyArtifactKind): void;
|
|
15
16
|
//# sourceMappingURL=english-only.d.ts.map
|
|
@@ -79,14 +79,14 @@ const NON_ENGLISH_SIGNATURES = {
|
|
|
79
79
|
]),
|
|
80
80
|
};
|
|
81
81
|
/**
|
|
82
|
-
* Validate that persisted
|
|
82
|
+
* Validate that a Planu-owned persisted artifact is authored in English.
|
|
83
83
|
*
|
|
84
84
|
* The detector is intentionally conservative: it ignores code fences, inline code,
|
|
85
85
|
* file paths, markdown headings, and BDD keywords before looking for common
|
|
86
86
|
* Spanish/Portuguese function words. It should block obvious non-English prose
|
|
87
87
|
* without punishing short technical descriptions.
|
|
88
88
|
*/
|
|
89
|
-
export function
|
|
89
|
+
export function validateEnglishOnlyArtifactText(text, kind = 'spec') {
|
|
90
90
|
const tokens = tokenizeProse(text);
|
|
91
91
|
if (tokens.length < MIN_PROSE_TOKENS) {
|
|
92
92
|
return { ok: true, detectedLanguage: 'unknown', signals: [] };
|
|
@@ -109,10 +109,34 @@ export function validateEnglishOnlySpecText(text) {
|
|
|
109
109
|
ok: false,
|
|
110
110
|
detectedLanguage: best.lang,
|
|
111
111
|
signals,
|
|
112
|
-
reason:
|
|
112
|
+
reason: `${artifactLabel(kind)} must be written in English. Detected ${languageName(best.lang)} prose ` +
|
|
113
113
|
`signals: ${signals.join(', ')}.`,
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Backward-compatible spec-specific entrypoint used by create_spec/update_status gates.
|
|
118
|
+
*/
|
|
119
|
+
export function validateEnglishOnlySpecText(text) {
|
|
120
|
+
return validateEnglishOnlyArtifactText(text, 'spec');
|
|
121
|
+
}
|
|
122
|
+
export function assertEnglishOnlyArtifactText(text, kind) {
|
|
123
|
+
const validation = validateEnglishOnlyArtifactText(text, kind);
|
|
124
|
+
if (!validation.ok) {
|
|
125
|
+
throw new Error(validation.reason ?? `${artifactLabel(kind)} must be written in English.`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function artifactLabel(kind) {
|
|
129
|
+
switch (kind) {
|
|
130
|
+
case 'spec':
|
|
131
|
+
return 'Spec documents';
|
|
132
|
+
case 'skill':
|
|
133
|
+
return 'Skill artifacts';
|
|
134
|
+
case 'agent':
|
|
135
|
+
return 'Agent instructions';
|
|
136
|
+
case 'rule':
|
|
137
|
+
return 'Rule artifacts';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
116
140
|
function tokenizeProse(text) {
|
|
117
141
|
const withoutCode = text
|
|
118
142
|
.replace(/```[\s\S]*?```/g, ' ')
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Write a UniversalRule to disk using the appropriate host strategy.
|
|
3
3
|
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from '../spec-language/english-only.js';
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Claude Code writer
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -14,6 +15,7 @@ async function writeForClaudeCode(rule, projectPath) {
|
|
|
14
15
|
await mkdir(rulesDir, { recursive: true });
|
|
15
16
|
const filePath = join(rulesDir, `${rule.id}.md`);
|
|
16
17
|
const content = rule.buildContent('claude-code');
|
|
18
|
+
assertEnglishOnlyArtifactText(`${rule.name}\n\n${rule.description}\n\n${content}`, 'rule');
|
|
17
19
|
await writeFile(filePath, content, 'utf-8');
|
|
18
20
|
return filePath;
|
|
19
21
|
}
|
|
@@ -39,7 +41,9 @@ async function writeForCodex(rule, projectPath) {
|
|
|
39
41
|
}
|
|
40
42
|
const open = OPEN_MARKER(rule.id);
|
|
41
43
|
const close = CLOSE_MARKER(rule.id);
|
|
42
|
-
const
|
|
44
|
+
const content = rule.buildContent('codex');
|
|
45
|
+
assertEnglishOnlyArtifactText(`${rule.name}\n\n${rule.description}\n\n${content}`, 'rule');
|
|
46
|
+
const block = `${open}\n${content}\n${close}`;
|
|
43
47
|
// Replace existing block or append
|
|
44
48
|
const openIdx = existing.indexOf(open);
|
|
45
49
|
const closeIdx = existing.indexOf(close);
|
|
@@ -74,7 +78,9 @@ async function writeForGemini(rule, projectPath) {
|
|
|
74
78
|
}
|
|
75
79
|
const open = OPEN_MARKER(rule.id);
|
|
76
80
|
const close = CLOSE_MARKER(rule.id);
|
|
77
|
-
const
|
|
81
|
+
const content = rule.buildContent('gemini');
|
|
82
|
+
assertEnglishOnlyArtifactText(`${rule.name}\n\n${rule.description}\n\n${content}`, 'rule');
|
|
83
|
+
const block = `${open}\n${content}\n${close}`;
|
|
78
84
|
const openIdx = existing.indexOf(open);
|
|
79
85
|
const closeIdx = existing.indexOf(close);
|
|
80
86
|
let updated;
|
|
@@ -1,31 +1,35 @@
|
|
|
1
1
|
// engine/universal-rules/rules/planu-english-specs.ts — Universal rule: specs are written in English
|
|
2
2
|
function buildBody() {
|
|
3
|
-
return `# Planu
|
|
3
|
+
return `# Planu Artifacts Must Be Written in English
|
|
4
4
|
|
|
5
5
|
Auto-generated by \`init_project\`. Do not edit manually.
|
|
6
6
|
|
|
7
7
|
## Rule
|
|
8
8
|
|
|
9
|
-
All generated
|
|
9
|
+
All generated Planu-owned artifacts are written in English, regardless of the user's conversation language:
|
|
10
10
|
|
|
11
11
|
- \`spec.md\`
|
|
12
|
+
- Planu skills, including \`SKILL.md\` and host-adapted skill blocks
|
|
13
|
+
- Planu agent instructions, including \`AGENTS.md\`, \`CLAUDE.md\`, \`GEMINI.md\`, and host-specific sections
|
|
14
|
+
- Planu rules, including \`.claude/rules/*.md\`, Codex rule blocks, and Gemini conventions
|
|
12
15
|
- inline \`## Technical\`, \`## Files\`, and \`## Progress\` sections
|
|
13
16
|
- acceptance criteria, implementation notes, validation notes, and reconciliation notes
|
|
14
17
|
|
|
15
|
-
User-facing chat may use the user's preferred language. The
|
|
18
|
+
User-facing chat may use the user's preferred language. The persisted Planu contract stays in English so every agent, tool, and CI gate can parse it consistently.
|
|
16
19
|
|
|
17
20
|
## Hard Blocks
|
|
18
21
|
|
|
19
22
|
- Do not create mixed-language acceptance criteria.
|
|
20
23
|
- Do not translate BDD keywords.
|
|
24
|
+
- Do not generate Planu-owned skills, agents, or rules in Spanish, Portuguese, or mixed language.
|
|
21
25
|
- Do not create standalone \`technical.md\`, \`progress.md\`, \`HU.md\`, \`FICHA-TECNICA.md\`, or \`PROGRESS.md\`.
|
|
22
26
|
- Do not approve a spec that contains unresolved placeholders such as \`to be determined\`, \`TBD\`, \`TODO\`, or equivalent filler.
|
|
23
27
|
`;
|
|
24
28
|
}
|
|
25
29
|
export const planuEnglishSpecsRule = {
|
|
26
30
|
id: 'planu-english-specs',
|
|
27
|
-
name: 'Planu English
|
|
28
|
-
description: 'Requires Planu spec artifacts to be written in English.',
|
|
31
|
+
name: 'Planu English Artifacts',
|
|
32
|
+
description: 'Requires Planu spec, skill, agent, and rule artifacts to be written in English.',
|
|
29
33
|
category: 'quality',
|
|
30
34
|
applicableHosts: ['all'],
|
|
31
35
|
defaultEnabled: true,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { assertEnglishOnlyArtifactText } from '../../../engine/spec-language/english-only.js';
|
|
7
8
|
const SKILLS_DIR = '.claude/skills';
|
|
8
9
|
/** Canonical skill slugs managed by SPEC-588. */
|
|
9
10
|
const SKILL_SLUGS = [
|
|
@@ -68,6 +69,7 @@ async function writeSkillFile(projectPath, slug, content) {
|
|
|
68
69
|
if (existing === content) {
|
|
69
70
|
return { path: dest, created: false };
|
|
70
71
|
}
|
|
72
|
+
assertEnglishOnlyArtifactText(content, 'skill');
|
|
71
73
|
await writeFile(dest, content, 'utf-8');
|
|
72
74
|
return { path: dest, created: existing === null };
|
|
73
75
|
}
|
|
@@ -12,6 +12,7 @@ import { mkdir, writeFile, readFile, access } from 'node:fs/promises';
|
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { detectCodexWorkspace } from './workspace-scope.js';
|
|
14
14
|
import { buildRulesForHost } from '../../engine/host-rules-templates/index.js';
|
|
15
|
+
import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
|
|
15
16
|
const CODEX_CONFIG_TOML = `# .openai/config.toml — Planu Codex workspace configuration
|
|
16
17
|
# Auto-generated by Planu init_project. Safe to customize.
|
|
17
18
|
|
|
@@ -79,6 +80,9 @@ async function writeIfMissing(filePath, content) {
|
|
|
79
80
|
// File exists — skip (idempotent)
|
|
80
81
|
}
|
|
81
82
|
catch {
|
|
83
|
+
if (filePath.endsWith('AGENTS.md')) {
|
|
84
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
85
|
+
}
|
|
82
86
|
await writeFile(filePath, content, 'utf-8');
|
|
83
87
|
}
|
|
84
88
|
}
|
|
@@ -91,6 +95,7 @@ const PLANU_RULES_START_REGEX = /<!-- planu:rules:start[^>]* -->/;
|
|
|
91
95
|
*/
|
|
92
96
|
async function injectRulesBlock(filePath, version) {
|
|
93
97
|
const block = buildRulesForHost('codex', version).rules;
|
|
98
|
+
assertEnglishOnlyArtifactText(block, 'rule');
|
|
94
99
|
let existing;
|
|
95
100
|
try {
|
|
96
101
|
existing = await readFile(filePath, 'utf-8');
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { writeFile, readFile, mkdir, access } from 'node:fs/promises';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { buildRulesForHost } from '../../engine/host-rules-templates/index.js';
|
|
8
|
+
import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// File content generators
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
@@ -144,6 +145,8 @@ export async function scaffoldGeminiConfig(projectPath, planuVersion = '1.96.0')
|
|
|
144
145
|
// Ensure parent directory exists
|
|
145
146
|
const parentDir = join(projectPath, relPath.split('/').slice(0, -1).join('/'));
|
|
146
147
|
await mkdir(parentDir, { recursive: true });
|
|
148
|
+
const artifactKind = relPath.includes('/skills/') ? 'skill' : 'agent';
|
|
149
|
+
assertEnglishOnlyArtifactText(content, artifactKind);
|
|
147
150
|
await writeFile(fullPath, content, 'utf-8');
|
|
148
151
|
filesCreated.push(relPath);
|
|
149
152
|
}
|
|
@@ -166,6 +169,7 @@ const PLANU_RULES_START_REGEX = /<!-- planu:rules:start[^>]* -->/;
|
|
|
166
169
|
*/
|
|
167
170
|
async function injectRulesBlock(filePath, version) {
|
|
168
171
|
const block = buildRulesForHost('gemini', version).rules;
|
|
172
|
+
assertEnglishOnlyArtifactText(block, 'rule');
|
|
169
173
|
let existing;
|
|
170
174
|
try {
|
|
171
175
|
existing = await readFile(filePath, 'utf-8');
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { detectHost } from '../engine/host-detection/detect-host.js';
|
|
6
|
+
import { validateEnglishOnlyArtifactText } from '../engine/spec-language/english-only.js';
|
|
6
7
|
import { hashContent } from '../engine/universal-rules/user-edit-detector.js';
|
|
7
8
|
import { hashProjectPath } from '../storage/base-store.js';
|
|
8
9
|
import { appendAutopilotLogEntry } from '../storage/autopilot-log-store.js';
|
|
@@ -96,6 +97,26 @@ export async function handleCreateSkill(input) {
|
|
|
96
97
|
const { projectPath, name, content, overwriteExisting } = input;
|
|
97
98
|
const description = input.description ?? '';
|
|
98
99
|
const host = resolveHost(input);
|
|
100
|
+
const languageValidation = validateEnglishOnlyArtifactText(`${name}\n\n${description}\n\n${content}`, 'skill');
|
|
101
|
+
if (!languageValidation.ok) {
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: 'text',
|
|
106
|
+
text: `English-only skill gate blocked create_skill.\n\n` +
|
|
107
|
+
`${languageValidation.reason ?? 'Non-English prose detected.'}\n\n` +
|
|
108
|
+
'Rewrite the skill name, description, and content in English before creating it.',
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
isError: true,
|
|
112
|
+
structuredContent: {
|
|
113
|
+
error: 'SKILL_LANGUAGE_GATE_BLOCKED',
|
|
114
|
+
detectedLanguage: languageValidation.detectedLanguage,
|
|
115
|
+
signals: languageValidation.signals,
|
|
116
|
+
fixHint: 'Rewrite skill artifacts in English.',
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
99
120
|
let result;
|
|
100
121
|
try {
|
|
101
122
|
switch (host) {
|
|
@@ -165,6 +165,36 @@ function buildPrePushScript(protectedBranches, stalenessThreshold, baseBranch) {
|
|
|
165
165
|
' fi',
|
|
166
166
|
'fi',
|
|
167
167
|
'',
|
|
168
|
+
'# Dependency freshness gate for pnpm projects',
|
|
169
|
+
'if command -v pnpm >/dev/null 2>&1 && [ -f "package.json" ] && [ -f "pnpm-lock.yaml" ]; then',
|
|
170
|
+
' echo "[Planu] Checking dependency freshness..."',
|
|
171
|
+
' pnpm install --frozen-lockfile --ignore-scripts >/dev/null || {',
|
|
172
|
+
' echo "ERROR: package.json and pnpm-lock.yaml are not synchronized."',
|
|
173
|
+
' echo "Run: pnpm install --lockfile-only --ignore-scripts"',
|
|
174
|
+
' exit 1',
|
|
175
|
+
' }',
|
|
176
|
+
' pnpm audit --audit-level=high || {',
|
|
177
|
+
' echo "ERROR: High or critical dependency vulnerabilities found."',
|
|
178
|
+
' echo "Run: pnpm audit and update the affected packages before pushing."',
|
|
179
|
+
' exit 1',
|
|
180
|
+
' }',
|
|
181
|
+
' OUTDATED_JSON=$(mktemp)',
|
|
182
|
+
' OUTDATED_ERR=$(mktemp)',
|
|
183
|
+
' pnpm outdated --format=json >"$OUTDATED_JSON" 2>"$OUTDATED_ERR"',
|
|
184
|
+
' OUTDATED_STATUS=$?',
|
|
185
|
+
' if [ "$OUTDATED_STATUS" -gt 1 ]; then',
|
|
186
|
+
' echo "ERROR: Failed to query dependency freshness from the registry."',
|
|
187
|
+
' cat "$OUTDATED_ERR"',
|
|
188
|
+
' rm -f "$OUTDATED_JSON" "$OUTDATED_ERR"',
|
|
189
|
+
' exit 1',
|
|
190
|
+
' fi',
|
|
191
|
+
" node -e \"const fs=require('fs');const raw=fs.readFileSync(process.argv[1],'utf8').trim();const data=raw?JSON.parse(raw):{};const count=Array.isArray(data)?data.length:Object.keys(data).length;if(count>0){console.error('ERROR: Outdated dependencies found. Run: bash scripts/check-updates.sh --apply, pnpm update, or update intentionally before pushing.');process.exit(1)}\" \"$OUTDATED_JSON\" || {",
|
|
192
|
+
' rm -f "$OUTDATED_JSON" "$OUTDATED_ERR"',
|
|
193
|
+
' exit 1',
|
|
194
|
+
' }',
|
|
195
|
+
' rm -f "$OUTDATED_JSON" "$OUTDATED_ERR"',
|
|
196
|
+
'fi',
|
|
197
|
+
'',
|
|
168
198
|
'exit 0',
|
|
169
199
|
].join('\n');
|
|
170
200
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPEC-972: Added host-aware tool filtering for Codex and other MCP hosts.
|
|
4
4
|
import { writeFile, mkdir, access } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
|
|
6
7
|
import { CORE_AGENTS_MD_TOOLS, loadHostRegistry, filterToolsByHost, formatToolsForAgentsMd, } from '../../engine/host-tool-filter.js';
|
|
7
8
|
const AGENTS_MD_PATH = 'AGENTS.md';
|
|
8
9
|
const CURSORRULES_PATH = '.cursorrules';
|
|
@@ -89,6 +90,7 @@ async function writeIfMissing(path, content) {
|
|
|
89
90
|
return false;
|
|
90
91
|
}
|
|
91
92
|
catch {
|
|
93
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
92
94
|
await writeFile(path, content, 'utf-8');
|
|
93
95
|
return true;
|
|
94
96
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// with project-specific conventions detected by init_project
|
|
3
3
|
import { writeFile, mkdir, access } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
|
|
5
6
|
const CONVENTIONS_MD_PATH = '.claude/rules/conventions.md';
|
|
6
7
|
function buildConventionsContent(knowledge) {
|
|
7
8
|
const stackItems = knowledge.stack;
|
|
@@ -44,6 +45,7 @@ export async function generateConventionsMdIfMissing(projectPath, knowledge) {
|
|
|
44
45
|
const rulesDir = join(projectPath, '.claude/rules');
|
|
45
46
|
await mkdir(rulesDir, { recursive: true });
|
|
46
47
|
const content = buildConventionsContent(knowledge);
|
|
48
|
+
assertEnglishOnlyArtifactText(content, 'rule');
|
|
47
49
|
await writeFile(conventionsPath, content, 'utf-8');
|
|
48
50
|
return true;
|
|
49
51
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Single hardcoded entry point for skill discovery in generated config files
|
|
3
3
|
import { writeFile, mkdir, access } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
|
|
5
6
|
const FIND_SKILLS_PATH = '.claude/skills/find-skills.md';
|
|
6
7
|
const FIND_SKILLS_CONTENT = `# /find-skills — Discover Available Workflows
|
|
7
8
|
|
|
@@ -42,6 +43,7 @@ export async function generateFindSkillsIfMissing(projectPath) {
|
|
|
42
43
|
catch {
|
|
43
44
|
const skillsDir = join(projectPath, '.claude', 'skills');
|
|
44
45
|
await mkdir(skillsDir, { recursive: true });
|
|
46
|
+
assertEnglishOnlyArtifactText(FIND_SKILLS_CONTENT, 'skill');
|
|
45
47
|
await writeFile(skillPath, FIND_SKILLS_CONTENT, 'utf-8');
|
|
46
48
|
return true;
|
|
47
49
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { detectLegalFramework, detectThirdParties, buildDefaultPrivacyConfig, } from '../../engine/pii-detector.js';
|
|
2
|
+
import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
|
|
2
3
|
import { readFile, writeFile, access, stat } from 'node:fs/promises';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { glob } from 'glob';
|
|
@@ -283,6 +284,10 @@ export async function injectSddIntoClaude(projectPath, projectSections) {
|
|
|
283
284
|
if (updated === content) {
|
|
284
285
|
continue; // nothing changed for this file
|
|
285
286
|
}
|
|
287
|
+
assertEnglishOnlyArtifactText(SDD_BLOCK, 'agent');
|
|
288
|
+
if (projectSections) {
|
|
289
|
+
assertEnglishOnlyArtifactText(projectSections, 'agent');
|
|
290
|
+
}
|
|
286
291
|
await writeFile(filePath, updated, 'utf-8');
|
|
287
292
|
anyModified = true;
|
|
288
293
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { writeFile, mkdir, access } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { detectAndCacheClient } from '../../engine/client-detection.js';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
|
|
5
6
|
const CURSOR_MDC_CONTENT = `---
|
|
6
7
|
description: Planu SDD workflow for Cursor
|
|
7
8
|
globs: ["**/*"]
|
|
@@ -70,6 +71,7 @@ async function writeIfMissing(filePath, content) {
|
|
|
70
71
|
}
|
|
71
72
|
catch {
|
|
72
73
|
await mkdir(join(filePath, '..'), { recursive: true });
|
|
74
|
+
assertEnglishOnlyArtifactText(content, filePath.includes('/skills/') ? 'skill' : 'agent');
|
|
73
75
|
await writeFile(filePath, content, 'utf-8');
|
|
74
76
|
return true;
|
|
75
77
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPEC-263: Autonomous CLAUDE.md that makes Planu mandatory
|
|
3
3
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
4
4
|
import { dirname } from 'node:path';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
|
|
5
6
|
const PLANU_WORKFLOW_MARKER = '<!-- planu-sdd-workflow -->';
|
|
6
7
|
const PLANU_WORKFLOW_CONTENT = `<!-- planu-sdd-workflow -->
|
|
7
8
|
## Planu SDD Workflow (MANDATORY)
|
|
@@ -61,6 +62,7 @@ export async function injectPlanuSection(claudeMdPath, section) {
|
|
|
61
62
|
await mkdir(dirname(claudeMdPath), { recursive: true });
|
|
62
63
|
}
|
|
63
64
|
const updated = injectOrReplacePlanuBlock(existing, section.content, section.marker);
|
|
65
|
+
assertEnglishOnlyArtifactText(section.content, 'agent');
|
|
64
66
|
await writeFile(claudeMdPath, updated, 'utf-8');
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPEC-263: Autonomous CLAUDE.md that makes Planu mandatory
|
|
3
3
|
import { writeFile, mkdir, access } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
|
|
5
6
|
const PLANU_WORKFLOW_RULES_PATH = '.claude/rules/planu-workflow.md';
|
|
6
7
|
const PLANU_WORKFLOW_RULES_CONTENT = `# Planu SDD Workflow (MANDATORY)
|
|
7
8
|
|
|
@@ -141,7 +142,9 @@ export async function generateAgentTeamsRules(projectPath) {
|
|
|
141
142
|
const rulesPath = join(projectPath, AGENT_TEAMS_RULES_PATH);
|
|
142
143
|
const rulesDir = join(projectPath, '.claude/rules');
|
|
143
144
|
await mkdir(rulesDir, { recursive: true });
|
|
144
|
-
|
|
145
|
+
const content = generateAgentTeamsRulesContent();
|
|
146
|
+
assertEnglishOnlyArtifactText(content, 'rule');
|
|
147
|
+
await writeFile(rulesPath, content, 'utf-8');
|
|
145
148
|
}
|
|
146
149
|
/**
|
|
147
150
|
* Write .claude/rules/agent-teams.md only if it does not exist yet.
|
|
@@ -166,6 +169,7 @@ export async function generateWorkflowRules(projectPath) {
|
|
|
166
169
|
const rulesPath = join(projectPath, PLANU_WORKFLOW_RULES_PATH);
|
|
167
170
|
const rulesDir = join(projectPath, '.claude/rules');
|
|
168
171
|
await mkdir(rulesDir, { recursive: true });
|
|
172
|
+
assertEnglishOnlyArtifactText(PLANU_WORKFLOW_RULES_CONTENT, 'rule');
|
|
169
173
|
await writeFile(rulesPath, PLANU_WORKFLOW_RULES_CONTENT, 'utf-8');
|
|
170
174
|
}
|
|
171
175
|
/**
|
|
@@ -227,6 +231,7 @@ export async function generateModeRulesIfMissing(projectPath) {
|
|
|
227
231
|
catch {
|
|
228
232
|
const rulesDir = join(projectPath, '.claude/rules');
|
|
229
233
|
await mkdir(rulesDir, { recursive: true });
|
|
234
|
+
assertEnglishOnlyArtifactText(PLANU_MODES_RULES_CONTENT, 'rule');
|
|
230
235
|
await writeFile(rulesPath, PLANU_MODES_RULES_CONTENT, 'utf-8');
|
|
231
236
|
return true;
|
|
232
237
|
}
|
|
@@ -254,6 +259,7 @@ export async function generateResponseStyleRulesIfMissing(projectPath) {
|
|
|
254
259
|
catch {
|
|
255
260
|
const rulesDir = join(projectPath, '.claude/rules');
|
|
256
261
|
await mkdir(rulesDir, { recursive: true });
|
|
262
|
+
assertEnglishOnlyArtifactText(RESPONSE_STYLE_RULES_CONTENT, 'rule');
|
|
257
263
|
await writeFile(rulesPath, RESPONSE_STYLE_RULES_CONTENT, 'utf-8');
|
|
258
264
|
return true;
|
|
259
265
|
}
|
|
@@ -2,6 +2,7 @@ import { generateRules, detectAgentPlatformFromFiles } from '../../engine/skill-
|
|
|
2
2
|
import { knowledgeStore } from '../../storage/index.js';
|
|
3
3
|
import { writeFile, access, mkdir } from 'node:fs/promises';
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
|
|
5
6
|
/**
|
|
6
7
|
* Detect agent platform, generate rules, and write them to disk.
|
|
7
8
|
* Only writes files that don't exist yet — never overwrites user custom rules.
|
|
@@ -22,6 +23,7 @@ export async function runRulesWriter(projectPath, projectId, knowledge, recommen
|
|
|
22
23
|
}
|
|
23
24
|
catch {
|
|
24
25
|
await mkdir(dirname(rulesFilePath), { recursive: true });
|
|
26
|
+
assertEnglishOnlyArtifactText(generatedRules.rulesFile.content, 'rule');
|
|
25
27
|
await writeFile(rulesFilePath, generatedRules.rulesFile.content, 'utf-8');
|
|
26
28
|
rulesWritten = true;
|
|
27
29
|
}
|
|
@@ -34,6 +36,7 @@ export async function runRulesWriter(projectPath, projectId, knowledge, recommen
|
|
|
34
36
|
}
|
|
35
37
|
catch {
|
|
36
38
|
await mkdir(dirname(filePath), { recursive: true });
|
|
39
|
+
assertEnglishOnlyArtifactText(file.content, file.path.includes('skill') ? 'skill' : 'rule');
|
|
37
40
|
await writeFile(filePath, file.content, 'utf-8');
|
|
38
41
|
additionalFilesWritten.push(file.path);
|
|
39
42
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { writeFile, mkdir, access, readFile } from 'node:fs/promises';
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
|
|
6
7
|
const SKILL_TARGET_PATH = '.claude/skills/planu-multi-teammate-review.md';
|
|
7
8
|
// Resolve the template path relative to this file (works after tsc compilation too)
|
|
8
9
|
function resolveTemplatePath() {
|
|
@@ -48,6 +49,7 @@ export async function generateMultiTeammateReviewSkillIfMissing(projectPath) {
|
|
|
48
49
|
const skillsDir = join(projectPath, '.claude/skills');
|
|
49
50
|
await mkdir(skillsDir, { recursive: true });
|
|
50
51
|
const content = await readTemplate();
|
|
52
|
+
assertEnglishOnlyArtifactText(content, 'skill');
|
|
51
53
|
await writeFile(skillPath, content, 'utf-8');
|
|
52
54
|
return true;
|
|
53
55
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPEC-519: Generate compact skill file for dense SDD mode
|
|
3
3
|
import { writeFile, mkdir, access } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
|
|
5
6
|
const COMPACT_SKILL_PATH = '.claude/skills/compact.md';
|
|
6
7
|
const COMPACT_SKILL_CONTENT = `# /compact — Dense SDD Mode
|
|
7
8
|
|
|
@@ -43,6 +44,7 @@ export async function generateCompactSkillIfMissing(projectPath) {
|
|
|
43
44
|
catch {
|
|
44
45
|
const skillsDir = join(projectPath, '.claude/skills');
|
|
45
46
|
await mkdir(skillsDir, { recursive: true });
|
|
47
|
+
assertEnglishOnlyArtifactText(COMPACT_SKILL_CONTENT, 'skill');
|
|
46
48
|
await writeFile(skillPath, COMPACT_SKILL_CONTENT, 'utf-8');
|
|
47
49
|
return true;
|
|
48
50
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type EnglishOnlyArtifactKind = 'spec' | 'skill' | 'agent' | 'rule';
|
|
2
|
+
export interface EnglishOnlyValidationResult {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
detectedLanguage: 'en' | 'es' | 'pt' | 'unknown';
|
|
5
|
+
reason?: string;
|
|
6
|
+
signals: string[];
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=spec-language.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"packageName": "@planu/core"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@planu/core-darwin-arm64": "
|
|
36
|
-
"@planu/core-darwin-x64": "
|
|
37
|
-
"@planu/core-linux-arm64-gnu": "
|
|
38
|
-
"@planu/core-linux-arm64-musl": "
|
|
39
|
-
"@planu/core-linux-x64-gnu": "
|
|
40
|
-
"@planu/core-linux-x64-musl": "
|
|
35
|
+
"@planu/core-darwin-arm64": "4.1.1",
|
|
36
|
+
"@planu/core-darwin-x64": "4.1.1",
|
|
37
|
+
"@planu/core-linux-arm64-gnu": "4.1.1",
|
|
38
|
+
"@planu/core-linux-arm64-musl": "4.1.1",
|
|
39
|
+
"@planu/core-linux-x64-gnu": "4.1.1",
|
|
40
|
+
"@planu/core-linux-x64-musl": "4.1.1"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
43
|
"node": ">=24.0.0"
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"test:integration": "vitest run tests/integration",
|
|
69
69
|
"check": "pnpm typecheck && pnpm lint && pnpm format:check",
|
|
70
70
|
"check:strict": "pnpm typecheck && pnpm lint && pnpm format:check && pnpm audit:deadcode && pnpm audit:circular && pnpm audit:types && pnpm audit:security && pnpm audit:licenses && pnpm audit:i18n",
|
|
71
|
+
"check:deps:fresh": "bash scripts/check-dependency-freshness.sh",
|
|
71
72
|
"audit:deadcode": "knip",
|
|
72
73
|
"audit:circular": "madge --circular --extensions ts src/",
|
|
73
74
|
"audit:types": "type-coverage --at-least 98 --ignore-catch --strict --ignore-files 'tests/**'",
|
|
@@ -149,8 +150,8 @@
|
|
|
149
150
|
"@commitlint/config-conventional": "^21.0.1",
|
|
150
151
|
"@eslint/js": "^10.0.1",
|
|
151
152
|
"@napi-rs/cli": "^3.6.2",
|
|
152
|
-
"@secretlint/secretlint-rule-no-homedir": "^13.0.
|
|
153
|
-
"@secretlint/secretlint-rule-preset-recommend": "^13.0.
|
|
153
|
+
"@secretlint/secretlint-rule-no-homedir": "^13.0.2",
|
|
154
|
+
"@secretlint/secretlint-rule-preset-recommend": "^13.0.2",
|
|
154
155
|
"@semantic-release/changelog": "^6.0.3",
|
|
155
156
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
156
157
|
"@semantic-release/git": "^10.0.1",
|
|
@@ -159,30 +160,30 @@
|
|
|
159
160
|
"@semantic-release/release-notes-generator": "^14.1.1",
|
|
160
161
|
"@stryker-mutator/core": "^9.6.1",
|
|
161
162
|
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
162
|
-
"@supabase/supabase-js": "^2.
|
|
163
|
-
"@types/node": "^25.
|
|
164
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
165
|
-
"@vitest/coverage-v8": "^4.1.
|
|
163
|
+
"@supabase/supabase-js": "^2.106.1",
|
|
164
|
+
"@types/node": "^25.9.1",
|
|
165
|
+
"@vitejs/plugin-vue": "^6.0.7",
|
|
166
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
166
167
|
"@vue/test-utils": "^2.4.10",
|
|
167
|
-
"eslint": "^10.
|
|
168
|
+
"eslint": "^10.4.0",
|
|
168
169
|
"eslint-config-prettier": "^10.1.8",
|
|
169
170
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
170
171
|
"eslint-plugin-import": "^2.32.0",
|
|
171
172
|
"happy-dom": "^20.9.0",
|
|
172
173
|
"husky": "^9.1.7",
|
|
173
174
|
"javascript-obfuscator": "^5.4.2",
|
|
174
|
-
"knip": "^6.
|
|
175
|
-
"lint-staged": "^17.0.
|
|
175
|
+
"knip": "^6.14.1",
|
|
176
|
+
"lint-staged": "^17.0.5",
|
|
176
177
|
"madge": "^8.0.0",
|
|
177
178
|
"prettier": "^3.8.3",
|
|
178
|
-
"secretlint": "^13.0.
|
|
179
|
+
"secretlint": "^13.0.2",
|
|
179
180
|
"semantic-release": "^25.0.3",
|
|
180
181
|
"tsc-alias": "^1.8.17",
|
|
181
182
|
"type-coverage": "^2.29.7",
|
|
182
183
|
"typescript": "^6.0.3",
|
|
183
|
-
"typescript-eslint": "^8.59.
|
|
184
|
-
"vite": "^8.0.
|
|
185
|
-
"vitest": "^4.1.
|
|
184
|
+
"typescript-eslint": "^8.59.4",
|
|
185
|
+
"vite": "^8.0.14",
|
|
186
|
+
"vitest": "^4.1.7",
|
|
186
187
|
"vue": "^3.5.34"
|
|
187
188
|
}
|
|
188
189
|
}
|