lee-spec-kit 0.8.7 → 0.9.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/README.en.md +4 -2
- package/README.md +4 -2
- package/dist/{bootstrap-G37N6RGB.js → bootstrap-Q77MTW3Q.js} +3 -3
- package/dist/{bootstrap-G37N6RGB.js.map → bootstrap-Q77MTW3Q.js.map} +1 -1
- package/dist/{chunk-LYFRLOFQ.js → chunk-3AFCPGGS.js} +62 -54
- package/dist/chunk-3AFCPGGS.js.map +1 -0
- package/dist/{hooks-UUAAVDS3.js → hooks-373Z6JG2.js} +51 -23
- package/dist/hooks-373Z6JG2.js.map +1 -0
- package/dist/index.js +1256 -168
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/en/common/README.md +51 -6
- package/templates/en/common/agents/agents.md +23 -4
- package/templates/en/common/agents/skills/create-pr.md +5 -1
- package/templates/en/common/designs/README.md +13 -1
- package/templates/en/common/features/README.md +4 -2
- package/templates/en/common/ideas/idea.md +30 -0
- package/templates/ko/common/README.md +52 -7
- package/templates/ko/common/agents/agents.md +23 -4
- package/templates/ko/common/agents/skills/create-pr.md +5 -1
- package/templates/ko/common/designs/README.md +13 -1
- package/templates/ko/common/features/README.md +4 -2
- package/templates/ko/common/ideas/idea.md +30 -0
- package/dist/chunk-LYFRLOFQ.js.map +0 -1
- package/dist/hooks-UUAAVDS3.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { hasLeeSpecKitCodexBootstrap } from './chunk-
|
|
2
|
+
import { hasLeeSpecKitCodexBootstrap } from './chunk-3AFCPGGS.js';
|
|
3
3
|
import { runGitCapture, runGitOrThrow } from './chunk-GR7JQBWF.js';
|
|
4
4
|
import { __dirname as __dirname$1 } from './chunk-7V7RMGEU.js';
|
|
5
5
|
import { program } from 'commander';
|
|
@@ -10,7 +10,8 @@ import chalk from 'chalk';
|
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import { spawn, execFileSync, spawnSync } from 'child_process';
|
|
12
12
|
import os from 'os';
|
|
13
|
-
import { createHash } from 'crypto';
|
|
13
|
+
import crypto, { createHash } from 'crypto';
|
|
14
|
+
import fs21 from 'fs';
|
|
14
15
|
|
|
15
16
|
async function walkFiles(fsAdapter, rootDir, options = {}) {
|
|
16
17
|
const out = [];
|
|
@@ -236,14 +237,15 @@ var koCli = {
|
|
|
236
237
|
"idea.nextSteps1": " 1. \uBC94\uC704, PRD Refs, \uC2B9\uACA9 \uBA54\uBAA8\uB97C \uC791\uC131",
|
|
237
238
|
"idea.nextSteps2": " 2. Feature\uB85C \uC2B9\uACA9: npx lee-spec-kit feature <name> --idea {ideaId}",
|
|
238
239
|
"idea.nextSteps3": " 3. Feature\uB85C \uB9CC\uB4E4\uC9C0 \uC54A\uC744 \uACBD\uC6B0 Dropped\uB85C \uD45C\uC2DC",
|
|
239
|
-
"
|
|
240
|
-
"
|
|
241
|
-
"
|
|
242
|
-
"
|
|
243
|
-
"
|
|
244
|
-
"
|
|
245
|
-
"
|
|
246
|
-
"
|
|
240
|
+
"integrations.codexBootstrapInstalled": "\u2705 \uC120\uD0DD\uC801 Codex bootstrap \uC124\uCE58 \uC644\uB8CC: {path}",
|
|
241
|
+
"integrations.codexBootstrapAlreadyInstalled": "\u2705 \uC120\uD0DD\uC801 Codex bootstrap \uC774 \uC774\uBBF8 \uC124\uCE58\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4: {path}",
|
|
242
|
+
"integrations.codexBootstrapRemoved": "\u2705 \uC120\uD0DD\uC801 Codex bootstrap \uC81C\uAC70 \uC644\uB8CC: {path}",
|
|
243
|
+
"integrations.codexBootstrapAlreadyAbsent": "\u2705 \uC120\uD0DD\uC801 Codex bootstrap \uC774 \uC774\uBBF8 \uC5C6\uC2B5\uB2C8\uB2E4: {path}",
|
|
244
|
+
"integrations.codexHooksInstalled": "\u2705 Repo-local Codex hooks \uC124\uCE58 \uC644\uB8CC: {path}",
|
|
245
|
+
"integrations.codexHooksAlreadyInstalled": "\u2705 Repo-local Codex hooks \uAC00 \uC774\uBBF8 \uC124\uCE58\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4: {path}",
|
|
246
|
+
"integrations.codexHooksRemoved": "\u2705 Repo-local Codex hooks \uC81C\uAC70 \uC644\uB8CC: {path}",
|
|
247
|
+
"integrations.codexHooksAlreadyAbsent": "\u2705 Repo-local Codex hooks \uAC00 \uC774\uBBF8 \uC5C6\uC2B5\uB2C8\uB2E4: {path}",
|
|
248
|
+
"integrations.codexHooksTrustRequired": "Codex\uC5D0\uC11C /hooks\uB97C \uC2E4\uD589\uD574 \uC124\uCE58\uB41C \uD504\uB85C\uC81D\uD2B8 \uD6C5\uC744 \uAC80\uD1A0\uD558\uACE0 \uC2E0\uB8B0\uD558\uC138\uC694. \uD6C5 \uC815\uC758\uAC00 \uBCC0\uACBD\uB418\uBA74 \uB2E4\uC2DC \uAC80\uD1A0\uD574\uC57C \uD569\uB2C8\uB2E4.",
|
|
247
249
|
"github.cmdGithubDescription": "GitHub \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uB3C4\uC6B0\uBBF8 (issue/pr \uBCF8\uBB38 \uD15C\uD50C\uB9BF \uC0DD\uC131, \uAC80\uC99D, merge \uC7AC\uC2DC\uB3C4)",
|
|
248
250
|
"github.cmdIssueDescription": "feature \uBB38\uC11C \uAE30\uBC18 GitHub issue \uBCF8\uBB38 \uC0DD\uC131/\uC0DD\uC131",
|
|
249
251
|
"github.cmdPrDescription": "GitHub PR \uBCF8\uBB38 \uC0DD\uC131/\uC0DD\uC131 + tasks \uB3D9\uAE30\uD654 + merge \uC7AC\uC2DC\uB3C4",
|
|
@@ -518,14 +520,15 @@ var enCli = {
|
|
|
518
520
|
"idea.nextSteps1": " 1. Fill scope, PRD refs, and promotion notes",
|
|
519
521
|
"idea.nextSteps2": " 2. Promote it with: npx lee-spec-kit feature <name> --idea {ideaId}",
|
|
520
522
|
"idea.nextSteps3": " 3. Mark it dropped if it should not become a feature",
|
|
521
|
-
"
|
|
522
|
-
"
|
|
523
|
-
"
|
|
524
|
-
"
|
|
525
|
-
"
|
|
526
|
-
"
|
|
527
|
-
"
|
|
528
|
-
"
|
|
523
|
+
"integrations.codexBootstrapInstalled": "\u2705 Optional Codex bootstrap installed: {path}",
|
|
524
|
+
"integrations.codexBootstrapAlreadyInstalled": "\u2705 Optional Codex bootstrap already installed: {path}",
|
|
525
|
+
"integrations.codexBootstrapRemoved": "\u2705 Optional Codex bootstrap removed: {path}",
|
|
526
|
+
"integrations.codexBootstrapAlreadyAbsent": "\u2705 Optional Codex bootstrap is already absent: {path}",
|
|
527
|
+
"integrations.codexHooksInstalled": "\u2705 Repo-local Codex hooks installed: {path}",
|
|
528
|
+
"integrations.codexHooksAlreadyInstalled": "\u2705 Repo-local Codex hooks already installed: {path}",
|
|
529
|
+
"integrations.codexHooksRemoved": "\u2705 Repo-local Codex hooks removed: {path}",
|
|
530
|
+
"integrations.codexHooksAlreadyAbsent": "\u2705 Repo-local Codex hooks are already absent: {path}",
|
|
531
|
+
"integrations.codexHooksTrustRequired": "Review and trust the installed project hooks with /hooks in Codex. Changed hook definitions require review again.",
|
|
529
532
|
"github.cmdGithubDescription": "GitHub workflow helpers (issue/pr templates, validation, merge retry)",
|
|
530
533
|
"github.cmdIssueDescription": "Generate/create GitHub issue body from feature docs with validation",
|
|
531
534
|
"github.cmdPrDescription": "Generate/create GitHub PR body with validation, tasks PR sync, and merge retry",
|
|
@@ -1301,6 +1304,10 @@ Before taking the next workflow step:
|
|
|
1301
1304
|
10. In standalone mode, do not hand-write \`git worktree add\`; run the exact \`nextAction.command\` from \`workflow-stage\` so the managed workspace path, stale directory cleanup, and \`.env\` / \`.env.*\` copy step stay consistent
|
|
1302
1305
|
11. Keep docs and code synchronized; if code changes materially, update the active feature docs in the same turn before stopping
|
|
1303
1306
|
12. When docs are synced to code, keep exactly one explicit marker like \`<!-- lee-spec-kit:workflow-sync 2026-04-16T12:34:56.789Z -->\` in a single active feature doc (prefer \`tasks.md\` or \`decisions.md\`): replace an existing marker timestamp or remove duplicates instead of appending another marker, so \`workflow-audit\` can prove the sync happened after the latest code change
|
|
1307
|
+
13. When \`workflow-stage --json\` returns \`nextAction.category === "pre_pr_review"\` and \`nextAction.executor === "subagent"\`, delegate a fresh read-only review using the returned \`model\`, \`reasoningEffort\`, and \`onUnavailable\` policy; do not select or require a named review skill
|
|
1308
|
+
14. The Pre-PR review subagent returns findings without modifying code; the main agent remediates findings and records the actual reviewer metadata, reviewed diff scope, evidence, and final decision
|
|
1309
|
+
15. For a local workflow, do not report completion directly after implementation approval; follow the exact returned \`local merge\` / \`local cleanup\` commands until \`workflow-stage\` proves integration, verification, and cleanup and returns \`done\`
|
|
1310
|
+
16. In a \`local-ff\` workflow, present the implementation approval summary exactly as returned because that checkpoint authorizes the remaining fast-forward integration, post-merge checks, and configured local cleanup; ask again at \`local_merge\` only when that category is explicitly configured as required
|
|
1304
1311
|
|
|
1305
1312
|
Approval and remote actions:
|
|
1306
1313
|
|
|
@@ -1882,6 +1889,14 @@ var DEFAULT_APPROVAL_REQUIRE_CHECK_CATEGORIES = [
|
|
|
1882
1889
|
"spec_approve",
|
|
1883
1890
|
"implementation_approve"
|
|
1884
1891
|
];
|
|
1892
|
+
var PRE_PR_REVIEW_REASONING_EFFORTS = [
|
|
1893
|
+
"low",
|
|
1894
|
+
"medium",
|
|
1895
|
+
"high",
|
|
1896
|
+
"xhigh",
|
|
1897
|
+
"max",
|
|
1898
|
+
"ultra"
|
|
1899
|
+
];
|
|
1885
1900
|
function createDefaultApprovalConfig() {
|
|
1886
1901
|
return {
|
|
1887
1902
|
mode: "category",
|
|
@@ -1889,6 +1904,14 @@ function createDefaultApprovalConfig() {
|
|
|
1889
1904
|
requireCheckCategories: [...DEFAULT_APPROVAL_REQUIRE_CHECK_CATEGORIES]
|
|
1890
1905
|
};
|
|
1891
1906
|
}
|
|
1907
|
+
function createDefaultPrePrReviewerConfig() {
|
|
1908
|
+
return {
|
|
1909
|
+
type: "subagent",
|
|
1910
|
+
model: "inherit",
|
|
1911
|
+
reasoningEffort: "high",
|
|
1912
|
+
onUnavailable: "inherit"
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1892
1915
|
|
|
1893
1916
|
// src/commands/init.ts
|
|
1894
1917
|
function checkGitRepo(cwd) {
|
|
@@ -1915,6 +1938,25 @@ function getGitTopLevelOrNull(cwd) {
|
|
|
1915
1938
|
return null;
|
|
1916
1939
|
}
|
|
1917
1940
|
}
|
|
1941
|
+
function getCurrentBranchOrMain(cwd) {
|
|
1942
|
+
try {
|
|
1943
|
+
const branch = execFileSync("git", ["branch", "--show-current"], {
|
|
1944
|
+
cwd,
|
|
1945
|
+
encoding: "utf-8",
|
|
1946
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1947
|
+
}).trim();
|
|
1948
|
+
return branch || "main";
|
|
1949
|
+
} catch {
|
|
1950
|
+
return "main";
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
function resolveInitialBaseBranch(cwd, docsRepo, projectRoot) {
|
|
1954
|
+
if (docsRepo === "standalone") {
|
|
1955
|
+
const firstProjectRoot = resolveStandaloneInitProjectRoots(projectRoot, cwd)[0];
|
|
1956
|
+
if (firstProjectRoot) return getCurrentBranchOrMain(firstProjectRoot);
|
|
1957
|
+
}
|
|
1958
|
+
return getCurrentBranchOrMain(getGitTopLevelOrNull(cwd) || cwd);
|
|
1959
|
+
}
|
|
1918
1960
|
function isSameOrWithinDir(parentDir, candidateDir) {
|
|
1919
1961
|
const resolvedParent = path8.resolve(parentDir);
|
|
1920
1962
|
const resolvedCandidate = path8.resolve(candidateDir);
|
|
@@ -2484,21 +2526,19 @@ async function runInit(options) {
|
|
|
2484
2526
|
createdAt: getLocalDateString(),
|
|
2485
2527
|
docsRepo,
|
|
2486
2528
|
workflow: {
|
|
2487
|
-
preset: workflowMode,
|
|
2488
2529
|
mode: workflowMode,
|
|
2489
2530
|
requireWorktree: docsRepo === "standalone",
|
|
2490
2531
|
codeDirtyScope: "auto",
|
|
2491
2532
|
taskCommitGate: "warn",
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2533
|
+
...workflowMode === "local" ? {
|
|
2534
|
+
baseBranch: resolveInitialBaseBranch(cwd, docsRepo, projectRoot),
|
|
2535
|
+
completionStrategy: "local-ff",
|
|
2536
|
+
deleteFeatureBranchAfterMerge: true,
|
|
2537
|
+
postMergeChecks: []
|
|
2538
|
+
} : {},
|
|
2495
2539
|
prePrReview: {
|
|
2496
|
-
skills: ["code-review-excellence"],
|
|
2497
|
-
fallback: "builtin-checklist",
|
|
2498
2540
|
evidenceMode: "path_required",
|
|
2499
|
-
|
|
2500
|
-
enforceExecutionEvidence: false,
|
|
2501
|
-
executionCommandPrefixes: []
|
|
2541
|
+
reviewer: createDefaultPrePrReviewerConfig()
|
|
2502
2542
|
}
|
|
2503
2543
|
},
|
|
2504
2544
|
pr: {
|
|
@@ -3316,9 +3356,118 @@ async function getNextIdeaId(docsDir) {
|
|
|
3316
3356
|
const width = Math.max(3, String(next).length);
|
|
3317
3357
|
return `I${String(next).padStart(width, "0")}`;
|
|
3318
3358
|
}
|
|
3359
|
+
|
|
3360
|
+
// src/config/legacy-approval.ts
|
|
3361
|
+
var LEGACY_APPROVAL_CATEGORY_STEPS = [
|
|
3362
|
+
["spec_write", 2],
|
|
3363
|
+
["spec_approve", 3],
|
|
3364
|
+
["plan_write", 4],
|
|
3365
|
+
["plan_approve", 5],
|
|
3366
|
+
["tasks_write", 6],
|
|
3367
|
+
["tasks_approve", 6],
|
|
3368
|
+
["issue_prepare", 8],
|
|
3369
|
+
["issue_create", 8],
|
|
3370
|
+
["branch_create", 9],
|
|
3371
|
+
["task_execute", 10],
|
|
3372
|
+
["implementation_approve", 10],
|
|
3373
|
+
["pre_pr_review", 12],
|
|
3374
|
+
["pr_prepare", 13],
|
|
3375
|
+
["pr_create", 13],
|
|
3376
|
+
["code_review", 14],
|
|
3377
|
+
["pr_merge", 14]
|
|
3378
|
+
];
|
|
3379
|
+
|
|
3380
|
+
// src/config/migrate.ts
|
|
3381
|
+
var OBSOLETE_PRE_PR_REVIEW_KEYS = [
|
|
3382
|
+
"skills",
|
|
3383
|
+
"fallback",
|
|
3384
|
+
"decisionEnum",
|
|
3385
|
+
"enforceExecutionEvidence",
|
|
3386
|
+
"executionCommandPrefixes"
|
|
3387
|
+
];
|
|
3319
3388
|
function hasOwnKey(value, key) {
|
|
3320
3389
|
return Object.prototype.hasOwnProperty.call(value, key);
|
|
3321
3390
|
}
|
|
3391
|
+
function isPlainRecord(value) {
|
|
3392
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3393
|
+
}
|
|
3394
|
+
function migrateLegacyWorkflowSettings(workflow) {
|
|
3395
|
+
const changedPaths = [];
|
|
3396
|
+
const hasCanonicalMode = workflow.mode === "github" || workflow.mode === "local";
|
|
3397
|
+
const legacyPreset = workflow.preset;
|
|
3398
|
+
const canonicalMode = hasCanonicalMode ? workflow.mode : legacyPreset === "local" ? "local" : "github";
|
|
3399
|
+
if (workflow.mode !== canonicalMode) {
|
|
3400
|
+
workflow.mode = canonicalMode;
|
|
3401
|
+
changedPaths.push("workflow.mode");
|
|
3402
|
+
}
|
|
3403
|
+
if (!hasCanonicalMode && legacyPreset === "strict" && workflow.requireWorktree === void 0) {
|
|
3404
|
+
workflow.requireWorktree = true;
|
|
3405
|
+
changedPaths.push("workflow.requireWorktree");
|
|
3406
|
+
}
|
|
3407
|
+
if (hasOwnKey(workflow, "preset")) {
|
|
3408
|
+
delete workflow.preset;
|
|
3409
|
+
changedPaths.push("workflow.preset");
|
|
3410
|
+
}
|
|
3411
|
+
if (hasOwnKey(workflow, "auto")) {
|
|
3412
|
+
delete workflow.auto;
|
|
3413
|
+
changedPaths.push("workflow.auto");
|
|
3414
|
+
}
|
|
3415
|
+
if (isPlainRecord(workflow.prePrReview)) {
|
|
3416
|
+
const prePrReview = workflow.prePrReview;
|
|
3417
|
+
for (const key of OBSOLETE_PRE_PR_REVIEW_KEYS) {
|
|
3418
|
+
if (!hasOwnKey(prePrReview, key)) continue;
|
|
3419
|
+
delete prePrReview[key];
|
|
3420
|
+
changedPaths.push(`workflow.prePrReview.${key}`);
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
return changedPaths;
|
|
3424
|
+
}
|
|
3425
|
+
function migrateLegacyApprovalSettings(approval) {
|
|
3426
|
+
if (approval.mode === "steps") {
|
|
3427
|
+
const requiredSteps = new Set(
|
|
3428
|
+
(Array.isArray(approval.requireCheckSteps) ? approval.requireCheckSteps : []).map(
|
|
3429
|
+
(value) => typeof value === "number" ? value : Number(value)
|
|
3430
|
+
).filter((value) => Number.isFinite(value))
|
|
3431
|
+
);
|
|
3432
|
+
const requireCheckCategories = [];
|
|
3433
|
+
const skipCheckCategories = [];
|
|
3434
|
+
for (const [category, step] of LEGACY_APPROVAL_CATEGORY_STEPS) {
|
|
3435
|
+
if (requiredSteps.has(step)) {
|
|
3436
|
+
requireCheckCategories.push(category);
|
|
3437
|
+
} else {
|
|
3438
|
+
skipCheckCategories.push(category);
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
const migratedApproval = { ...approval };
|
|
3442
|
+
delete migratedApproval.requireCheckSteps;
|
|
3443
|
+
delete migratedApproval.taskExecuteCheck;
|
|
3444
|
+
return {
|
|
3445
|
+
approval: {
|
|
3446
|
+
...migratedApproval,
|
|
3447
|
+
mode: "category",
|
|
3448
|
+
default: "keep",
|
|
3449
|
+
requireCheckCategories,
|
|
3450
|
+
skipCheckCategories
|
|
3451
|
+
},
|
|
3452
|
+
changedPaths: ["approval"]
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
const changedPaths = [];
|
|
3456
|
+
if (hasOwnKey(approval, "requireCheckSteps")) {
|
|
3457
|
+
delete approval.requireCheckSteps;
|
|
3458
|
+
changedPaths.push("approval.requireCheckSteps");
|
|
3459
|
+
}
|
|
3460
|
+
if (hasOwnKey(approval, "taskExecuteCheck")) {
|
|
3461
|
+
delete approval.taskExecuteCheck;
|
|
3462
|
+
changedPaths.push("approval.taskExecuteCheck");
|
|
3463
|
+
}
|
|
3464
|
+
return { approval, changedPaths };
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
// src/commands/update.ts
|
|
3468
|
+
function hasOwnKey2(value, key) {
|
|
3469
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
3470
|
+
}
|
|
3322
3471
|
function isLegacyGeneratedApprovalConfig(approval) {
|
|
3323
3472
|
const mode = typeof approval.mode === "string" ? approval.mode : "";
|
|
3324
3473
|
if (mode && mode !== "category" && mode !== "steps" && mode !== "builtin") {
|
|
@@ -3328,10 +3477,9 @@ function isLegacyGeneratedApprovalConfig(approval) {
|
|
|
3328
3477
|
"default",
|
|
3329
3478
|
"requireCheckSteps",
|
|
3330
3479
|
"requireCheckCategories",
|
|
3331
|
-
"skipCheckCategories"
|
|
3332
|
-
"taskExecuteCheck"
|
|
3480
|
+
"skipCheckCategories"
|
|
3333
3481
|
];
|
|
3334
|
-
return !overrideKeys.some((key) =>
|
|
3482
|
+
return !overrideKeys.some((key) => hasOwnKey2(approval, key));
|
|
3335
3483
|
}
|
|
3336
3484
|
function updateCommand(program2) {
|
|
3337
3485
|
program2.command("update").description("Update docs templates to the latest version").option("--agents", "Update agents/ folder only").option("--agents-md", "Sync project-scoped AGENTS.md entrypoint").option(
|
|
@@ -3517,38 +3665,6 @@ async function collectAgentsMdTargets(cwd, config) {
|
|
|
3517
3665
|
function isPlainObject(value) {
|
|
3518
3666
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3519
3667
|
}
|
|
3520
|
-
function normalizeSkillList(raw) {
|
|
3521
|
-
if (!Array.isArray(raw)) return [];
|
|
3522
|
-
const deduped = /* @__PURE__ */ new Set();
|
|
3523
|
-
for (const item of raw) {
|
|
3524
|
-
const value = String(item || "").trim();
|
|
3525
|
-
if (!value) continue;
|
|
3526
|
-
deduped.add(value);
|
|
3527
|
-
}
|
|
3528
|
-
return [...deduped];
|
|
3529
|
-
}
|
|
3530
|
-
function normalizeStringList(raw) {
|
|
3531
|
-
if (!Array.isArray(raw)) return [];
|
|
3532
|
-
const deduped = /* @__PURE__ */ new Set();
|
|
3533
|
-
for (const item of raw) {
|
|
3534
|
-
const value = String(item || "").trim();
|
|
3535
|
-
if (!value) continue;
|
|
3536
|
-
deduped.add(value);
|
|
3537
|
-
}
|
|
3538
|
-
return [...deduped];
|
|
3539
|
-
}
|
|
3540
|
-
function normalizeDecisionEnumList(raw) {
|
|
3541
|
-
if (!Array.isArray(raw)) return [];
|
|
3542
|
-
const deduped = /* @__PURE__ */ new Set();
|
|
3543
|
-
for (const item of raw) {
|
|
3544
|
-
const value = String(item || "").trim().toLowerCase();
|
|
3545
|
-
if (!value) continue;
|
|
3546
|
-
if (value === "approve" || value === "changes_requested" || value === "blocked") {
|
|
3547
|
-
deduped.add(value);
|
|
3548
|
-
}
|
|
3549
|
-
}
|
|
3550
|
-
return [...deduped];
|
|
3551
|
-
}
|
|
3552
3668
|
async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
3553
3669
|
const configPath = path8.join(docsDir, ".lee-spec-kit.json");
|
|
3554
3670
|
if (!await fs.pathExists(configPath)) {
|
|
@@ -3576,9 +3692,7 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
|
3576
3692
|
changedPaths.push("workflow");
|
|
3577
3693
|
}
|
|
3578
3694
|
const workflow = raw.workflow;
|
|
3579
|
-
|
|
3580
|
-
setIfMissing(workflow, "preset", inferredPreset, "workflow.preset");
|
|
3581
|
-
setIfMissing(workflow, "mode", "github", "workflow.mode");
|
|
3695
|
+
changedPaths.push(...migrateLegacyWorkflowSettings(workflow));
|
|
3582
3696
|
setIfMissing(
|
|
3583
3697
|
workflow,
|
|
3584
3698
|
"requireWorktree",
|
|
@@ -3591,31 +3705,62 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
|
3591
3705
|
}
|
|
3592
3706
|
setIfMissing(workflow, "codeDirtyScope", "auto", "workflow.codeDirtyScope");
|
|
3593
3707
|
setIfMissing(workflow, "taskCommitGate", "warn", "workflow.taskCommitGate");
|
|
3594
|
-
if (
|
|
3595
|
-
workflow
|
|
3596
|
-
|
|
3708
|
+
if (workflow.mode === "local") {
|
|
3709
|
+
setIfMissing(workflow, "baseBranch", "main", "workflow.baseBranch");
|
|
3710
|
+
if (typeof workflow.baseBranch !== "string" || !workflow.baseBranch.trim()) {
|
|
3711
|
+
workflow.baseBranch = "main";
|
|
3712
|
+
changedPaths.push("workflow.baseBranch");
|
|
3713
|
+
} else if (workflow.baseBranch !== workflow.baseBranch.trim()) {
|
|
3714
|
+
workflow.baseBranch = workflow.baseBranch.trim();
|
|
3715
|
+
changedPaths.push("workflow.baseBranch");
|
|
3716
|
+
}
|
|
3717
|
+
setIfMissing(
|
|
3718
|
+
workflow,
|
|
3719
|
+
"completionStrategy",
|
|
3720
|
+
"none",
|
|
3721
|
+
"workflow.completionStrategy"
|
|
3722
|
+
);
|
|
3723
|
+
if (workflow.completionStrategy !== "local-ff" && workflow.completionStrategy !== "none") {
|
|
3724
|
+
workflow.completionStrategy = "none";
|
|
3725
|
+
changedPaths.push("workflow.completionStrategy");
|
|
3726
|
+
}
|
|
3727
|
+
setIfMissing(
|
|
3728
|
+
workflow,
|
|
3729
|
+
"deleteFeatureBranchAfterMerge",
|
|
3730
|
+
true,
|
|
3731
|
+
"workflow.deleteFeatureBranchAfterMerge"
|
|
3732
|
+
);
|
|
3733
|
+
if (typeof workflow.deleteFeatureBranchAfterMerge !== "boolean") {
|
|
3734
|
+
workflow.deleteFeatureBranchAfterMerge = true;
|
|
3735
|
+
changedPaths.push("workflow.deleteFeatureBranchAfterMerge");
|
|
3736
|
+
}
|
|
3737
|
+
setIfMissing(workflow, "postMergeChecks", [], "workflow.postMergeChecks");
|
|
3738
|
+
if (!Array.isArray(workflow.postMergeChecks)) {
|
|
3739
|
+
workflow.postMergeChecks = [];
|
|
3740
|
+
changedPaths.push("workflow.postMergeChecks");
|
|
3741
|
+
} else {
|
|
3742
|
+
const normalizedPostMergeChecks = workflow.postMergeChecks.flatMap(
|
|
3743
|
+
(value) => {
|
|
3744
|
+
if (!isPlainObject(value) || typeof value.command !== "string") {
|
|
3745
|
+
return [];
|
|
3746
|
+
}
|
|
3747
|
+
const command = value.command.trim();
|
|
3748
|
+
if (!command) return [];
|
|
3749
|
+
const args = Array.isArray(value.args) ? value.args.filter((arg) => typeof arg === "string") : [];
|
|
3750
|
+
return [{ command, ...args.length > 0 ? { args } : {} }];
|
|
3751
|
+
}
|
|
3752
|
+
);
|
|
3753
|
+
if (JSON.stringify(normalizedPostMergeChecks) !== JSON.stringify(workflow.postMergeChecks)) {
|
|
3754
|
+
workflow.postMergeChecks = normalizedPostMergeChecks;
|
|
3755
|
+
changedPaths.push("workflow.postMergeChecks");
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3597
3758
|
}
|
|
3598
|
-
const workflowAuto = workflow.auto;
|
|
3599
|
-
setIfMissing(workflowAuto, "defaultPreset", "pr-handoff", "workflow.auto.defaultPreset");
|
|
3600
3759
|
if (!isPlainObject(workflow.prePrReview)) {
|
|
3601
3760
|
workflow.prePrReview = {};
|
|
3602
3761
|
changedPaths.push("workflow.prePrReview");
|
|
3603
3762
|
}
|
|
3604
3763
|
const prePrReview = workflow.prePrReview;
|
|
3605
|
-
if (prePrReview.skills === void 0) {
|
|
3606
|
-
prePrReview.skills = ["code-review-excellence"];
|
|
3607
|
-
changedPaths.push("workflow.prePrReview.skills");
|
|
3608
|
-
} else {
|
|
3609
|
-
const normalizedSkills = normalizeSkillList(prePrReview.skills);
|
|
3610
|
-
if (normalizedSkills.length === 0) {
|
|
3611
|
-
prePrReview.skills = ["code-review-excellence"];
|
|
3612
|
-
changedPaths.push("workflow.prePrReview.skills");
|
|
3613
|
-
} else if (JSON.stringify(normalizedSkills) !== JSON.stringify(prePrReview.skills)) {
|
|
3614
|
-
prePrReview.skills = normalizedSkills;
|
|
3615
|
-
changedPaths.push("workflow.prePrReview.skills");
|
|
3616
|
-
}
|
|
3617
|
-
}
|
|
3618
|
-
setIfMissing(prePrReview, "fallback", "builtin-checklist", "workflow.prePrReview.fallback");
|
|
3619
3764
|
setIfMissing(
|
|
3620
3765
|
prePrReview,
|
|
3621
3766
|
"evidenceMode",
|
|
@@ -3630,35 +3775,32 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
|
3630
3775
|
delete prePrReview.findings;
|
|
3631
3776
|
changedPaths.push("workflow.prePrReview.findings");
|
|
3632
3777
|
}
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3778
|
+
const defaultReviewer = createDefaultPrePrReviewerConfig();
|
|
3779
|
+
if (!isPlainObject(prePrReview.reviewer)) {
|
|
3780
|
+
prePrReview.reviewer = defaultReviewer;
|
|
3781
|
+
changedPaths.push("workflow.prePrReview.reviewer");
|
|
3636
3782
|
} else {
|
|
3637
|
-
const
|
|
3638
|
-
if (
|
|
3639
|
-
|
|
3640
|
-
changedPaths.push("workflow.prePrReview.
|
|
3641
|
-
}
|
|
3642
|
-
|
|
3643
|
-
|
|
3783
|
+
const reviewer = prePrReview.reviewer;
|
|
3784
|
+
if (reviewer.type !== "subagent") {
|
|
3785
|
+
reviewer.type = defaultReviewer.type;
|
|
3786
|
+
changedPaths.push("workflow.prePrReview.reviewer.type");
|
|
3787
|
+
}
|
|
3788
|
+
if (typeof reviewer.model !== "string" || !reviewer.model.trim()) {
|
|
3789
|
+
reviewer.model = defaultReviewer.model;
|
|
3790
|
+
changedPaths.push("workflow.prePrReview.reviewer.model");
|
|
3791
|
+
} else if (reviewer.model !== reviewer.model.trim()) {
|
|
3792
|
+
reviewer.model = reviewer.model.trim();
|
|
3793
|
+
changedPaths.push("workflow.prePrReview.reviewer.model");
|
|
3794
|
+
}
|
|
3795
|
+
if (typeof reviewer.reasoningEffort !== "string" || !PRE_PR_REVIEW_REASONING_EFFORTS.includes(
|
|
3796
|
+
reviewer.reasoningEffort
|
|
3797
|
+
)) {
|
|
3798
|
+
reviewer.reasoningEffort = defaultReviewer.reasoningEffort;
|
|
3799
|
+
changedPaths.push("workflow.prePrReview.reviewer.reasoningEffort");
|
|
3644
3800
|
}
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
"enforceExecutionEvidence",
|
|
3649
|
-
false,
|
|
3650
|
-
"workflow.prePrReview.enforceExecutionEvidence"
|
|
3651
|
-
);
|
|
3652
|
-
if (prePrReview.executionCommandPrefixes === void 0) {
|
|
3653
|
-
prePrReview.executionCommandPrefixes = [];
|
|
3654
|
-
changedPaths.push("workflow.prePrReview.executionCommandPrefixes");
|
|
3655
|
-
} else {
|
|
3656
|
-
const normalizedExecutionCommandPrefixes = normalizeStringList(
|
|
3657
|
-
prePrReview.executionCommandPrefixes
|
|
3658
|
-
);
|
|
3659
|
-
if (JSON.stringify(normalizedExecutionCommandPrefixes) !== JSON.stringify(prePrReview.executionCommandPrefixes)) {
|
|
3660
|
-
prePrReview.executionCommandPrefixes = normalizedExecutionCommandPrefixes;
|
|
3661
|
-
changedPaths.push("workflow.prePrReview.executionCommandPrefixes");
|
|
3801
|
+
if (reviewer.onUnavailable !== "inherit" && reviewer.onUnavailable !== "error") {
|
|
3802
|
+
reviewer.onUnavailable = defaultReviewer.onUnavailable;
|
|
3803
|
+
changedPaths.push("workflow.prePrReview.reviewer.onUnavailable");
|
|
3662
3804
|
}
|
|
3663
3805
|
}
|
|
3664
3806
|
if (!isPlainObject(raw.pr)) {
|
|
@@ -3677,7 +3819,10 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
|
3677
3819
|
changedPaths.push("approval");
|
|
3678
3820
|
} else {
|
|
3679
3821
|
const approval = raw.approval;
|
|
3680
|
-
|
|
3822
|
+
const migration = migrateLegacyApprovalSettings(approval);
|
|
3823
|
+
raw.approval = migration.approval;
|
|
3824
|
+
changedPaths.push(...migration.changedPaths);
|
|
3825
|
+
if (isPlainObject(raw.approval) && isLegacyGeneratedApprovalConfig(raw.approval)) {
|
|
3681
3826
|
raw.approval = createDefaultApprovalConfig();
|
|
3682
3827
|
changedPaths.push("approval");
|
|
3683
3828
|
}
|
|
@@ -7298,30 +7443,27 @@ async function runDetect(options) {
|
|
|
7298
7443
|
function registerCodexIntegration(parent) {
|
|
7299
7444
|
parent.command("codex").alias("codex-bootstrap").description(
|
|
7300
7445
|
"Install or remove the optional Codex bootstrap that re-reads the current workspace ./AGENTS.md"
|
|
7301
|
-
).option(
|
|
7302
|
-
"--remove",
|
|
7303
|
-
"Remove the lee-spec-kit managed Codex bootstrap block"
|
|
7304
|
-
).action(async (options) => {
|
|
7446
|
+
).option("--remove", "Remove the lee-spec-kit managed Codex bootstrap block").action(async (options) => {
|
|
7305
7447
|
const lang = DEFAULT_LANG;
|
|
7306
7448
|
try {
|
|
7307
7449
|
const {
|
|
7308
7450
|
getCodexConfigPath,
|
|
7309
7451
|
removeLeeSpecKitCodexBootstrap,
|
|
7310
7452
|
upsertLeeSpecKitCodexBootstrap
|
|
7311
|
-
} = await import('./bootstrap-
|
|
7453
|
+
} = await import('./bootstrap-Q77MTW3Q.js');
|
|
7312
7454
|
const filePath = getCodexConfigPath();
|
|
7313
7455
|
if (options.remove) {
|
|
7314
7456
|
const result2 = await removeLeeSpecKitCodexBootstrap(filePath);
|
|
7315
|
-
const message = result2.changed ? tr(lang, "cli", "
|
|
7457
|
+
const message = result2.changed ? tr(lang, "cli", "integrations.codexBootstrapRemoved", {
|
|
7316
7458
|
path: filePath
|
|
7317
|
-
}) : tr(lang, "cli", "
|
|
7459
|
+
}) : tr(lang, "cli", "integrations.codexBootstrapAlreadyAbsent", {
|
|
7318
7460
|
path: filePath
|
|
7319
7461
|
});
|
|
7320
7462
|
console.log(chalk.green(message));
|
|
7321
7463
|
return;
|
|
7322
7464
|
}
|
|
7323
7465
|
const result = await upsertLeeSpecKitCodexBootstrap(filePath);
|
|
7324
|
-
const key = result.action === "noop" ? "
|
|
7466
|
+
const key = result.action === "noop" ? "integrations.codexBootstrapAlreadyInstalled" : "integrations.codexBootstrapInstalled";
|
|
7325
7467
|
console.log(chalk.green(tr(lang, "cli", key, { path: filePath })));
|
|
7326
7468
|
} catch (error) {
|
|
7327
7469
|
const cliError = toCliError(error);
|
|
@@ -7338,7 +7480,10 @@ function registerCodexIntegration(parent) {
|
|
|
7338
7480
|
function registerCodexHooksIntegration(parent) {
|
|
7339
7481
|
parent.command("codex-hooks").description(
|
|
7340
7482
|
"Install or remove workspace-local Codex official hooks for the lee-spec-kit docs workflow"
|
|
7341
|
-
).option(
|
|
7483
|
+
).option(
|
|
7484
|
+
"--remove",
|
|
7485
|
+
"Remove lee-spec-kit managed workspace-local Codex hooks"
|
|
7486
|
+
).action(async (options) => {
|
|
7342
7487
|
const lang = DEFAULT_LANG;
|
|
7343
7488
|
try {
|
|
7344
7489
|
const config = await getConfig(process.cwd());
|
|
@@ -7353,24 +7498,42 @@ function registerCodexHooksIntegration(parent) {
|
|
|
7353
7498
|
removeLeeSpecKitCodexHooks,
|
|
7354
7499
|
resolveCodexHooksRepoRoot,
|
|
7355
7500
|
upsertLeeSpecKitCodexHooks
|
|
7356
|
-
} = await import('./hooks-
|
|
7357
|
-
const
|
|
7358
|
-
if (!
|
|
7501
|
+
} = await import('./hooks-373Z6JG2.js');
|
|
7502
|
+
const workflowRoot = config.docsRepo === "standalone" ? resolveConfiguredStandaloneWorkspaceRoot(config) : resolveCodexHooksRepoRoot(process.cwd());
|
|
7503
|
+
if (!workflowRoot) {
|
|
7359
7504
|
throw createCliError(
|
|
7360
7505
|
"PRECONDITION_FAILED",
|
|
7361
7506
|
"Standalone workspaceRoot is missing or invalid. Run `npx lee-spec-kit update --agents-md` from the shared workspace root to migrate this project."
|
|
7362
7507
|
);
|
|
7363
7508
|
}
|
|
7364
|
-
const
|
|
7509
|
+
const repoRoots = config.docsRepo === "standalone" ? [workflowRoot, ...resolveStandaloneProjectRoots(config)] : [workflowRoot];
|
|
7510
|
+
const uniqueRepoRoots = [...new Set(repoRoots)];
|
|
7511
|
+
const filePaths = uniqueRepoRoots.map(
|
|
7512
|
+
(repoRoot) => getRepoHooksConfigPath(repoRoot)
|
|
7513
|
+
);
|
|
7514
|
+
const displayPath = filePaths.join(", ");
|
|
7365
7515
|
if (options.remove) {
|
|
7366
|
-
const
|
|
7367
|
-
|
|
7368
|
-
|
|
7516
|
+
const results2 = await Promise.all(
|
|
7517
|
+
uniqueRepoRoots.map(
|
|
7518
|
+
(repoRoot) => removeLeeSpecKitCodexHooks(repoRoot)
|
|
7519
|
+
)
|
|
7520
|
+
);
|
|
7521
|
+
const key2 = results2.some((result) => result.changed) ? "integrations.codexHooksRemoved" : "integrations.codexHooksAlreadyAbsent";
|
|
7522
|
+
console.log(chalk.green(tr(lang, "cli", key2, { path: displayPath })));
|
|
7369
7523
|
return;
|
|
7370
7524
|
}
|
|
7371
|
-
const
|
|
7372
|
-
|
|
7373
|
-
|
|
7525
|
+
const results = await Promise.all(
|
|
7526
|
+
uniqueRepoRoots.map(
|
|
7527
|
+
(repoRoot) => upsertLeeSpecKitCodexHooks(repoRoot, workflowRoot)
|
|
7528
|
+
)
|
|
7529
|
+
);
|
|
7530
|
+
const key = results.every((result) => result.action === "noop") ? "integrations.codexHooksAlreadyInstalled" : "integrations.codexHooksInstalled";
|
|
7531
|
+
console.log(chalk.green(tr(lang, "cli", key, { path: displayPath })));
|
|
7532
|
+
console.log(
|
|
7533
|
+
chalk.yellow(
|
|
7534
|
+
tr(lang, "cli", "integrations.codexHooksTrustRequired")
|
|
7535
|
+
)
|
|
7536
|
+
);
|
|
7374
7537
|
} catch (error) {
|
|
7375
7538
|
const cliError = toCliError(error);
|
|
7376
7539
|
const suggestions = getCliErrorSuggestions(cliError.code, lang);
|
|
@@ -7388,27 +7551,227 @@ function integrationsCommand(program2) {
|
|
|
7388
7551
|
registerCodexIntegration(integrations);
|
|
7389
7552
|
registerCodexHooksIntegration(integrations);
|
|
7390
7553
|
}
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7554
|
+
function isSameOrWithin2(parentDir, candidatePath) {
|
|
7555
|
+
const relative = path8.relative(parentDir, candidatePath);
|
|
7556
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relative);
|
|
7557
|
+
}
|
|
7558
|
+
function isPrePrEvidenceSatisfied(check) {
|
|
7559
|
+
const evidence = check.evidence?.trim();
|
|
7560
|
+
if (!evidence) return false;
|
|
7561
|
+
if (check.evidenceMode === "any") return true;
|
|
7562
|
+
const normalizedEvidence = evidence.replaceAll("\\", "/");
|
|
7563
|
+
const docsDir = path8.resolve(check.docsDir);
|
|
7564
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
7565
|
+
if (path8.isAbsolute(normalizedEvidence)) {
|
|
7566
|
+
candidates.add(path8.resolve(normalizedEvidence));
|
|
7567
|
+
} else {
|
|
7568
|
+
candidates.add(path8.resolve(docsDir, normalizedEvidence));
|
|
7569
|
+
candidates.add(path8.resolve(check.featureDir, normalizedEvidence));
|
|
7570
|
+
const [evidenceRoot, ...evidenceParts] = normalizedEvidence.split("/");
|
|
7571
|
+
if (evidenceRoot?.toLowerCase() === path8.basename(docsDir).toLowerCase() && evidenceParts.length > 0) {
|
|
7572
|
+
candidates.add(path8.resolve(docsDir, ...evidenceParts));
|
|
7573
|
+
}
|
|
7574
|
+
}
|
|
7575
|
+
let realDocsDir;
|
|
7576
|
+
try {
|
|
7577
|
+
realDocsDir = fs21.realpathSync(docsDir);
|
|
7578
|
+
} catch {
|
|
7579
|
+
return false;
|
|
7580
|
+
}
|
|
7581
|
+
for (const candidate of candidates) {
|
|
7582
|
+
try {
|
|
7583
|
+
const realCandidate = fs21.realpathSync(candidate);
|
|
7584
|
+
if (!isSameOrWithin2(realDocsDir, realCandidate)) continue;
|
|
7585
|
+
if (fs21.statSync(realCandidate).isFile()) return true;
|
|
7586
|
+
} catch {
|
|
7587
|
+
continue;
|
|
7588
|
+
}
|
|
7589
|
+
}
|
|
7590
|
+
return false;
|
|
7591
|
+
}
|
|
7592
|
+
var BRANCH_LABELS2 = ["Branch", "\uBE0C\uB79C\uCE58"];
|
|
7593
|
+
function resolveLocalCompletionStrategy(config) {
|
|
7594
|
+
return config.workflow?.completionStrategy === "local-ff" ? "local-ff" : "none";
|
|
7595
|
+
}
|
|
7596
|
+
async function resolveLocalIntegrationContext(config, feature) {
|
|
7597
|
+
const projectRoot = resolveProjectRoot(config, feature);
|
|
7598
|
+
const featureWorktree = path8.resolve(feature.git.projectGitCwd);
|
|
7599
|
+
const baseBranch = String(config.workflow?.baseBranch || "main").trim() || "main";
|
|
7600
|
+
assertValidBranch(projectRoot, baseBranch, "baseBranch");
|
|
7601
|
+
const tasksContent = await readTasks(feature.path);
|
|
7602
|
+
const configuredFeatureBranch = extractMetadataValue(
|
|
7603
|
+
tasksContent,
|
|
7604
|
+
BRANCH_LABELS2
|
|
7605
|
+
);
|
|
7606
|
+
const featureBranch = configuredFeatureBranch || `feat/${feature.slug}`;
|
|
7607
|
+
assertValidBranch(projectRoot, featureBranch, "feature branch");
|
|
7608
|
+
if (featureBranch === baseBranch) {
|
|
7609
|
+
throw new Error(
|
|
7610
|
+
"Local feature branch must differ from workflow.baseBranch."
|
|
7611
|
+
);
|
|
7612
|
+
}
|
|
7613
|
+
const state = await readLocalIntegrationState(projectRoot, feature);
|
|
7614
|
+
const featureBranchTip = resolveRef(
|
|
7615
|
+
projectRoot,
|
|
7616
|
+
`refs/heads/${featureBranch}`
|
|
7617
|
+
);
|
|
7618
|
+
const featureTip = featureBranchTip || state?.featureTip || null;
|
|
7619
|
+
const baseTip = resolveRef(projectRoot, `refs/heads/${baseBranch}`);
|
|
7620
|
+
const currentBranch = currentGitBranch(projectRoot);
|
|
7621
|
+
const registeredWorktree = resolveRegisteredBranchWorktree(
|
|
7622
|
+
projectRoot,
|
|
7623
|
+
featureBranch
|
|
7624
|
+
);
|
|
7625
|
+
const managedFeatureWorktree = !!registeredWorktree && path8.resolve(registeredWorktree) !== path8.resolve(projectRoot);
|
|
7626
|
+
const effectiveFeatureWorktree = registeredWorktree || (fs.existsSync(featureWorktree) ? featureWorktree : projectRoot);
|
|
7627
|
+
return {
|
|
7628
|
+
projectRoot,
|
|
7629
|
+
featureWorktree: effectiveFeatureWorktree,
|
|
7630
|
+
docsGitCwd: feature.git.docsGitCwd,
|
|
7631
|
+
baseBranch,
|
|
7632
|
+
featureBranch,
|
|
7633
|
+
featureTip,
|
|
7634
|
+
baseTip,
|
|
7635
|
+
currentBranch,
|
|
7636
|
+
state,
|
|
7637
|
+
baseContainsFeature: !!featureTip && !!baseTip && isAncestor(projectRoot, featureTip, `refs/heads/${baseBranch}`),
|
|
7638
|
+
projectRootClean: isClean(projectRoot),
|
|
7639
|
+
featureWorktreeClean: isClean(effectiveFeatureWorktree),
|
|
7640
|
+
docsClean: isClean(feature.git.docsGitCwd),
|
|
7641
|
+
managedFeatureWorktree,
|
|
7642
|
+
featureBranchExists: !!featureBranchTip,
|
|
7643
|
+
postMergeChecks: normalizePostMergeChecks(config.workflow?.postMergeChecks),
|
|
7644
|
+
deleteFeatureBranchAfterMerge: config.workflow?.deleteFeatureBranchAfterMerge !== false
|
|
7645
|
+
};
|
|
7646
|
+
}
|
|
7647
|
+
async function writeLocalIntegrationState(projectRoot, feature, state) {
|
|
7648
|
+
const statePath = resolveStatePath(projectRoot, feature);
|
|
7649
|
+
await fs.ensureDir(path8.dirname(statePath));
|
|
7650
|
+
await fs.writeJson(statePath, state, { spaces: 2 });
|
|
7651
|
+
}
|
|
7652
|
+
async function readLocalIntegrationState(projectRoot, feature) {
|
|
7653
|
+
const statePath = resolveStatePath(projectRoot, feature);
|
|
7654
|
+
if (!await fs.pathExists(statePath)) return null;
|
|
7655
|
+
try {
|
|
7656
|
+
const state = await fs.readJson(statePath);
|
|
7657
|
+
if (state?.version !== 1 || state.featureRef !== feature.folderName || typeof state.featureTip !== "string") {
|
|
7658
|
+
return null;
|
|
7659
|
+
}
|
|
7660
|
+
return state;
|
|
7661
|
+
} catch {
|
|
7662
|
+
return null;
|
|
7663
|
+
}
|
|
7664
|
+
}
|
|
7665
|
+
function localCleanupComplete(context) {
|
|
7666
|
+
return context.baseContainsFeature && context.currentBranch === context.baseBranch && !context.managedFeatureWorktree && (!context.deleteFeatureBranchAfterMerge || !context.featureBranchExists) && context.projectRootClean && context.docsClean && context.state?.status === "cleaned" && context.state.featureTip === context.featureTip && context.state.mergedBaseTip === context.baseTip;
|
|
7667
|
+
}
|
|
7668
|
+
function runPostMergeChecks(projectRoot, checks) {
|
|
7669
|
+
const results = [];
|
|
7670
|
+
for (const check of checks) {
|
|
7671
|
+
const args = Array.isArray(check.args) ? check.args.map(String) : [];
|
|
7672
|
+
const result = runProcess(check.command, args, projectRoot);
|
|
7673
|
+
results.push({
|
|
7674
|
+
command: check.command,
|
|
7675
|
+
args,
|
|
7676
|
+
exitCode: result.code
|
|
7677
|
+
});
|
|
7678
|
+
if (result.code !== 0) break;
|
|
7679
|
+
}
|
|
7680
|
+
return results;
|
|
7681
|
+
}
|
|
7682
|
+
function gitRun(cwd, args) {
|
|
7683
|
+
return runProcess("git", args, cwd);
|
|
7684
|
+
}
|
|
7685
|
+
function currentGitBranch(cwd) {
|
|
7686
|
+
return runGitCapture(["branch", "--show-current"], cwd) || runGitCapture(["rev-parse", "--abbrev-ref", "HEAD"], cwd) || null;
|
|
7687
|
+
}
|
|
7688
|
+
function resolveRef(cwd, ref) {
|
|
7689
|
+
return runGitCapture(["rev-parse", "--verify", ref], cwd) || null;
|
|
7690
|
+
}
|
|
7691
|
+
function isAncestor(cwd, ancestor, descendant) {
|
|
7692
|
+
return runProcess(
|
|
7693
|
+
"git",
|
|
7694
|
+
["merge-base", "--is-ancestor", ancestor, descendant],
|
|
7695
|
+
cwd
|
|
7696
|
+
).code === 0;
|
|
7697
|
+
}
|
|
7698
|
+
function resolveProjectRoot(config, feature) {
|
|
7699
|
+
if (config.docsRepo === "standalone") {
|
|
7700
|
+
const roots = resolveStandaloneProjectRoots(
|
|
7701
|
+
config,
|
|
7702
|
+
feature.type === "single" ? void 0 : feature.type
|
|
7703
|
+
);
|
|
7704
|
+
if (roots[0]) return path8.resolve(roots[0]);
|
|
7705
|
+
}
|
|
7706
|
+
return runGitCapture(
|
|
7707
|
+
["rev-parse", "--show-toplevel"],
|
|
7708
|
+
feature.git.projectGitCwd
|
|
7709
|
+
) || path8.resolve(feature.git.projectGitCwd);
|
|
7710
|
+
}
|
|
7711
|
+
function resolveStatePath(projectRoot, feature) {
|
|
7712
|
+
const commonDirRaw = runGitCapture(["rev-parse", "--git-common-dir"], projectRoot) || ".git";
|
|
7713
|
+
const commonDir = path8.isAbsolute(commonDirRaw) ? commonDirRaw : path8.resolve(projectRoot, commonDirRaw);
|
|
7714
|
+
const key = crypto.createHash("sha256").update(`${feature.type}:${feature.folderName}`).digest("hex").slice(0, 24);
|
|
7715
|
+
return path8.join(
|
|
7716
|
+
commonDir,
|
|
7717
|
+
"lee-spec-kit",
|
|
7718
|
+
"local-integrations",
|
|
7719
|
+
`${key}.json`
|
|
7720
|
+
);
|
|
7721
|
+
}
|
|
7722
|
+
async function readTasks(featureDir) {
|
|
7723
|
+
const tasksPath = path8.join(featureDir, "tasks.md");
|
|
7724
|
+
return await fs.pathExists(tasksPath) ? fs.readFile(tasksPath, "utf-8") : "";
|
|
7725
|
+
}
|
|
7726
|
+
function extractMetadataValue(content, labels) {
|
|
7727
|
+
for (const label of labels) {
|
|
7728
|
+
const match = content.match(
|
|
7729
|
+
new RegExp(`^\\s*-\\s*\\*\\*${label}\\*\\*:\\s*(.*?)\\s*$`, "mi")
|
|
7730
|
+
);
|
|
7731
|
+
const value = String(match?.[1] || "").trim().replace(/^`(.+)`$/, "$1");
|
|
7732
|
+
if (value && value !== "-") return value;
|
|
7733
|
+
}
|
|
7734
|
+
return null;
|
|
7735
|
+
}
|
|
7736
|
+
function assertValidBranch(cwd, branch, label) {
|
|
7737
|
+
if (runProcess("git", ["check-ref-format", "--branch", branch], cwd).code !== 0) {
|
|
7738
|
+
throw new Error(`Invalid ${label}: ${branch}`);
|
|
7739
|
+
}
|
|
7740
|
+
}
|
|
7741
|
+
function isClean(cwd) {
|
|
7742
|
+
return (runGitCapture(
|
|
7743
|
+
["status", "--porcelain", "--untracked-files=normal"],
|
|
7744
|
+
cwd
|
|
7745
|
+
) || "") === "";
|
|
7746
|
+
}
|
|
7747
|
+
function resolveRegisteredBranchWorktree(projectRoot, branch) {
|
|
7748
|
+
const output = runGitCapture(["worktree", "list", "--porcelain"], projectRoot) || "";
|
|
7749
|
+
let currentPath = null;
|
|
7750
|
+
for (const line of output.split(/\r?\n/u)) {
|
|
7751
|
+
if (line.startsWith("worktree ")) {
|
|
7752
|
+
currentPath = line.slice("worktree ".length).trim();
|
|
7753
|
+
continue;
|
|
7754
|
+
}
|
|
7755
|
+
if (line === `branch refs/heads/${branch}` && currentPath) {
|
|
7756
|
+
return currentPath;
|
|
7757
|
+
}
|
|
7758
|
+
}
|
|
7759
|
+
return null;
|
|
7760
|
+
}
|
|
7761
|
+
function normalizePostMergeChecks(value) {
|
|
7762
|
+
if (!Array.isArray(value)) return [];
|
|
7763
|
+
return value.filter(
|
|
7764
|
+
(entry) => !!entry && typeof entry === "object" && typeof entry.command === "string" && !!entry.command.trim()
|
|
7765
|
+
).map((entry) => ({
|
|
7766
|
+
command: entry.command.trim(),
|
|
7767
|
+
args: Array.isArray(entry.args) ? entry.args.map(String) : []
|
|
7768
|
+
}));
|
|
7769
|
+
}
|
|
7770
|
+
|
|
7771
|
+
// src/utils/workflow-stage.ts
|
|
7409
7772
|
var DOC_STATUS_LABELS = ["Doc Status", "\uBB38\uC11C \uC0C1\uD0DC"];
|
|
7410
7773
|
var ISSUE_LABELS = ["Issue", "Issue Number", "\uC774\uC288", "\uC774\uC288 \uBC88\uD638"];
|
|
7411
|
-
var
|
|
7774
|
+
var BRANCH_LABELS3 = ["Branch", "\uBE0C\uB79C\uCE58"];
|
|
7412
7775
|
var PR_LABELS = ["PR", "Pull Request"];
|
|
7413
7776
|
var PR_STATUS_LABELS = ["PR Status", "PR \uC0C1\uD0DC"];
|
|
7414
7777
|
var PRE_PR_REVIEW_LABELS = ["Pre-PR Review", "PR \uC804 \uB9AC\uBDF0"];
|
|
@@ -7416,12 +7779,14 @@ var PRE_PR_EVIDENCE_LABELS = ["Pre-PR Evidence", "PR \uC804 \uB9AC\uBDF0 Evidenc
|
|
|
7416
7779
|
var PRE_PR_DECISION_LABELS = ["Pre-PR Decision", "PR \uC804 \uB9AC\uBDF0 Decision"];
|
|
7417
7780
|
function resolveWorkflowRequirements(config) {
|
|
7418
7781
|
const workflow = config.workflow || {};
|
|
7419
|
-
const
|
|
7782
|
+
const hasCanonicalMode = workflow.mode === "github" || workflow.mode === "local";
|
|
7783
|
+
const workflowMode = hasCanonicalMode ? workflow.mode : workflow.preset === "local" ? "local" : "github";
|
|
7420
7784
|
const isLocalWorkflow = workflowMode === "local";
|
|
7785
|
+
const legacyStrictRequiresWorktree = !hasCanonicalMode && workflow.preset === "strict";
|
|
7421
7786
|
return {
|
|
7422
7787
|
requireIssue: workflow.requireIssue ?? !isLocalWorkflow,
|
|
7423
7788
|
requireBranch: workflow.requireBranch ?? true,
|
|
7424
|
-
requireWorktree: config.docsRepo === "standalone" ? true : workflow.requireWorktree ??
|
|
7789
|
+
requireWorktree: config.docsRepo === "standalone" ? true : workflow.requireWorktree ?? legacyStrictRequiresWorktree,
|
|
7425
7790
|
requirePr: workflow.requirePr ?? !isLocalWorkflow,
|
|
7426
7791
|
requireReview: workflow.requireReview ?? !isLocalWorkflow,
|
|
7427
7792
|
requireMerge: workflow.requireMerge ?? !isLocalWorkflow,
|
|
@@ -7470,7 +7835,7 @@ function parseTasksDoc(content) {
|
|
|
7470
7835
|
const issueRaw = extractFieldValue2(content, ISSUE_LABELS);
|
|
7471
7836
|
const issueNumberMatch = issueRaw?.match(/^#(\d+)$/);
|
|
7472
7837
|
const issueNumber = issueNumberMatch ? Number(issueNumberMatch[1]) : null;
|
|
7473
|
-
const branchRaw = extractFieldValue2(content,
|
|
7838
|
+
const branchRaw = extractFieldValue2(content, BRANCH_LABELS3);
|
|
7474
7839
|
const prRaw = extractFieldValue2(content, PR_LABELS);
|
|
7475
7840
|
const prePrDecision = extractFieldValue2(content, PRE_PR_DECISION_LABELS);
|
|
7476
7841
|
const tasks = [];
|
|
@@ -7964,8 +8329,13 @@ function nextTodoTask(tasks) {
|
|
|
7964
8329
|
function allTasksDone(tasks) {
|
|
7965
8330
|
return tasks.tasks.length > 0 && tasks.tasks.every((task) => task.status === "DONE");
|
|
7966
8331
|
}
|
|
7967
|
-
function prePrSatisfied(tasks) {
|
|
7968
|
-
return tasks.prePrReviewStatus === "done" &&
|
|
8332
|
+
function prePrSatisfied(config, feature, tasks) {
|
|
8333
|
+
return tasks.prePrReviewStatus === "done" && isPrePrEvidenceSatisfied({
|
|
8334
|
+
docsDir: config.docsDir,
|
|
8335
|
+
featureDir: feature.path,
|
|
8336
|
+
evidence: tasks.prePrEvidence,
|
|
8337
|
+
evidenceMode: config.workflow?.prePrReview?.evidenceMode
|
|
8338
|
+
}) && !!tasks.prePrDecision && tasks.prePrDecisionOutcome === "approve";
|
|
7969
8339
|
}
|
|
7970
8340
|
function issueExistsRemotely(issueNumber, feature) {
|
|
7971
8341
|
if (!issueNumber) return false;
|
|
@@ -7985,12 +8355,32 @@ function prExistsRemotely(prRef, feature) {
|
|
|
7985
8355
|
);
|
|
7986
8356
|
return result.code === 0;
|
|
7987
8357
|
}
|
|
7988
|
-
function buildAction(category, summary, approvalRequired, command = null) {
|
|
8358
|
+
function buildAction(category, summary, approvalRequired, command = null, reviewer) {
|
|
7989
8359
|
return {
|
|
7990
8360
|
category,
|
|
7991
8361
|
summary,
|
|
7992
8362
|
approvalRequired,
|
|
7993
|
-
command
|
|
8363
|
+
command,
|
|
8364
|
+
...reviewer ? {
|
|
8365
|
+
executor: reviewer.type,
|
|
8366
|
+
model: reviewer.model,
|
|
8367
|
+
reasoningEffort: reviewer.reasoningEffort,
|
|
8368
|
+
onUnavailable: reviewer.onUnavailable
|
|
8369
|
+
} : {}
|
|
8370
|
+
};
|
|
8371
|
+
}
|
|
8372
|
+
function resolvePrePrReviewer(config) {
|
|
8373
|
+
const defaults = createDefaultPrePrReviewerConfig();
|
|
8374
|
+
const configured = config.workflow?.prePrReview?.reviewer;
|
|
8375
|
+
const model = typeof configured?.model === "string" && configured.model.trim() ? configured.model.trim() : defaults.model;
|
|
8376
|
+
const reasoningEffort = PRE_PR_REVIEW_REASONING_EFFORTS.includes(
|
|
8377
|
+
configured?.reasoningEffort
|
|
8378
|
+
) ? configured?.reasoningEffort : defaults.reasoningEffort;
|
|
8379
|
+
return {
|
|
8380
|
+
type: "subagent",
|
|
8381
|
+
model,
|
|
8382
|
+
reasoningEffort,
|
|
8383
|
+
onUnavailable: configured?.onUnavailable === "error" ? "error" : defaults.onUnavailable
|
|
7994
8384
|
};
|
|
7995
8385
|
}
|
|
7996
8386
|
function buildStageOption(label, reply, category, summary, command = null) {
|
|
@@ -8012,7 +8402,9 @@ function resolveActionApprovalRequired(config, category, builtinRequiresUserChec
|
|
|
8012
8402
|
const requiredSteps = new Set(
|
|
8013
8403
|
(approval.requireCheckSteps ?? []).map((value) => typeof value === "number" ? value : Number(value)).filter((value) => Number.isFinite(value))
|
|
8014
8404
|
);
|
|
8015
|
-
const legacyStep =
|
|
8405
|
+
const legacyStep = LEGACY_APPROVAL_CATEGORY_STEPS.find(
|
|
8406
|
+
([legacyCategory]) => legacyCategory === category
|
|
8407
|
+
)?.[1];
|
|
8016
8408
|
return typeof legacyStep === "number" ? requiredSteps.has(legacyStep) : builtinRequiresUserCheck;
|
|
8017
8409
|
}
|
|
8018
8410
|
const requiredCategories = new Set(
|
|
@@ -8595,8 +8987,11 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
8595
8987
|
"implementation_approve",
|
|
8596
8988
|
true
|
|
8597
8989
|
);
|
|
8990
|
+
const localIntegrationApproval = config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) === "local-ff";
|
|
8991
|
+
const localBaseBranch = config.workflow?.baseBranch?.trim() || "main";
|
|
8992
|
+
const localCleanupSummary = config.workflow?.deleteFeatureBranchAfterMerge === false ? "remove any managed Feature worktree" : "remove any managed Feature worktree and delete the integrated local Feature branch";
|
|
8598
8993
|
const actionOptions = approvalRequired ? buildApprovalActionOptions({
|
|
8599
|
-
approveSummary: "Approve the completed implementation and continue to the pre-PR or PR preparation stage.",
|
|
8994
|
+
approveSummary: localIntegrationApproval ? `Approve the completed implementation and authorize fast-forward integration into ${localBaseBranch}, post-merge verification, and cleanup that will ${localCleanupSummary}.` : "Approve the completed implementation and continue to the pre-PR or PR preparation stage.",
|
|
8600
8995
|
holdSummary: "Request implementation changes before the workflow continues."
|
|
8601
8996
|
}) : void 0;
|
|
8602
8997
|
return {
|
|
@@ -8607,7 +9002,7 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
8607
9002
|
stage: "implementation_approve",
|
|
8608
9003
|
nextAction: buildAction(
|
|
8609
9004
|
"implementation_approve",
|
|
8610
|
-
"Share the completed implementation, get user approval, and record the completion checkpoint in tasks.md.",
|
|
9005
|
+
localIntegrationApproval ? `Share the completed implementation and get user approval for the remaining local completion flow: fast-forward into ${localBaseBranch}, run post-merge checks, then ${localCleanupSummary}. Record that approval in tasks.md.` : "Share the completed implementation, get user approval, and record the completion checkpoint in tasks.md.",
|
|
8611
9006
|
approvalRequired
|
|
8612
9007
|
),
|
|
8613
9008
|
approvalRequired,
|
|
@@ -8617,7 +9012,7 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
8617
9012
|
blockedReasonCode: "IMPLEMENTATION_APPROVAL_REQUIRED"
|
|
8618
9013
|
};
|
|
8619
9014
|
}
|
|
8620
|
-
if (requirements.prePrReviewEnabled && !prePrSatisfied(tasks)) {
|
|
9015
|
+
if (requirements.prePrReviewEnabled && !prePrSatisfied(config, feature, tasks)) {
|
|
8621
9016
|
return {
|
|
8622
9017
|
status: "ok",
|
|
8623
9018
|
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
@@ -8626,14 +9021,97 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
8626
9021
|
stage: "pre_pr_review",
|
|
8627
9022
|
nextAction: buildAction(
|
|
8628
9023
|
"pre_pr_review",
|
|
8629
|
-
"
|
|
8630
|
-
false
|
|
9024
|
+
"Delegate an independent read-only Pre-PR review to a fresh subagent and record its findings, decision, and reviewer metadata as evidence.",
|
|
9025
|
+
false,
|
|
9026
|
+
null,
|
|
9027
|
+
resolvePrePrReviewer(config)
|
|
8631
9028
|
),
|
|
8632
9029
|
approvalRequired: false,
|
|
8633
9030
|
implementationAllowed: false,
|
|
8634
9031
|
blockedReasonCode: "PRE_PR_REVIEW_NOT_APPROVED"
|
|
8635
9032
|
};
|
|
8636
9033
|
}
|
|
9034
|
+
if (config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) === "local-ff") {
|
|
9035
|
+
const localState = await resolveLocalIntegrationContext(config, feature);
|
|
9036
|
+
const localMergeBaseCommand = `npx lee-spec-kit local merge ${buildFeatureArgs(feature)} --json`;
|
|
9037
|
+
if (!localState.baseContainsFeature) {
|
|
9038
|
+
const approvalRequired = resolveActionApprovalRequired(
|
|
9039
|
+
config,
|
|
9040
|
+
"local_merge",
|
|
9041
|
+
false
|
|
9042
|
+
);
|
|
9043
|
+
const command = approvalRequired ? `${localMergeBaseCommand} --confirm OK` : localMergeBaseCommand;
|
|
9044
|
+
const actionOptions = approvalRequired ? buildApprovalActionOptions({
|
|
9045
|
+
approveSummary: `Fast-forward ${localState.featureBranch} into ${localState.baseBranch} and run the configured post-merge checks.`,
|
|
9046
|
+
holdSummary: "Keep the completed Feature branch unmerged for now."
|
|
9047
|
+
}) : void 0;
|
|
9048
|
+
return {
|
|
9049
|
+
status: "ok",
|
|
9050
|
+
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
9051
|
+
docsDir: config.docsDir,
|
|
9052
|
+
featureRef: buildFeatureRef(feature),
|
|
9053
|
+
stage: "local_merge",
|
|
9054
|
+
nextAction: buildAction(
|
|
9055
|
+
"local_merge",
|
|
9056
|
+
`Fast-forward ${localState.featureBranch} into ${localState.baseBranch}; do not create a merge commit.`,
|
|
9057
|
+
approvalRequired,
|
|
9058
|
+
command
|
|
9059
|
+
),
|
|
9060
|
+
approvalRequired,
|
|
9061
|
+
implementationAllowed: false,
|
|
9062
|
+
primaryActionLabel: actionOptions ? "A" : void 0,
|
|
9063
|
+
actionOptions,
|
|
9064
|
+
blockedReasonCode: "LOCAL_MERGE_REQUIRED"
|
|
9065
|
+
};
|
|
9066
|
+
}
|
|
9067
|
+
if (!localState.state || !["verified", "cleaned"].includes(localState.state.status) || localState.state.featureTip !== localState.featureTip || localState.state.mergedBaseTip !== localState.baseTip) {
|
|
9068
|
+
return {
|
|
9069
|
+
status: "ok",
|
|
9070
|
+
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
9071
|
+
docsDir: config.docsDir,
|
|
9072
|
+
featureRef: buildFeatureRef(feature),
|
|
9073
|
+
stage: "local_verify",
|
|
9074
|
+
nextAction: buildAction(
|
|
9075
|
+
"local_verify",
|
|
9076
|
+
"Run the configured post-merge checks and record verification against the integrated Feature tip.",
|
|
9077
|
+
false,
|
|
9078
|
+
localMergeBaseCommand
|
|
9079
|
+
),
|
|
9080
|
+
approvalRequired: false,
|
|
9081
|
+
implementationAllowed: false,
|
|
9082
|
+
blockedReasonCode: "LOCAL_VERIFICATION_REQUIRED"
|
|
9083
|
+
};
|
|
9084
|
+
}
|
|
9085
|
+
if (!localCleanupComplete(localState)) {
|
|
9086
|
+
return {
|
|
9087
|
+
status: "ok",
|
|
9088
|
+
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
9089
|
+
docsDir: config.docsDir,
|
|
9090
|
+
featureRef: buildFeatureRef(feature),
|
|
9091
|
+
stage: "local_cleanup",
|
|
9092
|
+
nextAction: buildAction(
|
|
9093
|
+
"local_cleanup",
|
|
9094
|
+
"Remove the clean managed worktree and delete the local Feature branch according to workflow policy.",
|
|
9095
|
+
false,
|
|
9096
|
+
`npx lee-spec-kit local cleanup ${buildFeatureArgs(feature)} --json`
|
|
9097
|
+
),
|
|
9098
|
+
approvalRequired: false,
|
|
9099
|
+
implementationAllowed: false,
|
|
9100
|
+
blockedReasonCode: "LOCAL_CLEANUP_REQUIRED"
|
|
9101
|
+
};
|
|
9102
|
+
}
|
|
9103
|
+
return {
|
|
9104
|
+
status: "ok",
|
|
9105
|
+
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
9106
|
+
docsDir: config.docsDir,
|
|
9107
|
+
featureRef: buildFeatureRef(feature),
|
|
9108
|
+
stage: "done",
|
|
9109
|
+
nextAction: null,
|
|
9110
|
+
approvalRequired: false,
|
|
9111
|
+
implementationAllowed: false,
|
|
9112
|
+
blockedReasonCode: null
|
|
9113
|
+
};
|
|
9114
|
+
}
|
|
8637
9115
|
if (requirements.requirePr) {
|
|
8638
9116
|
const prReady = prDraft.status === "ready";
|
|
8639
9117
|
const prCreated = !!tasks.prLink && prExistsRemotely(tasks.prLink, feature);
|
|
@@ -9694,13 +10172,13 @@ function collectManagedWorktreeRepoRoots(config, projectRoot) {
|
|
|
9694
10172
|
const line = rawLine.trim();
|
|
9695
10173
|
if (!line.startsWith("worktree ")) continue;
|
|
9696
10174
|
const worktreePath = path8.resolve(line.slice("worktree ".length).trim());
|
|
9697
|
-
if (
|
|
10175
|
+
if (isSameOrWithin3(path8.resolve(managedRoot), worktreePath)) {
|
|
9698
10176
|
roots.add(worktreePath);
|
|
9699
10177
|
}
|
|
9700
10178
|
}
|
|
9701
10179
|
return [...roots];
|
|
9702
10180
|
}
|
|
9703
|
-
function
|
|
10181
|
+
function isSameOrWithin3(parentDir, candidateDir) {
|
|
9704
10182
|
const resolvedParent = path8.resolve(parentDir);
|
|
9705
10183
|
const resolvedCandidate = path8.resolve(candidateDir);
|
|
9706
10184
|
return resolvedParent === resolvedCandidate || resolvedCandidate.startsWith(`${resolvedParent}${path8.sep}`);
|
|
@@ -9721,10 +10199,10 @@ function parseStagedPaths(output) {
|
|
|
9721
10199
|
staged.set(`path:${normalizeSlashes3(parts[1])}`, `${status}:path`);
|
|
9722
10200
|
}
|
|
9723
10201
|
return [...staged.entries()].map(([encodedPath, encodedStatus]) => {
|
|
9724
|
-
const [role,
|
|
10202
|
+
const [role, path30] = encodedPath.split(":", 2);
|
|
9725
10203
|
const [status, entryRole] = encodedStatus.split(":", 2);
|
|
9726
10204
|
return {
|
|
9727
|
-
path:
|
|
10205
|
+
path: path30,
|
|
9728
10206
|
status,
|
|
9729
10207
|
role: entryRole || role || "path"
|
|
9730
10208
|
};
|
|
@@ -9834,6 +10312,610 @@ function toAllowedSet(values, extras) {
|
|
|
9834
10312
|
function normalizeSlashes3(value) {
|
|
9835
10313
|
return value.replace(/\\/g, "/");
|
|
9836
10314
|
}
|
|
10315
|
+
var DESIGN_DOC_KINDS = [
|
|
10316
|
+
"ux-design",
|
|
10317
|
+
"design-system",
|
|
10318
|
+
"visual-reference"
|
|
10319
|
+
];
|
|
10320
|
+
var ALLOWED_DESIGN_KINDS = new Set(DESIGN_DOC_KINDS);
|
|
10321
|
+
var FILENAME_RULES = [
|
|
10322
|
+
{
|
|
10323
|
+
pattern: /(?:^|-)(?:system|backend|frontend)-architecture(?:-|$)|(?:^|-)architecture-overview(?:-|$)/i,
|
|
10324
|
+
classification: {
|
|
10325
|
+
kind: "architecture-overview",
|
|
10326
|
+
suggestedLocations: ["docs/prd/*-overview.md"]
|
|
10327
|
+
}
|
|
10328
|
+
},
|
|
10329
|
+
{
|
|
10330
|
+
pattern: /(?:^|-)(?:data-model|api-design|database-schema|implementation-plan|implementation-roadmap)(?:-|$)/i,
|
|
10331
|
+
classification: {
|
|
10332
|
+
kind: "feature-plan",
|
|
10333
|
+
suggestedLocations: [
|
|
10334
|
+
"docs/ideas/I###-*.md (before Feature promotion)",
|
|
10335
|
+
"the active Feature plan.md"
|
|
10336
|
+
]
|
|
10337
|
+
}
|
|
10338
|
+
},
|
|
10339
|
+
{
|
|
10340
|
+
pattern: /(?:^|-)(?:open-source-research|candidate-comparison|technology-comparison)(?:-|$)/i,
|
|
10341
|
+
classification: {
|
|
10342
|
+
kind: "idea",
|
|
10343
|
+
suggestedLocations: [
|
|
10344
|
+
"docs/ideas/I###-*.md (before Feature promotion)",
|
|
10345
|
+
"the active Feature decisions.md"
|
|
10346
|
+
]
|
|
10347
|
+
}
|
|
10348
|
+
},
|
|
10349
|
+
{
|
|
10350
|
+
pattern: /(?:^|-)(?:technical-decision|architecture-decision|tradeoff|adr)(?:-|$)/i,
|
|
10351
|
+
classification: {
|
|
10352
|
+
kind: "feature-decisions",
|
|
10353
|
+
suggestedLocations: [
|
|
10354
|
+
"docs/ideas/I###-*.md (before Feature promotion)",
|
|
10355
|
+
"the active Feature decisions.md"
|
|
10356
|
+
]
|
|
10357
|
+
}
|
|
10358
|
+
}
|
|
10359
|
+
];
|
|
10360
|
+
async function collectDocsTaxonomyViolations(docsDir) {
|
|
10361
|
+
const designsDir = path8.join(docsDir, "designs");
|
|
10362
|
+
if (!await fs.pathExists(designsDir)) return [];
|
|
10363
|
+
const markdownFiles = await collectMarkdownFiles(designsDir);
|
|
10364
|
+
const violations = [];
|
|
10365
|
+
for (const filePath of markdownFiles) {
|
|
10366
|
+
if (path8.basename(filePath).toLowerCase() === "readme.md") continue;
|
|
10367
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
10368
|
+
const displayPath = `docs/${normalizeSlashes4(path8.relative(docsDir, filePath))}`;
|
|
10369
|
+
const frontmatter = parseLeeSpecKitFrontmatter(content);
|
|
10370
|
+
if (frontmatter.present && !frontmatter.valid) {
|
|
10371
|
+
violations.push(
|
|
10372
|
+
createViolation(
|
|
10373
|
+
displayPath,
|
|
10374
|
+
"INVALID_DOC_FRONTMATTER",
|
|
10375
|
+
frontmatter.kind,
|
|
10376
|
+
null,
|
|
10377
|
+
"frontmatter",
|
|
10378
|
+
"lee-spec-kit frontmatter must contain a well-formed kind entry.",
|
|
10379
|
+
[]
|
|
10380
|
+
)
|
|
10381
|
+
);
|
|
10382
|
+
continue;
|
|
10383
|
+
}
|
|
10384
|
+
if (frontmatter.kind) {
|
|
10385
|
+
if (!isKnownDocKind(frontmatter.kind)) {
|
|
10386
|
+
violations.push(
|
|
10387
|
+
createViolation(
|
|
10388
|
+
displayPath,
|
|
10389
|
+
"INVALID_DOC_KIND",
|
|
10390
|
+
frontmatter.kind,
|
|
10391
|
+
null,
|
|
10392
|
+
"frontmatter",
|
|
10393
|
+
`Unknown lee-spec-kit document kind: ${frontmatter.kind}`,
|
|
10394
|
+
[]
|
|
10395
|
+
)
|
|
10396
|
+
);
|
|
10397
|
+
continue;
|
|
10398
|
+
}
|
|
10399
|
+
if (!ALLOWED_DESIGN_KINDS.has(frontmatter.kind)) {
|
|
10400
|
+
violations.push(
|
|
10401
|
+
createViolation(
|
|
10402
|
+
displayPath,
|
|
10403
|
+
"MISPLACED_MANAGED_DOC",
|
|
10404
|
+
frontmatter.kind,
|
|
10405
|
+
frontmatter.kind,
|
|
10406
|
+
"frontmatter",
|
|
10407
|
+
`Document kind "${frontmatter.kind}" does not belong in docs/designs/.`,
|
|
10408
|
+
suggestedLocationsForKind(frontmatter.kind)
|
|
10409
|
+
)
|
|
10410
|
+
);
|
|
10411
|
+
continue;
|
|
10412
|
+
}
|
|
10413
|
+
if (frontmatter.scope && frontmatter.scope !== "project") {
|
|
10414
|
+
violations.push(
|
|
10415
|
+
createViolation(
|
|
10416
|
+
displayPath,
|
|
10417
|
+
"INVALID_DOC_SCOPE",
|
|
10418
|
+
frontmatter.kind,
|
|
10419
|
+
frontmatter.kind,
|
|
10420
|
+
"frontmatter",
|
|
10421
|
+
"Documents in docs/designs/ must use lee-spec-kit scope: project.",
|
|
10422
|
+
[]
|
|
10423
|
+
)
|
|
10424
|
+
);
|
|
10425
|
+
}
|
|
10426
|
+
continue;
|
|
10427
|
+
}
|
|
10428
|
+
const inferred = classifyFilename(
|
|
10429
|
+
path8.basename(filePath, path8.extname(filePath))
|
|
10430
|
+
);
|
|
10431
|
+
if (inferred) {
|
|
10432
|
+
violations.push(
|
|
10433
|
+
createViolation(
|
|
10434
|
+
displayPath,
|
|
10435
|
+
"MISPLACED_MANAGED_DOC",
|
|
10436
|
+
null,
|
|
10437
|
+
inferred.kind,
|
|
10438
|
+
"filename",
|
|
10439
|
+
`The filename strongly suggests a ${inferred.kind} document, which does not belong in docs/designs/.`,
|
|
10440
|
+
inferred.suggestedLocations
|
|
10441
|
+
)
|
|
10442
|
+
);
|
|
10443
|
+
continue;
|
|
10444
|
+
}
|
|
10445
|
+
violations.push(
|
|
10446
|
+
createViolation(
|
|
10447
|
+
displayPath,
|
|
10448
|
+
"DOC_KIND_MISSING",
|
|
10449
|
+
null,
|
|
10450
|
+
null,
|
|
10451
|
+
null,
|
|
10452
|
+
"Add lee-spec-kit frontmatter with kind: ux-design, design-system, or visual-reference.",
|
|
10453
|
+
[]
|
|
10454
|
+
)
|
|
10455
|
+
);
|
|
10456
|
+
}
|
|
10457
|
+
return violations.sort((a, b) => a.path.localeCompare(b.path));
|
|
10458
|
+
}
|
|
10459
|
+
function createViolation(displayPath, violationCode, declaredKind, detectedKind, detectedBy, message, suggestedLocations) {
|
|
10460
|
+
return {
|
|
10461
|
+
path: displayPath,
|
|
10462
|
+
violationCode,
|
|
10463
|
+
declaredKind,
|
|
10464
|
+
detectedKind,
|
|
10465
|
+
detectedBy,
|
|
10466
|
+
confidence: detectedBy ? "high" : null,
|
|
10467
|
+
message,
|
|
10468
|
+
suggestedLocations
|
|
10469
|
+
};
|
|
10470
|
+
}
|
|
10471
|
+
async function collectMarkdownFiles(rootDir) {
|
|
10472
|
+
const files = [];
|
|
10473
|
+
const entries = await fs.readdir(rootDir, { withFileTypes: true });
|
|
10474
|
+
for (const entry of entries) {
|
|
10475
|
+
const absolutePath = path8.join(rootDir, entry.name);
|
|
10476
|
+
if (entry.isDirectory()) {
|
|
10477
|
+
if (entry.name.toLowerCase() === "assets") continue;
|
|
10478
|
+
files.push(...await collectMarkdownFiles(absolutePath));
|
|
10479
|
+
continue;
|
|
10480
|
+
}
|
|
10481
|
+
if (entry.isFile() && path8.extname(entry.name).toLowerCase() === ".md") {
|
|
10482
|
+
files.push(absolutePath);
|
|
10483
|
+
}
|
|
10484
|
+
}
|
|
10485
|
+
return files;
|
|
10486
|
+
}
|
|
10487
|
+
function parseLeeSpecKitFrontmatter(content) {
|
|
10488
|
+
const normalized = content.replace(/^\uFEFF/, "");
|
|
10489
|
+
if (!normalized.startsWith("---\n") && !normalized.startsWith("---\r\n")) {
|
|
10490
|
+
return { present: false, valid: true, kind: null, scope: null };
|
|
10491
|
+
}
|
|
10492
|
+
const lines = normalized.split(/\r?\n/);
|
|
10493
|
+
const closingIndex = lines.findIndex(
|
|
10494
|
+
(line, index) => index > 0 && line.trim() === "---"
|
|
10495
|
+
);
|
|
10496
|
+
if (closingIndex < 0) {
|
|
10497
|
+
return { present: true, valid: false, kind: null, scope: null };
|
|
10498
|
+
}
|
|
10499
|
+
const frontmatterLines = lines.slice(1, closingIndex);
|
|
10500
|
+
const rootIndex = frontmatterLines.findIndex(
|
|
10501
|
+
(line) => /^lee-spec-kit:\s*$/.test(line.trim())
|
|
10502
|
+
);
|
|
10503
|
+
if (rootIndex < 0) {
|
|
10504
|
+
return { present: true, valid: false, kind: null, scope: null };
|
|
10505
|
+
}
|
|
10506
|
+
let kind = null;
|
|
10507
|
+
let scope = null;
|
|
10508
|
+
for (const line of frontmatterLines.slice(rootIndex + 1)) {
|
|
10509
|
+
if (/^\S/.test(line)) break;
|
|
10510
|
+
const match = line.match(/^\s+(kind|scope):\s*(.*?)\s*$/);
|
|
10511
|
+
if (!match) continue;
|
|
10512
|
+
const value = stripYamlScalarQuotes(match[2]);
|
|
10513
|
+
if (match[1] === "kind") kind = value || null;
|
|
10514
|
+
if (match[1] === "scope") scope = value || null;
|
|
10515
|
+
}
|
|
10516
|
+
return { present: true, valid: !!kind, kind, scope };
|
|
10517
|
+
}
|
|
10518
|
+
function stripYamlScalarQuotes(value) {
|
|
10519
|
+
const trimmed = value.trim();
|
|
10520
|
+
if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
10521
|
+
return trimmed.slice(1, -1).trim();
|
|
10522
|
+
}
|
|
10523
|
+
return trimmed;
|
|
10524
|
+
}
|
|
10525
|
+
function classifyFilename(stem) {
|
|
10526
|
+
for (const rule of FILENAME_RULES) {
|
|
10527
|
+
if (rule.pattern.test(stem)) return rule.classification;
|
|
10528
|
+
}
|
|
10529
|
+
return null;
|
|
10530
|
+
}
|
|
10531
|
+
function isKnownDocKind(kind) {
|
|
10532
|
+
return (/* @__PURE__ */ new Set([
|
|
10533
|
+
...DESIGN_DOC_KINDS,
|
|
10534
|
+
"product-requirements",
|
|
10535
|
+
"architecture-overview",
|
|
10536
|
+
"idea",
|
|
10537
|
+
"feature-spec",
|
|
10538
|
+
"feature-plan",
|
|
10539
|
+
"feature-decisions"
|
|
10540
|
+
])).has(kind);
|
|
10541
|
+
}
|
|
10542
|
+
function suggestedLocationsForKind(kind) {
|
|
10543
|
+
if (kind === "architecture-overview") return ["docs/prd/*-overview.md"];
|
|
10544
|
+
if (kind === "idea") return ["docs/ideas/I###-*.md"];
|
|
10545
|
+
if (kind === "product-requirements") return ["docs/prd/"];
|
|
10546
|
+
if (kind === "feature-spec") return ["the active Feature spec.md"];
|
|
10547
|
+
if (kind === "feature-plan") return ["the active Feature plan.md"];
|
|
10548
|
+
if (kind === "feature-decisions") return ["the active Feature decisions.md"];
|
|
10549
|
+
return [];
|
|
10550
|
+
}
|
|
10551
|
+
function normalizeSlashes4(value) {
|
|
10552
|
+
return value.replace(/\\/g, "/");
|
|
10553
|
+
}
|
|
10554
|
+
|
|
10555
|
+
// src/commands/docs-audit.ts
|
|
10556
|
+
function docsAuditCommand(program2) {
|
|
10557
|
+
program2.command("docs-audit").description(
|
|
10558
|
+
"Audit whether managed documents are stored in the right location"
|
|
10559
|
+
).option("--json", "Output JSON for agents and automation").action(async (options) => {
|
|
10560
|
+
try {
|
|
10561
|
+
const payload = await collectDocsAudit(process.cwd());
|
|
10562
|
+
if (options.json) {
|
|
10563
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
10564
|
+
return;
|
|
10565
|
+
}
|
|
10566
|
+
console.log(`${payload.status}: ${payload.reasonCode}`);
|
|
10567
|
+
for (const violation of payload.violations) {
|
|
10568
|
+
console.log(`- ${violation.path}: ${violation.message}`);
|
|
10569
|
+
}
|
|
10570
|
+
} catch (error) {
|
|
10571
|
+
const cliError = toCliError(error);
|
|
10572
|
+
const payload = {
|
|
10573
|
+
status: "error",
|
|
10574
|
+
reasonCode: cliError.code === "CONFIG_NOT_FOUND" ? "CONFIG_NOT_FOUND" : "UNEXPECTED_ERROR",
|
|
10575
|
+
mode: "warn",
|
|
10576
|
+
docsDir: null,
|
|
10577
|
+
violations: []
|
|
10578
|
+
};
|
|
10579
|
+
if (options.json) {
|
|
10580
|
+
console.log(
|
|
10581
|
+
JSON.stringify({ ...payload, error: cliError.message }, null, 2)
|
|
10582
|
+
);
|
|
10583
|
+
} else {
|
|
10584
|
+
process.stderr.write(`[${cliError.code}] ${cliError.message}
|
|
10585
|
+
`);
|
|
10586
|
+
}
|
|
10587
|
+
process.exitCode = 1;
|
|
10588
|
+
}
|
|
10589
|
+
});
|
|
10590
|
+
}
|
|
10591
|
+
async function collectDocsAudit(cwd) {
|
|
10592
|
+
const config = await getConfig(cwd);
|
|
10593
|
+
if (!config) {
|
|
10594
|
+
throw createCliError(
|
|
10595
|
+
"CONFIG_NOT_FOUND",
|
|
10596
|
+
"Config file not found. Run `init` first."
|
|
10597
|
+
);
|
|
10598
|
+
}
|
|
10599
|
+
const violations = await collectDocsTaxonomyViolations(config.docsDir);
|
|
10600
|
+
return {
|
|
10601
|
+
status: violations.length > 0 ? "warning" : "ok",
|
|
10602
|
+
reasonCode: violations.length > 0 ? "DOCS_TAXONOMY_WARNING" : "DOCS_TAXONOMY_OK",
|
|
10603
|
+
mode: "warn",
|
|
10604
|
+
docsDir: config.docsDir,
|
|
10605
|
+
violations
|
|
10606
|
+
};
|
|
10607
|
+
}
|
|
10608
|
+
|
|
10609
|
+
// src/commands/local.ts
|
|
10610
|
+
function localCommand(program2) {
|
|
10611
|
+
const local = program2.command("local").description("Integrate and clean up local workflow feature branches");
|
|
10612
|
+
local.command("merge [feature-name]").description("Fast-forward a completed local feature and verify the result").option("--component <component>", "Component name for multi projects").option(
|
|
10613
|
+
"--confirm <token>",
|
|
10614
|
+
"Approval token when local_merge requires approval"
|
|
10615
|
+
).option("--json", "Output JSON for agents and hooks").action(
|
|
10616
|
+
async (featureName, options) => {
|
|
10617
|
+
await runLocalAction(
|
|
10618
|
+
options,
|
|
10619
|
+
() => runLocalMerge(featureName, options)
|
|
10620
|
+
);
|
|
10621
|
+
}
|
|
10622
|
+
);
|
|
10623
|
+
local.command("cleanup [feature-name]").description("Remove the verified local feature worktree and branch").option("--component <component>", "Component name for multi projects").option("--json", "Output JSON for agents and hooks").action(
|
|
10624
|
+
async (featureName, options) => {
|
|
10625
|
+
await runLocalAction(
|
|
10626
|
+
options,
|
|
10627
|
+
() => runLocalCleanup(featureName, options)
|
|
10628
|
+
);
|
|
10629
|
+
}
|
|
10630
|
+
);
|
|
10631
|
+
}
|
|
10632
|
+
async function runLocalAction(options, action) {
|
|
10633
|
+
try {
|
|
10634
|
+
const payload = await action();
|
|
10635
|
+
if (options.json) {
|
|
10636
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
10637
|
+
} else {
|
|
10638
|
+
console.log(`${payload.status}: ${payload.reasonCode}`);
|
|
10639
|
+
}
|
|
10640
|
+
if (payload.status !== "ok") process.exitCode = 1;
|
|
10641
|
+
} catch (error) {
|
|
10642
|
+
const cliError = toCliError(error);
|
|
10643
|
+
const payload = {
|
|
10644
|
+
status: "error",
|
|
10645
|
+
reasonCode: cliError.code,
|
|
10646
|
+
featureRef: null,
|
|
10647
|
+
error: cliError.message
|
|
10648
|
+
};
|
|
10649
|
+
if (options.json) {
|
|
10650
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
10651
|
+
} else {
|
|
10652
|
+
process.stderr.write(`[${cliError.code}] ${cliError.message}
|
|
10653
|
+
`);
|
|
10654
|
+
}
|
|
10655
|
+
process.exitCode = 1;
|
|
10656
|
+
}
|
|
10657
|
+
}
|
|
10658
|
+
async function runLocalMerge(featureName, options) {
|
|
10659
|
+
const cwd = process.cwd();
|
|
10660
|
+
const stage = await collectWorkflowStage(cwd, featureName, options.component);
|
|
10661
|
+
if (stage.status !== "ok" || !stage.nextAction || !["local_merge", "local_verify"].includes(stage.nextAction.category)) {
|
|
10662
|
+
return blocked(
|
|
10663
|
+
"LOCAL_MERGE_STAGE_REQUIRED",
|
|
10664
|
+
stage.featureRef,
|
|
10665
|
+
"The active workflow is not at local_merge or local_verify."
|
|
10666
|
+
);
|
|
10667
|
+
}
|
|
10668
|
+
if (stage.approvalRequired && options.confirm !== "OK") {
|
|
10669
|
+
return blocked(
|
|
10670
|
+
"APPROVAL_REQUIRED",
|
|
10671
|
+
stage.featureRef,
|
|
10672
|
+
"Re-run with --confirm OK after the configured local_merge approval."
|
|
10673
|
+
);
|
|
10674
|
+
}
|
|
10675
|
+
const selection = await resolveFeatureSelection(
|
|
10676
|
+
cwd,
|
|
10677
|
+
featureName,
|
|
10678
|
+
options.component
|
|
10679
|
+
);
|
|
10680
|
+
if (selection.status !== "selected" || !selection.matchedFeature) {
|
|
10681
|
+
return blocked(
|
|
10682
|
+
"FEATURE_SELECTION_REQUIRED",
|
|
10683
|
+
null,
|
|
10684
|
+
"Select exactly one Feature."
|
|
10685
|
+
);
|
|
10686
|
+
}
|
|
10687
|
+
const config = await getConfig(cwd);
|
|
10688
|
+
if (!config || config.workflow?.mode !== "local") {
|
|
10689
|
+
return blocked(
|
|
10690
|
+
"LOCAL_WORKFLOW_REQUIRED",
|
|
10691
|
+
selection.matchedFeature.folderName
|
|
10692
|
+
);
|
|
10693
|
+
}
|
|
10694
|
+
const feature = selection.matchedFeature;
|
|
10695
|
+
let context = await resolveLocalIntegrationContext(config, feature);
|
|
10696
|
+
if (!context.featureTip || !context.baseTip) {
|
|
10697
|
+
return blocked(
|
|
10698
|
+
"LOCAL_MERGE_REF_NOT_FOUND",
|
|
10699
|
+
feature.folderName,
|
|
10700
|
+
"Both the configured base branch and Feature tip must exist."
|
|
10701
|
+
);
|
|
10702
|
+
}
|
|
10703
|
+
if (!context.featureWorktreeClean || !context.projectRootClean || !context.docsClean) {
|
|
10704
|
+
return blocked(
|
|
10705
|
+
"LOCAL_MERGE_DIRTY_WORKTREE",
|
|
10706
|
+
feature.folderName,
|
|
10707
|
+
"Feature worktree, base worktree, and docs repo must be clean."
|
|
10708
|
+
);
|
|
10709
|
+
}
|
|
10710
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10711
|
+
let state = context.state || {
|
|
10712
|
+
version: 1,
|
|
10713
|
+
featureRef: feature.folderName,
|
|
10714
|
+
component: feature.type,
|
|
10715
|
+
baseBranch: context.baseBranch,
|
|
10716
|
+
featureBranch: context.featureBranch,
|
|
10717
|
+
featureTip: context.featureTip,
|
|
10718
|
+
mergedBaseTip: context.baseTip,
|
|
10719
|
+
status: "merged",
|
|
10720
|
+
mergedAt: now,
|
|
10721
|
+
verifiedAt: null,
|
|
10722
|
+
cleanedAt: null,
|
|
10723
|
+
verification: []
|
|
10724
|
+
};
|
|
10725
|
+
if (!context.baseContainsFeature) {
|
|
10726
|
+
if (!isAncestor(
|
|
10727
|
+
context.projectRoot,
|
|
10728
|
+
`refs/heads/${context.baseBranch}`,
|
|
10729
|
+
context.featureTip
|
|
10730
|
+
)) {
|
|
10731
|
+
return blocked(
|
|
10732
|
+
"LOCAL_MERGE_NOT_FAST_FORWARD",
|
|
10733
|
+
feature.folderName,
|
|
10734
|
+
`${context.baseBranch} is not an ancestor of ${context.featureBranch}.`
|
|
10735
|
+
);
|
|
10736
|
+
}
|
|
10737
|
+
if (currentGitBranch(context.projectRoot) !== context.baseBranch) {
|
|
10738
|
+
const checkout = gitRun(context.projectRoot, [
|
|
10739
|
+
"checkout",
|
|
10740
|
+
context.baseBranch
|
|
10741
|
+
]);
|
|
10742
|
+
if (checkout.code !== 0) {
|
|
10743
|
+
return blocked(
|
|
10744
|
+
"LOCAL_BASE_CHECKOUT_FAILED",
|
|
10745
|
+
feature.folderName,
|
|
10746
|
+
checkout.stderr
|
|
10747
|
+
);
|
|
10748
|
+
}
|
|
10749
|
+
}
|
|
10750
|
+
const merge = gitRun(context.projectRoot, [
|
|
10751
|
+
"merge",
|
|
10752
|
+
"--ff-only",
|
|
10753
|
+
context.featureBranch
|
|
10754
|
+
]);
|
|
10755
|
+
if (merge.code !== 0) {
|
|
10756
|
+
return blocked(
|
|
10757
|
+
"LOCAL_MERGE_NOT_FAST_FORWARD",
|
|
10758
|
+
feature.folderName,
|
|
10759
|
+
merge.stderr
|
|
10760
|
+
);
|
|
10761
|
+
}
|
|
10762
|
+
const mergedBaseTip = resolveRef(
|
|
10763
|
+
context.projectRoot,
|
|
10764
|
+
`refs/heads/${context.baseBranch}`
|
|
10765
|
+
);
|
|
10766
|
+
state = {
|
|
10767
|
+
...state,
|
|
10768
|
+
featureTip: context.featureTip,
|
|
10769
|
+
mergedBaseTip: mergedBaseTip || context.featureTip,
|
|
10770
|
+
status: "merged",
|
|
10771
|
+
mergedAt: now,
|
|
10772
|
+
verifiedAt: null,
|
|
10773
|
+
cleanedAt: null,
|
|
10774
|
+
verification: []
|
|
10775
|
+
};
|
|
10776
|
+
await writeLocalIntegrationState(context.projectRoot, feature, state);
|
|
10777
|
+
context = await resolveLocalIntegrationContext(config, feature);
|
|
10778
|
+
}
|
|
10779
|
+
if (!context.baseContainsFeature) {
|
|
10780
|
+
return blocked("LOCAL_MERGE_VERIFICATION_FAILED", feature.folderName);
|
|
10781
|
+
}
|
|
10782
|
+
const verification = runPostMergeChecks(
|
|
10783
|
+
context.projectRoot,
|
|
10784
|
+
context.postMergeChecks
|
|
10785
|
+
);
|
|
10786
|
+
const failedCheck = verification.find((entry) => entry.exitCode !== 0);
|
|
10787
|
+
if (failedCheck) {
|
|
10788
|
+
await writeLocalIntegrationState(context.projectRoot, feature, {
|
|
10789
|
+
...state,
|
|
10790
|
+
status: "merged",
|
|
10791
|
+
verification,
|
|
10792
|
+
verifiedAt: null
|
|
10793
|
+
});
|
|
10794
|
+
return {
|
|
10795
|
+
...blocked("LOCAL_POST_MERGE_CHECK_FAILED", feature.folderName),
|
|
10796
|
+
baseBranch: context.baseBranch,
|
|
10797
|
+
featureBranch: context.featureBranch,
|
|
10798
|
+
featureTip: context.featureTip,
|
|
10799
|
+
baseTip: resolveRef(
|
|
10800
|
+
context.projectRoot,
|
|
10801
|
+
`refs/heads/${context.baseBranch}`
|
|
10802
|
+
),
|
|
10803
|
+
verification
|
|
10804
|
+
};
|
|
10805
|
+
}
|
|
10806
|
+
const baseTip = resolveRef(
|
|
10807
|
+
context.projectRoot,
|
|
10808
|
+
`refs/heads/${context.baseBranch}`
|
|
10809
|
+
);
|
|
10810
|
+
const verifiedFeatureTip = context.featureTip || state.featureTip;
|
|
10811
|
+
const verifiedState = {
|
|
10812
|
+
...state,
|
|
10813
|
+
featureTip: verifiedFeatureTip,
|
|
10814
|
+
mergedBaseTip: baseTip || verifiedFeatureTip,
|
|
10815
|
+
status: "verified",
|
|
10816
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10817
|
+
verification
|
|
10818
|
+
};
|
|
10819
|
+
await writeLocalIntegrationState(context.projectRoot, feature, verifiedState);
|
|
10820
|
+
return {
|
|
10821
|
+
status: "ok",
|
|
10822
|
+
reasonCode: "LOCAL_MERGE_VERIFIED",
|
|
10823
|
+
featureRef: feature.folderName,
|
|
10824
|
+
baseBranch: context.baseBranch,
|
|
10825
|
+
featureBranch: context.featureBranch,
|
|
10826
|
+
featureTip: context.featureTip,
|
|
10827
|
+
baseTip,
|
|
10828
|
+
verification
|
|
10829
|
+
};
|
|
10830
|
+
}
|
|
10831
|
+
async function runLocalCleanup(featureName, options) {
|
|
10832
|
+
const cwd = process.cwd();
|
|
10833
|
+
const stage = await collectWorkflowStage(cwd, featureName, options.component);
|
|
10834
|
+
if (stage.status !== "ok" || stage.nextAction?.category !== "local_cleanup") {
|
|
10835
|
+
return blocked(
|
|
10836
|
+
"LOCAL_CLEANUP_STAGE_REQUIRED",
|
|
10837
|
+
stage.featureRef,
|
|
10838
|
+
"The active workflow is not at local_cleanup."
|
|
10839
|
+
);
|
|
10840
|
+
}
|
|
10841
|
+
const selection = await resolveFeatureSelection(
|
|
10842
|
+
cwd,
|
|
10843
|
+
featureName,
|
|
10844
|
+
options.component
|
|
10845
|
+
);
|
|
10846
|
+
if (selection.status !== "selected" || !selection.matchedFeature) {
|
|
10847
|
+
return blocked("FEATURE_SELECTION_REQUIRED", null);
|
|
10848
|
+
}
|
|
10849
|
+
const config = await getConfig(cwd);
|
|
10850
|
+
if (!config || config.workflow?.mode !== "local") {
|
|
10851
|
+
return blocked(
|
|
10852
|
+
"LOCAL_WORKFLOW_REQUIRED",
|
|
10853
|
+
selection.matchedFeature.folderName
|
|
10854
|
+
);
|
|
10855
|
+
}
|
|
10856
|
+
const feature = selection.matchedFeature;
|
|
10857
|
+
let context = await resolveLocalIntegrationContext(config, feature);
|
|
10858
|
+
if (!context.baseContainsFeature || !context.state || !["verified", "cleaned"].includes(context.state.status) || context.state.featureTip !== context.featureTip || context.state.mergedBaseTip !== context.baseTip) {
|
|
10859
|
+
return blocked("LOCAL_MERGE_NOT_VERIFIED", feature.folderName);
|
|
10860
|
+
}
|
|
10861
|
+
if (!context.featureWorktreeClean || !context.projectRootClean || !context.docsClean) {
|
|
10862
|
+
return blocked("LOCAL_CLEANUP_DIRTY_WORKTREE", feature.folderName);
|
|
10863
|
+
}
|
|
10864
|
+
if (currentGitBranch(context.projectRoot) !== context.baseBranch) {
|
|
10865
|
+
return blocked("LOCAL_BASE_NOT_CHECKED_OUT", feature.folderName);
|
|
10866
|
+
}
|
|
10867
|
+
if (context.managedFeatureWorktree) {
|
|
10868
|
+
const remove = gitRun(context.projectRoot, [
|
|
10869
|
+
"worktree",
|
|
10870
|
+
"remove",
|
|
10871
|
+
context.featureWorktree
|
|
10872
|
+
]);
|
|
10873
|
+
if (remove.code !== 0) {
|
|
10874
|
+
return blocked(
|
|
10875
|
+
"LOCAL_WORKTREE_REMOVE_FAILED",
|
|
10876
|
+
feature.folderName,
|
|
10877
|
+
remove.stderr
|
|
10878
|
+
);
|
|
10879
|
+
}
|
|
10880
|
+
}
|
|
10881
|
+
if (context.deleteFeatureBranchAfterMerge && context.featureBranchExists) {
|
|
10882
|
+
const removeBranch = gitRun(context.projectRoot, [
|
|
10883
|
+
"branch",
|
|
10884
|
+
"-d",
|
|
10885
|
+
context.featureBranch
|
|
10886
|
+
]);
|
|
10887
|
+
if (removeBranch.code !== 0) {
|
|
10888
|
+
return blocked(
|
|
10889
|
+
"LOCAL_BRANCH_DELETE_FAILED",
|
|
10890
|
+
feature.folderName,
|
|
10891
|
+
removeBranch.stderr
|
|
10892
|
+
);
|
|
10893
|
+
}
|
|
10894
|
+
}
|
|
10895
|
+
await writeLocalIntegrationState(context.projectRoot, feature, {
|
|
10896
|
+
...context.state,
|
|
10897
|
+
status: "cleaned",
|
|
10898
|
+
cleanedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10899
|
+
});
|
|
10900
|
+
context = await resolveLocalIntegrationContext(config, feature);
|
|
10901
|
+
return {
|
|
10902
|
+
status: "ok",
|
|
10903
|
+
reasonCode: "LOCAL_CLEANUP_COMPLETE",
|
|
10904
|
+
featureRef: feature.folderName,
|
|
10905
|
+
baseBranch: context.baseBranch,
|
|
10906
|
+
featureBranch: context.featureBranch,
|
|
10907
|
+
featureTip: context.featureTip,
|
|
10908
|
+
baseTip: context.baseTip
|
|
10909
|
+
};
|
|
10910
|
+
}
|
|
10911
|
+
function blocked(reasonCode, featureRef, error) {
|
|
10912
|
+
return {
|
|
10913
|
+
status: "blocked",
|
|
10914
|
+
reasonCode,
|
|
10915
|
+
featureRef,
|
|
10916
|
+
...error ? { error } : {}
|
|
10917
|
+
};
|
|
10918
|
+
}
|
|
9837
10919
|
function isBannerDisabled() {
|
|
9838
10920
|
const v = (process.env.LEE_SPEC_KIT_NO_BANNER || "").trim();
|
|
9839
10921
|
return v === "1";
|
|
@@ -10005,8 +11087,10 @@ function configureRootCommandSurface() {
|
|
|
10005
11087
|
["detect", "Workflow Policy Commands:"],
|
|
10006
11088
|
["github", "Workflow Policy Commands:"],
|
|
10007
11089
|
["workflow-stage", "Workflow Policy Commands:"],
|
|
11090
|
+
["local", "Workflow Policy Commands:"],
|
|
10008
11091
|
["integrations", "Codex Integration Commands:"],
|
|
10009
11092
|
["commit-audit", "Codex Integration Commands:"],
|
|
11093
|
+
["docs-audit", "Codex Integration Commands:"],
|
|
10010
11094
|
["workflow-audit", "Codex Integration Commands:"]
|
|
10011
11095
|
]);
|
|
10012
11096
|
for (const command of program.commands) {
|
|
@@ -10019,7 +11103,9 @@ function configureRootCommandSurface() {
|
|
|
10019
11103
|
}
|
|
10020
11104
|
}
|
|
10021
11105
|
var cliVersion = getCliVersion();
|
|
10022
|
-
program.name("lee-spec-kit").description(
|
|
11106
|
+
program.name("lee-spec-kit").description(
|
|
11107
|
+
"Document-centered harness engineering toolkit for AI agent development"
|
|
11108
|
+
).version(cliVersion).option("--no-banner", "Hide banner in help output");
|
|
10023
11109
|
if (shouldShowBanner()) {
|
|
10024
11110
|
program.addHelpText("beforeAll", getBanner({ version: cliVersion }));
|
|
10025
11111
|
}
|
|
@@ -10037,6 +11123,8 @@ workflowStageCommand(program);
|
|
|
10037
11123
|
integrationsCommand(program);
|
|
10038
11124
|
workflowAuditCommand(program);
|
|
10039
11125
|
commitAuditCommand(program);
|
|
11126
|
+
docsAuditCommand(program);
|
|
11127
|
+
localCommand(program);
|
|
10040
11128
|
configureRootCommandSurface();
|
|
10041
11129
|
await program.parseAsync();
|
|
10042
11130
|
//# sourceMappingURL=index.js.map
|