peaks-cli 1.1.1 → 1.1.2

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/bin/peaks.js CHANGED
File without changes
@@ -126,6 +126,18 @@ export function registerRequestCommands(program, io) {
126
126
  try {
127
127
  const role = options.role;
128
128
  const newState = parseStateForRole(role, options.state);
129
+ // Resolve the artifact's real session up front. Falling back to a literal
130
+ // 'default' (the previous behavior) points the bypass counter at a
131
+ // non-existent .peaks/default/ dir and crashes with ENOENT, so when
132
+ // --session-id is omitted we look the artifact up to find its session.
133
+ let resolvedSessionId = options.sessionId;
134
+ if (resolvedSessionId === undefined) {
135
+ const { showRequestArtifact: showForSession } = await import('../../services/artifacts/request-artifact-service.js');
136
+ const located = await showForSession({ projectRoot: options.project, role, requestId });
137
+ if (located !== null) {
138
+ resolvedSessionId = located.sessionId;
139
+ }
140
+ }
129
141
  if (options.allowIncomplete === true && (options.reason === undefined || options.reason.trim().length === 0)) {
130
142
  printResult(io, fail('request.transition', 'BYPASS_REASON_REQUIRED', '--allow-incomplete requires --reason explaining why prerequisites are skipped', { role, requestId }, ['Add --reason "<short justification>" or remove --allow-incomplete and produce the missing artifacts']), options.json);
131
143
  process.exitCode = 1;
@@ -142,7 +154,7 @@ export function registerRequestCommands(program, io) {
142
154
  return;
143
155
  }
144
156
  // Check bypass count
145
- const sessionRoot = (await import('node:path')).join(options.project, '.peaks', options.sessionId ?? 'default');
157
+ const sessionRoot = (await import('node:path')).join(options.project, '.peaks', resolvedSessionId ?? 'default');
146
158
  if (isBypassLimitReached(sessionRoot)) {
147
159
  printResult(io, fail('request.transition', 'BYPASS_LIMIT_REACHED', `--allow-incomplete limit reached (${MAX_BYPASSES_PER_SESSION} per session)`, { role, requestId, limit: MAX_BYPASSES_PER_SESSION }, ['Produce the missing artifacts instead of bypassing.']), options.json);
148
160
  process.exitCode = 1;
@@ -35,6 +35,15 @@ const ALLOWLIST_PATTERNS = [
35
35
  function isAllowlisted(line) {
36
36
  return ALLOWLIST_PATTERNS.some((pattern) => pattern.test(line));
37
37
  }
38
+ /**
39
+ * Remove inline code spans (`...`) before applying placeholder rules. Content
40
+ * inside backticks is literal example text — e.g. a documented command syntax
41
+ * `peaks sop init <id>` — not an unfilled prose placeholder. Lint checks prose,
42
+ * not code, so a `<...>` token only counts when it appears outside code spans.
43
+ */
44
+ function stripInlineCode(line) {
45
+ return line.replace(/`[^`]*`/g, '');
46
+ }
38
47
  export async function lintRequestArtifact(options) {
39
48
  const showOptions = {
40
49
  projectRoot: options.projectRoot,
@@ -50,14 +59,24 @@ export async function lintRequestArtifact(options) {
50
59
  }
51
60
  const lines = artifact.content.split(/\r?\n/);
52
61
  const findings = [];
62
+ let insideFence = false;
53
63
  for (let index = 0; index < lines.length; index += 1) {
54
64
  const rawLine = lines[index];
55
65
  if (rawLine === undefined)
56
66
  continue;
67
+ // Fenced code blocks hold literal examples, not prose to fill; skip their
68
+ // contents entirely (the fence delimiters themselves toggle the state).
69
+ if (/^\s*```/.test(rawLine)) {
70
+ insideFence = !insideFence;
71
+ continue;
72
+ }
73
+ if (insideFence)
74
+ continue;
57
75
  if (isAllowlisted(rawLine))
58
76
  continue;
77
+ const testLine = stripInlineCode(rawLine);
59
78
  for (const rule of RULES) {
60
- if (rule.test(rawLine)) {
79
+ if (rule.test(testLine)) {
61
80
  findings.push({
62
81
  line: index + 1,
63
82
  text: rawLine.trim(),
@@ -1,5 +1,5 @@
1
- import { join } from 'node:path';
2
- import { readFile } from 'node:fs/promises';
1
+ import { join, dirname, basename } from 'node:path';
2
+ import { readFile, readdir } from 'node:fs/promises';
3
3
  import { pathExists } from '../../shared/fs.js';
4
4
  export const VALID_REQUEST_TYPES = [
5
5
  'feature',
@@ -111,6 +111,36 @@ export function getPrerequisitesFor(role, newState, requestType = DEFAULT_REQUES
111
111
  function resolvePrerequisitePath(prerequisite, requestId) {
112
112
  return prerequisite.relativePath.replace('<rid>', requestId);
113
113
  }
114
+ /**
115
+ * Resolve a prerequisite to an on-disk path, tolerating the numbered filename
116
+ * prefix that `request init` writes (e.g. `001-<rid>.md`). When the prerequisite
117
+ * path contains `<rid>`, we accept either the legacy bare `<rid>.md` form or any
118
+ * `NNN-<rid>.md` numbered form — mirroring the matcher in request-artifact-service.
119
+ * Returns the matched absolute path, or null when nothing matches.
120
+ */
121
+ async function resolvePrerequisiteAbsolutePath(sessionRoot, prerequisite, requestId) {
122
+ const relative = resolvePrerequisitePath(prerequisite, requestId);
123
+ const exact = join(sessionRoot, relative);
124
+ if (await pathExists(exact)) {
125
+ return exact;
126
+ }
127
+ // Only `<rid>`-templated prerequisites can carry a numbered prefix; fixed paths
128
+ // (e.g. rd/tech-doc.md) are matched exactly above.
129
+ if (!prerequisite.relativePath.includes('<rid>')) {
130
+ return null;
131
+ }
132
+ const dir = dirname(exact);
133
+ const targetSuffix = `-${basename(exact)}`;
134
+ let entries;
135
+ try {
136
+ entries = await readdir(dir);
137
+ }
138
+ catch {
139
+ return null;
140
+ }
141
+ const match = entries.find((name) => /^\d+-/.test(name) && name.endsWith(targetSuffix));
142
+ return match ? join(dir, match) : null;
143
+ }
114
144
  export async function checkPrerequisites(options) {
115
145
  const requirements = getPrerequisitesFor(options.role, options.newState, options.requestType);
116
146
  if (requirements.length === 0) {
@@ -120,8 +150,8 @@ export async function checkPrerequisites(options) {
120
150
  const missing = [];
121
151
  for (const prerequisite of requirements) {
122
152
  const relative = resolvePrerequisitePath(prerequisite, options.requestId);
123
- const absolute = join(sessionRoot, relative);
124
- if (!(await pathExists(absolute))) {
153
+ const absolute = await resolvePrerequisiteAbsolutePath(sessionRoot, prerequisite, options.requestId);
154
+ if (absolute === null) {
125
155
  missing.push({ path: relative, description: prerequisite.description });
126
156
  continue;
127
157
  }
@@ -25,6 +25,16 @@ function classifyFile(filePath) {
25
25
  return 'source';
26
26
  return 'unknown';
27
27
  }
28
+ /**
29
+ * Peaks' own artifact workspace. Changes here (PRD/RD/QA markdown, session
30
+ * state) are never the "code change" a request type describes, so they must be
31
+ * excluded from the diff — otherwise a PRD-planning-phase handoff that only
32
+ * wrote `.peaks/**` markdown would be misclassified as a docs change.
33
+ */
34
+ function isArtifactWorkspaceFile(filePath) {
35
+ const normalized = filePath.replace(/\\/g, '/');
36
+ return normalized === '.peaks' || normalized.startsWith('.peaks/');
37
+ }
28
38
  function tryGitDiffFiles(projectRoot, baseRef) {
29
39
  try {
30
40
  // Combine: tracked changes vs baseRef + untracked files. Use porcelain status for untracked too.
@@ -32,7 +42,7 @@ function tryGitDiffFiles(projectRoot, baseRef) {
32
42
  const tracked = trackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
33
43
  const untrackedRaw = execFileSync('git', ['-C', projectRoot, 'ls-files', '--others', '--exclude-standard'], { encoding: 'utf8' });
34
44
  const untracked = untrackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
35
- const merged = Array.from(new Set([...tracked, ...untracked]));
45
+ const merged = Array.from(new Set([...tracked, ...untracked])).filter((file) => !isArtifactWorkspaceFile(file));
36
46
  return { ok: true, files: merged };
37
47
  }
38
48
  catch {
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.1.1";
1
+ export declare const CLI_VERSION = "1.1.2";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.1.1";
1
+ export const CLI_VERSION = "1.1.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Peaks CLI and short skill family for Claude Code automation.",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",