raptor-aios 0.12.2 → 0.13.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
@@ -5,6 +5,39 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.13.0] - 2026-06-18
9
+
10
+ ### Fixed
11
+
12
+ - **`raptor new` parou de "sobrescrever" specs anteriores — resolução de feature por slug
13
+ exato.** `featureDir()`/`auditableDir()` casavam o slug informado por **sufixo**
14
+ (`d.endsWith('-'+slug)`) e devolviam o primeiro match, então um slug curto resolvia
15
+ silenciosamente para um diretório irmão mais longo (`home-screen` →
16
+ `001-dashboard-home-screen`). Os ~22 comandos a jusante (`plan`, `tasks`, `implement`,
17
+ `clarify`, `verify`, `status`, `audit`, …) passavam a operar sobre a feature **errada**,
18
+ parecendo sobrescrever os artefatos da anterior. Agora o match é **exato** (nome completo
19
+ `NNN-slug` ou slug após o número); múltiplos diretórios casando viram **erro de
20
+ ambiguidade** em vez de adivinhação.
21
+ - **`raptor new` não quebra mais quando `.raptor/specs` não existe.** Git não versiona
22
+ diretórios vazios, então um clone novo (ou após `git clean`) podia não ter a pasta; o
23
+ `readdir` agora tolera a ausência e trata como "sem features".
24
+
25
+ ### Changed
26
+
27
+ - **Branches de feature passam a empilhar (forkam do HEAD atual, não mais de `main`).** Com
28
+ `git.branch_per_feature`, cada feature nascia de uma `main` sem as specs das irmãs
29
+ (commitadas só nas branches delas); ao trocar de branch o Git esvaziava a pasta da spec
30
+ anterior (deixando só a casca `contracts/`), dando a impressão de perda de artefatos. Agora
31
+ a branch nova forka da **branch ativa**, então as specs anteriores são herdadas no working
32
+ tree e o histórico empilha. A primeira feature ainda nasce de `main` naturalmente (é onde
33
+ você começa); HEAD destacado cai no fallback da branch-base.
34
+ - **Numeração `NNN` à prova de colisão entre branches.** O próximo número agora vem da
35
+ **união** das pastas em `.raptor/specs/` **e** dos números embutidos nos nomes de **todas
36
+ as branches** (locais e remotas, via novo `listBranches()` do core), eliminando o
37
+ `feat/002-*` duplicado que surgia quando o contador só via a árvore da branch atual. O
38
+ reuso-por-slug também consulta as branches, preservando a idempotência de `raptor new`
39
+ (re-rodar um slug que vive só em uma branch irmã volta para o número dela).
40
+
8
41
  ## [0.12.2] - 2026-06-16
9
42
 
10
43
  ### Changed
@@ -14,6 +14,15 @@ export function branchExists(exec, cwd, name) {
14
14
  });
15
15
  return r.status === 0;
16
16
  }
17
+ export function listBranches(exec, cwd) {
18
+ const r = exec(["for-each-ref", "--format=%(refname:short)", "refs/heads", "refs/remotes"], { cwd });
19
+ if (r.status !== 0 || !r.stdout)
20
+ return [];
21
+ return r.stdout
22
+ .split("\n")
23
+ .map((s) => s.trim())
24
+ .filter(Boolean);
25
+ }
17
26
  export function isWorkingTreeClean(exec, cwd) {
18
27
  const r = exec(["status", "--porcelain"], { cwd });
19
28
  return r.status === 0 && r.stdout === "";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raptor/core",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js"
6
6
  }
@@ -3,7 +3,7 @@ import { BaseCommand } from "../base-command.js";
3
3
  import { step, heading } from "../shared/ui.js";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
5
5
  import { join } from "node:path";
