@planu/cli 4.11.6 → 4.11.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 +6 -0
- package/dist/engine/actuals/git-analyzer.js +4 -4
- package/dist/engine/diff-spec-generator.js +12 -5
- package/dist/engine/git-safe-input.d.ts +6 -0
- package/dist/engine/git-safe-input.js +41 -0
- package/dist/tools/code-impact-handler.js +4 -2
- package/dist/tools/update-status/index.js +22 -10
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Planu — engine/actuals/git-analyzer.ts (SPEC-061)
|
|
2
2
|
// Derives actuals from git log history for a given spec.
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { isCanonicalSpecId } from '../../tools/schemas/spec-id.js';
|
|
5
5
|
const MAX_SESSION_GAP_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
6
6
|
const REVIEW_HOURS_RATIO = 0.15;
|
|
7
7
|
const MINIMUM_DEV_HOURS = 1;
|
|
@@ -121,7 +121,7 @@ export function calculateLinesChanged(commits) {
|
|
|
121
121
|
* Returns null if specId is invalid or no commits are found.
|
|
122
122
|
*/
|
|
123
123
|
export function deriveActualsFromGit(projectPath, specId) {
|
|
124
|
-
if (!
|
|
124
|
+
if (!isCanonicalSpecId(specId)) {
|
|
125
125
|
return Promise.resolve(null);
|
|
126
126
|
}
|
|
127
127
|
const gitOutput = runGitLog(projectPath, specId);
|
|
@@ -155,7 +155,7 @@ export function deriveActualsFromGit(projectPath, specId) {
|
|
|
155
155
|
}
|
|
156
156
|
function runGitLog(projectPath, specId) {
|
|
157
157
|
try {
|
|
158
|
-
const result =
|
|
158
|
+
const result = execFileSync('git', ['log', '--format=%H|%aI|%s', '--numstat', '--all', '--grep', specId], {
|
|
159
159
|
cwd: projectPath,
|
|
160
160
|
encoding: 'utf-8',
|
|
161
161
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// engine/diff-spec-generator.ts — Diff-to-Spec generator (SPEC-350)
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { assertPositivePrNumber, assertSafeGitDiffRange } from './git-safe-input.js';
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
// Diff analysis
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
@@ -88,9 +89,12 @@ export function generateSpecDraft(analysis, sourceType, sourceRef, titleOverride
|
|
|
88
89
|
// ---------------------------------------------------------------------------
|
|
89
90
|
export function runGitDiff(repoPath, commitRange) {
|
|
90
91
|
const range = commitRange ?? 'HEAD~1..HEAD';
|
|
91
|
-
|
|
92
|
+
assertSafeGitDiffRange(range);
|
|
92
93
|
try {
|
|
93
|
-
return
|
|
94
|
+
return execFileSync('git', ['-C', repoPath, 'diff', range], {
|
|
95
|
+
encoding: 'utf-8',
|
|
96
|
+
timeout: 15_000,
|
|
97
|
+
});
|
|
94
98
|
}
|
|
95
99
|
catch (err) {
|
|
96
100
|
throw new Error(`Failed to run git diff: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
@@ -98,9 +102,12 @@ export function runGitDiff(repoPath, commitRange) {
|
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
104
|
export function runGhPrDiff(repoPath, prNumber) {
|
|
101
|
-
|
|
105
|
+
assertPositivePrNumber(prNumber);
|
|
102
106
|
try {
|
|
103
|
-
return
|
|
107
|
+
return execFileSync('gh', ['pr', 'diff', String(prNumber), '--repo', repoPath], {
|
|
108
|
+
encoding: 'utf-8',
|
|
109
|
+
timeout: 20_000,
|
|
110
|
+
});
|
|
104
111
|
}
|
|
105
112
|
catch (err) {
|
|
106
113
|
throw new Error(`Failed to run gh pr diff: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function isSafeGitRef(value: string): boolean;
|
|
2
|
+
export declare function assertSafeGitRef(value: string, label?: string): void;
|
|
3
|
+
export declare function isSafeGitDiffRange(value: string): boolean;
|
|
4
|
+
export declare function assertSafeGitDiffRange(value: string): void;
|
|
5
|
+
export declare function assertPositivePrNumber(value: number): void;
|
|
6
|
+
//# sourceMappingURL=git-safe-input.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/@{}:+~-]{0,200}$/;
|
|
2
|
+
function hasShellMetacharacters(value) {
|
|
3
|
+
return /[\s`$;&|<>\\\n\r]/.test(value);
|
|
4
|
+
}
|
|
5
|
+
export function isSafeGitRef(value) {
|
|
6
|
+
return (value.length > 0 &&
|
|
7
|
+
SAFE_GIT_REF_PATTERN.test(value) &&
|
|
8
|
+
!value.startsWith('-') &&
|
|
9
|
+
!value.includes('..') &&
|
|
10
|
+
!hasShellMetacharacters(value));
|
|
11
|
+
}
|
|
12
|
+
export function assertSafeGitRef(value, label = 'git ref') {
|
|
13
|
+
if (!isSafeGitRef(value)) {
|
|
14
|
+
throw new Error(`Invalid ${label}. Use a canonical commit hash or git ref without whitespace or shell metacharacters.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function isSafeGitDiffRange(value) {
|
|
18
|
+
if (value.length === 0 || hasShellMetacharacters(value)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (value.includes('...')) {
|
|
22
|
+
const parts = value.split('...');
|
|
23
|
+
return parts.length === 2 && parts.every((part) => part !== '' && isSafeGitRef(part));
|
|
24
|
+
}
|
|
25
|
+
if (value.includes('..')) {
|
|
26
|
+
const parts = value.split('..');
|
|
27
|
+
return parts.length === 2 && parts.every((part) => part !== '' && isSafeGitRef(part));
|
|
28
|
+
}
|
|
29
|
+
return isSafeGitRef(value);
|
|
30
|
+
}
|
|
31
|
+
export function assertSafeGitDiffRange(value) {
|
|
32
|
+
if (!isSafeGitDiffRange(value)) {
|
|
33
|
+
throw new Error('Invalid commitRange. Use a canonical git ref or a safe range like base..head or base...head.');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function assertPositivePrNumber(value) {
|
|
37
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
38
|
+
throw new Error('Invalid prNumber. Use a positive integer pull request number.');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=git-safe-input.js.map
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
import { listSpecs } from '../storage/spec-store.js';
|
|
3
3
|
import { hashProjectPath } from '../storage/base-store.js';
|
|
4
4
|
import { analyzeCodeImpact, checkCodeChangeCompliance, suggestSpecUpdates, runGitDiffImpact, } from '../engine/code-impact-analyzer.js';
|
|
5
|
-
import {
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { assertSafeGitRef } from '../engine/git-safe-input.js';
|
|
6
7
|
function getCommitDiff(commitHash, projectPath) {
|
|
8
|
+
assertSafeGitRef(commitHash, 'commit ref');
|
|
7
9
|
try {
|
|
8
|
-
return
|
|
10
|
+
return execFileSync('git', ['show', commitHash], {
|
|
9
11
|
cwd: projectPath,
|
|
10
12
|
encoding: 'utf-8',
|
|
11
13
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -14,6 +14,7 @@ import { checkApprovalGate } from '../../engine/approval-workflow.js';
|
|
|
14
14
|
import * as approvalStore from '../../storage/approval-store.js';
|
|
15
15
|
import { isLocked, getLock } from '../../storage/spec-lock-store.js';
|
|
16
16
|
import { runValidateGate, checkDoneGates, checkComplianceGate, checkQaGate, checkApprovedFormatGate, readApprovedValidationReportGate, checkSpecReviewGate, writeSpecReviewArtifact, } from './dod-gates.js';
|
|
17
|
+
import { writeImplementationReviewReport } from '../../engine/validator/validation-report-writer.js';
|
|
17
18
|
import { checkLifecycleEvidenceTransitionGate } from './evidence-gate.js';
|
|
18
19
|
import { buildStatusResponse, buildValidateBlockedResponse, buildDryRunResponse, } from './response-builder.js';
|
|
19
20
|
import { recordDoneMetrics, syncSpecFiles, tryReconcile, recordTerminalTransitionEvent, } from './file-sync.js';
|
|
@@ -570,16 +571,6 @@ export async function handleUpdateStatus(params, server) {
|
|
|
570
571
|
validateScoreSource = 'validateSpec';
|
|
571
572
|
}
|
|
572
573
|
}
|
|
573
|
-
if (newStatus === 'done' && !(params.force ?? params.forceStatus ?? false)) {
|
|
574
|
-
validationReportGate = await readApprovedValidationReportGate(specId, projectId, false);
|
|
575
|
-
if (!validationReportGate.ok) {
|
|
576
|
-
return validationReportGate.error;
|
|
577
|
-
}
|
|
578
|
-
if (validationReportGate.score !== null) {
|
|
579
|
-
validateScore = validationReportGate.score;
|
|
580
|
-
validateScoreSource = 'validation-report';
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
574
|
if (newStatus === 'done' &&
|
|
584
575
|
effectiveGatePath &&
|
|
585
576
|
!shouldSkipStrictLayoutGateForLegacyTestHarness()) {
|
|
@@ -627,6 +618,27 @@ export async function handleUpdateStatus(params, server) {
|
|
|
627
618
|
lintWarnings: [],
|
|
628
619
|
testWarnings: [],
|
|
629
620
|
};
|
|
621
|
+
if (newStatus === 'done' && !(params.force ?? params.forceStatus ?? false)) {
|
|
622
|
+
if (!isDryRun && effectiveGatePath) {
|
|
623
|
+
await writeImplementationReviewReport({
|
|
624
|
+
projectId,
|
|
625
|
+
specId,
|
|
626
|
+
spec,
|
|
627
|
+
projectPath: effectiveGatePath,
|
|
628
|
+
score: validateScore,
|
|
629
|
+
lintPassed: lintWarnings.length === 0,
|
|
630
|
+
conventionRegression: conventionWarnings.length > 0,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
validationReportGate = await readApprovedValidationReportGate(specId, projectId, false);
|
|
634
|
+
if (!validationReportGate.ok) {
|
|
635
|
+
return validationReportGate.error;
|
|
636
|
+
}
|
|
637
|
+
if (validationReportGate.score !== null) {
|
|
638
|
+
validateScore = validationReportGate.score;
|
|
639
|
+
validateScoreSource = 'validation-report';
|
|
640
|
+
}
|
|
641
|
+
}
|
|
630
642
|
// SPEC-731: dry_run short-circuit — all gates have been evaluated above.
|
|
631
643
|
// Do NOT call transitionSpec, appendTransitionEvent, or acquire any lock.
|
|
632
644
|
if (isDryRun) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.11.
|
|
3
|
+
"version": "4.11.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",
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
"packageName": "@planu/core"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
|
-
"@planu/core-darwin-arm64": "4.11.
|
|
38
|
-
"@planu/core-darwin-x64": "4.11.
|
|
39
|
-
"@planu/core-linux-arm64-gnu": "4.11.
|
|
40
|
-
"@planu/core-linux-arm64-musl": "4.11.
|
|
41
|
-
"@planu/core-linux-x64-gnu": "4.11.
|
|
42
|
-
"@planu/core-linux-x64-musl": "4.11.
|
|
43
|
-
"@planu/core-win32-arm64-msvc": "4.11.
|
|
44
|
-
"@planu/core-win32-x64-msvc": "4.11.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.11.7",
|
|
38
|
+
"@planu/core-darwin-x64": "4.11.7",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.11.7",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.11.7",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.11.7",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.11.7",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.11.7",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.11.7"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
package/planu-native.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "4.11.
|
|
5
|
+
"version": "4.11.7",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": [
|
|
8
8
|
"npx",
|