@planu/cli 3.9.5 → 3.9.7

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 CHANGED
@@ -1,3 +1,8 @@
1
+ ## [3.9.6] - 2026-05-15
2
+
3
+ **Tarball SHA-256:** `56592815d33401b0cd7aa1d02ca26c7dda8dd131176841ccda3163932e53b43b`
4
+
5
+
1
6
  ## [3.9.5] - 2026-05-15
2
7
 
3
8
  **Tarball SHA-256:** `11fa506c006e59292069158b32b580ab861cfb71ea5fefa60c5308fbf55b129a`
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: planu-context-assets
3
+ description: Create project-specific Planu rules or skills from the current app context so recurring standards and workflows become reusable agent guidance instead of MCP tool bloat.
4
+ triggers:
5
+ - create project rules
6
+ - create project skills
7
+ - capture this workflow
8
+ - make this a rule
9
+ - make this a skill
10
+ - codify this pattern
11
+ - project context changed
12
+ - app conventions
13
+ version: 1.0.0
14
+ ---
15
+
16
+ # /planu-context-assets — Create Rules or Skills from App Context
17
+
18
+ ## When to invoke
19
+
20
+ Use this skill when project context reveals a recurring convention, workflow, integration, domain rule, or implementation pattern that future agents should reuse.
21
+
22
+ Examples:
23
+
24
+ - A design system rule is discovered while implementing UI.
25
+ - A release or deployment workflow becomes stable.
26
+ - A legacy cleanup pattern repeats across modules.
27
+ - A domain invariant must never be violated.
28
+ - A framework-specific project convention is discovered during analysis.
29
+
30
+ ## Decision rule
31
+
32
+ Create a **rule** when the guidance is mandatory and should constrain every future agent:
33
+
34
+ - architecture boundaries
35
+ - spec quality gates
36
+ - release policy
37
+ - BDD/SDD invariants
38
+ - security or data handling rules
39
+ - "never do X" / "always do Y" project standards
40
+
41
+ Create a **skill** when the guidance is an optional workflow invoked for a specific task:
42
+
43
+ - release workflow
44
+ - Figma implementation workflow
45
+ - legacy characterization workflow
46
+ - domain bundle generation
47
+ - integration setup
48
+ - migration playbook
49
+ - debugging or audit procedure
50
+
51
+ Do not create either when the information is one-off, speculative, or already covered by an existing rule/skill.
52
+
53
+ ## Workflow
54
+
55
+ 1. Inspect the current context and existing project assets:
56
+ - `AGENTS.md`
57
+ - `CLAUDE.md`
58
+ - `.claude/rules/`
59
+ - `.claude/skills/`
60
+ - `.gemini/conventions.md`
61
+ - `.gemini/skills/`
62
+ - `.opencode/rules/`
63
+ - `.opencode/skills/`
64
+ 2. Decide whether the new asset is a rule, a skill, or nothing.
65
+ 3. Keep the asset small and operational:
66
+ - one purpose
67
+ - explicit trigger
68
+ - concrete steps or constraints
69
+ - verification command when applicable
70
+ 4. Use Planu host-aware tools:
71
+ - `create_rule` for mandatory constraints
72
+ - `create_skill` for reusable workflows
73
+ 5. Use `host: "auto"` unless the user explicitly targets a host.
74
+ 6. Prefer updating an existing Planu-owned asset over creating a duplicate.
75
+
76
+ ## Quality bar
77
+
78
+ - Rules must be enforceable and written as constraints.
79
+ - Skills must describe when to invoke, what to inspect, what to do, and what output to produce.
80
+ - Do not reference legacy MCP tools that are not part of the 14-tool public core.
81
+ - Do not create broad "misc" skills.
82
+ - Do not preserve dead workflows "just in case".
83
+
84
+ ## Output
85
+
86
+ Report:
87
+
88
+ - asset type: rule or skill
89
+ - asset name
90
+ - host target
91
+ - path or file updated
92
+ - why it should exist
93
+ - when it should trigger
94
+
@@ -4,12 +4,20 @@ import { planuDogfoodBugsRule } from './rules/planu-dogfood-bugs.js';
4
4
  import { planuWorkflowRule } from './rules/planu-workflow.js';
5
5
  import { planuModesRule } from './rules/planu-modes.js';
6
6
  import { agentTeamsRule } from './rules/agent-teams.js';
7
+ import { planuEnglishSpecsRule } from './rules/planu-english-specs.js';
8
+ import { planuBddCriteriaRule } from './rules/planu-bdd-criteria.js';
9
+ import { planuApprovalGatesRule } from './rules/planu-approval-gates.js';
10
+ import { planuReleasePolicyRule } from './rules/planu-release-policy.js';
7
11
  /**
8
12
  * The full catalog of universal Planu rules.
9
13
  * Order matters: rules are installed in catalog order.
10
14
  */
