raptor-aios 0.6.2 → 0.7.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
@@ -3,6 +3,27 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.7.0] - 2026-06-08
7
+
8
+ ### Added
9
+
10
+ - **Governed gate waivers sourced from ADRs.** `raptor verify` (and `checklist`) now read `.raptor/memory/decisions.md`: an accepted ADR that declares an `Overrides: gate.id` line downgrades a failing **required/advisory** gate to a non-blocking `overridden` result, recorded in the report as `⊝ overridden by ADR-NNN: <justification>`. **Critical** gates can never be waived this way — they keep requiring human sign-off (C4/C5), and an ADR aimed at one is explicitly reported as ignored. The ADR shape is deliberately tolerant (any heading level, `Status:` defaults to accepted, multiple `Overrides:` lines per ADR), but gate ids are matched strictly (`gate.<seg>.<seg>`) so prose bullets and version tokens (`v1.2`, `ADR-1.2`) are never parsed as gates.
11
+ - **Honest M1 store opt-out.** A feature that genuinely touches no restricted OS API no longer needs invented permissions: keep the lists empty and declare `stores.no_restricted_apis: "<reason>"`. The M1 gate (`gate.mobile.stores`) then passes — mirroring `a11y.wcag_level: "n/a"` (M3) and `perf_budget.scope: "none"` (M5). An empty `stores:` block with no opt-out and no ADR still fails.
12
+
13
+ ### Internal
14
+
15
+ - New `gates/adr-overrides.ts` (`parseAdrOverrides` / `readAdrOverrides`); `runGates` gains an `overrides` option and an `overridden` status/summary count; `GateOverride` type added. `verify` warns when an ADR override targets an unknown gate id. The runner no longer mutates a gate's own result object when reporting an ignored critical override. Spec templates and the mobile-opinionated preset document both opt-out paths.
16
+
17
+ ## [0.6.3] - 2026-06-06
18
+
19
+ ### Fixed
20
+
21
+ - **`raptor new --jira=<KEY>` no longer creates an empty, mislabelled spec when the Jira card can't be read.** Previously, when a ticket was given but Jira was not connected (or the token had expired, or the issue was unreachable), `new` warned and then degraded to seeding a spec stamped with the Jira ID but **no card content** — and created the branch too. Now, when a fetch was expected (the default), an unreadable card is a **hard stop**: `new` aborts **before any side effect** (no spec, no directory, no branch) with an actionable message that states the reason and points to `raptor jira connect` (or configuring the Jira MCP server). The intentional offline path is unchanged — pass `--no-jira-fetch` to record the ticket ID only.
22
+
23
+ ### Internal
24
+
25
+ - `New.fetchJiraContext` now returns a discriminated result (`{ ok, context }` | `{ ok: false, reason }`) so the caller can abort with the specific failure reason instead of silently degrading. Removed the now-unreachable `"id only (fetch unavailable)"` status tag.
26
+
6
27
  ## [0.6.2] - 2026-06-06
7
28
 
8
29
  ### Changed
package/README.md CHANGED
@@ -360,7 +360,7 @@ Cada gate exige campos no *frontmatter* de `spec.md` ou `plan.md` — declarativ
360
360
 
361
361
  | Gate | Nível | Exige no frontmatter | Verificável por |
362
362
  | --------------------------------------------- | ----- | ------------------------------------------------------------ | ------------------ |
363
- | **M1 — App/Play Store Compliance** | 🟡 | `spec.stores.ios_permissions`, `.android_permissions` | `verify stores` |
363
+ | **M1 — App/Play Store Compliance** | 🟡 | `spec.stores.ios_permissions`, `.android_permissions` (ou `stores.no_restricted_apis: "<motivo>"` p/ opt-out honesto) | `verify stores` |
364
364
  | **M2 — Privacidade & Residência (LGPD/GDPR)** | 🟡 | `plan.privacy.lawful_basis`, `.residency`, `.retention` | — |
