showdar-skills 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.12.0]
10
+
11
+ ### Added
12
+
13
+ - Canonical `showdar route --stdin|--prompt [--json]` runtime bridge projecting lifecycle primary, advisors, the seven public Intent keys, advisory domain matches, and installed/missing skill availability.
14
+ - Specialized domain discovery overlay using deterministic Unicode phrase matching over `router/skill-map.yaml` inline trigger arrays; insurance terminology, underwriting/rating flows, and insurer API/UI review map to `showdar-insurance-domain`, `showdar-insurance-workflows`, and `showdar-insurance-review` without granting authority.
15
+ - `showdar git-start --type --name [--base] [--dry-run] [--json]` for deterministic safe local task-branch preparation without inferred staging, commit, merge, or push.
16
+ - Task branch isolation guidance: branch-before-edit on integration/default branches, repository-policy priority, one branch per coherent task, and conservative dirty-worktree blocking.
17
+
18
+ ### Changed
19
+
20
+ - Managed harness instructions and commands invoke the canonical runtime router with native/static fallback when the CLI is unavailable.
21
+ - Init/add guidance reflects the complete installed set; add refreshes existing commands and instructions even when the skill is already installed.
22
+ - Showdar Git documents one branch per coherent task, repository-policy priority, and explicit completion/integration authority.
23
+ - Task completion defaults to verify and report; merge and push each require their own explicit authority.
24
+
25
+ ### Safety / Compatibility
26
+
27
+ - Domain discovery does not grant authority and never replaces the lifecycle primary or rewrites Intent/mutation.
28
+ - Phase 6G authority, state, evidence, trace, and manifest contracts unchanged; no engine vendoring into projects.
29
+ - No implicit branch commit/merge/push; no automatic stash, reset, restore, or clean.
30
+ - Existing v0.11 projects remain compatible; no migration required.
31
+
9
32
  ## [0.11.0]
10
33
 
11
34
  ### Added
package/README.md CHANGED
@@ -40,6 +40,88 @@ Claude Code as supported installation targets. Choose `backend`, `qa`, or
40
40
  `product` when that gives discovery a more precise context; use `full` when
41
41
  you want all capabilities available.
42
42
 
43
+ ## Runtime routing and task branches
44
+
45
+ ```bash
46
+ printf '%s\n' 'Implement the approved underwriting form and add tests' | showdar route --stdin --json
47
+ showdar route --prompt "Explain insurance terminology"
48
+ showdar git-start --dry-run --type feature --name "IDP-123 Add pricing form"
49
+ showdar git-start --type feature --name "IDP-123 Add pricing form"
50
+ ```
51
+
52
+ Prefer `--stdin` for arbitrary or multiline prompts. Supply stdin as literal data;
53
+ never interpolate request text into a shell command. Both commands support `--json`
54
+ with a schemaVersion 1 envelope (`command`, `ok`, `data`, `warnings`, `errors`).
55
+
56
+ `route` is read-only. It consumes the existing Phase 6G authority resolver and thin
57
+ route, projecting exactly the seven public Intent keys plus lifecycle primary and
58
+ advisors. It never exposes internal capabilities, frames or authority diagnostics.
59
+ It neither authorizes work nor creates a branch. It preserves canonical advisor
60
+ semantics: a security-review phrase is not guaranteed to become a security advisor.
61
+
62
+ Domain discovery is a separate advisory overlay using the canonical
63
+ `router/skill-map.yaml` inline trigger arrays and grouped domain catalog metadata.
64
+ It uses deterministic Unicode phrase matching, not lifecycle scoring. Insurance
65
+ terminology can recommend `showdar-insurance-domain`, underwriting/rating business
66
+ flows `showdar-insurance-workflows`, and insurer API/UI terminology review
67
+ `showdar-insurance-review`. These skills do not replace lifecycle primaryCapability,
68
+ rewrite Intent/mutation, or grant authority. The existing ASCII/token ranking
69
+ helper is unsuitable for Vietnamese phrase discovery. No duplicate trigger table
70
+ or second authority engine is introduced.
71
+
72
+ When `.showdar.json` exists, availability checks managed native skill files and
73
+ recorded, still-present globally satisfied skills. Missing lifecycle primary,
74
+ advisors and domain matches are reported separately with `showdar add <name>`
75
+ suggestions; another installed skill never substitutes for the canonical route.
76
+ Without a project manifest, `managed` is false and installed/missing fields are
77
+ null (unknown), not fabricated. Invalid manifests fail explicitly. Explicit named
78
+ installed skills may load directly; this adds no mutation authority.
79
+
80
+ Managed project guidance invokes `showdar route --stdin --json` for automatic
81
+ selection, then loads the installed lifecycle skill, domain overlays and advisors.
82
+ The runtime bridge requires the `showdar` CLI in the harness environment. If it is
83
+ unavailable, native skill descriptions/static discovery are the fallback. No npm
84
+ install, npx fetch, package dependency mutation or remote download happens.
85
+ Projects receive only existing skills/commands/instructions/manifest; no wholesale
86
+ `src/`, `engine/` or `router/` copy. Init and add regenerate complete installed-set
87
+ guidance, including re-adding an existing skill. Global installs keep native discovery
88
+ and do not write project AGENTS.md or CLAUDE.md.
89
+
90
+ The four workflows remain native discoverable/installable skills; `route` does not
91
+ introduce workflow selection. Profiles remain primitive-only: `full` contains all
92
+ 18 primitives, and the insurance profile remains unchanged.
93
+
94
+ Before the first local-write task source edit on develop/development/dev/main/master
95
+ or the repository default/integration branch, prepare a task branch. Priority is
96
+ repository instructions/documented convention, explicit current user instruction,
97
+ clearly detected convention, then Showdar defaults. Explicit trunk/direct-work policy
98
+ wins. One coherent task uses one branch across plan/build/test/review. Completion
99
+ means verify and report; commit, merge and push need their own authority.
100
+
101
+ `git-start` prepares local branch state only. Types: feature (new capability), fix
102
+ (defect), refactor (internal structure), test, docs, chore (tooling/config), release,
103
+ hotfix (explicit urgent production repair). Slugs are lowercase, hyphenated, ticket
104
+ preserving and bounded; task text is passed as data and never executed.
105
+
106
+ Base resolution uses explicit `--base`, repository-local `showdar.gitBase`, established
107
+ local develop/development/dev, then local default/main/master. The helper does not
108
+ parse prose instructions or create develop. Read policy first and supply its base
109
+ when convention differs. Local Git config `showdar.gitBranchPrefix` supports custom
110
+ prefixes; `showdar.gitDirectWork=true` records an explicit direct-work convention.
111
+ Hotfix requires a convention via `--base` or `showdar.gitBase`.
112
+
113
+ Dry-run validates Git state/type/slug/base/collision and changes no refs. Execution
114
+ revalidates, then creates/switches only a task branch. Already on the matching branch
115
+ is a no-op; another task branch is blocked. An existing target is reused automatically
116
+ only when its tip equals both current HEAD and base; divergent/ambiguous targets stop
117
+ without overwriting history. Detached HEAD and active Git operations stop.
118
+
119
+ Dirty worktrees are blocked before any switch: the helper cannot prove ownership.
120
+ Confirmed current-task-only changes may use equivalent repository-specific branch
121
+ preparation after explicit ownership inspection. No automatic stash/reset/restore/clean,
122
+ staging, commit, merge or push occurs. Branch preparation itself does not authorize
123
+ source edits.
124
+
43
125
  ## Why Showdar?