6
- import { appendAuditEvent, buildCanonicalPrompt, deriveBranchName, executeWorkflow, extractFigmaRefs, hashString, inferKindFromJiraIssueType, loadAgentsConfig, mapDesignToSpecContext, mapIssueToSpecContext, parseFrontmatter, renderBundled, selectAgent, writeDesignScaffold, } from "../_core/dist/index.js";
6
+ import { appendAuditEvent, buildCanonicalPrompt, deriveBranchName, execSyncGitExecutor, executeWorkflow, listBranches, extractFigmaRefs, hashString, inferKindFromJiraIssueType, loadAgentsConfig, mapDesignToSpecContext, mapIssueToSpecContext, parseFrontmatter, renderBundled, selectAgent, writeDesignScaffold, } from "../_core/dist/index.js";
7
7
  import { currentActor, readConfig, readInitOptions, requireProjectRoot, } from "../shared/project.js";
8
8
  import { ensureWorkItemBranch, resolveInteractive, } from "../shared/git-prompt.js";
9
9
  import { jiraConn, jiraReadyNonInteractive, openJiraClient, } from "../shared/jira.js";
@@ -14,6 +14,12 @@ import { waitForPhaseArtifact, } from "../shared/artifact-io.js";
14
14
  import { resolveWorkflow } from "../shared/workflow-resolver.js";
15
15
  import { writeFeaturePrompt } from "../shared/feature-prompt.js";
16
16
  import { cliCommandExecutor } from "../workflow/executor.js";