365
365
  | **M3 — Acessibilidade (WCAG AA)** | 🟡 | `spec.a11y.wcag_level`, `.criteria` | `verify a11y` |
366
366
  | **M4 — Matriz de SO** | 🟡 | `plan.os_matrix.ios_min`, `.android_min` | `verify os-matrix` |
@@ -419,6 +419,17 @@ observability:
419
419
 
420
420
  </details>
421
421
 
422
+ > 🧯 **Feature sem APIs restritas?** Não invente permissões. Há dois caminhos honestos e auditáveis para o M1:
423
+ > 1. **Opt-out no spec** — deixe as listas vazias e declare `stores.no_restricted_apis: "<motivo>"` (espelha `a11y.wcag_level: "n/a"` e `perf_budget.scope: "none"`).
424
+ > 2. **Override por ADR** — registre um ADR aceito em `.raptor/memory/decisions.md` com `Overrides: gate.mobile.stores`; o `verify` o lê em runtime e marca o gate como `⊝ overridden by ADR-NNN`. Gates **críticos** nunca são waivados por ADR (exigem assinatura humana).
425
+ >
426
+ > ```markdown
427
+ > ## ADR-001: Feature sem APIs restritas de OS
428
+ > - Status: accepted
429
+ > - Overrides: gate.mobile.stores
430
+ > - Justification: Regras de negócio puras; não toca camera/location/etc.
431
+ > ```
432
+
422
433
  ---
423
434
 
424
435
  ## 🔬 A ponta de verificação (`verify`)
@@ -0,0 +1,90 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const INACTIVE_STATUS = new Set([
4
+ "proposed",
5
+ "draft",
6
+ "rejected",
7
+ "superseded",
8
+ "deprecated",
9
+ "withdrawn",
10
+ ]);
11
+ function extractAdrLabel(heading) {
12
+ const m = heading.match(/\bADR[-\s]?([\w.]+)/i);
13
+ if (!m)
14
+ return null;
15
+ return `ADR-${m[1]}`;
16
+ }
17
+ const HEADING_RE = /^#{1,}\s+(.*)$/;
18
+ const OVERRIDES_RE = /^[-*\s]*overrides\s*:\s*(.+)$/i;
19
+ const STATUS_RE = /^[-*\s]*status\s*:\s*(.+)$/i;
20
+ const JUSTIFICATION_RE = /^[-*\s]*justification\s*:\s*(.+)$/i;
21
+ const GATE_ID_RE = /^gate\.[a-z0-9_]+(?:\.[a-z0-9_]+)+$/i;
22
+ function fieldValue(line, re) {
23
+ const m = line.match(re);
24
+ return m ? m[1].trim() : null;
25
+ }
26
+ function parseGateIds(values) {
27
+ const ids = [];
28
+ for (const value of values) {
29
+ for (const t of value.split(/[,\s]+/)) {
30
+ const id = t.trim();
31
+ if (GATE_ID_RE.test(id) && !ids.includes(id))
32
+ ids.push(id);
33
+ }
34
+ }
35
+ return ids;
36
+ }
37
+ export function parseAdrOverrides(markdown) {
38
+ const lines = markdown.split(/\r?\n/);
39
+ const out = [];
40
+ let label = null;
41
+ let status = null;
42
+ let overridesLines = [];
43
+ let justification = null;
44
+ const flush = () => {
45
+ if (!label || overridesLines.length === 0)
46
+ return;
47
+ if (status && INACTIVE_STATUS.has(status.toLowerCase()))
48
+ return;
49
+ const gates = parseGateIds(overridesLines);
50
+ const reason = justification?.trim() || label;
51
+ for (const gate of gates) {
52
+ out.push({ gate, adr: label, justification: reason });
53
+ }
54
+ };
55
+ for (const line of lines) {
56
+ const heading = line.match(HEADING_RE);
57
+ if (heading) {
58
+ flush();
59
+ label = extractAdrLabel(heading[1]);
60
+ status = null;
61
+ overridesLines = [];
62
+ justification = null;
63
+ continue;
64
+ }
65
+ if (!label)
66
+ continue;
67
+ const ov = fieldValue(line, OVERRIDES_RE);
68
+ if (ov !== null)
69
+ overridesLines.push(ov);
70
+ const st = fieldValue(line, STATUS_RE);
71
+ if (st !== null)
72
+ status = st;
73
+ const ju = fieldValue(line, JUSTIFICATION_RE);
74
+ if (ju !== null)
75
+ justification = ju;
76
+ }
77
+ flush();
78
+ return out;
79
+ }
80
+ export function readAdrOverrides(projectRoot) {
81
+ const path = join(projectRoot, ".raptor", "memory", "decisions.md");
82
+ if (!existsSync(path))
83
+ return {};
84
+ const raw = readFileSync(path, "utf8");
85
+ const map = {};
86
+ for (const o of parseAdrOverrides(raw)) {
87
+ map[o.gate] = { adr: o.adr, justification: o.justification };
88
+ }
89
+ return map;
90
+ }
@@ -6,3 +6,4 @@ export * from "./phase-gates.js";
6
6
  export * from "./m7-gates.js";