44
126
 
45
127
  - **18 focused primitive skills** plus 4 adaptive workflow skills (22 installable), including an opt-in insurance domain profile.
package/bin/showdar.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import { startTaskBranch, formatGitStart } from '../src/git-start.js';
5
+ import { routeRequest, formatRoute } from '../src/runtime-route.js';
3
6
  import { homedir } from 'node:os';
4
7
  import { readFile } from 'node:fs/promises';
5
8
  import { fileURLToPath } from 'node:url';
@@ -33,6 +36,14 @@ function scopeAfter(args) {
33
36
 
34
37
  function printHelp(version, command = null) {
35
38
  const scopeUsage = '[--scope <project|global>]';
39
+ if (command === 'route') {
40
+ console.log('Usage: showdar route (--stdin | --prompt <text>) [--json]');
41
+ return;
42
+ }
43
+ if (command === 'git-start') {
44
+ console.log('Usage: showdar git-start --type <type> --name <task> [--base <branch>] [--dry-run] [--json]');
45
+ return;
46
+ }
36
47
  if (command === 'init') {
37
48
  console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>]\n\nDefaults: scope project, profile full, AI target universal.\nProject scope writes native skills, one native instruction surface, and project .showdar.json. Global scope writes verified user skill directories and ~/.showdar/global.json without instruction files. Codex and universal use .agents/skills in project scope and ~/.agents/skills in global scope; cursor uses .cursor/skills in project scope and ~/.cursor/skills in global scope; --ai all writes each shared destination once, generates OpenCode and Claude commands, and writes only the AGENTS.md block.\n\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
38
49
  return;
@@ -45,7 +56,7 @@ function printHelp(version, command = null) {
45
56
  console.log(`Showdar Skills ${version}\n\nUsage:\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n\nExamples:\n showdar add debug\n showdar add showdar-security\n showdar add test --ai cursor\n showdar add review --scope global --ai claude\n\nDefault scope: project. Default AI target: universal, or the configured .showdar.json value when present.`);
46
57
  return;
47
58
  }
48
- console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>] [--pack <local-path>]\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n showdar add-pack <local-path>\n showdar remove-pack <name>\n showdar add-workflow <local-path>\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n showdar create-pack <path> [--vendor <vendor>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]\n showdar validate-pack <local-path> [--json]\n showdar inspect-pack <local-path> [--json] [--checkpoint <file>]\n showdar doctor ${scopeUsage} [--extensions] [--json]\n showdar update-pack <local-path> [--dry-run] [--json]\n showdar validate\n showdar list [--extensions] [--json]\n showdar remove ${scopeUsage}\n\nExtension packs accept local directories/workspace paths only; tarball, URL, Git, and registry sources are rejected.\n\nDefaults: scope project, profile full, AI target universal.\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
59
+ console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>] [--pack <local-path>]\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n showdar route (--stdin | --prompt <text>) [--json]\n showdar git-start --type <type> --name <task> [--base <branch>] [--dry-run] [--json]\n showdar add-pack <local-path>\n showdar remove-pack <name>\n showdar add-workflow <local-path>\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n showdar create-pack <path> [--vendor <vendor>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]\n showdar validate-pack <local-path> [--json]\n showdar inspect-pack <local-path> [--json] [--checkpoint <file>]\n showdar doctor ${scopeUsage} [--extensions] [--json]\n showdar update-pack <local-path> [--dry-run] [--json]\n showdar validate\n showdar list [--extensions] [--json]\n showdar remove ${scopeUsage}\n\nExtension packs accept local directories/workspace paths only; tarball, URL, Git, and registry sources are rejected.\n\nDefaults: scope project, profile full, AI target universal.\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
49
60
  }