17
+ function parseFeatureBranch(name) {
18
+ const m = /(?:^|\/)(\d{3})-(.+)$/.exec(name);
19
+ if (!m)
20
+ return null;
21
+ return { nnn: m[1], slug: (m[2] ?? "").split(".")[0] };
22
+ }
17
23
  export default class New extends BaseCommand {
18
24
  static description = "Create a new feature spec in the current Raptor project";
19
25
  static examples = [
@@ -88,17 +94,25 @@ export default class New extends BaseCommand {
88
94
  if (!/^[a-z][a-z0-9-]*$/.test(args.slug)) {
89
95
  this.error("Slug must be kebab-case (lowercase letters, digits, hyphens; starts with a letter).");
90
96
  }
91
- const existing = readdirSync(specsDir).filter((d) => /^\d{3}-/.test(d));
97
+ const existing = existsSync(specsDir)
98
+ ? readdirSync(specsDir).filter((d) => /^\d{3}-/.test(d))
99
+ : [];
100
+ const branchFeatures = listBranches(execSyncGitExecutor, root)
101
+ .map(parseFeatureBranch)
102
+ .filter((b) => b !== null);
92
103
  const reusedDir = existing.find((d) => d.replace(/^\d{3}-/, "") === args.slug);
93
- const reusing = reusedDir !== undefined;
94
- const numbers = existing
104
+ const reusedBranch = branchFeatures.find((b) => b.slug === args.slug);
105
+ const reusedNnn = reusedDir
106
+ ? reusedDir.slice(0, 3)
107
+ : reusedBranch?.nnn;
108
+ const reusing = reusedNnn !== undefined;
109
+ const dirNumbers = existing
95
110
  .map((d) => parseInt(d.slice(0, 3), 10))
96
111
  .filter((n) => !Number.isNaN(n));
97
- const nextN = (numbers.length ? Math.max(...numbers) : 0) + 1;
98
- const nnn = reusing
99
- ? reusedDir.slice(0, 3)
100
- : String(nextN).padStart(3, "0");
101
- const featureName = reusing ? reusedDir : `${nnn}-${args.slug}`;
112
+ const branchNumbers = branchFeatures.map((b) => parseInt(b.nnn, 10));
113
+ const maxN = Math.max(0, ...dirNumbers, ...branchNumbers);
114
+ const nnn = reusing ? reusedNnn : String(maxN + 1).padStart(3, "0");
115
+ const featureName = reusing && reusedDir ? reusedDir : `${nnn}-${args.slug}`;
102
116
  const featureDir = join(specsDir, featureName);
103
117
  const actor0 = currentActor("human");
104
118
  runBeforeHook(this, {
@@ -21,7 +21,7 @@ export async function confirmPrompt(question) {
21
21
  }
22
22
  async function promptDirtyTree(cmd, branchName) {
23
23
  cmd.log("");
24
- cmd.warn(`Your working tree has uncommitted changes, and the new branch "${branchName}" forks from the base branch.`);
24
+ cmd.warn(`Your working tree has uncommitted changes, and the new branch "${branchName}" will fork from the current branch.`);
25
25
  cmd.log(" How do you want to proceed?");
26
26
  cmd.log(" 1) stash — set the changes aside, branch from a clean base");
27
27
  cmd.log(" 2) carry — take the uncommitted changes onto the new branch");
@@ -86,7 +86,7 @@ export async function ensureWorkItemBranch(input) {
86
86
  }
87
87
  return { outcome: "switched", branch: branchName, base: null };
88
88
  }
89
- const base = defaultBaseBranch(exec, root);
89
+ const base = currentBranch(exec, root) ?? defaultBaseBranch(exec, root);
90
90
  const st = createBranch(exec, root, branchName, base);
91
91
  if (st !== 0) {
92
92
  const hint = carry
@@ -59,17 +59,30 @@ export function activePresetIds(cfg) {
59
59
  ordered.push(singular);
60
60
  return ordered;
61
61
  }
62
- export function featureDir(root, slug) {
63
- const specs = join(root, '.raptor', 'specs');
64
- const dirs = existsSync(specs) ? readdirSync(specs) : [];
65
- const match = dirs.find((d) => d === slug || d.endsWith(`-${slug}`));
66
- if (match)
67
- return join(specs, match);
62
+ function resolveFeatureDir(base, slug) {
63
+ if (!existsSync(base))
64
+ return null;
65
+ const dirs = readdirSync(base);
66
+ const matches = dirs.filter((d) => d === slug || d.replace(/^\d{3}-/, '') === slug);
67
+ if (matches.length === 1)
68
+ return join(base, matches[0]);
69
+ if (matches.length > 1) {
70
+ throw new Error(`Ambiguous feature '${slug}' — matches ${matches
71
+ .sort()
72
+ .join(', ')}. Pass the full directory name (e.g. ${matches.sort()[0]}).`);
73
+ }
68
74
  if (/^\d{3}-/.test(slug)) {
69
- const direct = join(specs, slug);
75
+ const direct = join(base, slug);
70
76
  if (existsSync(direct))
71
77
  return direct;
72
78
  }
79
+ return null;
80
+ }
81
+ export function featureDir(root, slug) {
82
+ const specs = join(root, '.raptor', 'specs');
83
+ const resolved = resolveFeatureDir(specs, slug);
84
+ if (resolved)
85
+ return resolved;
73
86
  throw new Error(`feature not found: ${slug} (under ${specs})`);
74
87
  }
75
88
  export function auditableDir(root, slug) {
@@ -78,17 +91,9 @@ export function auditableDir(root, slug) {
78
91
  join(root, '.raptor', 'hotfixes'),
79
92
  ];
80
93
  for (const base of candidates) {
81
- if (!existsSync(base))
82
- continue;
83
- const dirs = readdirSync(base);
84
- const match = dirs.find((d) => d === slug || d.endsWith(`-${slug}`));
85
- if (match)
86
- return join(base, match);
87
- if (/^\d{3}-/.test(slug)) {
88
- const direct = join(base, slug);
89
- if (existsSync(direct))
90
- return direct;
91
- }
94
+ const resolved = resolveFeatureDir(base, slug);
95
+ if (resolved)
96
+ return resolved;
92
97
  }
93
98
  throw new Error(`slug not found: ${slug} (checked .raptor/specs and .raptor/hotfixes)`);
94
99
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "raptor-aios",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
4
4
  "description": "Raptor — Spec-Driven Development (SDD) CLI for modern mobile apps. Constitutional gates, audit trail, real verification (a11y/perf/stores/OS matrix), and AI-agent slash commands.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,7 @@ const CLI = join(ROOT, "packages", "cli");
29
29
  const CORE = join(ROOT, "packages", "core");
30
30
  const OUT = join(ROOT, "build", "npm");
31
31
 
32
- const VERSION = "0.12.2";
32
+ const VERSION = "0.13.0";
33
33
 
34
34
  function log(msg) {
35
35
  process.stdout.write(` ${msg}\n`);