@planu/cli 5.3.2 → 5.3.3
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 +17 -0
- package/dist/config/runtime-policy.json +9 -0
- package/dist/engine/autopilot/complete-loop.js +4 -1
- package/dist/engine/execution/outbox-worker.d.ts +8 -0
- package/dist/engine/execution/outbox-worker.js +102 -31
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +4 -4
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +4 -4
- package/dist/engine/runtime-policy.js +14 -0
- package/dist/index.js +3 -1
- package/dist/storage/runtime-db.d.ts +23 -1
- package/dist/storage/runtime-db.js +79 -9
- package/dist/tools/create-spec/post-creation.js +7 -2
- package/dist/tools/create-spec/spec-created-outbox-consumer.d.ts +2 -0
- package/dist/tools/create-spec/spec-created-outbox-consumer.js +94 -6
- package/dist/tools/create-spec-helpers.d.ts +2 -2
- package/dist/tools/create-spec-helpers.js +2 -2
- package/dist/tools/git/branch-ops.d.ts +7 -3
- package/dist/tools/git/branch-ops.js +68 -18
- package/dist/tools/update-status-actions.js +4 -2
- package/dist/types/git.d.ts +4 -0
- package/dist/types/outbox-worker.d.ts +2 -0
- package/dist/types/runtime-policy.d.ts +9 -0
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
|
@@ -19,10 +19,11 @@ function randomSuffix() {
|
|
|
19
19
|
.toString(16)
|
|
20
20
|
.padStart(8, '0');
|
|
21
21
|
}
|
|
22
|
-
export async function handleCreateBranch(projectId, specId, config, resolvedProjectPath) {
|
|
22
|
+
export async function handleCreateBranch(projectId, specId, config, resolvedProjectPath, options) {
|
|
23
23
|
if (!specId) {
|
|
24
24
|
return compactError('❌ Error: specId is required for create-branch action');
|
|
25
25
|
}
|
|
26
|
+
const mutateCheckout = options?.mutateCheckout ?? true;
|
|
26
27
|
const suppliedPath = resolvedProjectPath?.trim();
|
|
27
28
|
const projectPath = suppliedPath && suppliedPath.length > 0 ? suppliedPath : await resolveProjectPath(projectId);
|
|
28
29
|
const spec = await specStore.getSpec(projectId, specId);
|
|
@@ -40,11 +41,18 @@ export async function handleCreateBranch(projectId, specId, config, resolvedProj
|
|
|
40
41
|
try {
|
|
41
42
|
const { stdout } = await git(projectPath, ['branch', '--list', branchName]);
|
|
42
43
|
if (stdout.trim().length > 0) {
|
|
43
|
-
|
|
44
|
+
if (mutateCheckout) {
|
|
45
|
+
await git(projectPath, ['checkout', branchName]);
|
|
46
|
+
}
|
|
47
|
+
// Record the branch even when it already exists — otherwise callers keep re-entering
|
|
48
|
+
// this path and a background checkout is retried forever.
|
|
49
|
+
await specStore.updateSpec(projectId, specId, { gitBranch: branchName });
|
|
44
50
|
const result = {
|
|
45
51
|
action: 'create-branch',
|
|
46
52
|
branchName,
|
|
47
|
-
message:
|
|
53
|
+
message: mutateCheckout
|
|
54
|
+
? `Switched to existing branch '${branchName}'`
|
|
55
|
+
: `Branch '${branchName}' already exists (checkout left untouched)`,
|
|
48
56
|
};
|
|
49
57
|
return compactResult(formatKeyValue(result));
|
|
50
58
|
}
|
|
@@ -52,7 +60,14 @@ export async function handleCreateBranch(projectId, specId, config, resolvedProj
|
|
|
52
60
|
catch {
|
|
53
61
|
// branch list failed — proceed with creation
|
|
54
62
|
}
|
|
55
|
-
|
|
63
|
+
if (mutateCheckout) {
|
|
64
|
+
await git(projectPath, ['checkout', '-b', branchName]);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// SPEC-1396: automatic caller — cut the branch from the resolved base without moving HEAD.
|
|
68
|
+
const base = await resolveBaseBranch(projectPath, mergeConfig(config).baseBranch);
|
|
69
|
+
await git(projectPath, base ? ['branch', branchName, base] : ['branch', branchName]);
|
|
70
|
+
}
|
|
56
71
|
await specStore.updateSpec(projectId, specId, { gitBranch: branchName });
|
|
57
72
|
const result = {
|
|
58
73
|
action: 'create-branch',
|
|
@@ -117,14 +132,19 @@ async function hasDirtyState(projectPath) {
|
|
|
117
132
|
* Auto-setup: detect git workflow, sync base branch, create feature branch.
|
|
118
133
|
* Combines detectGitFlowType + resolveBaseBranch + fetch/pull + create-branch
|
|
119
134
|
* into a single atomic operation for spec creation.
|
|
135
|
+
*
|
|
136
|
+
* SPEC-1396: `mutateCheckout: false` creates the branch ref without touching HEAD
|
|
137
|
+
* or the working tree. Automatic callers (outbox recovery, cascades) MUST use it —
|
|
138
|
+
* a background checkout silently moves the shared checkout onto a foreign branch.
|
|
120
139
|
*/
|
|
121
|
-
export async function handleAutoSetup(projectId, specId, config) {
|
|
140
|
+
export async function handleAutoSetup(projectId, specId, config, options) {
|
|
122
141
|
if (!specId) {
|
|
123
142
|
return compactError('❌ Error: specId is required for auto-setup action');
|
|
124
143
|
}
|
|
144
|
+
const mutateCheckout = options?.mutateCheckout ?? true;
|
|
125
145
|
const projectPath = await resolveProjectPath(projectId);
|
|
126
146
|
// Check for dirty state — abort if uncommitted changes exist
|
|
127
|
-
if (await hasDirtyState(projectPath)) {
|
|
147
|
+
if (mutateCheckout && (await hasDirtyState(projectPath))) {
|
|
128
148
|
return compactError('❌ Error: Working tree has uncommitted changes. Commit or stash them before auto-setup.\nSuggestion: git stash or git commit your changes first');
|
|
129
149
|
}
|
|
130
150
|
const spec = await specStore.getSpec(projectId, specId);
|
|
@@ -139,16 +159,39 @@ export async function handleAutoSetup(projectId, specId, config) {
|
|
|
139
159
|
if (!baseBranch) {
|
|
140
160
|
return compactError('❌ Error: No base branch found (develop/main/master). Is this a git repository?');
|
|
141
161
|
}
|
|
142
|
-
// 3.
|
|
143
|
-
await git(projectPath, ['checkout', baseBranch]);
|
|
162
|
+
// 3. Sync base branch with remote (switching only when the caller owns the checkout)
|
|
144
163
|
let syncedBase = false;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
await git(projectPath, ['
|
|
148
|
-
|
|
164
|
+
let baseRef = baseBranch;
|
|
165
|
+
if (mutateCheckout) {
|
|
166
|
+
await git(projectPath, ['checkout', baseBranch]);
|
|
167
|
+
try {
|
|
168
|
+
await git(projectPath, ['fetch', 'origin', baseBranch]);
|
|
169
|
+
await git(projectPath, ['pull', '--ff-only', 'origin', baseBranch]);
|
|
170
|
+
syncedBase = true;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// No remote or fetch failed — continue with local base (offline-friendly)
|
|
174
|
+
}
|
|
149
175
|
}
|
|
150
|
-
|
|
151
|
-
|
|
176
|
+
else {
|
|
177
|
+
try {
|
|
178
|
+
// Fast-forwards the local base ref without touching HEAD or the working tree.
|
|
179
|
+
await git(projectPath, ['fetch', 'origin', `${baseBranch}:${baseBranch}`]);
|
|
180
|
+
syncedBase = true;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// git refuses the refspec when the base is the branch currently checked out. Fetch the
|
|
184
|
+
// remote-tracking ref instead and cut the feature branch from it.
|
|
185
|
+
try {
|
|
186
|
+
await git(projectPath, ['fetch', 'origin', baseBranch]);
|
|
187
|
+
await git(projectPath, ['rev-parse', '--verify', `origin/${baseBranch}`]);
|
|
188
|
+
baseRef = `origin/${baseBranch}`;
|
|
189
|
+
syncedBase = true;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// No remote or offline — use the local base as-is.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
152
195
|
}
|
|
153
196
|
// 4. Create feature branch from the updated base
|
|
154
197
|
const prefixes = mergedConfig.branchPrefix ?? DEFAULT_BRANCH_PREFIXES;
|
|
@@ -160,14 +203,19 @@ export async function handleAutoSetup(projectId, specId, config) {
|
|
|
160
203
|
try {
|
|
161
204
|
const { stdout } = await git(projectPath, ['branch', '--list', branchName]);
|
|
162
205
|
if (stdout.trim().length > 0) {
|
|
163
|
-
|
|
206
|
+
if (mutateCheckout) {
|
|
207
|
+
await git(projectPath, ['checkout', branchName]);
|
|
208
|
+
}
|
|
209
|
+
await specStore.updateSpec(projectId, specId, { gitBranch: branchName });
|
|
164
210
|
const result = {
|
|
165
211
|
action: 'auto-setup',
|
|
166
212
|
gitFlowType,
|
|
167
213
|
baseBranch,
|
|
168
214
|
newBranch: branchName,
|
|
169
215
|
syncedBase,
|
|
170
|
-
message:
|
|
216
|
+
message: mutateCheckout
|
|
217
|
+
? `Switched to existing branch '${branchName}' (${gitFlowType} workflow detected)`
|
|
218
|
+
: `Branch '${branchName}' already exists (checkout left untouched)`,
|
|
171
219
|
};
|
|
172
220
|
return compactResult(formatKeyValue(result));
|
|
173
221
|
}
|
|
@@ -175,7 +223,7 @@ export async function handleAutoSetup(projectId, specId, config) {
|
|
|
175
223
|
catch {
|
|
176
224
|
// branch list failed — proceed with creation
|
|
177
225
|
}
|
|
178
|
-
await git(projectPath, ['checkout', '-b', branchName]);
|
|
226
|
+
await git(projectPath, mutateCheckout ? ['checkout', '-b', branchName] : ['branch', branchName, baseRef]);
|
|
179
227
|
await specStore.updateSpec(projectId, specId, { gitBranch: branchName });
|
|
180
228
|
const result = {
|
|
181
229
|
action: 'auto-setup',
|
|
@@ -183,7 +231,9 @@ export async function handleAutoSetup(projectId, specId, config) {
|
|
|
183
231
|
baseBranch,
|
|
184
232
|
newBranch: branchName,
|
|
185
233
|
syncedBase,
|
|
186
|
-
message:
|
|
234
|
+
message: mutateCheckout
|
|
235
|
+
? `Created branch '${branchName}' from '${baseBranch}' (${gitFlowType} workflow detected)`
|
|
236
|
+
: `Created branch '${branchName}' from '${baseBranch}' without switching the checkout`,
|
|
187
237
|
};
|
|
188
238
|
return compactResult(formatKeyValue(result));
|
|
189
239
|
}
|
|
@@ -54,11 +54,13 @@ export async function runImplementingSideEffects(projectId, specId, transitionPr
|
|
|
54
54
|
let branchResult;
|
|
55
55
|
try {
|
|
56
56
|
const { handleCreateBranch } = await import('./git/branch-ops.js');
|
|
57
|
-
branchResult = await withAudit(projectPath, 'update_status(implementing)', 'handleCreateBranch', () => handleCreateBranch(projectId, specId, undefined, projectPath), (r) => ({ isError: r.isError }));
|
|
57
|
+
branchResult = await withAudit(projectPath, 'update_status(implementing)', 'handleCreateBranch', () => handleCreateBranch(projectId, specId, undefined, projectPath, { mutateCheckout: false }), (r) => ({ isError: r.isError }));
|
|
58
58
|
}
|
|
59
59
|
catch {
|
|
60
60
|
const { handleCreateBranch } = await import('./git/branch-ops.js');
|
|
61
|
-
branchResult = await handleCreateBranch(projectId, specId, undefined, projectPath
|
|
61
|
+
branchResult = await handleCreateBranch(projectId, specId, undefined, projectPath, {
|
|
62
|
+
mutateCheckout: false,
|
|
63
|
+
});
|
|
62
64
|
}
|
|
63
65
|
if (!branchResult.isError) {
|
|
64
66
|
try {
|
package/dist/types/git.d.ts
CHANGED
|
@@ -144,6 +144,10 @@ export interface GitSetupResult {
|
|
|
144
144
|
data: Record<string, unknown>;
|
|
145
145
|
branch: string;
|
|
146
146
|
}
|
|
147
|
+
/** SPEC-1396: automatic callers set `mutateCheckout: false` so the shared checkout never moves. */
|
|
148
|
+
export interface GitAutoSetupOptions {
|
|
149
|
+
mutateCheckout?: boolean;
|
|
150
|
+
}
|
|
147
151
|
export interface AutoSetupResult {
|
|
148
152
|
action: 'auto-setup';
|
|
149
153
|
gitFlowType: GitFlowType;
|
|
@@ -12,6 +12,8 @@ export interface OutboxFenceRecord {
|
|
|
12
12
|
readonly state: 'leased' | 'failed' | 'dead-letter' | 'delivered';
|
|
13
13
|
readonly attempts?: number;
|
|
14
14
|
readonly lastError?: string;
|
|
15
|
+
/** Durable retry deadline (ISO timestamp). Only set for `failed` fences; optional for schemaVersion 1 compatibility. */
|
|
16
|
+
readonly nextAttemptAt?: string;
|
|
15
17
|
}
|
|
16
18
|
export declare const SPEC_CREATED_POST_COMMIT_TASKS: readonly ['git-setup', 'analysis', 'quality-score', 'duplicate-detection', 'complexity-advice', 'prior-decisions', 'suggestions', 'challenge-readiness'];
|
|
17
19
|
export type SpecCreatedPostCommitTask = (typeof SPEC_CREATED_POST_COMMIT_TASKS)[number];
|
|
@@ -47,5 +47,14 @@ export interface RuntimePolicy {
|
|
|
47
47
|
maximumRememberedTransitions: number;
|
|
48
48
|
minimumHeartbeatMs: number;
|
|
49
49
|
};
|
|
50
|
+
outbox: {
|
|
51
|
+
sliceRecordBudget: number;
|
|
52
|
+
sliceTimeBudgetMs: number;
|
|
53
|
+
backoffBaseMs: number;
|
|
54
|
+
backoffCapMs: number;
|
|
55
|
+
maxAttempts: number;
|
|
56
|
+
diagnosticMaxLength: number;
|
|
57
|
+
continuationDelayMs: number;
|
|
58
|
+
};
|
|
50
59
|
}
|
|
51
60
|
//# sourceMappingURL=runtime-policy.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.3",
|
|
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",
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"packageName": "@planu/core"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"@planu/core-darwin-arm64": "5.3.
|
|
39
|
-
"@planu/core-darwin-x64": "5.3.
|
|
40
|
-
"@planu/core-linux-arm64-gnu": "5.3.
|
|
41
|
-
"@planu/core-linux-arm64-musl": "5.3.
|
|
42
|
-
"@planu/core-linux-x64-gnu": "5.3.
|
|
43
|
-
"@planu/core-linux-x64-musl": "5.3.
|
|
44
|
-
"@planu/core-win32-arm64-msvc": "5.3.
|
|
45
|
-
"@planu/core-win32-x64-msvc": "5.3.
|
|
38
|
+
"@planu/core-darwin-arm64": "5.3.3",
|
|
39
|
+
"@planu/core-darwin-x64": "5.3.3",
|
|
40
|
+
"@planu/core-linux-arm64-gnu": "5.3.3",
|
|
41
|
+
"@planu/core-linux-arm64-musl": "5.3.3",
|
|
42
|
+
"@planu/core-linux-x64-gnu": "5.3.3",
|
|
43
|
+
"@planu/core-linux-x64-musl": "5.3.3",
|
|
44
|
+
"@planu/core-win32-arm64-msvc": "5.3.3",
|
|
45
|
+
"@planu/core-win32-x64-msvc": "5.3.3"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
48
|
"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": "5.3.
|
|
5
|
+
"version": "5.3.3",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|