50
61
 
51
62
  async function main() {
@@ -59,10 +70,33 @@ async function main() {
59
70
  console.log(version);
60
71
  return;
61
72
  }
62
- if (args.includes('--help') || args.includes('-h')) return printHelp(version, command);
73
+ if (!['route', 'git-start'].includes(command) && (args.includes('--help') || args.includes('-h'))) return printHelp(version, command);
63
74
 
64
75
  const scope = ['init', 'status', 'doctor', 'remove', 'add'].includes(command) ? scopeAfter(args) : null;
65
76
 
77
+ if (command === 'git-start') {
78
+ const { values } = parseArgs({ args: args.slice(1), options: { type: { type: 'string' }, name: { type: 'string' }, base: { type: 'string' }, 'dry-run': { type: 'boolean' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' } } });
79
+ if (values.help) return printHelp(version, command);
80
+ const data = startTaskBranch({ cwd: projectRoot, type: values.type, name: values.name, base: values.base, dryRun: values['dry-run'] });
81
+ console.log(values.json ? JSON.stringify({ schemaVersion: 1, command, ok: true, data, warnings: [], errors: [] }, null, 2) : formatGitStart(data));
82
+ return;
83
+ }
84
+
85
+ if (command === 'route') {
86
+ const { values } = parseArgs({ args: args.slice(1), options: { stdin: { type: 'boolean' }, prompt: { type: 'string' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' } } });
87
+ if (values.help) return printHelp(version, command);
88
+ if (Boolean(values.stdin) === (values.prompt !== undefined)) throw new Error('Select exactly one of --stdin or --prompt.');
89
+ let prompt = values.prompt;
90
+ if (values.stdin) {
91
+ process.stdin.setEncoding('utf8');
92
+ prompt = '';
93
+ for await (const chunk of process.stdin) prompt += chunk;
94
+ }
95
+ const data = await routeRequest({ prompt, cwd: projectRoot, packageRoot });
96
+ console.log(values.json ? JSON.stringify({ schemaVersion: 1, command, ok: true, data, warnings: [], errors: [] }, null, 2) : formatRoute(data));
97
+ return;
98
+ }
99
+
66
100
  if (command === 'list') {
67
101
  if (args.includes('--extensions')) {
68
102
  const isJson = args.includes('--json');
@@ -342,6 +376,9 @@ async function main() {
342
376
  }
343
377
 
344
378
  main().catch((error) => {
345
- console.error(`showdar: ${error.message}`);
379
+ const command = process.argv[2];
380
+ if (['route', 'git-start'].includes(command) && process.argv.slice(3).includes('--json')) {
381
+ console.log(JSON.stringify({ schemaVersion: 1, command, ok: false, data: null, warnings: [], errors: [{ code: 'INVALID_REQUEST', message: error.message }] }, null, 2));
382
+ } else console.error(`showdar: ${error.message}`);
346
383
  process.exitCode = 1;
347
384
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showdar-skills",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Production-grade software engineering lifecycle skills for coding agents.",
5
5
  "type": "module",
6
6
  "bin": { "showdar": "./bin/showdar.js" },
@@ -38,7 +38,7 @@ routes:
38
38
  triggers: ["resume interrupted implementation", "recover partial work", "session stopped", "continue partial work", "missing context"]
39
39
  skill: showdar-recover
40
40
  git:
41
- triggers: ["commit", "stage only task files", "merge branch", "rebase branch", "cherry-pick commit", "git conflict", "push branch", "clean merged branches", "finish git workflow"]
41
+ triggers: ["commit", "stage only task files", "merge branch", "rebase branch", "cherry-pick commit", "git conflict", "push branch", "clean merged branches", "finish git workflow", "start task branch", "create feature branch", "create fix branch", "branch for this task"]
42
42
  skill: showdar-git
43
43
  requirements:
44
44
  triggers: ["requirements", "business rules", "acceptance criteria", "user stories", "use cases", "requirement matrix", "missing requirement", "ambiguity"]
@@ -46,6 +46,29 @@ This skill owns repository state transitions, not source-code review or GitHub a
46
46
  - Push, tag, remote deletion, pull with mutation, GitHub operations, and release actions require explicit intent.
47
47
  - Report exactly what changed after each Git mutation.
48
48
 
49
+ ## Task branch isolation
50
+
51
+ Before the first task-owned source edit for a local-write engineering task, inspect Git state (`git status --short`, current branch, branch history and repository policy). Direct develop/main edits are not the default: on develop, development, dev, main, master or the default/integration branch, prepare a dedicated task branch before source mutation.
52
+
53
+ Priority: repository instructions/documented branching convention; explicit current user instruction; clearly detected existing convention; Showdar safe default. Explicit trunk-based/direct-work policy wins: do not impose GitFlow. Branch preparation does not authorize source mutation.
54
+
55
+ Use one branch per coherent task across understand, requirements, plan, design, build, test and review. Continue the current task branch; never create a branch per skill or nested branches.
56
+
57
+ Default types: new user-visible capability → `feature`; observed defect → `fix`; internal restructuring → `refactor`; tests only → `test`; documentation only → `docs`; tooling/config/maintenance → `chore`; release preparation → `release`; explicit urgent production repair → `hotfix`. Names use a bounded lowercase hyphen slug preserving ticket keys.
58
+
59
+ Prefer established local develop for normal work; otherwise the repository default/integration branch. Do not create develop automatically. Explicit hotfix follows repository convention. Read prose policy yourself; the helper does not parse repository instructions. Supply `--base` or repository-local Git config `showdar.gitBase`, `showdar.gitBranchPrefix`, `showdar.gitDirectWork` for deterministic custom conventions.
60
+
61
+ ```bash
62
+ showdar git-start --dry-run --type feature --name "Task description"
63
+ showdar git-start --type feature --name "Task description"
64
+ ```
65
+
66
+ The helper revalidates before creating/switching and never edits source. A clean worktree can branch. Confirmed current-task changes may be carried only through equivalent repository-specific Git preparation with proven ownership and Git permission; the helper conservatively blocks dirty changes before switching because it cannot prove ownership. Unrelated/ambiguous changes → STOP. No hidden stash, reset, restore or clean.
67
+
68
+ Existing same-name branches must be inspected. The helper reuses only an unused branch with the same tip as both current HEAD and base; an already-active matching branch is a continue/no-op. Any other collision → STOP; never reset or overwrite branch history. Already on another task branch → inspect task identity before switching.
69
+
70
+ Default completion: task branch → verify → report. No inferred commit, merge or push. Merge only when explicitly requested or clearly included in current repository/user task authority; push remains independently explicit.
71
+
49
72
  ## Workflow
50
73
 
51
74
  ### 1. Inspect Git state
@@ -273,7 +296,8 @@ report -> SHA, staged paths, exclusions, and clean/dirty final state
273
296
  ### Feature completion without push
274
297
 
275
298
  ```text
276
- feature branch -> selective commit -> switch develop -> merge using repository convention -> verify
299
+ task branch -> verify -> report
300
+ selective commit only when requested; merge only with current task authority
277
301
  remote -> not touched because no push request was given
278
302
  ```
279
303
 
@@ -1,5 +1,10 @@
1
1
  import { ALL_SKILLS } from './catalog.js';
2
2
 
3
+ const RUNTIME_GUIDANCE = `Automatic Showdar selection: route the current request through \`showdar route --stdin --json\` when the Showdar CLI is available. Supply the prompt as literal stdin data, never as interpolated shell syntax. Honor the canonical lifecycle primary; do not substitute another installed skill when it is missing. Report missing skills and suggest \`showdar add <name>\`; never auto-install. Include installed domain matches only as specialized context overlays; load returned advisors only when installed. Domain discovery does not grant authority or replace the lifecycle route.
4
+ Explicit named-skill requests may load that installed skill directly without automatic routing. Workflow skills remain native discoverable choices; the router does not select workflows. If the CLI is unavailable, use native skill discovery/static descriptions below. Do not fetch a CLI through npx or install dependencies automatically.
5
+ Routing does not authorize mutation. Inspect the returned mutation class and current task authority before work. For local-write tasks, before the first task-owned source edit inspect Git state. On develop, development, dev, main, master or the repository default/integration branch, DO NOT begin source edits yet: prepare one branch per coherent task first. Follow repository instructions/documented convention, explicit current user instruction, clearly detected convention, then the Showdar safe default. Explicit trunk/direct-work policy wins.
6
+ Use \`showdar git-start --dry-run --type <type> --name <task>\`, then \`showdar git-start --type <type> --name <task>\`, or equivalent repository-specific preparation. Confirm the task branch before mutating skill execution. Reuse that branch across plan/build/test/review; do not nest branches per skill. Dirty ownership or branch collisions require inspection; never infer stash, reset, restore or clean. Git-start does not authorize source mutation, commit, merge or push. Completion defaults to verify and report on the task branch; merge and push require their own explicit authority.`;
7
+
3
8
  const CANONICAL_ROUTE_ORDER = [
4
9
  ['map repository architecture, dependencies, or impact', 'showdar-understand'],
5
10
  ['plan implementation of agreed behavior and scope', 'showdar-plan'],
@@ -37,7 +42,7 @@ export function renderShowdarInstruction(skillIds) {
37
42
  routes.push(`- ${getSkillDescription(id)} -> \`${id}\``);
38
43
  }
39
44
  }
40
- return `${routes.join('\n')}\n`;
45
+ return `${routes.join('\n')}\n\n${RUNTIME_GUIDANCE}\n`;
41
46
  }
42
47
 
43
48
  export function renderShowdarCommand(skillId) {
@@ -48,6 +53,7 @@ description: ${description}
48
53
 
49
54
  Load and follow \`${skillId}\`.
50
55
  Ground in the current repository.
56
+ ${RUNTIME_GUIDANCE}
51
57
  Request: \$ARGUMENTS
52
58
  `;
53
59
  }
@@ -60,6 +66,8 @@ description: Invoke a specific Showdar flagship skill explicitly
60
66
  ---
61
67
  Select exactly one requested Showdar skill and follow it: ${ids}. Whole-task intent (complete feature, end-to-end fix, release lifecycle, active incident) selects a workflow; single primitive intent stays primitive. If the requested name is ambiguous, choose the smallest matching skill from this list and say which one was selected.
62
68
 
69
+ ${RUNTIME_GUIDANCE}
70
+
63
71
  Request: \$ARGUMENTS
64
72
  `;
65
73
  }
@@ -0,0 +1,87 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ export const TASK_BRANCH_TYPES = ['feature', 'fix', 'refactor', 'test', 'docs', 'chore', 'release', 'hotfix'];
6
+
7
+ function git(cwd, args, optional = false) {
8
+ try { return execFileSync('git', ['--no-optional-locks', ...args], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10000 }).trim(); }
9
+ catch (error) {
10
+ if (optional && error.status === 1) return null;
11
+ throw new Error(`Git operation failed (${args[0]}): ${error.stderr?.toString().trim() || error.message}`);
12
+ }
13
+ }
14
+
15
+ export function taskSlug(name, type) {
16
+ if (typeof name !== 'string' || !name.trim()) throw new Error('--name requires a task name.');
17
+ const words = name.normalize('NFKD').replace(/\p{M}/gu, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').split('-');
18
+ // Drop a redundant task verb, including one following a useful ticket key.
19
+ const offset = /^[a-z]+$/.test(words[0]) && /^\d+$/.test(words[1] ?? '') ? 2 : 0;
20
+ if ([type, 'add'].includes(words[offset])) words.splice(offset, 1);
21
+ const slug = words.join('-').slice(0, 80).replace(/-+$/g, '');
22
+ if (!slug) throw new Error('Task name contains no safe branch characters.');
23
+ return slug;
24
+ }
25
+
26
+ function inspect({ cwd, type, name, base }) {
27
+ if (!TASK_BRANCH_TYPES.includes(type)) throw new Error(`Invalid task type. Expected ${TASK_BRANCH_TYPES.join(', ')}.`);
28
+ const root = git(cwd, ['rev-parse', '--show-toplevel']);
29
+ const current = git(root, ['symbolic-ref', '--quiet', '--short', 'HEAD'], true);
30
+ if (!current) throw new Error('Detached HEAD: choose a repository branch before git-start.');
31
+ const head = git(root, ['rev-parse', 'HEAD']);
32
+ const dirty = git(root, ['status', '--porcelain=v1', '--untracked-files=all']);
33
+ for (const marker of ['MERGE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD', 'rebase-merge', 'rebase-apply', 'BISECT_LOG', 'sequencer']) {
34
+ const file = git(root, ['rev-parse', '--git-path', marker]);
35
+ if (existsSync(path.resolve(root, file))) throw new Error(`Active Git operation: ${marker}.`);
36
+ }
37
+ const config = key => git(root, ['config', '--local', '--get', key], true);
38
+ const prefix = config('showdar.gitBranchPrefix') ?? type;
39
+ const branch = `${prefix}/${taskSlug(name, type)}`;
40
+ git(root, ['check-ref-format', '--branch', branch]);
41
+ if (branch.startsWith('-')) throw new Error('Invalid task branch.');
42
+ const configuredBase = config('showdar.gitBase');
43
+ const defaultRef = git(root, ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], true)?.replace(/^origin\//, '');
44
+ const localBranch = value => value && !value.startsWith('-') && git(root, ['show-ref', '--verify', '--quiet', `refs/heads/${value}`], true) !== null;
45
+ if (base && !localBranch(base)) throw new Error('Explicit --base must be an existing local branch.');
46
+ if (configuredBase && !localBranch(configuredBase)) throw new Error('Configured showdar.gitBase must be an existing local branch.');
47
+ if (type === 'hotfix' && !base && !configuredBase) throw new Error('Hotfix requires --base or repository showdar.gitBase convention.');
48
+ const resolvedBase = base ?? configuredBase ?? ['develop', 'development', 'dev', defaultRef, 'main', 'master'].find(localBranch);
49
+ if (!resolvedBase) throw new Error('Cannot infer integration base; use --base with the repository convention.');
50
+ const baseHead = git(root, ['rev-parse', `refs/heads/${resolvedBase}`]);
51
+ const integration = new Set(['develop', 'development', 'dev', 'main', 'master', defaultRef, resolvedBase]);
52
+ let action;
53
+ if (current === branch) action = 'continue';
54
+ else {
55
+ if (dirty) throw new Error('Dirty worktree ownership is ambiguous. Preserve changes; git-start never stashes or carries unproven task changes.');
56
+ if (!integration.has(current)) throw new Error('Already on another task branch; continue that coherent task or select its integration branch explicitly.');
57
+ const direct = config('showdar.gitDirectWork');
58
+ if (direct !== null && !['true', 'false'].includes(direct)) throw new Error('showdar.gitDirectWork must be true or false.');
59
+ if (direct === 'true') action = 'direct';
60
+ else if (localBranch(branch)) {
61
+ const targetHead = git(root, ['rev-parse', `refs/heads/${branch}`]);
62
+ // Equal tips are the only automatically provable safe unused branch.
63
+ if (targetHead !== head || targetHead !== baseHead) throw new Error('Existing task branch collision: history/ownership is ambiguous.');
64
+ action = 'switch';
65
+ } else {
66
+ if (baseHead !== head && git(root, ['merge-base', '--is-ancestor', head, baseHead], true) === null) throw new Error('Current branch diverges from base; inspect repository history before branching.');
67
+ action = 'create';
68
+ }
69
+ }
70
+ return { root, current, head, base: resolvedBase, baseHead, branch, action, dirty: dirty ? 'present' : 'clean' };
71
+ }
72
+
73
+ export function startTaskBranch(options) {
74
+ const proposal = inspect(options);
75
+ if (options.dryRun || ['continue', 'direct'].includes(proposal.action)) return { ...proposal, dryRun: Boolean(options.dryRun) };
76
+ const fresh = inspect(options);
77
+ if (JSON.stringify(fresh) !== JSON.stringify(proposal)) throw new Error('Git state changed during preflight; retry after inspection.');
78
+ if (fresh.action === 'create') git(fresh.root, ['switch', '-c', fresh.branch, fresh.baseHead]);
79
+ else git(fresh.root, ['switch', fresh.branch]);
80
+ const current = git(fresh.root, ['branch', '--show-current']);
81
+ if (current !== fresh.branch) throw new Error('Task branch switch was not confirmed.');
82
+ return { ...fresh, dryRun: false, current };
83
+ }
84
+
85
+ export function formatGitStart(data) {
86
+ return `Task branch: ${data.branch}\nBase: ${data.base}\nAction: ${data.action}${data.dryRun ? ' (dry-run)' : ''}\nWorktree: ${data.dirty}\nBranch preparation does not authorize source edits, commit, merge or push.`;
87
+ }
package/src/project.js CHANGED
@@ -588,6 +588,23 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
588
588
  if (effectiveScope !== 'project' && effectiveScope !== 'global') throw new Error(`Unknown scope "${effectiveScope}".`);
589
589
 
590
590
  if (existingManifest?.skills?.includes(skillId)) {
591
+ const refreshedFiles = [];
592
+ const priorOwned = ownedPathSet(existingManifest);
593
+ for (const target of existingManifest.commandHarness ?? []) {
594
+ const commandRoot = effectiveScope === 'global' ? globalCommandRootForTarget(target, { homeRoot: home }) : commandRootFor(target, cwd);
595
+ if (commandRoot) await generateCommandFiles({ baseRoot, skillIds: existingManifest.skills, target, commandRoot, priorOwned, newFiles: refreshedFiles });
596
+ }
597
+ if (effectiveScope === 'project' && existingManifest.instructions) {
598
+ const instruction = existingManifest.instructions;
599
+ const file = path.join(cwd, instruction.file);
600
+ if (instruction.kind === 'block') await writeManagedBlock(file, existingManifest.skills);
601
+ else if (instruction.kind === 'file') await writeCursorRule(file, existingManifest.skills);
602
+ }
603
+ if (refreshedFiles.length) {
604
+ const files = new Map(existingManifest.files.map(f => [f.path, f]));
605
+ for (const f of refreshedFiles) files.set(f.path, f);
606
+ await writeJsonAtomic(effectiveScope === 'global' ? globalManifestPath(home) : path.join(cwd, PROJECT_MANIFEST), { ...existingManifest, files: [...files.values()] });
607
+ }
591
608
  return { skill: skillId, root: '', destination: '', added: false, scope: effectiveScope, ai: effectiveAi, profile: existingManifest.profile };
592
609
  }
593
610
 
@@ -629,41 +646,19 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
629
646
  }
630
647
  }
631
648
 
649
+ const allSkillIds = [...new Set([...(existingManifest?.skills ?? []), skillId])];
632
650
  const newCommands = [];
633
- for (const { target, root } of commandHarnesses) {
634
- const shortName = skillId.replace(/^showdar-/, '');
635
- const dest = path.join(root, `${shortName}.md`);
636
- const rel = manifestPathFor(baseRoot, dest);
637
- if ((await exists(dest)) && !priorOwned.has(rel)) {
638
- throw new Error(`Refusing to overwrite existing non-Showdar-managed command: ${dest}`);
639
- }
640
- await mkdir(path.dirname(dest), { recursive: true });
641
- const content = renderShowdarCommand(skillId);
642
- await writeTextAtomic(dest, content);
643
- files.push({ path: rel, hash: await hashTree(dest) });
644
- newCommands.push({ target, name: shortName, path: rel });
645
- }
646
-
647
- if (commandHarnesses.length > 0) {
648
- for (const { target, root } of commandHarnesses) {
649
- const aggregatorDest = path.join(root, 'skill.md');
650
- const aggregatorRel = manifestPathFor(baseRoot, aggregatorDest);
651
- const addAllSkillIds = [...new Set([...(existingManifest?.skills ?? []), skillId])];
652
- const aggregatorContent = renderShowdarAggregator(addAllSkillIds);
653
- if ((await exists(aggregatorDest)) && !priorOwned.has(aggregatorRel)) {
654
- throw new Error(`Refusing to overwrite existing non-Showdar-managed command: ${aggregatorDest}`);
655
- }
656
- await mkdir(path.dirname(aggregatorDest), { recursive: true });
657
- await writeTextAtomic(aggregatorDest, aggregatorContent);
658
- files.push({ path: aggregatorRel, hash: await hashTree(aggregatorDest) });
659
- newCommands.push({ target, name: 'skill', path: aggregatorRel });
660
- }
651
+ const harnessTargets = [...new Set([...(existingManifest?.commandHarness ?? []), ...commandHarnesses.map(c => c.target)])];
652
+ for (const target of harnessTargets) {
653
+ const root = effectiveScope === 'global' ? globalCommandRootForTarget(target, { homeRoot: home }) : commandRootFor(target, cwd);
654
+ if (!root) continue;
655
+ const generated = await generateCommandFiles({ baseRoot, skillIds: allSkillIds, target, commandRoot: root, priorOwned, newFiles: files, managedRoots });
656
+ newCommands.push(...generated.map(c => ({ target, name: c.shortName, path: manifestPathFor(baseRoot, c.destination) })));
661
657
  }
662
658
 
663
659
  const merged = new Map((existingManifest?.files ?? []).map((e) => [e.path, e]));
664
660
  for (const f of files) merged.set(f.path, f);
665
661
 
666
- const allSkillIds = [...new Set([...(existingManifest?.skills ?? []), skillId])];
667
662
  const instructionFile = existingManifest?.instructions ?? (effectiveScope === 'project' ? instructionSurfaceFor(effectiveAi, cwd) : null);
668
663
  const commandHarness = [...new Set([...(existingManifest?.commandHarness ?? []), ...commandHarnesses.map((c) => c.target)])];
669
664
 
@@ -673,10 +668,10 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
673
668
  packageVersion,
674
669
  profile: existingManifest?.profile ?? null,
675
670
  ai: effectiveAi,
676
- targets,
671
+ targets: [...new Set([...(existingManifest?.targets ?? []), ...targets])],
677
672
  skills: allSkillIds,
678
673
  satisfiedByGlobal: existingManifest?.satisfiedByGlobal ?? [],
679
- commands: [...(existingManifest?.commands ?? []), ...newCommands],
674
+ commands: [...new Map([...(existingManifest?.commands ?? []), ...newCommands].map(c => [c.path, c])).values()],
680
675
  files: [...merged.values()],
681
676
  instructions: instructionFile ? { file: instructionFile.file, kind: instructionFile.kind } : null,
682
677
  commandHarness,
@@ -0,0 +1,99 @@
1
+ import path from 'node:path';
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { ALL_SKILLS, SKILLS } from './catalog.js';
5
+ import { resolveIntentFromPrompt } from './intent-resolver/index.js';
6
+ import { assertSafeManagedPath } from './path-safety.js';
7
+
8
+ async function readJson(file) {
9
+ try {
10
+ const value = JSON.parse(await readFile(file, 'utf8'));
11
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Manifest must be an object.');
12
+ return value;
13
+ }
14
+ catch (error) { if (error.code === 'ENOENT') return null; throw new Error(`Cannot read Showdar manifest: ${error.message}`); }
15
+ }
16
+
17
+ async function skillPresent(root, relative) {
18
+ const file = path.resolve(root, relative, 'SKILL.md');
19
+ await assertSafeManagedPath(root, file);
20
+ try { return (await stat(file)).isFile(); }
21
+ catch (error) { if (error.code === 'ENOENT') return false; throw error; }
22
+ }
23
+
24
+ // ponytail: inline JSON trigger arrays only; use a YAML parser if the canonical format changes.
25
+ // The canonical map uses JSON-compatible inline trigger arrays. Read only that
26
+ // documented subset; this is discovery data, never an authority input.
27
+ export async function discoverDomain(prompt, packageRoot) {
28
+ const map = await readFile(path.join(packageRoot, 'router/skill-map.yaml'), 'utf8');
29
+ const specialized = new Set(SKILLS.filter(s => s.id !== `showdar-${s.domain}`).map(s => s.id));
30
+ const text = String(prompt).normalize('NFC').toLowerCase();
31
+ const matches = [];
32
+ for (const entry of map.matchAll(/^ [\w-]+:\n triggers: (\[[^\n]*\])\n skill: ([\w-]+)$/gm)) {
33
+ if (!specialized.has(entry[2])) continue;
34
+ const triggers = JSON.parse(entry[1]);
35
+ if (triggers.some(trigger => {
36
+ const phrase = trigger.normalize('NFC').toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
37
+ return new RegExp(`(?<![\\p{L}\\p{N}])${phrase}(?![\\p{L}\\p{N}])`, 'u').test(text);
38
+ }) || text.includes(entry[2])) matches.push(entry[2]);
39
+ }
40
+ return [...new Set(matches)].sort();
41
+ }
42
+
43
+ export function projectLifecycle(route) {
44
+ return { primary: route.primary.skill, advisors: [...route.advisors] };
45
+ }
46
+
47
+ export async function routeRequest({ prompt, cwd, packageRoot, home = homedir() }) {
48
+ if (typeof prompt !== 'string' || !prompt.trim()) throw new Error('A non-empty prompt is required. Use --stdin or --prompt.');
49
+ const canonical = resolveIntentFromPrompt(prompt);
50
+ const lifecycle = projectLifecycle(canonical);
51
+ const domain = { matches: await discoverDomain(prompt, packageRoot) };
52
+ await assertSafeManagedPath(cwd, path.join(cwd, '.showdar.json'));
53
+ const manifest = await readJson(path.join(cwd, '.showdar.json'));
54
+ const installed = new Set();
55
+ if (manifest) {
56
+ if (manifest.version !== 2 || !Array.isArray(manifest.skills) || !Array.isArray(manifest.files)) throw new Error('Invalid managed Showdar manifest v2.');
57
+ for (const id of manifest.skills) {
58
+ if (typeof id !== 'string' || !ALL_SKILLS.some(s => s.id === id)) continue;
59
+ for (const entry of manifest.files) {
60
+ if (typeof entry.path === 'string' && path.basename(entry.path) === id && await skillPresent(cwd, entry.path)) installed.add(id);
61
+ }
62
+ }
63
+ if (manifest.satisfiedByGlobal?.length) {
64
+ const globalPath = path.join(home, '.showdar/global.json');
65
+ await assertSafeManagedPath(home, globalPath);
66
+ const global = await readJson(globalPath);
67
+ const owned = new Set((global?.files ?? []).map(e => e.path));
68
+ for (const entry of manifest.satisfiedByGlobal) {
69
+ if (manifest.skills.includes(entry.skill) && typeof entry.path === 'string' && path.basename(entry.path) === entry.skill && owned.has(entry.path) && await skillPresent(home, entry.path)) installed.add(entry.skill);
70
+ }
71
+ }
72
+ }
73
+ return {
74
+ intent: canonical.intent,
75
+ lifecycle,
76
+ domain,
77
+ availability: {
78
+ managed: manifest !== null,
79
+ installed: manifest ? [...installed].sort() : null,
80
+ missingLifecyclePrimary: manifest ? !installed.has(lifecycle.primary) : null,
81
+ missingAdvisors: manifest ? lifecycle.advisors.filter(id => !installed.has(id)) : null,
82
+ missingDomainMatches: manifest ? domain.matches.filter(id => !installed.has(id)) : null,
83
+ },
84
+ };
85
+ }
86
+
87
+ export function formatRoute(data) {
88
+ const { lifecycle, domain, intent, availability: a } = data;
89
+ const lines = [`Lifecycle: ${lifecycle.primary}`, `Domain: ${domain.matches.join(', ') || 'none'}`, `Advisors: ${lifecycle.advisors.join(', ') || 'none'}`, `Mutation: ${intent.mutation}`];
90
+ lines.push(`Availability: ${!a.managed ? 'not-managed / unknown' : a.missingLifecyclePrimary ? 'lifecycle not installed' : 'lifecycle installed'}`);
91
+ if (a.managed) {
92
+ for (const id of [lifecycle.primary, ...lifecycle.advisors, ...domain.matches]) {
93
+ const present = a.installed.includes(id);
94
+ lines.push(`${id}: ${present ? 'installed' : 'not installed'}`);
95
+ if (!present) lines.push(`Suggested command: showdar add ${id.replace(/^showdar-/, '')}`);
96
+ }
97
+ }
98
+ return lines.join('\n');
99
+ }