7
7
  export * from "./design-gates.js";
8
8
  export { runGates, formatReport } from "./runner.js";
9
+ export { parseAdrOverrides, readAdrOverrides, } from "./adr-overrides.js";
@@ -1,5 +1,6 @@
1
1
  export async function runGates(gates, ctx, opts = {}) {
2
2
  const skip = opts.skip ?? new Set();
3
+ const overrides = opts.overrides ?? {};
3
4
  const results = [];
4
5
  for (const gate of gates) {
5
6
  if (skip.has(gate.id)) {
@@ -26,6 +27,23 @@ export async function runGates(gates, ctx, opts = {}) {
26
27
  }
27
28
  try {
28
29
  const res = await gate.run(ctx);
30
+ const ovr = overrides[gate.id];
31
+ if (res.status === "fail" && ovr) {
32
+ if (gate.level === "critical") {
33
+ results.push({
34
+ ...res,
35
+ message: `${res.message ?? "failed"} — ADR override ${ovr.adr} ignored: critical gates require human sign-off (C4/C5)`,
36
+ });
37
+ }
38
+ else {
39
+ results.push({
40
+ ...res,
41
+ status: "overridden",
42
+ message: `overridden by ${ovr.adr}: ${ovr.justification}`,
43
+ });
44
+ }
45
+ continue;
46
+ }
29
47
  results.push(res);
30
48
  }
31
49
  catch (err) {
@@ -43,6 +61,7 @@ export async function runGates(gates, ctx, opts = {}) {
43
61
  pass: results.filter((r) => r.status === "pass").length,
44
62
  fail: results.filter((r) => r.status === "fail").length,
45
63
  skipped: results.filter((r) => r.status === "skipped").length,
64
+ overridden: results.filter((r) => r.status === "overridden").length,
46
65
  };
47
66
  const blocked = results.some((r) => r.status === "fail" && (r.level === "critical" || r.level === "required"));
48
67
  return { results, blocked, summary };
@@ -50,7 +69,13 @@ export async function runGates(gates, ctx, opts = {}) {
50
69
  export function formatReport(report) {
51
70
  const lines = [];
52
71
  for (const r of report.results) {
53
- const mark = r.status === "pass" ? "✓" : r.status === "skipped" ? "⊘" : "✗";
72
+ const mark = r.status === "pass"
73
+ ? "✓"
74
+ : r.status === "skipped"
75
+ ? "⊘"
76
+ : r.status === "overridden"
77
+ ? "⊝"
78
+ : "✗";
54
79
  const art = r.article ? ` [${r.article}]` : "";
55
80
  const lvl = r.level.toUpperCase();
56
81
  lines.push(`${mark} ${lvl.padEnd(9)}${art.padEnd(5)} ${r.id} — ${r.title}`);
@@ -58,6 +83,9 @@ export function formatReport(report) {
58
83
  lines.push(` ${r.message}`);
59
84
  }
60
85
  lines.push("");
61
- lines.push(`Summary: ${report.summary.pass} pass, ${report.summary.fail} fail, ${report.summary.skipped} skipped${report.blocked ? " — BLOCKED" : ""}`);
86
+ const overriddenPart = report.summary.overridden > 0
87
+ ? `, ${report.summary.overridden} overridden`
88
+ : "";
89
+ lines.push(`Summary: ${report.summary.pass} pass, ${report.summary.fail} fail, ${report.summary.skipped} skipped${overriddenPart}${report.blocked ? " — BLOCKED" : ""}`);
62
90
  return lines.join("\n");
63
91
  }
@@ -55,10 +55,16 @@ export const gateMobileStores = {
55
55
  if (!spec.exists)
56
56
  return fail(this, "spec.md missing");
57
57
  const s = spec.data.stores;
58
- const iosCount = s?.ios_permissions?.length ?? 0;
59
- const androidCount = s?.android_permissions?.length ?? 0;
60
- if (!s || (iosCount === 0 && androidCount === 0)) {
61
- return fail(this, "spec frontmatter missing stores.ios_permissions / stores.android_permissions");
58
+ if (!s)
59
+ return fail(this, "spec frontmatter missing the stores block");
60
+ const iosCount = s.ios_permissions?.length ?? 0;
61
+ const androidCount = s.android_permissions?.length ?? 0;
62
+ if (iosCount === 0 && androidCount === 0) {
63
+ const optOutRaw = s.no_restricted_apis;
64
+ const optOut = typeof optOutRaw === "string" ? optOutRaw.trim() : "";
65
+ if (optOut)
66
+ return pass(this, { no_restricted_apis: optOut });
67
+ return fail(this, 'spec declares no store permissions — list them under stores.ios_permissions / stores.android_permissions, or declare stores.no_restricted_apis: "<why this feature touches no restricted OS API>" to opt out (M1)');
62
68
  }
63
69
  return pass(this, { ios: iosCount, android: androidCount });
64
70
  },
@@ -5,10 +5,10 @@ const articles = [
5
5
  title: "App Store & Play Store Compliance",
6
6
  statement: "Every feature that touches restricted OS APIs (camera, microphone, contacts, location, photos, notifications, biometrics, health, HomeKit, etc.) MUST declare the exact store-level permission strings and the corresponding `NSxxxUsageDescription` / Android manifest permission in its `spec.md` frontmatter under `stores:`.",
7
7
  rationale: "App Store and Play Store rejections are the single largest source of mobile release delays and often surface only during submission review — after the code has been merged and the release train is in motion. Declaring permissions at spec time forces the conversation before engineering cost is sunk.",
8
- enforcement: "`gate.mobile.stores` (level `required`) validates that `spec.stores.ios_permissions` and `spec.stores.android_permissions` are declared and non-empty whenever a feature targets mobile.",
8
+ enforcement: '`gate.mobile.stores` (level `required`) validates that `spec.stores.ios_permissions` / `spec.stores.android_permissions` are declared whenever a feature touches restricted OS APIs. A feature that genuinely touches none opts out honestly by declaring `stores.no_restricted_apis: "<reason>"` — mirroring `a11y.wcag_level: "n/a"` (M3) and `perf_budget.scope: "none"` (M5).',
9
9
  violation: {
10
10
  level: "required",
11
- description: "Spec cannot be approved until the `stores:` frontmatter is populated. An ADR in `memory/decisions.md` MAY override with explicit justification (e.g., pure business logic with no platform API).",
11
+ description: 'Spec cannot be approved with an empty `stores:` block unless it declares `stores.no_restricted_apis` with a reason. Alternatively, an accepted ADR in `memory/decisions.md` that lists `Overrides: gate.mobile.stores` waives the gate at verify time (the override is recorded as `overridden by ADR-NNN`).',
12
12
  },
13
13
  },
14
14
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raptor/core",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js"
6
6
  }
@@ -12,6 +12,8 @@ acceptance:
12
12
  stores:
13
13
  ios_permissions: []
14
14
  android_permissions: []
15
+ # If the feature touches NO restricted OS API, keep the lists empty and add:
16
+ # no_restricted_apis: "<why — e.g. pure business logic, no camera/location>"
15
17
  a11y:
16
18
  wcag_level: "AA"
17
19
  criteria: []
@@ -20,7 +22,7 @@ a11y:
20
22
  <!--
21
23
  REQUIRED FRONTMATTER (gates read the keys above — fill before `raptor approve`):
22
24
  acceptance.ids: [AC-1, AC-2] # every AC heading must be listed (analyze + M7)
23
- stores.ios_permissions / android_permissions # M1, mobile: "PERMISSION — why"
25
+ stores.ios_permissions / android_permissions # M1, mobile: "PERMISSION — why" (or stores.no_restricted_apis: "<reason>")
24
26
  a11y.wcag_level + a11y.criteria # M3 (use "n/a" + justification when non-visual)
25
27
  (Frontmatter comments are stripped on approve, so this guidance lives in the body.)
26
28
  -->
@@ -2,7 +2,7 @@ import { Args, Flags } from "@oclif/core";
2
2
  import { BaseCommand } from "../base-command.js";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { basename, join } from "node:path";
5
- import { appendAuditEvent, buildCanonicalPrompt, discoverChecklists, formatReport, gateChecklistFrontmatter, gateChecklistLinksToDecisions, gateChecklistNonEmpty, generateChecklistFrontmatter, hashString, loadAgentsConfig, parseChecklistItems, parsePlan, runGates, selectAgent, summarizeChecklist, } from "../_core/dist/index.js";
5
+ import { appendAuditEvent, buildCanonicalPrompt, discoverChecklists, formatReport, gateChecklistFrontmatter, gateChecklistLinksToDecisions, gateChecklistNonEmpty, generateChecklistFrontmatter, hashString, loadAgentsConfig, parseChecklistItems, parsePlan, readAdrOverrides, runGates, selectAgent, summarizeChecklist, } from "../_core/dist/index.js";
6
6
  import { parseFrontmatter } from "../_core/dist/index.js";
7
7
  import { currentActor, featureDir, requireProjectRoot, } from "../shared/project.js";
8
8
  import { writeFeaturePrompt } from "../shared/feature-prompt.js";
@@ -171,7 +171,7 @@ export default class Checklist extends BaseCommand {
171
171
  const report = await runGates(gates, {
172
172
  projectRoot: root,
173
173
  featureDir: dir,
174
- });
174
+ }, { overrides: readAdrOverrides(root) });
175
175
  this.log(`=== Checklist Gates for ${featureName} ===`);
176
176
  this.log(formatReport(report));
177
177
  if (report.blocked)
@@ -113,7 +113,26 @@ export default class New extends BaseCommand {
113
113
  const specNeedsSeeding = !existsSync(specPath);
114
114
  let jiraContext = null;
115
115
  if (specNeedsSeeding && flags.jira && flags["jira-fetch"]) {
116
- jiraContext = await this.fetchJiraContext(root, flags.jira);
116
+ const result = await this.fetchJiraContext(root, flags.jira);
117
+ if (result.ok) {
118
+ jiraContext = result.context;
119
+ }
120
+ else {
121
+ this.error([
122
+ `${result.reason}`,
123
+ ``,
124
+ `Aborting: --jira=${flags.jira} was given but its Jira card could not be read,`,
125
+ `so there is no card content to seed the spec from. No spec or branch was`,
126
+ `created — a spec stamped with a Jira ID but no Jira content would be misleading.`,
127
+ ``,
128
+ `To fix, do one of:`,
129
+ ` • Connect Jira, then retry:`,
130
+ ` raptor jira connect`,
131
+ ` (or configure the Jira MCP server under 'jira:' in .raptor/raptor.yml)`,
132
+ ` • Record the ticket ID only, without fetching the card:`,
133
+ ` raptor new ${args.slug} --jira=${flags.jira} --no-jira-fetch`,
134
+ ].join("\n"), { exit: 1 });
135
+ }
117
136
  }
118
137
  const designText = specNeedsSeeding
119
138
  ? [
@@ -271,9 +290,7 @@ export default class New extends BaseCommand {
271
290
  ? "seeded spec"
272
291
  : !specNeedsSeeding
273
292
  ? "id recorded — spec already authored, not re-seeded"
274
- : flags["jira-fetch"]
275
- ? "id only (fetch unavailable)"
276
- : "id only";
293
+ : "id only";
277
294
  this.log(` Jira: ${flags.jira} (${tag})`);
278
295
  }
279
296
  if (designContext.hasDesign) {
@@ -416,28 +433,27 @@ export default class New extends BaseCommand {
416
433
  async fetchJiraContext(root, key) {
417
434
  const conn = jiraConn(readConfig(root));
418
435
  if (!conn) {
419
- this.warn(`--jira=${key} given but Jira is not connected — recording the ID only. Run 'raptor jira connect' to seed specs.`);
420
- return null;
436
+ return { ok: false, reason: `Jira is not connected.` };
421
437
  }
422
438
  if (!jiraReadyNonInteractive(conn)) {
423
- this.warn(`Jira tokens missing run 'raptor jira connect'. Recording ID only.`);
424
- return null;
439
+ return { ok: false, reason: `Jira credentials are missing.` };
425
440
  }
426
441
  let client;
427
442
  try {
428
443
  client = await openJiraClient(conn);
429
444
  }
430
445
  catch {
431
- this.warn(`Jira session expired run 'raptor jira connect'. Recording ID only.`);
432
- return null;
446
+ return { ok: false, reason: `Jira session expired.` };
433
447
  }
434
448
  try {
435
449
  const issue = await client.getJiraIssue(conn.cloudId, key);
436
- return mapIssueToSpecContext(issue);
450
+ return { ok: true, context: mapIssueToSpecContext(issue) };
437
451
  }
438
452
  catch (err) {
439
- this.warn(`Could not fetch ${key} (${err instanceof Error ? err.message : String(err)}) — recording ID only.`);
440
- return null;
453
+ return {
454
+ ok: false,
455
+ reason: `Could not fetch ${key} from Jira (${err instanceof Error ? err.message : String(err)}).`,
456
+ };
441
457
  }
442
458
  finally {
443
459
  await client.close();
@@ -2,7 +2,7 @@ import { Args, Flags } from "@oclif/core";
2
2
  import { BaseCommand } from "../base-command.js";
3
3
  import { existsSync, readdirSync } from "node:fs";
4
4
  import { basename, join } from "node:path";
5
- import { BUILTIN_GATES, formatReport, gateById, getPreset, PROJECT_GATES, runGates, } from "../_core/dist/index.js";
5
+ import { BUILTIN_GATES, formatReport, gateById, getPreset, PROJECT_GATES, readAdrOverrides, runGates, } from "../_core/dist/index.js";
6
6
  const PROJECT_GATE_IDS = new Set(PROJECT_GATES.map((g) => g.id));
7
7
  import { featureDir, readConfig, requireProjectRoot, } from "../shared/project.js";
8
8
  export default class Verify extends BaseCommand {
@@ -43,8 +43,14 @@ export default class Verify extends BaseCommand {
43
43
  if (flags.justification)
44
44
  for (const id of skipSet)
45
45
  skipJustifications[id] = flags.justification;
46
+ const overrides = readAdrOverrides(root);
47
+ const knownGateIds = new Set([...BUILTIN_GATES, ...PROJECT_GATES, ...presetGates].map((g) => g.id));
48
+ for (const id of Object.keys(overrides)) {
49
+ if (!knownGateIds.has(id))
50
+ this.warn(`ADR override targets unknown gate id "${id}" — ignored (check the spelling in memory/decisions.md).`);
51
+ }
46
52
  if (!args.feature) {
47
- const projectReport = await runGates(PROJECT_GATES, { projectRoot: root }, { skip: skipSet, skipJustifications });
53
+ const projectReport = await runGates(PROJECT_GATES, { projectRoot: root }, { skip: skipSet, skipJustifications, overrides });
48
54
  this.log("=== Project gates ===");
49
55
  this.log(formatReport(projectReport));
50
56
  const specs = join(root, ".raptor", "specs");
@@ -63,7 +69,7 @@ export default class Verify extends BaseCommand {
63
69
  ...BUILTIN_GATES.filter((g) => !PROJECT_GATE_IDS.has(g.id)),
64
70
  ...presetGates,
65
71
  ];
66
- const r = await runGates(featureGates, { projectRoot: root, featureDir: dir }, { skip: skipSet, skipJustifications });
72
+ const r = await runGates(featureGates, { projectRoot: root, featureDir: dir }, { skip: skipSet, skipJustifications, overrides });
67
73
  this.log(`\n--- ${f} ---`);
68
74
  this.log(formatReport(r));
69
75
  if (r.blocked)
@@ -88,7 +94,7 @@ export default class Verify extends BaseCommand {
88
94
  this.warn(`--skip=${id} ignored: critical gates cannot be skipped (see C4/C5).`);
89
95
  }
90
96
  }
91
- const report = await runGates(gatesToRun, { projectRoot: root, featureDir: dir }, { skip: skipSet, skipJustifications });
97
+ const report = await runGates(gatesToRun, { projectRoot: root, featureDir: dir }, { skip: skipSet, skipJustifications, overrides });
92
98
  this.log(`=== Gates for feature ${featureName}${preset ? ` (preset: ${preset.id})` : ""} ===`);
93
99
  this.log(formatReport(report));
94
100
  if (report.blocked)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "raptor-aios",
3
- "version": "0.6.2",
3
+ "version": "0.7.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.6.2";
32
+ const VERSION = "0.7.0";
33
33
 
34
34
  function log(msg) {
35
35
  process.stdout.write(` ${msg}\n`);
@@ -10,9 +10,12 @@ audit_ref: ~
10
10
  # --- Gates read the keys below. Fill them before `raptor approve`. ---
11
11
  acceptance:
12
12
  ids: [] # e.g. [AC-1, AC-2] — every AC heading below must also appear here (analyze + M7)
13
- stores: # M1 — store-level permission strings (mobile). Use [] when not applicable.
13
+ stores: # M1 — store-level permission strings (mobile).
14
14
  ios_permissions: [] # e.g. ["NSCameraUsageDescription — scan delivery QR codes"]
15
15
  android_permissions: [] # e.g. ["android.permission.CAMERA — scan delivery QR codes"]
16
+ # If this feature touches NO restricted OS API, leave the lists empty AND set
17
+ # no_restricted_apis to the reason (honest opt-out — mirrors a11y "n/a"):
18
+ # no_restricted_apis: "Pure business logic — no camera/location/etc."
16
19
  a11y: # M3 — WCAG target (set wcag_level: "n/a" + a justification when truly non-visual)
17
20
  wcag_level: "AA"
18
21
  criteria: [] # e.g. ["All controls have accessible labels", "Contrast >= 4.5:1"]