11
15
  export const UNIVERSAL_RULES = [
12
16
  planuWorkflowRule,
17
+ planuEnglishSpecsRule,
18
+ planuBddCriteriaRule,
19
+ planuApprovalGatesRule,
20
+ planuReleasePolicyRule,
13
21
  planuModesRule,
14
22
  agentTeamsRule,
15
23
  planuDogfoodBugsRule,
@@ -6,8 +6,14 @@ import { UNIVERSAL_RULES } from './catalog.js';
6
6
  import { writeRuleForHost } from './host-writer.js';
7
7
  import { hashContent, readManifest, upsertManifestEntry } from './user-edit-detector.js';
8
8
  /** Path of the manifest relative to projectPath. */
9
- function manifestPath(projectPath) {
10
- return join(projectPath, '.claude', 'rules', '.planu-rules-manifest.json');
9
+ function manifestPath(projectPath, host) {
10
+ if (host === 'claude-code') {
11
+ return join(projectPath, '.claude', 'rules', '.planu-rules-manifest.json');
12
+ }
13
+ if (host === 'gemini') {
14
+ return join(projectPath, '.gemini', '.planu-rules-manifest.json');
15
+ }
16
+ return join(projectPath, '.planu', 'rules-manifest.codex.json');
11
17
  }
12
18
  /**
13
19
  * Install all default-enabled universal rules applicable to `host`.
@@ -15,7 +21,7 @@ function manifestPath(projectPath) {
15
21
  * Returns the list of rules that were written.
16
22
  */
17
23
  export async function installUniversalRules(projectPath, host) {
18
- const mPath = manifestPath(projectPath);
24
+ const mPath = manifestPath(projectPath, host);
19
25
  const manifest = (await readManifest(mPath)) ?? { rules: [] };
20
26
  const installed = [];
21
27
  for (const rule of UNIVERSAL_RULES) {
@@ -0,0 +1,3 @@
1
+ import type { UniversalRule } from '../../../types/universal-rules/index.js';
2
+ export declare const planuApprovalGatesRule: UniversalRule;
3
+ //# sourceMappingURL=planu-approval-gates.d.ts.map
@@ -0,0 +1,41 @@
1
+ // engine/universal-rules/rules/planu-approval-gates.ts — Universal rule: approval and done gates
2
+ function buildBody() {
3
+ return `# Planu Approval and Done Gates
4
+
5
+ Auto-generated by \`init_project\`. Do not edit manually.
6
+
7
+ ## Approval Gate
8
+
9
+ Before moving a spec to \`approved\`, run and pass:
10
+
11
+ 1. \`challenge_spec\`
12
+ 2. \`check_readiness\`
13
+ 3. BDD criteria completeness
14
+ 4. files-to-create / files-to-modify ownership
15
+ 5. test plan and verification commands
16
+
17
+ If the user explicitly forces approval, record the reason in the audit trail and make the missing risk visible.
18
+
19
+ ## Done Gate
20
+
21
+ Before moving a spec to \`done\`, run \`validate\`.
22
+
23
+ If implementation intentionally diverged from the approved spec, run \`reconcile_spec\` first and make the divergence explicit before marking done.
24
+
25
+ ## Hard Blocks
26
+
27
+ - Do not approve specs with placeholders.
28
+ - Do not mark done without validation evidence.
29
+ - Do not hide intentional drift; reconcile it.
30
+ `;
31
+ }
32
+ export const planuApprovalGatesRule = {
33
+ id: 'planu-approval-gates',
34
+ name: 'Planu Approval Gates',
35
+ description: 'Requires challenge/readiness before approval and validate/reconcile before done.',
36
+ category: 'safety',
37
+ applicableHosts: ['all'],
38
+ defaultEnabled: true,
39
+ buildContent: (_host) => buildBody(),
40
+ };
41
+ //# sourceMappingURL=planu-approval-gates.js.map
@@ -0,0 +1,3 @@
1
+ import type { UniversalRule } from '../../../types/universal-rules/index.js';
2
+ export declare const planuBddCriteriaRule: UniversalRule;
3
+ //# sourceMappingURL=planu-bdd-criteria.d.ts.map
@@ -0,0 +1,45 @@
1
+ // engine/universal-rules/rules/planu-bdd-criteria.ts — Universal rule: BDD acceptance criteria
2
+ function buildBody() {
3
+ return `# Planu BDD Acceptance Criteria
4
+
5
+ Auto-generated by \`init_project\`. Do not edit manually.
6
+
7
+ ## Rule
8
+
9
+ Every acceptance criterion must be executable enough for an implementation agent and a validation gate.
10
+
11
+ Required format:
12
+
13
+ \`\`\`gherkin
14
+ GIVEN <initial state or context>
15
+ WHEN <action or event>
16
+ THEN <observable result>
17
+ AND <additional observable constraint>
18
+ \`\`\`
19
+
20
+ ## Required Detail
21
+
22
+ Each criterion must identify:
23
+
24
+ - files or modules that are expected to change when known
25
+ - user-visible behavior or API behavior
26
+ - test expectation or verification command
27
+ - ownership when the work spans more than one wave or agent
28
+
29
+ ## Hard Blocks
30
+
31
+ - No loose checklist-only acceptance criteria.
32
+ - No vague outcomes such as "works correctly", "improves UX", or "handles errors" without observable behavior.
33
+ - No approval when criteria are missing \`GIVEN\`, \`WHEN\`, or \`THEN\`.
34
+ `;
35
+ }
36
+ export const planuBddCriteriaRule = {
37
+ id: 'planu-bdd-criteria',
38
+ name: 'Planu BDD Criteria',
39
+ description: 'Requires complete GIVEN/WHEN/THEN acceptance criteria.',
40
+ category: 'quality',
41
+ applicableHosts: ['all'],
42
+ defaultEnabled: true,
43
+ buildContent: (_host) => buildBody(),
44
+ };
45
+ //# sourceMappingURL=planu-bdd-criteria.js.map
@@ -0,0 +1,3 @@
1
+ import type { UniversalRule } from '../../../types/universal-rules/index.js';
2
+ export declare const planuEnglishSpecsRule: UniversalRule;
3
+ //# sourceMappingURL=planu-english-specs.d.ts.map
@@ -0,0 +1,36 @@
1
+ // engine/universal-rules/rules/planu-english-specs.ts — Universal rule: specs are written in English
2
+ function buildBody() {
3
+ return `# Planu Specs Must Be Written in English
4
+
5
+ Auto-generated by \`init_project\`. Do not edit manually.
6
+
7
+ ## Rule
8
+
9
+ All generated spec artifacts are written in English, regardless of the user's conversation language:
10
+
11
+ - \`spec.md\`
12
+ - \`technical.md\`
13
+ - architecture notes inside \`planu/specs/**\`
14
+ - acceptance criteria
15
+ - implementation notes
16
+ - validation and reconciliation notes
17
+
18
+ User-facing chat may use the user's preferred language. The spec contract itself stays in English so every agent, tool, and CI gate can parse it consistently.
19
+
20
+ ## Hard Blocks
21
+
22
+ - Do not create mixed-language acceptance criteria.
23
+ - Do not translate BDD keywords.
24
+ - Do not approve a spec that contains unresolved placeholders such as \`to be determined\`, \`TBD\`, \`TODO\`, or equivalent filler.
25
+ `;
26
+ }
27
+ export const planuEnglishSpecsRule = {
28
+ id: 'planu-english-specs',
29
+ name: 'Planu English Specs',
30
+ description: 'Requires Planu spec artifacts to be written in English.',
31
+ category: 'quality',
32
+ applicableHosts: ['all'],
33
+ defaultEnabled: true,
34
+ buildContent: (_host) => buildBody(),
35
+ };
36
+ //# sourceMappingURL=planu-english-specs.js.map
@@ -0,0 +1,3 @@
1
+ import type { UniversalRule } from '../../../types/universal-rules/index.js';
2
+ export declare const planuReleasePolicyRule: UniversalRule;
3
+ //# sourceMappingURL=planu-release-policy.d.ts.map
@@ -0,0 +1,38 @@
1
+ // engine/universal-rules/rules/planu-release-policy.ts — Universal rule: release decision
2
+ function buildBody() {
3
+ return `# Planu Release Policy
4
+
5
+ Auto-generated by \`init_project\`. Do not edit manually.
6
+
7
+ ## Rule
8
+
9
+ When implementation changes are complete and validated, always address release explicitly:
10
+
11
+ - create the release when the user has already requested shipping or publishing
12
+ - otherwise ask before publishing, tagging, or pushing externally visible release artifacts
13
+
14
+ ## Required Release Checklist
15
+
16
+ - tests and validation commands passed
17
+ - changelog or release notes updated when applicable
18
+ - package version and git tag stay aligned
19
+ - main/develop branch state is reconciled when the repository uses both
20
+ - published package smoke check is run after release when applicable
21
+
22
+ ## Hard Blocks
23
+
24
+ - Do not silently skip release discussion after completed product/runtime changes.
25
+ - Do not publish without validation evidence.
26
+ - Do not leave release notes promising capabilities that are not in the public runtime.
27
+ `;
28
+ }
29
+ export const planuReleasePolicyRule = {
30
+ id: 'planu-release-policy',
31
+ name: 'Planu Release Policy',
32
+ description: 'Requires an explicit release decision after validated implementation work.',
33
+ category: 'safety',
34
+ applicableHosts: ['all'],
35
+ defaultEnabled: true,
36
+ buildContent: (_host) => buildBody(),
37
+ };
38
+ //# sourceMappingURL=planu-release-policy.js.map
@@ -0,0 +1,21 @@
1
+ import type { ProjectKnowledge } from '../../types/index.js';
2
+ import type { HostId, InstalledRule } from '../../types/universal-rules/index.js';
3
+ export type InitAssetHost = HostId | 'opencode';
4
+ export interface InitHostAssetsResult {
5
+ detectedHosts: InitAssetHost[];
6
+ rulesInstalled: InstalledRule[];
7
+ skillsInstalled: {
8
+ host: HostId;
9
+ name: string;
10
+ path: string;
11
+ }[];
12
+ opencodeConfigured: boolean;
13
+ }
14
+ /**
15
+ * Detect every project host that should receive Planu-owned assets.
16
+ * Environment identifies the active MCP host; files identify other checked-in
17
+ * host configs that should stay in sync too.
18
+ */
19
+ export declare function detectInitAssetHosts(projectPath: string): Promise<InitAssetHost[]>;
20
+ export declare function installInitHostAssets(projectPath: string, _knowledge: ProjectKnowledge): Promise<InitHostAssetsResult>;
21
+ //# sourceMappingURL=host-assets-writer.d.ts.map
@@ -0,0 +1,171 @@
1
+ // tools/init-project/host-assets-writer.ts — Host-aware rules/skills installed by init_project
2
+ import { access, readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { detectHost } from '../../engine/host-detection/detect-host.js';
5
+ import { handleCreateSkill } from '../create-skill.js';
6
+ const CORE_SKILL_TEMPLATES = [
7
+ 'planu-new-spec.md',
8
+ 'planu-validate.md',
9
+ 'planu-release.md',
10
+ 'planu-resume-work.md',
11
+ 'planu-multi-teammate-review.md',
12
+ 'planu-context-assets.md',
13
+ ];
14
+ async function fileExists(path) {
15
+ try {
16
+ await access(path);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ function addHost(hosts, host) {
24
+ if (!hosts.includes(host)) {
25
+ hosts.push(host);
26
+ }
27
+ }
28
+ /**
29
+ * Detect every project host that should receive Planu-owned assets.
30
+ * Environment identifies the active MCP host; files identify other checked-in
31
+ * host configs that should stay in sync too.
32
+ */
33
+ export async function detectInitAssetHosts(projectPath) {
34
+ const hosts = [];
35
+ const activeHost = detectHost();
36
+ if (activeHost === 'claude-code' || activeHost === 'codex' || activeHost === 'gemini') {
37
+ addHost(hosts, activeHost);
38
+ }
39
+ const checks = await Promise.all([
40
+ fileExists(join(projectPath, 'CLAUDE.md')),
41
+ fileExists(join(projectPath, '.claude')),
42
+ fileExists(join(projectPath, 'AGENTS.md')),
43
+ fileExists(join(projectPath, '.openai')),
44
+ fileExists(join(projectPath, 'GEMINI.md')),
45
+ fileExists(join(projectPath, '.gemini')),
46
+ fileExists(join(projectPath, 'opencode.json')),
47
+ fileExists(join(projectPath, '.opencode')),
48
+ ]);
49
+ const [hasClaudeMd, hasClaudeDir, hasAgentsMd, hasOpenAiDir, hasGeminiMd, hasGeminiDir, hasOpenCodeJson, hasOpenCodeDir,] = checks;
50
+ if (hasClaudeMd || hasClaudeDir) {
51
+ addHost(hosts, 'claude-code');
52
+ }
53
+ if (hasAgentsMd || hasOpenAiDir) {
54
+ addHost(hosts, 'codex');
55
+ }
56
+ if (hasGeminiMd || hasGeminiDir) {
57
+ addHost(hosts, 'gemini');
58
+ }
59
+ if (hasOpenCodeJson || hasOpenCodeDir) {
60
+ addHost(hosts, 'opencode');
61
+ }
62
+ // Portable default for new/unknown projects: AGENTS.md, not Claude-specific files.
63
+ if (hosts.length === 0) {
64
+ addHost(hosts, 'codex');
65
+ }
66
+ return hosts;
67
+ }
68
+ async function installRulesForHost(projectPath, host) {
69
+ try {
70
+ const { installUniversalRules } = await import('../../engine/universal-rules/installer.js');
71
+ return await installUniversalRules(projectPath, host);
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ }
77
+ function parseSkillTemplate(raw, fallbackName) {
78
+ const frontmatter = /^---\n([\s\S]*?)\n---\n?/.exec(raw);
79
+ const name = /(?:^|\n)name:\s*([^\n]+)/.exec(frontmatter?.[1] ?? '')?.[1]?.trim() ??
80
+ fallbackName.replace(/\.md$/, '');
81
+ const description = /(?:^|\n)description:\s*([^\n]+)/.exec(frontmatter?.[1] ?? '')?.[1]?.trim() ??
82
+ `Planu workflow skill: ${name}`;
83
+ const content = frontmatter ? raw.slice(frontmatter[0].length) : raw;
84
+ return {
85
+ name,
86
+ description: description.replace(/^["']|["']$/g, ''),
87
+ content: content.trimEnd() + '\n',
88
+ };
89
+ }
90
+ async function readCoreSkillTemplate(fileName) {
91
+ try {
92
+ const { fileURLToPath } = await import('node:url');
93
+ const templatePath = join(fileURLToPath(new URL('.', import.meta.url)), '../../config/skill-templates', fileName);
94
+ const raw = await readFile(templatePath, 'utf-8');
95
+ return parseSkillTemplate(raw, fileName);
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ async function installCoreSkillsForHost(projectPath, host) {
102
+ const installed = [];
103
+ for (const fileName of CORE_SKILL_TEMPLATES) {
104
+ const template = await readCoreSkillTemplate(fileName);
105
+ if (template === null) {
106
+ continue;
107
+ }
108
+ const result = await handleCreateSkill({
109
+ projectPath,
110
+ host,
111
+ name: template.name,
112
+ description: template.description,
113
+ content: template.content,
114
+ overwriteExisting: false,
115
+ });
116
+ if (result.isError === true) {
117
+ continue;
118
+ }
119
+ const path = typeof result.structuredContent === 'object' &&
120
+ 'skillFilePath' in result.structuredContent &&
121
+ typeof result.structuredContent.skillFilePath === 'string'
122
+ ? result.structuredContent.skillFilePath
123
+ : host === 'codex'
124
+ ? join(projectPath, 'AGENTS.md')
125
+ : host === 'gemini'
126
+ ? join(projectPath, '.gemini', 'skills', `${template.name}.md`)
127
+ : join(projectPath, '.claude', 'skills', template.name, 'SKILL.md');
128
+ installed.push({ host, name: template.name, path });
129
+ }
130
+ return installed;
131
+ }
132
+ async function scaffoldHostConfig(projectPath, host) {
133
+ try {
134
+ if (host === 'codex') {
135
+ const { scaffoldCodexConfig } = await import('../../hosts/codex/config-scaffold.js');
136
+ await scaffoldCodexConfig(projectPath);
137
+ return true;
138
+ }
139
+ if (host === 'gemini') {
140
+ const { scaffoldGeminiConfig } = await import('../../hosts/gemini/config-scaffold.js');
141
+ await scaffoldGeminiConfig(projectPath);
142
+ return true;
143
+ }
144
+ if (host === 'opencode') {
145
+ const { scaffoldOpenCodeConfig } = await import('../../engine/opencode/config-scaffold.js');
146
+ scaffoldOpenCodeConfig(projectPath);
147
+ return true;
148
+ }
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ return false;
154
+ }
155
+ export async function installInitHostAssets(projectPath, _knowledge) {
156
+ const detectedHosts = await detectInitAssetHosts(projectPath);
157
+ const rulesInstalled = [];
158
+ const skillsInstalled = [];
159
+ let opencodeConfigured = false;
160
+ for (const host of detectedHosts) {
161
+ const configured = await scaffoldHostConfig(projectPath, host);
162
+ if (host === 'opencode') {
163
+ opencodeConfigured ||= configured;
164
+ continue;
165
+ }
166
+ rulesInstalled.push(...(await installRulesForHost(projectPath, host)));
167
+ skillsInstalled.push(...(await installCoreSkillsForHost(projectPath, host)));
168
+ }
169
+ return { detectedHosts, rulesInstalled, skillsInstalled, opencodeConfigured };
170
+ }
171
+ //# sourceMappingURL=host-assets-writer.js.map
@@ -44,6 +44,14 @@ export interface ScaffoldWriteResult {
44
44
  pluginsInstalled: Pick<PluginInstallResult, 'marketplaceAdded' | 'installed' | 'skipped' | 'failed'> | null;
45
45
  /** SPEC-685: Number of workflow skill files written by generateWorkflowSkills. */
46
46
  workflowSkillsWritten: number;
47
+ /** Hosts that received Planu init assets (rules/skills/config). */
48
+ detectedAssetHosts: string[];
49
+ /** Number of host-aware universal rules written by init_project. */
50
+ hostRulesWritten: number;
51
+ /** Number of host-aware core skills written by init_project. */
52
+ hostSkillsWritten: number;
53
+ /** Whether OpenCode Planu assets were configured. */
54
+ opencodeConfigScaffolded: boolean;
47
55
  }
48
56
  /**
49
57
  * Write all scaffold files to disk:
@@ -27,6 +27,7 @@ import { generateWorkflowSkills } from '../../engine/skill-generator/workflow-sk
27
27
  * - ESLint/Prettier config
28
28
  * - Architecture rules
29
29
  */
30
+ // eslint-disable-next-line complexity
30
31
  export async function runScaffoldWriter(projectPath, projectId, knowledge, recommendedSkills, permissionsMode, pluginsMode) {
31
32
  // Generate and write AI rules files
32
33
  const rulesResult = await runRulesWriter(projectPath, projectId, knowledge, recommendedSkills);
@@ -44,15 +45,33 @@ export async function runScaffoldWriter(projectPath, projectId, knowledge, recom
44
45
  /* best-effort */
45
46
  }
46
47
  }
47
- // Auto-inject SDD workflow into CLAUDE.md (best-effort, idempotent)
48
- let claudeMdUpdated = false;
48
+ let detectedAssetHosts = [];
49
+ let hostRulesWritten = 0;
50
+ let hostSkillsWritten = 0;
51
+ let opencodeConfigScaffolded = false;
49
52
  try {
50
- const projectSections = buildProjectSections(knowledge);
51
- claudeMdUpdated = await injectSddIntoClaude(projectPath, projectSections);
53
+ const { installInitHostAssets } = await import('./host-assets-writer.js');
54
+ const hostAssets = await installInitHostAssets(projectPath, knowledge);
55
+ detectedAssetHosts = hostAssets.detectedHosts;
56
+ hostRulesWritten = hostAssets.rulesInstalled.length;
57
+ hostSkillsWritten = hostAssets.skillsInstalled.length;
58
+ opencodeConfigScaffolded = hostAssets.opencodeConfigured;
52
59
  }
53
60
  catch {
54
61
  /* best-effort */
55
62
  }
63
+ const shouldWriteClaudeAssets = detectedAssetHosts.includes('claude-code');
64
+ // Auto-inject SDD workflow into CLAUDE.md (best-effort, idempotent)
65
+ let claudeMdUpdated = false;
66
+ if (shouldWriteClaudeAssets) {
67
+ try {
68
+ const projectSections = buildProjectSections(knowledge);
69
+ claudeMdUpdated = await injectSddIntoClaude(projectPath, projectSections);
70
+ }
71
+ catch {
72
+ /* best-effort */
73
+ }
74
+ }
56
75
  // SPEC-178: Scaffold ESLint and Prettier config if missing (best-effort)
57
76
  let eslintCreated = false;
58
77
  let prettierCreated = false;
@@ -70,69 +89,85 @@ export async function runScaffoldWriter(projectPath, projectId, knowledge, recom
70
89
  const { written: architectureRulesWritten, skipped: architectureRulesSkipped } = generateAndWriteRules(projectPath);
71
90
  // SPEC-263: Inject Planu SDD Workflow section into CLAUDE.md (idempotent, best-effort)
72
91
  let planuWorkflowInjected = false;
73
- try {
74
- const claudeMdPath = `${projectPath}/CLAUDE.md`;
75
- const section = generatePlanuSection(projectPath, projectId);
76
- await injectPlanuSection(claudeMdPath, section);
77
- planuWorkflowInjected = true;
78
- }
79
- catch {
80
- /* best-effort */
92
+ if (shouldWriteClaudeAssets) {
93
+ try {
94
+ const claudeMdPath = `${projectPath}/CLAUDE.md`;
95
+ const section = generatePlanuSection(projectPath, projectId);
96
+ await injectPlanuSection(claudeMdPath, section);
97
+ planuWorkflowInjected = true;
98
+ }
99
+ catch {
100
+ /* best-effort */
101
+ }
81
102
  }
82
103
  // SPEC-263: Configure Planu hooks in .claude.json (idempotent, best-effort)
83
104
  let planuHooksConfigured = false;
84
- try {
85
- planuHooksConfigured = await configurePlanuHooks(projectPath);
86
- }
87
- catch {
88
- /* best-effort */
105
+ if (shouldWriteClaudeAssets) {
106
+ try {
107
+ planuHooksConfigured = await configurePlanuHooks(projectPath);
108
+ }
109
+ catch {
110
+ /* best-effort */
111
+ }
89
112
  }
90
113
  // SPEC-263: Write .claude/rules/planu-workflow.md if missing (best-effort)
91
114
  let planuRulesWritten = false;
92
- try {
93
- planuRulesWritten = await generateWorkflowRulesIfMissing(projectPath);
94
- }
95
- catch {
96
- /* best-effort */
115
+ if (shouldWriteClaudeAssets) {
116
+ try {
117
+ planuRulesWritten = await generateWorkflowRulesIfMissing(projectPath);
118
+ }
119
+ catch {
120
+ /* best-effort */
121
+ }
97
122
  }
98
123
  // SPEC-291: Write .claude/rules/agent-teams.md if missing (best-effort)
99
124
  let agentTeamsRulesWritten = false;
100
- try {
101
- agentTeamsRulesWritten = await generateAgentTeamsRulesIfMissing(projectPath);
102
- }
103
- catch {
104
- /* best-effort */
125
+ if (shouldWriteClaudeAssets) {
126
+ try {
127
+ agentTeamsRulesWritten = await generateAgentTeamsRulesIfMissing(projectPath);
128
+ }
129
+ catch {
130
+ /* best-effort */
131
+ }
105
132
  }
106
133
  // SPEC-494: Write .claude/rules/planu-modes.md if missing (best-effort)
107
134
  let modeRulesWritten = false;
108
- try {
109
- modeRulesWritten = await generateModeRulesIfMissing(projectPath);
110
- }
111
- catch {
112
- /* best-effort */
135
+ if (shouldWriteClaudeAssets) {
136
+ try {
137
+ modeRulesWritten = await generateModeRulesIfMissing(projectPath);
138
+ }
139
+ catch {
140
+ /* best-effort */
141
+ }
113
142
  }
114
143
  // SPEC-517: Write .claude/rules/planu-response-style.md if missing (best-effort)
115
144
  let responseStyleRulesWritten = false;
116
- try {
117
- responseStyleRulesWritten = await generateResponseStyleRulesIfMissing(projectPath);
118
- }
119
- catch {
120
- /* best-effort */
145
+ if (shouldWriteClaudeAssets) {
146
+ try {
147
+ responseStyleRulesWritten = await generateResponseStyleRulesIfMissing(projectPath);
148
+ }
149
+ catch {
150
+ /* best-effort */
151
+ }
121
152
  }
122
153
  // SPEC-519: Write .claude/skills/compact.md if missing (best-effort)
123
154
  let compactSkillWritten = false;
124
- try {
125
- compactSkillWritten = await generateCompactSkillIfMissing(projectPath);
126
- }
127
- catch {
128
- /* best-effort */
155
+ if (shouldWriteClaudeAssets) {
156
+ try {
157
+ compactSkillWritten = await generateCompactSkillIfMissing(projectPath);
158
+ }
159
+ catch {
160
+ /* best-effort */
161
+ }
129
162
  }
130
163
  // SPEC-655: Write .claude/skills/find-skills.md if missing (best-effort, idempotent)
131
- try {
132
- await generateFindSkillsIfMissing(projectPath);
133
- }
134
- catch {
135
- /* best-effort */
164
+ if (shouldWriteClaudeAssets) {
165
+ try {
166
+ await generateFindSkillsIfMissing(projectPath);
167
+ }
168
+ catch {
169
+ /* best-effort */
170
+ }
136
171
  }
137
172
  // SPEC-347: Inject git auto-stage snippet for planu HTML files (best-effort)
138
173
  let gitAutoStageInjected = false;
@@ -167,11 +202,13 @@ export async function runScaffoldWriter(projectPath, projectId, knowledge, recom
167
202
  }
168
203
  // SPEC-530: Generate .claude/rules/conventions.md (best-effort, idempotent)
169
204
  let conventionsMdWritten = false;
170
- try {
171
- conventionsMdWritten = await generateConventionsMdIfMissing(projectPath, knowledge);
172
- }
173
- catch {
174
- /* best-effort */
205
+ if (shouldWriteClaudeAssets) {
206
+ try {
207
+ conventionsMdWritten = await generateConventionsMdIfMissing(projectPath, knowledge);
208
+ }
209
+ catch {
210
+ /* best-effort */
211
+ }
175
212
  }
176
213
  // SPEC-591: Scaffold .gemini/ if a Gemini workspace marker is present (best-effort, idempotent)
177
214
  let geminiConfigScaffolded = false;
@@ -188,11 +225,14 @@ export async function runScaffoldWriter(projectPath, projectId, knowledge, recom
188
225
  }
189
226
  // SPEC-593: Write .claude/skills/planu-multi-teammate-review.md if missing (best-effort, idempotent)
190
227
  let multiTeammateReviewSkillWritten = false;
191
- try {
192
- multiTeammateReviewSkillWritten = await generateMultiTeammateReviewSkillIfMissing(projectPath);
193
- }
194
- catch {
195
- /* best-effort */
228
+ if (shouldWriteClaudeAssets) {
229
+ try {
230
+ multiTeammateReviewSkillWritten =
231
+ await generateMultiTeammateReviewSkillIfMissing(projectPath);
232
+ }
233
+ catch {
234
+ /* best-effort */
235
+ }
196
236
  }
197
237
  // SPEC-592: Scaffold Codex workspace config if Codex env is detected (best-effort, no-op outside Codex)
198
238
  let codexConfigScaffolded = false;
@@ -213,17 +253,19 @@ export async function runScaffoldWriter(projectPath, projectId, knowledge, recom
213
253
  // (idempotent — skips files already up-to-date; best-effort, no-op elsewhere)
214
254
  let planuSubagentsScaffolded = false;
215
255
  let planuUxSkillsScaffolded = 0;
216
- try {
217
- const { scaffoldPlanuSubagents, scaffoldPlanuSkills } = await import('../../hosts/claude-code/ux/index.js');
218
- const [subagentResult, skillResult] = await Promise.all([
219
- scaffoldPlanuSubagents(projectPath),
220
- scaffoldPlanuSkills(projectPath),
221
- ]);
222
- planuSubagentsScaffolded = subagentResult.created.length > 0;
223
- planuUxSkillsScaffolded = skillResult.created.length;
224
- }
225
- catch {
226
- /* best-effort */
256
+ if (shouldWriteClaudeAssets) {
257
+ try {
258
+ const { scaffoldPlanuSubagents, scaffoldPlanuSkills } = await import('../../hosts/claude-code/ux/index.js');
259
+ const [subagentResult, skillResult] = await Promise.all([
260
+ scaffoldPlanuSubagents(projectPath),
261
+ scaffoldPlanuSkills(projectPath),
262
+ ]);
263
+ planuSubagentsScaffolded = subagentResult.created.length > 0;
264
+ planuUxSkillsScaffolded = skillResult.created.length;
265
+ }
266
+ catch {
267
+ /* best-effort */
268
+ }
227
269
  }
228
270
  // SPEC-594: Configure .claude/settings.json permissions (opt-in; idempotent; best-effort)
229
271
  let permissionsConfigured = null;
@@ -282,12 +324,14 @@ export async function runScaffoldWriter(projectPath, projectId, knowledge, recom
282
324
  }
283
325
  // SPEC-685: Generate all 22 workflow skill files (idempotent, best-effort)
284
326
  let workflowSkillsWritten = 0;
285
- try {
286
- const skillResult = await generateWorkflowSkills(projectPath, knowledge);
287
- workflowSkillsWritten = skillResult.written.length;
288
- }
289
- catch {
290
- /* best-effort — never fail init_project */
327
+ if (shouldWriteClaudeAssets) {
328
+ try {
329
+ const skillResult = await generateWorkflowSkills(projectPath, knowledge);
330
+ workflowSkillsWritten = skillResult.written.length;
331
+ }
332
+ catch {
333
+ /* best-effort — never fail init_project */
334
+ }
291
335
  }
292
336
  // SPEC-587: Install spec-sanctity PostToolUse hook when Claude Code is detected
293
337
  // (idempotent — overwrites only when content differs; best-effort, no-op elsewhere)
@@ -364,6 +408,10 @@ export async function runScaffoldWriter(projectPath, projectId, knowledge, recom
364
408
  permissionsConfigured,
365
409
  pluginsInstalled,
366
410
  workflowSkillsWritten,
411
+ detectedAssetHosts,
412
+ hostRulesWritten,
413
+ hostSkillsWritten,
414
+ opencodeConfigScaffolded,
367
415
  };
368
416
  }
369
417
  //# sourceMappingURL=scaffold-writer.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "3.9.5",
3
+ "version": "3.9.7",
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": "3.1.0",
36
- "@planu/core-darwin-x64": "3.1.0",
37
- "@planu/core-linux-arm64-gnu": "3.1.0",
38
- "@planu/core-linux-arm64-musl": "3.1.0",
39
- "@planu/core-linux-x64-gnu": "3.1.0",
40
- "@planu/core-linux-x64-musl": "3.1.0"
35
+ "@planu/core-darwin-arm64": "3.9.7",
36
+ "@planu/core-darwin-x64": "3.9.7",
37
+ "@planu/core-linux-arm64-gnu": "3.9.7",
38
+ "@planu/core-linux-arm64-musl": "3.9.7",
39
+ "@planu/core-linux-x64-gnu": "3.9.7",
40
+ "@planu/core-linux-x64-musl": "3.9.7"
41
41
  },
42
42
  "engines": {
43
43
  "node": ">=24.0.0"