lee-spec-kit 0.8.8 → 0.9.1
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 +2 -0
- package/README.md +3 -1
- package/dist/bootstrap-Q77MTW3Q.js +0 -0
- package/dist/chunk-3AFCPGGS.js +0 -0
- package/dist/chunk-7V7RMGEU.js +0 -0
- package/dist/chunk-GR7JQBWF.js +0 -0
- package/dist/hooks-373Z6JG2.js +0 -0
- package/dist/index.js +1477 -192
- package/dist/index.js.map +1 -1
- package/package.json +13 -15
- package/templates/en/common/README.md +55 -10
- package/templates/en/common/agents/agents.md +30 -5
- package/templates/en/common/agents/git-workflow.md +27 -8
- package/templates/en/common/agents/skills/create-pr.md +5 -1
- package/templates/en/common/agents/skills/split-feature.md +2 -1
- package/templates/en/common/agents/ui-ux-design.md +128 -0
- package/templates/en/common/designs/README.md +52 -2
- package/templates/en/common/features/README.md +5 -3
- package/templates/en/common/features/feature-base/decisions.md +3 -1
- package/templates/en/common/features/feature-base/spec.md +3 -0
- package/templates/en/common/features/feature-base/tasks.md +1 -0
- package/templates/en/common/ideas/idea.md +30 -0
- package/templates/ko/common/README.md +56 -11
- package/templates/ko/common/agents/agents.md +30 -5
- package/templates/ko/common/agents/git-workflow.md +27 -8
- package/templates/ko/common/agents/skills/create-pr.md +5 -1
- package/templates/ko/common/agents/skills/split-feature.md +4 -3
- package/templates/ko/common/agents/ui-ux-design.md +128 -0
- package/templates/ko/common/designs/README.md +52 -2
- package/templates/ko/common/features/README.md +5 -3
- package/templates/ko/common/features/feature-base/decisions.md +3 -1
- package/templates/ko/common/features/feature-base/spec.md +3 -0
- package/templates/ko/common/features/feature-base/tasks.md +1 -0
- package/templates/ko/common/ideas/idea.md +30 -0
package/dist/index.js
CHANGED
|
@@ -10,7 +10,9 @@ 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';
|
|
15
|
+
import fs26 from 'fs/promises';
|
|
14
16
|
|
|
15
17
|
async function walkFiles(fsAdapter, rootDir, options = {}) {
|
|
16
18
|
const out = [];
|
|
@@ -236,15 +238,15 @@ var koCli = {
|
|
|
236
238
|
"idea.nextSteps1": " 1. \uBC94\uC704, PRD Refs, \uC2B9\uACA9 \uBA54\uBAA8\uB97C \uC791\uC131",
|
|
237
239
|
"idea.nextSteps2": " 2. Feature\uB85C \uC2B9\uACA9: npx lee-spec-kit feature <name> --idea {ideaId}",
|
|
238
240
|
"idea.nextSteps3": " 3. Feature\uB85C \uB9CC\uB4E4\uC9C0 \uC54A\uC744 \uACBD\uC6B0 Dropped\uB85C \uD45C\uC2DC",
|
|
239
|
-
"
|
|
240
|
-
"
|
|
241
|
-
"
|
|
242
|
-
"
|
|
243
|
-
"
|
|
244
|
-
"
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
-
"
|
|
241
|
+
"integrations.codexBootstrapInstalled": "\u2705 \uC120\uD0DD\uC801 Codex bootstrap \uC124\uCE58 \uC644\uB8CC: {path}",
|
|
242
|
+
"integrations.codexBootstrapAlreadyInstalled": "\u2705 \uC120\uD0DD\uC801 Codex bootstrap \uC774 \uC774\uBBF8 \uC124\uCE58\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4: {path}",
|
|
243
|
+
"integrations.codexBootstrapRemoved": "\u2705 \uC120\uD0DD\uC801 Codex bootstrap \uC81C\uAC70 \uC644\uB8CC: {path}",
|
|
244
|
+
"integrations.codexBootstrapAlreadyAbsent": "\u2705 \uC120\uD0DD\uC801 Codex bootstrap \uC774 \uC774\uBBF8 \uC5C6\uC2B5\uB2C8\uB2E4: {path}",
|
|
245
|
+
"integrations.codexHooksInstalled": "\u2705 Repo-local Codex hooks \uC124\uCE58 \uC644\uB8CC: {path}",
|
|
246
|
+
"integrations.codexHooksAlreadyInstalled": "\u2705 Repo-local Codex hooks \uAC00 \uC774\uBBF8 \uC124\uCE58\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4: {path}",
|
|
247
|
+
"integrations.codexHooksRemoved": "\u2705 Repo-local Codex hooks \uC81C\uAC70 \uC644\uB8CC: {path}",
|
|
248
|
+
"integrations.codexHooksAlreadyAbsent": "\u2705 Repo-local Codex hooks \uAC00 \uC774\uBBF8 \uC5C6\uC2B5\uB2C8\uB2E4: {path}",
|
|
249
|
+
"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.",
|
|
248
250
|
"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)",
|
|
249
251
|
"github.cmdIssueDescription": "feature \uBB38\uC11C \uAE30\uBC18 GitHub issue \uBCF8\uBB38 \uC0DD\uC131/\uC0DD\uC131",
|
|
250
252
|
"github.cmdPrDescription": "GitHub PR \uBCF8\uBB38 \uC0DD\uC131/\uC0DD\uC131 + tasks \uB3D9\uAE30\uD654 + merge \uC7AC\uC2DC\uB3C4",
|
|
@@ -519,15 +521,15 @@ var enCli = {
|
|
|
519
521
|
"idea.nextSteps1": " 1. Fill scope, PRD refs, and promotion notes",
|
|
520
522
|
"idea.nextSteps2": " 2. Promote it with: npx lee-spec-kit feature <name> --idea {ideaId}",
|
|
521
523
|
"idea.nextSteps3": " 3. Mark it dropped if it should not become a feature",
|
|
522
|
-
"
|
|
523
|
-
"
|
|
524
|
-
"
|
|
525
|
-
"
|
|
526
|
-
"
|
|
527
|
-
"
|
|
528
|
-
"
|
|
529
|
-
"
|
|
530
|
-
"
|
|
524
|
+
"integrations.codexBootstrapInstalled": "\u2705 Optional Codex bootstrap installed: {path}",
|
|
525
|
+
"integrations.codexBootstrapAlreadyInstalled": "\u2705 Optional Codex bootstrap already installed: {path}",
|
|
526
|
+
"integrations.codexBootstrapRemoved": "\u2705 Optional Codex bootstrap removed: {path}",
|
|
527
|
+
"integrations.codexBootstrapAlreadyAbsent": "\u2705 Optional Codex bootstrap is already absent: {path}",
|
|
528
|
+
"integrations.codexHooksInstalled": "\u2705 Repo-local Codex hooks installed: {path}",
|
|
529
|
+
"integrations.codexHooksAlreadyInstalled": "\u2705 Repo-local Codex hooks already installed: {path}",
|
|
530
|
+
"integrations.codexHooksRemoved": "\u2705 Repo-local Codex hooks removed: {path}",
|
|
531
|
+
"integrations.codexHooksAlreadyAbsent": "\u2705 Repo-local Codex hooks are already absent: {path}",
|
|
532
|
+
"integrations.codexHooksTrustRequired": "Review and trust the installed project hooks with /hooks in Codex. Changed hook definitions require review again.",
|
|
531
533
|
"github.cmdGithubDescription": "GitHub workflow helpers (issue/pr templates, validation, merge retry)",
|
|
532
534
|
"github.cmdIssueDescription": "Generate/create GitHub issue body from feature docs with validation",
|
|
533
535
|
"github.cmdPrDescription": "Generate/create GitHub PR body with validation, tasks PR sync, and merge retry",
|
|
@@ -1230,7 +1232,8 @@ var ENGINE_MANAGED_AGENT_FILES = [
|
|
|
1230
1232
|
"agents.md",
|
|
1231
1233
|
"git-workflow.md",
|
|
1232
1234
|
"issue-template.md",
|
|
1233
|
-
"pr-template.md"
|
|
1235
|
+
"pr-template.md",
|
|
1236
|
+
"ui-ux-design.md"
|
|
1234
1237
|
];
|
|
1235
1238
|
var ENGINE_MANAGED_AGENT_DIRS = ["skills"];
|
|
1236
1239
|
var ENGINE_MANAGED_FEATURE_PATH = path8.join(
|
|
@@ -1303,6 +1306,10 @@ Before taking the next workflow step:
|
|
|
1303
1306
|
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
|
|
1304
1307
|
11. Keep docs and code synchronized; if code changes materially, update the active feature docs in the same turn before stopping
|
|
1305
1308
|
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
|
|
1309
|
+
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
|
|
1310
|
+
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
|
|
1311
|
+
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\`
|
|
1312
|
+
16. In a \`local-ff\` or \`local-squash\` workflow, keep implementation approval and local merge approval distinct when \`local_merge\` is required: the first accepts the implementation, and the second authorizes the configured integration strategy, post-merge checks, and local cleanup
|
|
1306
1313
|
|
|
1307
1314
|
Approval and remote actions:
|
|
1308
1315
|
|
|
@@ -1310,14 +1317,20 @@ Approval and remote actions:
|
|
|
1310
1317
|
- If \`workflow-stage --json\` reports \`approvalRequired === true\`, stop at that boundary and ask the user before proceeding
|
|
1311
1318
|
- If \`workflow-stage --json\` returns labeled \`actionOptions\` at any approval boundary, keep the same option labels and exact \`reply\` tokens in the user prompt and do not improvise different reply formats
|
|
1312
1319
|
- If \`workflow-stage --json\` reports \`nextAction.category === "task_commit"\`, make the docs commit and project commit for the just-finished task before starting the next task or moving to the next stage
|
|
1313
|
-
- Before \`git commit\`, prefer \`npx lee-spec-kit commit-audit --json\` when
|
|
1320
|
+
- Before \`git commit\`, prefer \`npx lee-spec-kit commit-audit --json\`; Feature-scoped commits use \`#123\` when an Issue is linked and the stable Feature ID such as \`F027\` for issue-less local workflows
|
|
1314
1321
|
- Before remote GitHub actions, share the plan or artifact being sent
|
|
1315
1322
|
- Respect repo policy from docs and config first; hooks only enforce guardrails and continuation checks
|
|
1316
1323
|
|
|
1317
1324
|
Validation:
|
|
1318
1325
|
|
|
1319
|
-
- Prefer \`npx lee-spec-kit commit-audit --json\` for commit-time staged docs path validation
|
|
1326
|
+
- Prefer \`npx lee-spec-kit commit-audit --json\` for commit-time staged docs path validation and canonical commit-subject validation
|
|
1320
1327
|
- Prefer \`npx lee-spec-kit workflow-audit --json\` as the default docs-sync validator for Codex hooks and end-of-turn checks; it expects the active feature docs to carry one fresh \`lee-spec-kit:workflow-sync\` marker after meaningful code/doc sync
|
|
1328
|
+
|
|
1329
|
+
Optional UI/UX design policy:
|
|
1330
|
+
|
|
1331
|
+
- Only when the user request explicitly mentions a design system, UI/visual redesign, design consistency, shared UI/component-library consolidation, branding/theme/token redesign, or implementation from Figma/design images, read and apply \`npx lee-spec-kit docs get ui-ux-design --json\`
|
|
1332
|
+
- Do not apply that policy merely because the target is web/frontend, to a non-UI/backend Feature, or to a simple bug fix unrelated to durable design rules
|
|
1333
|
+
- Treat it as optional guidance, not a \`requiredDocs\` entry or workflow approval gate
|
|
1321
1334
|
`;
|
|
1322
1335
|
function renderManagedSegment(lang, docsRepo) {
|
|
1323
1336
|
return `${LEE_SPEC_KIT_AGENTS_BEGIN}
|
|
@@ -1882,7 +1895,16 @@ async function getConfig(cwd) {
|
|
|
1882
1895
|
// src/config/types.ts
|
|
1883
1896
|
var DEFAULT_APPROVAL_REQUIRE_CHECK_CATEGORIES = [
|
|
1884
1897
|
"spec_approve",
|
|
1885
|
-
"implementation_approve"
|
|
1898
|
+
"implementation_approve",
|
|
1899
|
+
"local_merge"
|
|
1900
|
+
];
|
|
1901
|
+
var PRE_PR_REVIEW_REASONING_EFFORTS = [
|
|
1902
|
+
"low",
|
|
1903
|
+
"medium",
|
|
1904
|
+
"high",
|
|
1905
|
+
"xhigh",
|
|
1906
|
+
"max",
|
|
1907
|
+
"ultra"
|
|
1886
1908
|
];
|
|
1887
1909
|
function createDefaultApprovalConfig() {
|
|
1888
1910
|
return {
|
|
@@ -1891,6 +1913,14 @@ function createDefaultApprovalConfig() {
|
|
|
1891
1913
|
requireCheckCategories: [...DEFAULT_APPROVAL_REQUIRE_CHECK_CATEGORIES]
|
|
1892
1914
|
};
|
|
1893
1915
|
}
|
|
1916
|
+
function createDefaultPrePrReviewerConfig() {
|
|
1917
|
+
return {
|
|
1918
|
+
type: "subagent",
|
|
1919
|
+
model: "inherit",
|
|
1920
|
+
reasoningEffort: "high",
|
|
1921
|
+
onUnavailable: "inherit"
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1894
1924
|
|
|
1895
1925
|
// src/commands/init.ts
|
|
1896
1926
|
function checkGitRepo(cwd) {
|
|
@@ -1917,6 +1947,25 @@ function getGitTopLevelOrNull(cwd) {
|
|
|
1917
1947
|
return null;
|
|
1918
1948
|
}
|
|
1919
1949
|
}
|
|
1950
|
+
function getCurrentBranchOrMain(cwd) {
|
|
1951
|
+
try {
|
|
1952
|
+
const branch = execFileSync("git", ["branch", "--show-current"], {
|
|
1953
|
+
cwd,
|
|
1954
|
+
encoding: "utf-8",
|
|
1955
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1956
|
+
}).trim();
|
|
1957
|
+
return branch || "main";
|
|
1958
|
+
} catch {
|
|
1959
|
+
return "main";
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
function resolveInitialBaseBranch(cwd, docsRepo, projectRoot) {
|
|
1963
|
+
if (docsRepo === "standalone") {
|
|
1964
|
+
const firstProjectRoot = resolveStandaloneInitProjectRoots(projectRoot, cwd)[0];
|
|
1965
|
+
if (firstProjectRoot) return getCurrentBranchOrMain(firstProjectRoot);
|
|
1966
|
+
}
|
|
1967
|
+
return getCurrentBranchOrMain(getGitTopLevelOrNull(cwd) || cwd);
|
|
1968
|
+
}
|
|
1920
1969
|
function isSameOrWithinDir(parentDir, candidateDir) {
|
|
1921
1970
|
const resolvedParent = path8.resolve(parentDir);
|
|
1922
1971
|
const resolvedCandidate = path8.resolve(candidateDir);
|
|
@@ -2486,21 +2535,19 @@ async function runInit(options) {
|
|
|
2486
2535
|
createdAt: getLocalDateString(),
|
|
2487
2536
|
docsRepo,
|
|
2488
2537
|
workflow: {
|
|
2489
|
-
preset: workflowMode,
|
|
2490
2538
|
mode: workflowMode,
|
|
2491
2539
|
requireWorktree: docsRepo === "standalone",
|
|
2492
2540
|
codeDirtyScope: "auto",
|
|
2493
2541
|
taskCommitGate: "warn",
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2542
|
+
...workflowMode === "local" ? {
|
|
2543
|
+
baseBranch: resolveInitialBaseBranch(cwd, docsRepo, projectRoot),
|
|
2544
|
+
completionStrategy: "local-ff",
|
|
2545
|
+
deleteFeatureBranchAfterMerge: true,
|
|
2546
|
+
postMergeChecks: []
|
|
2547
|
+
} : {},
|
|
2497
2548
|
prePrReview: {
|
|
2498
|
-
skills: ["code-review-excellence"],
|
|
2499
|
-
fallback: "builtin-checklist",
|
|
2500
2549
|
evidenceMode: "path_required",
|
|
2501
|
-
|
|
2502
|
-
enforceExecutionEvidence: false,
|
|
2503
|
-
executionCommandPrefixes: []
|
|
2550
|
+
reviewer: createDefaultPrePrReviewerConfig()
|
|
2504
2551
|
}
|
|
2505
2552
|
},
|
|
2506
2553
|
pr: {
|
|
@@ -3318,9 +3365,118 @@ async function getNextIdeaId(docsDir) {
|
|
|
3318
3365
|
const width = Math.max(3, String(next).length);
|
|
3319
3366
|
return `I${String(next).padStart(width, "0")}`;
|
|
3320
3367
|
}
|
|
3368
|
+
|
|
3369
|
+
// src/config/legacy-approval.ts
|
|
3370
|
+
var LEGACY_APPROVAL_CATEGORY_STEPS = [
|
|
3371
|
+
["spec_write", 2],
|
|
3372
|
+
["spec_approve", 3],
|
|
3373
|
+
["plan_write", 4],
|
|
3374
|
+
["plan_approve", 5],
|
|
3375
|
+
["tasks_write", 6],
|
|
3376
|
+
["tasks_approve", 6],
|
|
3377
|
+
["issue_prepare", 8],
|
|
3378
|
+
["issue_create", 8],
|
|
3379
|
+
["branch_create", 9],
|
|
3380
|
+
["task_execute", 10],
|
|
3381
|
+
["implementation_approve", 10],
|
|
3382
|
+
["pre_pr_review", 12],
|
|
3383
|
+
["pr_prepare", 13],
|
|
3384
|
+
["pr_create", 13],
|
|
3385
|
+
["code_review", 14],
|
|
3386
|
+
["pr_merge", 14]
|
|
3387
|
+
];
|
|
3388
|
+
|
|
3389
|
+
// src/config/migrate.ts
|
|
3390
|
+
var OBSOLETE_PRE_PR_REVIEW_KEYS = [
|
|
3391
|
+
"skills",
|
|
3392
|
+
"fallback",
|
|
3393
|
+
"decisionEnum",
|
|
3394
|
+
"enforceExecutionEvidence",
|
|
3395
|
+
"executionCommandPrefixes"
|
|
3396
|
+
];
|
|
3321
3397
|
function hasOwnKey(value, key) {
|
|
3322
3398
|
return Object.prototype.hasOwnProperty.call(value, key);
|
|
3323
3399
|
}
|
|
3400
|
+
function isPlainRecord(value) {
|
|
3401
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3402
|
+
}
|
|
3403
|
+
function migrateLegacyWorkflowSettings(workflow) {
|
|
3404
|
+
const changedPaths = [];
|
|
3405
|
+
const hasCanonicalMode = workflow.mode === "github" || workflow.mode === "local";
|
|
3406
|
+
const legacyPreset = workflow.preset;
|
|
3407
|
+
const canonicalMode = hasCanonicalMode ? workflow.mode : legacyPreset === "local" ? "local" : "github";
|
|
3408
|
+
if (workflow.mode !== canonicalMode) {
|
|
3409
|
+
workflow.mode = canonicalMode;
|
|
3410
|
+
changedPaths.push("workflow.mode");
|
|
3411
|
+
}
|
|
3412
|
+
if (!hasCanonicalMode && legacyPreset === "strict" && workflow.requireWorktree === void 0) {
|
|
3413
|
+
workflow.requireWorktree = true;
|
|
3414
|
+
changedPaths.push("workflow.requireWorktree");
|
|
3415
|
+
}
|
|
3416
|
+
if (hasOwnKey(workflow, "preset")) {
|
|
3417
|
+
delete workflow.preset;
|
|
3418
|
+
changedPaths.push("workflow.preset");
|
|
3419
|
+
}
|
|
3420
|
+
if (hasOwnKey(workflow, "auto")) {
|
|
3421
|
+
delete workflow.auto;
|
|
3422
|
+
changedPaths.push("workflow.auto");
|
|
3423
|
+
}
|
|
3424
|
+
if (isPlainRecord(workflow.prePrReview)) {
|
|
3425
|
+
const prePrReview = workflow.prePrReview;
|
|
3426
|
+
for (const key of OBSOLETE_PRE_PR_REVIEW_KEYS) {
|
|
3427
|
+
if (!hasOwnKey(prePrReview, key)) continue;
|
|
3428
|
+
delete prePrReview[key];
|
|
3429
|
+
changedPaths.push(`workflow.prePrReview.${key}`);
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
return changedPaths;
|
|
3433
|
+
}
|
|
3434
|
+
function migrateLegacyApprovalSettings(approval) {
|
|
3435
|
+
if (approval.mode === "steps") {
|
|
3436
|
+
const requiredSteps = new Set(
|
|
3437
|
+
(Array.isArray(approval.requireCheckSteps) ? approval.requireCheckSteps : []).map(
|
|
3438
|
+
(value) => typeof value === "number" ? value : Number(value)
|
|
3439
|
+
).filter((value) => Number.isFinite(value))
|
|
3440
|
+
);
|
|
3441
|
+
const requireCheckCategories = [];
|
|
3442
|
+
const skipCheckCategories = [];
|
|
3443
|
+
for (const [category, step] of LEGACY_APPROVAL_CATEGORY_STEPS) {
|
|
3444
|
+
if (requiredSteps.has(step)) {
|
|
3445
|
+
requireCheckCategories.push(category);
|
|
3446
|
+
} else {
|
|
3447
|
+
skipCheckCategories.push(category);
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
const migratedApproval = { ...approval };
|
|
3451
|
+
delete migratedApproval.requireCheckSteps;
|
|
3452
|
+
delete migratedApproval.taskExecuteCheck;
|
|
3453
|
+
return {
|
|
3454
|
+
approval: {
|
|
3455
|
+
...migratedApproval,
|
|
3456
|
+
mode: "category",
|
|
3457
|
+
default: "keep",
|
|
3458
|
+
requireCheckCategories,
|
|
3459
|
+
skipCheckCategories
|
|
3460
|
+
},
|
|
3461
|
+
changedPaths: ["approval"]
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
const changedPaths = [];
|
|
3465
|
+
if (hasOwnKey(approval, "requireCheckSteps")) {
|
|
3466
|
+
delete approval.requireCheckSteps;
|
|
3467
|
+
changedPaths.push("approval.requireCheckSteps");
|
|
3468
|
+
}
|
|
3469
|
+
if (hasOwnKey(approval, "taskExecuteCheck")) {
|
|
3470
|
+
delete approval.taskExecuteCheck;
|
|
3471
|
+
changedPaths.push("approval.taskExecuteCheck");
|
|
3472
|
+
}
|
|
3473
|
+
return { approval, changedPaths };
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
// src/commands/update.ts
|
|
3477
|
+
function hasOwnKey2(value, key) {
|
|
3478
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
3479
|
+
}
|
|
3324
3480
|
function isLegacyGeneratedApprovalConfig(approval) {
|
|
3325
3481
|
const mode = typeof approval.mode === "string" ? approval.mode : "";
|
|
3326
3482
|
if (mode && mode !== "category" && mode !== "steps" && mode !== "builtin") {
|
|
@@ -3330,10 +3486,14 @@ function isLegacyGeneratedApprovalConfig(approval) {
|
|
|
3330
3486
|
"default",
|
|
3331
3487
|
"requireCheckSteps",
|
|
3332
3488
|
"requireCheckCategories",
|
|
3333
|
-
"skipCheckCategories"
|
|
3334
|
-
"taskExecuteCheck"
|
|
3489
|
+
"skipCheckCategories"
|
|
3335
3490
|
];
|
|
3336
|
-
return !overrideKeys.some((key) =>
|
|
3491
|
+
return !overrideKeys.some((key) => hasOwnKey2(approval, key));
|
|
3492
|
+
}
|
|
3493
|
+
function isPreviousDefaultApprovalConfig(approval) {
|
|
3494
|
+
const keys = Object.keys(approval).sort();
|
|
3495
|
+
const categories = Array.isArray(approval.requireCheckCategories) ? approval.requireCheckCategories : [];
|
|
3496
|
+
return JSON.stringify(keys) === JSON.stringify(["default", "mode", "requireCheckCategories"]) && approval.mode === "category" && approval.default === "skip" && categories.length === 2 && categories[0] === "spec_approve" && categories[1] === "implementation_approve";
|
|
3337
3497
|
}
|
|
3338
3498
|
function updateCommand(program2) {
|
|
3339
3499
|
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(
|
|
@@ -3519,38 +3679,6 @@ async function collectAgentsMdTargets(cwd, config) {
|
|
|
3519
3679
|
function isPlainObject(value) {
|
|
3520
3680
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3521
3681
|
}
|
|
3522
|
-
function normalizeSkillList(raw) {
|
|
3523
|
-
if (!Array.isArray(raw)) return [];
|
|
3524
|
-
const deduped = /* @__PURE__ */ new Set();
|
|
3525
|
-
for (const item of raw) {
|
|
3526
|
-
const value = String(item || "").trim();
|
|
3527
|
-
if (!value) continue;
|
|
3528
|
-
deduped.add(value);
|
|
3529
|
-
}
|
|
3530
|
-
return [...deduped];
|
|
3531
|
-
}
|
|
3532
|
-
function normalizeStringList(raw) {
|
|
3533
|
-
if (!Array.isArray(raw)) return [];
|
|
3534
|
-
const deduped = /* @__PURE__ */ new Set();
|
|
3535
|
-
for (const item of raw) {
|
|
3536
|
-
const value = String(item || "").trim();
|
|
3537
|
-
if (!value) continue;
|
|
3538
|
-
deduped.add(value);
|
|
3539
|
-
}
|
|
3540
|
-
return [...deduped];
|
|
3541
|
-
}
|
|
3542
|
-
function normalizeDecisionEnumList(raw) {
|
|
3543
|
-
if (!Array.isArray(raw)) return [];
|
|
3544
|
-
const deduped = /* @__PURE__ */ new Set();
|
|
3545
|
-
for (const item of raw) {
|
|
3546
|
-
const value = String(item || "").trim().toLowerCase();
|
|
3547
|
-
if (!value) continue;
|
|
3548
|
-
if (value === "approve" || value === "changes_requested" || value === "blocked") {
|
|
3549
|
-
deduped.add(value);
|
|
3550
|
-
}
|
|
3551
|
-
}
|
|
3552
|
-
return [...deduped];
|
|
3553
|
-
}
|
|
3554
3682
|
async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
3555
3683
|
const configPath = path8.join(docsDir, ".lee-spec-kit.json");
|
|
3556
3684
|
if (!await fs.pathExists(configPath)) {
|
|
@@ -3578,9 +3706,7 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
|
3578
3706
|
changedPaths.push("workflow");
|
|
3579
3707
|
}
|
|
3580
3708
|
const workflow = raw.workflow;
|
|
3581
|
-
|
|
3582
|
-
setIfMissing(workflow, "preset", inferredPreset, "workflow.preset");
|
|
3583
|
-
setIfMissing(workflow, "mode", "github", "workflow.mode");
|
|
3709
|
+
changedPaths.push(...migrateLegacyWorkflowSettings(workflow));
|
|
3584
3710
|
setIfMissing(
|
|
3585
3711
|
workflow,
|
|
3586
3712
|
"requireWorktree",
|
|
@@ -3593,31 +3719,62 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
|
3593
3719
|
}
|
|
3594
3720
|
setIfMissing(workflow, "codeDirtyScope", "auto", "workflow.codeDirtyScope");
|
|
3595
3721
|
setIfMissing(workflow, "taskCommitGate", "warn", "workflow.taskCommitGate");
|
|
3596
|
-
if (
|
|
3597
|
-
workflow
|
|
3598
|
-
|
|
3722
|
+
if (workflow.mode === "local") {
|
|
3723
|
+
setIfMissing(workflow, "baseBranch", "main", "workflow.baseBranch");
|
|
3724
|
+
if (typeof workflow.baseBranch !== "string" || !workflow.baseBranch.trim()) {
|
|
3725
|
+
workflow.baseBranch = "main";
|
|
3726
|
+
changedPaths.push("workflow.baseBranch");
|
|
3727
|
+
} else if (workflow.baseBranch !== workflow.baseBranch.trim()) {
|
|
3728
|
+
workflow.baseBranch = workflow.baseBranch.trim();
|
|
3729
|
+
changedPaths.push("workflow.baseBranch");
|
|
3730
|
+
}
|
|
3731
|
+
setIfMissing(
|
|
3732
|
+
workflow,
|
|
3733
|
+
"completionStrategy",
|
|
3734
|
+
"none",
|
|
3735
|
+
"workflow.completionStrategy"
|
|
3736
|
+
);
|
|
3737
|
+
if (workflow.completionStrategy !== "local-ff" && workflow.completionStrategy !== "local-squash" && workflow.completionStrategy !== "none") {
|
|
3738
|
+
workflow.completionStrategy = "none";
|
|
3739
|
+
changedPaths.push("workflow.completionStrategy");
|
|
3740
|
+
}
|
|
3741
|
+
setIfMissing(
|
|
3742
|
+
workflow,
|
|
3743
|
+
"deleteFeatureBranchAfterMerge",
|
|
3744
|
+
true,
|
|
3745
|
+
"workflow.deleteFeatureBranchAfterMerge"
|
|
3746
|
+
);
|
|
3747
|
+
if (typeof workflow.deleteFeatureBranchAfterMerge !== "boolean") {
|
|
3748
|
+
workflow.deleteFeatureBranchAfterMerge = true;
|
|
3749
|
+
changedPaths.push("workflow.deleteFeatureBranchAfterMerge");
|
|
3750
|
+
}
|
|
3751
|
+
setIfMissing(workflow, "postMergeChecks", [], "workflow.postMergeChecks");
|
|
3752
|
+
if (!Array.isArray(workflow.postMergeChecks)) {
|
|
3753
|
+
workflow.postMergeChecks = [];
|
|
3754
|
+
changedPaths.push("workflow.postMergeChecks");
|
|
3755
|
+
} else {
|
|
3756
|
+
const normalizedPostMergeChecks = workflow.postMergeChecks.flatMap(
|
|
3757
|
+
(value) => {
|
|
3758
|
+
if (!isPlainObject(value) || typeof value.command !== "string") {
|
|
3759
|
+
return [];
|
|
3760
|
+
}
|
|
3761
|
+
const command = value.command.trim();
|
|
3762
|
+
if (!command) return [];
|
|
3763
|
+
const args = Array.isArray(value.args) ? value.args.filter((arg) => typeof arg === "string") : [];
|
|
3764
|
+
return [{ command, ...args.length > 0 ? { args } : {} }];
|
|
3765
|
+
}
|
|
3766
|
+
);
|
|
3767
|
+
if (JSON.stringify(normalizedPostMergeChecks) !== JSON.stringify(workflow.postMergeChecks)) {
|
|
3768
|
+
workflow.postMergeChecks = normalizedPostMergeChecks;
|
|
3769
|
+
changedPaths.push("workflow.postMergeChecks");
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3599
3772
|
}
|
|
3600
|
-
const workflowAuto = workflow.auto;
|
|
3601
|
-
setIfMissing(workflowAuto, "defaultPreset", "pr-handoff", "workflow.auto.defaultPreset");
|
|
3602
3773
|
if (!isPlainObject(workflow.prePrReview)) {
|
|
3603
3774
|
workflow.prePrReview = {};
|
|
3604
3775
|
changedPaths.push("workflow.prePrReview");
|
|
3605
3776
|
}
|
|
3606
3777
|
const prePrReview = workflow.prePrReview;
|
|
3607
|
-
if (prePrReview.skills === void 0) {
|
|
3608
|
-
prePrReview.skills = ["code-review-excellence"];
|
|
3609
|
-
changedPaths.push("workflow.prePrReview.skills");
|
|
3610
|
-
} else {
|
|
3611
|
-
const normalizedSkills = normalizeSkillList(prePrReview.skills);
|
|
3612
|
-
if (normalizedSkills.length === 0) {
|
|
3613
|
-
prePrReview.skills = ["code-review-excellence"];
|
|
3614
|
-
changedPaths.push("workflow.prePrReview.skills");
|
|
3615
|
-
} else if (JSON.stringify(normalizedSkills) !== JSON.stringify(prePrReview.skills)) {
|
|
3616
|
-
prePrReview.skills = normalizedSkills;
|
|
3617
|
-
changedPaths.push("workflow.prePrReview.skills");
|
|
3618
|
-
}
|
|
3619
|
-
}
|
|
3620
|
-
setIfMissing(prePrReview, "fallback", "builtin-checklist", "workflow.prePrReview.fallback");
|
|
3621
3778
|
setIfMissing(
|
|
3622
3779
|
prePrReview,
|
|
3623
3780
|
"evidenceMode",
|
|
@@ -3632,35 +3789,32 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
|
3632
3789
|
delete prePrReview.findings;
|
|
3633
3790
|
changedPaths.push("workflow.prePrReview.findings");
|
|
3634
3791
|
}
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3792
|
+
const defaultReviewer = createDefaultPrePrReviewerConfig();
|
|
3793
|
+
if (!isPlainObject(prePrReview.reviewer)) {
|
|
3794
|
+
prePrReview.reviewer = defaultReviewer;
|
|
3795
|
+
changedPaths.push("workflow.prePrReview.reviewer");
|
|
3638
3796
|
} else {
|
|
3639
|
-
const
|
|
3640
|
-
if (
|
|
3641
|
-
|
|
3642
|
-
changedPaths.push("workflow.prePrReview.
|
|
3643
|
-
}
|
|
3644
|
-
|
|
3645
|
-
|
|
3797
|
+
const reviewer = prePrReview.reviewer;
|
|
3798
|
+
if (reviewer.type !== "subagent") {
|
|
3799
|
+
reviewer.type = defaultReviewer.type;
|
|
3800
|
+
changedPaths.push("workflow.prePrReview.reviewer.type");
|
|
3801
|
+
}
|
|
3802
|
+
if (typeof reviewer.model !== "string" || !reviewer.model.trim()) {
|
|
3803
|
+
reviewer.model = defaultReviewer.model;
|
|
3804
|
+
changedPaths.push("workflow.prePrReview.reviewer.model");
|
|
3805
|
+
} else if (reviewer.model !== reviewer.model.trim()) {
|
|
3806
|
+
reviewer.model = reviewer.model.trim();
|
|
3807
|
+
changedPaths.push("workflow.prePrReview.reviewer.model");
|
|
3808
|
+
}
|
|
3809
|
+
if (typeof reviewer.reasoningEffort !== "string" || !PRE_PR_REVIEW_REASONING_EFFORTS.includes(
|
|
3810
|
+
reviewer.reasoningEffort
|
|
3811
|
+
)) {
|
|
3812
|
+
reviewer.reasoningEffort = defaultReviewer.reasoningEffort;
|
|
3813
|
+
changedPaths.push("workflow.prePrReview.reviewer.reasoningEffort");
|
|
3646
3814
|
}
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
"enforceExecutionEvidence",
|
|
3651
|
-
false,
|
|
3652
|
-
"workflow.prePrReview.enforceExecutionEvidence"
|
|
3653
|
-
);
|
|
3654
|
-
if (prePrReview.executionCommandPrefixes === void 0) {
|
|
3655
|
-
prePrReview.executionCommandPrefixes = [];
|
|
3656
|
-
changedPaths.push("workflow.prePrReview.executionCommandPrefixes");
|
|
3657
|
-
} else {
|
|
3658
|
-
const normalizedExecutionCommandPrefixes = normalizeStringList(
|
|
3659
|
-
prePrReview.executionCommandPrefixes
|
|
3660
|
-
);
|
|
3661
|
-
if (JSON.stringify(normalizedExecutionCommandPrefixes) !== JSON.stringify(prePrReview.executionCommandPrefixes)) {
|
|
3662
|
-
prePrReview.executionCommandPrefixes = normalizedExecutionCommandPrefixes;
|
|
3663
|
-
changedPaths.push("workflow.prePrReview.executionCommandPrefixes");
|
|
3815
|
+
if (reviewer.onUnavailable !== "inherit" && reviewer.onUnavailable !== "error") {
|
|
3816
|
+
reviewer.onUnavailable = defaultReviewer.onUnavailable;
|
|
3817
|
+
changedPaths.push("workflow.prePrReview.reviewer.onUnavailable");
|
|
3664
3818
|
}
|
|
3665
3819
|
}
|
|
3666
3820
|
if (!isPlainObject(raw.pr)) {
|
|
@@ -3679,7 +3833,10 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
|
3679
3833
|
changedPaths.push("approval");
|
|
3680
3834
|
} else {
|
|
3681
3835
|
const approval = raw.approval;
|
|
3682
|
-
|
|
3836
|
+
const migration = migrateLegacyApprovalSettings(approval);
|
|
3837
|
+
raw.approval = migration.approval;
|
|
3838
|
+
changedPaths.push(...migration.changedPaths);
|
|
3839
|
+
if (isPlainObject(raw.approval) && (isLegacyGeneratedApprovalConfig(raw.approval) || isPreviousDefaultApprovalConfig(raw.approval))) {
|
|
3683
3840
|
raw.approval = createDefaultApprovalConfig();
|
|
3684
3841
|
changedPaths.push("approval");
|
|
3685
3842
|
}
|
|
@@ -6554,6 +6711,14 @@ var BUILTIN_DOC_DEFINITIONS = [
|
|
|
6554
6711
|
id: "split-feature",
|
|
6555
6712
|
title: { ko: "feature \uBD84\uD560 \uAC00\uC774\uB4DC", en: "feature split guide" },
|
|
6556
6713
|
relativePath: (_, lang) => path8.join(lang, "common", "agents", "skills", "split-feature.md")
|
|
6714
|
+
},
|
|
6715
|
+
{
|
|
6716
|
+
id: "ui-ux-design",
|
|
6717
|
+
title: {
|
|
6718
|
+
ko: "\uC120\uD0DD\uC801 UI/UX \uB514\uC790\uC778 \uBB38\uC11C \uC815\uCC45",
|
|
6719
|
+
en: "Optional UI/UX Design Documentation Policy"
|
|
6720
|
+
},
|
|
6721
|
+
relativePath: (_, lang) => path8.join(lang, "common", "agents", "ui-ux-design.md")
|
|
6557
6722
|
}
|
|
6558
6723
|
];
|
|
6559
6724
|
var DOC_FOLLOWUPS = {
|
|
@@ -6574,7 +6739,8 @@ var DOC_FOLLOWUPS = {
|
|
|
6574
6739
|
"execute-task": ["git-workflow", "split-feature"],
|
|
6575
6740
|
"create-issue": ["issue-doc"],
|
|
6576
6741
|
"create-pr": ["pr-doc"],
|
|
6577
|
-
"split-feature": []
|
|
6742
|
+
"split-feature": [],
|
|
6743
|
+
"ui-ux-design": []
|
|
6578
6744
|
};
|
|
6579
6745
|
function getBuiltinDocIds() {
|
|
6580
6746
|
return BUILTIN_DOC_DEFINITIONS.map((doc) => doc.id);
|
|
@@ -6593,6 +6759,7 @@ function normalizeBuiltinDocId(input) {
|
|
|
6593
6759
|
if (normalized === "split-feature") {
|
|
6594
6760
|
return "split-feature";
|
|
6595
6761
|
}
|
|
6762
|
+
if (normalized === "ui-ux-design") return "ui-ux-design";
|
|
6596
6763
|
if (normalized === "agents") return "agents";
|
|
6597
6764
|
return null;
|
|
6598
6765
|
}
|
|
@@ -7054,11 +7221,9 @@ function findPlaceholderDecisionRange(content) {
|
|
|
7054
7221
|
const block = content.slice(match.index, end);
|
|
7055
7222
|
const isPlaceholder = /\{(?:Decision Title|결정 제목)\}/.test(match[0]) || /-\s+\*\*Decision\*\*:\s*(Final choice|최종 선택)/.test(block);
|
|
7056
7223
|
if (!isPlaceholder) return null;
|
|
7057
|
-
const sequence = Number(match[1] || "1");
|
|
7058
7224
|
return {
|
|
7059
7225
|
start: match.index,
|
|
7060
|
-
end
|
|
7061
|
-
sequence: Number.isFinite(sequence) ? sequence : 1
|
|
7226
|
+
end
|
|
7062
7227
|
};
|
|
7063
7228
|
}
|
|
7064
7229
|
function formatOptions(options) {
|
|
@@ -7099,7 +7264,7 @@ async function runDecisionAdd(featureName, options) {
|
|
|
7099
7264
|
const optionItems = (options.option || []).map((value) => value.trim()).filter(Boolean);
|
|
7100
7265
|
const content = await fs.readFile(target.path, "utf-8");
|
|
7101
7266
|
const placeholderRange = findPlaceholderDecisionRange(content);
|
|
7102
|
-
const decisionSequence = placeholderRange
|
|
7267
|
+
const decisionSequence = placeholderRange ? 1 : getNextDecisionSequence(content);
|
|
7103
7268
|
const decisionId = `D${String(decisionSequence).padStart(3, "0")}`;
|
|
7104
7269
|
const recordedAt = localDate();
|
|
7105
7270
|
const block = formatDecisionBlock({
|
|
@@ -7311,16 +7476,16 @@ function registerCodexIntegration(parent) {
|
|
|
7311
7476
|
const filePath = getCodexConfigPath();
|
|
7312
7477
|
if (options.remove) {
|
|
7313
7478
|
const result2 = await removeLeeSpecKitCodexBootstrap(filePath);
|
|
7314
|
-
const message = result2.changed ? tr(lang, "cli", "
|
|
7479
|
+
const message = result2.changed ? tr(lang, "cli", "integrations.codexBootstrapRemoved", {
|
|
7315
7480
|
path: filePath
|
|
7316
|
-
}) : tr(lang, "cli", "
|
|
7481
|
+
}) : tr(lang, "cli", "integrations.codexBootstrapAlreadyAbsent", {
|
|
7317
7482
|
path: filePath
|
|
7318
7483
|
});
|
|
7319
7484
|
console.log(chalk.green(message));
|
|
7320
7485
|
return;
|
|
7321
7486
|
}
|
|
7322
7487
|
const result = await upsertLeeSpecKitCodexBootstrap(filePath);
|
|
7323
|
-
const key = result.action === "noop" ? "
|
|
7488
|
+
const key = result.action === "noop" ? "integrations.codexBootstrapAlreadyInstalled" : "integrations.codexBootstrapInstalled";
|
|
7324
7489
|
console.log(chalk.green(tr(lang, "cli", key, { path: filePath })));
|
|
7325
7490
|
} catch (error) {
|
|
7326
7491
|
const cliError = toCliError(error);
|
|
@@ -7375,7 +7540,7 @@ function registerCodexHooksIntegration(parent) {
|
|
|
7375
7540
|
(repoRoot) => removeLeeSpecKitCodexHooks(repoRoot)
|
|
7376
7541
|
)
|
|
7377
7542
|
);
|
|
7378
|
-
const key2 = results2.some((result) => result.changed) ? "
|
|
7543
|
+
const key2 = results2.some((result) => result.changed) ? "integrations.codexHooksRemoved" : "integrations.codexHooksAlreadyAbsent";
|
|
7379
7544
|
console.log(chalk.green(tr(lang, "cli", key2, { path: displayPath })));
|
|
7380
7545
|
return;
|
|
7381
7546
|
}
|
|
@@ -7384,10 +7549,12 @@ function registerCodexHooksIntegration(parent) {
|
|
|
7384
7549
|
(repoRoot) => upsertLeeSpecKitCodexHooks(repoRoot, workflowRoot)
|
|
7385
7550
|
)
|
|
7386
7551
|
);
|
|
7387
|
-
const key = results.every((result) => result.action === "noop") ? "
|
|
7552
|
+
const key = results.every((result) => result.action === "noop") ? "integrations.codexHooksAlreadyInstalled" : "integrations.codexHooksInstalled";
|
|
7388
7553
|
console.log(chalk.green(tr(lang, "cli", key, { path: displayPath })));
|
|
7389
7554
|
console.log(
|
|
7390
|
-
chalk.yellow(
|
|
7555
|
+
chalk.yellow(
|
|
7556
|
+
tr(lang, "cli", "integrations.codexHooksTrustRequired")
|
|
7557
|
+
)
|
|
7391
7558
|
);
|
|
7392
7559
|
} catch (error) {
|
|
7393
7560
|
const cliError = toCliError(error);
|
|
@@ -7406,27 +7573,294 @@ function integrationsCommand(program2) {
|
|
|
7406
7573
|
registerCodexIntegration(integrations);
|
|
7407
7574
|
registerCodexHooksIntegration(integrations);
|
|
7408
7575
|
}
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
}
|
|
7576
|
+
|
|
7577
|
+
// src/utils/commit-conventions.ts
|
|
7578
|
+
var PROJECT_COMMIT_TYPES = [
|
|
7579
|
+
"feat",
|
|
7580
|
+
"fix",
|
|
7581
|
+
"refactor",
|
|
7582
|
+
"test",
|
|
7583
|
+
"style",
|
|
7584
|
+
"chore"
|
|
7585
|
+
];
|
|
7586
|
+
function resolveFeatureCommitScope(input) {
|
|
7587
|
+
const issueNumber = Number(input.issueNumber || 0);
|
|
7588
|
+
if (Number.isInteger(issueNumber) && issueNumber > 0) {
|
|
7589
|
+
return `#${issueNumber}`;
|
|
7590
|
+
}
|
|
7591
|
+
if (input.workflowMode !== "local") {
|
|
7592
|
+
return null;
|
|
7593
|
+
}
|
|
7594
|
+
const featureId = String(input.featureId || "").trim().toUpperCase();
|
|
7595
|
+
return /^F\d{3,}$/.test(featureId) ? featureId : null;
|
|
7596
|
+
}
|
|
7597
|
+
function matchesProjectCommitConvention(message, scope) {
|
|
7598
|
+
return matchesCommitConvention(message, scope, PROJECT_COMMIT_TYPES);
|
|
7599
|
+
}
|
|
7600
|
+
function matchesDocsCommitConvention(message, scope) {
|
|
7601
|
+
return matchesCommitConvention(message, scope, ["docs"]);
|
|
7602
|
+
}
|
|
7603
|
+
function matchesCommitConvention(message, scope, types) {
|
|
7604
|
+
const normalized = String(message || "").trim();
|
|
7605
|
+
const normalizedScope = String(scope || "").trim();
|
|
7606
|
+
if (!normalized || !normalizedScope) return false;
|
|
7607
|
+
const typePattern = types.join("|");
|
|
7608
|
+
const match = normalized.match(
|
|
7609
|
+
new RegExp(`^(${typePattern})\\((#[0-9]+|F[0-9]{3,})\\):\\s+\\S.+$`, "i")
|
|
7610
|
+
);
|
|
7611
|
+
if (!match) return false;
|
|
7612
|
+
return match[2].toLowerCase() === normalizedScope.toLowerCase();
|
|
7613
|
+
}
|
|
7614
|
+
function isSameOrWithin2(parentDir, candidatePath) {
|
|
7615
|
+
const relative = path8.relative(parentDir, candidatePath);
|
|
7616
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relative);
|
|
7617
|
+
}
|
|
7618
|
+
function isPrePrEvidenceSatisfied(check) {
|
|
7619
|
+
const evidence = check.evidence?.trim();
|
|
7620
|
+
if (!evidence) return false;
|
|
7621
|
+
if (check.evidenceMode === "any") return true;
|
|
7622
|
+
const normalizedEvidence = evidence.replaceAll("\\", "/");
|
|
7623
|
+
const docsDir = path8.resolve(check.docsDir);
|
|
7624
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
7625
|
+
if (path8.isAbsolute(normalizedEvidence)) {
|
|
7626
|
+
candidates.add(path8.resolve(normalizedEvidence));
|
|
7627
|
+
} else {
|
|
7628
|
+
candidates.add(path8.resolve(docsDir, normalizedEvidence));
|
|
7629
|
+
candidates.add(path8.resolve(check.featureDir, normalizedEvidence));
|
|
7630
|
+
const [evidenceRoot, ...evidenceParts] = normalizedEvidence.split("/");
|
|
7631
|
+
if (evidenceRoot?.toLowerCase() === path8.basename(docsDir).toLowerCase() && evidenceParts.length > 0) {
|
|
7632
|
+
candidates.add(path8.resolve(docsDir, ...evidenceParts));
|
|
7633
|
+
}
|
|
7634
|
+
}
|
|
7635
|
+
let realDocsDir;
|
|
7636
|
+
try {
|
|
7637
|
+
realDocsDir = fs21.realpathSync(docsDir);
|
|
7638
|
+
} catch {
|
|
7639
|
+
return false;
|
|
7640
|
+
}
|
|
7641
|
+
for (const candidate of candidates) {
|
|
7642
|
+
try {
|
|
7643
|
+
const realCandidate = fs21.realpathSync(candidate);
|
|
7644
|
+
if (!isSameOrWithin2(realDocsDir, realCandidate)) continue;
|
|
7645
|
+
if (fs21.statSync(realCandidate).isFile()) return true;
|
|
7646
|
+
} catch {
|
|
7647
|
+
continue;
|
|
7648
|
+
}
|
|
7649
|
+
}
|
|
7650
|
+
return false;
|
|
7651
|
+
}
|
|
7652
|
+
var BRANCH_LABELS2 = ["Branch", "\uBE0C\uB79C\uCE58"];
|
|
7653
|
+
function resolveLocalCompletionStrategy(config) {
|
|
7654
|
+
const strategy = config.workflow?.completionStrategy;
|
|
7655
|
+
return strategy === "local-ff" || strategy === "local-squash" ? strategy : "none";
|
|
7656
|
+
}
|
|
7657
|
+
async function resolveLocalIntegrationContext(config, feature) {
|
|
7658
|
+
const projectRoot = resolveProjectRoot(config, feature);
|
|
7659
|
+
const featureWorktree = path8.resolve(feature.git.projectGitCwd);
|
|
7660
|
+
const baseBranch = String(config.workflow?.baseBranch || "main").trim() || "main";
|
|
7661
|
+
assertValidBranch(projectRoot, baseBranch, "baseBranch");
|
|
7662
|
+
const tasksContent = await readTasks(feature.path);
|
|
7663
|
+
const configuredFeatureBranch = extractMetadataValue(
|
|
7664
|
+
tasksContent,
|
|
7665
|
+
BRANCH_LABELS2
|
|
7666
|
+
);
|
|
7667
|
+
const featureBranch = configuredFeatureBranch || `feat/${feature.slug}`;
|
|
7668
|
+
assertValidBranch(projectRoot, featureBranch, "feature branch");
|
|
7669
|
+
if (featureBranch === baseBranch) {
|
|
7670
|
+
throw new Error(
|
|
7671
|
+
"Local feature branch must differ from workflow.baseBranch."
|
|
7672
|
+
);
|
|
7673
|
+
}
|
|
7674
|
+
const state = await readLocalIntegrationState(projectRoot, feature);
|
|
7675
|
+
const configuredStrategy = resolveLocalCompletionStrategy(config);
|
|
7676
|
+
const completionStrategy = configuredStrategy === "local-squash" ? "local-squash" : "local-ff";
|
|
7677
|
+
const evidenceRef = resolveEvidenceRef(feature);
|
|
7678
|
+
const featureBranchTip = resolveRef(
|
|
7679
|
+
projectRoot,
|
|
7680
|
+
`refs/heads/${featureBranch}`
|
|
7681
|
+
);
|
|
7682
|
+
const featureTip = featureBranchTip || state?.featureTip || null;
|
|
7683
|
+
const baseTip = resolveRef(projectRoot, `refs/heads/${baseBranch}`);
|
|
7684
|
+
const currentBranch = currentGitBranch(projectRoot);
|
|
7685
|
+
const registeredWorktree = resolveRegisteredBranchWorktree(
|
|
7686
|
+
projectRoot,
|
|
7687
|
+
featureBranch
|
|
7688
|
+
);
|
|
7689
|
+
const managedFeatureWorktree = !!registeredWorktree && path8.resolve(registeredWorktree) !== path8.resolve(projectRoot);
|
|
7690
|
+
const effectiveFeatureWorktree = registeredWorktree || (fs.existsSync(featureWorktree) ? featureWorktree : projectRoot);
|
|
7691
|
+
const baseContainsFeature = !!featureTip && !!baseTip && isAncestor(projectRoot, featureTip, `refs/heads/${baseBranch}`);
|
|
7692
|
+
const integratedCommit = state?.integratedCommit || state?.mergedBaseTip;
|
|
7693
|
+
const evidenceTip = resolveRef(projectRoot, evidenceRef);
|
|
7694
|
+
const featureTree = featureTip ? resolveTree(projectRoot, featureTip) : null;
|
|
7695
|
+
const integratedTree = integratedCommit ? resolveTree(projectRoot, integratedCommit) : null;
|
|
7696
|
+
const squashCommitMatchesSource = completionStrategy === "local-squash" && state?.strategy === "local-squash" && !!featureTip && state.featureTip === featureTip && !!integratedCommit && !!baseTip && isAncestor(projectRoot, integratedCommit, `refs/heads/${baseBranch}`) && !!featureTree && integratedTree === featureTree && state.integratedTree === featureTree;
|
|
7697
|
+
const squashEvidencePresent = !!featureTip && evidenceTip === featureTip;
|
|
7698
|
+
const squashIntegrationComplete = squashCommitMatchesSource && squashEvidencePresent;
|
|
7699
|
+
return {
|
|
7700
|
+
projectRoot,
|
|
7701
|
+
featureWorktree: effectiveFeatureWorktree,
|
|
7702
|
+
docsGitCwd: feature.git.docsGitCwd,
|
|
7703
|
+
baseBranch,
|
|
7704
|
+
featureBranch,
|
|
7705
|
+
featureTip,
|
|
7706
|
+
baseTip,
|
|
7707
|
+
currentBranch,
|
|
7708
|
+
state,
|
|
7709
|
+
completionStrategy,
|
|
7710
|
+
baseContainsFeature,
|
|
7711
|
+
integrationComplete: completionStrategy === "local-squash" ? squashIntegrationComplete : baseContainsFeature,
|
|
7712
|
+
evidenceRef,
|
|
7713
|
+
squashCommitMatchesSource,
|
|
7714
|
+
squashEvidencePresent,
|
|
7715
|
+
projectRootClean: isClean(projectRoot),
|
|
7716
|
+
featureWorktreeClean: isClean(effectiveFeatureWorktree),
|
|
7717
|
+
docsClean: isClean(feature.git.docsGitCwd),
|
|
7718
|
+
managedFeatureWorktree,
|
|
7719
|
+
featureBranchExists: !!featureBranchTip,
|
|
7720
|
+
postMergeChecks: normalizePostMergeChecks(config.workflow?.postMergeChecks),
|
|
7721
|
+
deleteFeatureBranchAfterMerge: config.workflow?.deleteFeatureBranchAfterMerge !== false
|
|
7722
|
+
};
|
|
7723
|
+
}
|
|
7724
|
+
async function writeLocalIntegrationState(projectRoot, feature, state) {
|
|
7725
|
+
const statePath = resolveStatePath(projectRoot, feature);
|
|
7726
|
+
await fs.ensureDir(path8.dirname(statePath));
|
|
7727
|
+
await fs.writeJson(statePath, state, { spaces: 2 });
|
|
7728
|
+
}
|
|
7729
|
+
async function readLocalIntegrationState(projectRoot, feature) {
|
|
7730
|
+
const statePath = resolveStatePath(projectRoot, feature);
|
|
7731
|
+
if (!await fs.pathExists(statePath)) return null;
|
|
7732
|
+
try {
|
|
7733
|
+
const state = await fs.readJson(statePath);
|
|
7734
|
+
if (state?.version !== 1 || state.featureRef !== feature.folderName || typeof state.featureTip !== "string") {
|
|
7735
|
+
return null;
|
|
7736
|
+
}
|
|
7737
|
+
return state;
|
|
7738
|
+
} catch {
|
|
7739
|
+
return null;
|
|
7740
|
+
}
|
|
7741
|
+
}
|
|
7742
|
+
function localCleanupComplete(context) {
|
|
7743
|
+
return context.integrationComplete && 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;
|
|
7744
|
+
}
|
|
7745
|
+
function runPostMergeChecks(projectRoot, checks) {
|
|
7746
|
+
const results = [];
|
|
7747
|
+
for (const check of checks) {
|
|
7748
|
+
const args = Array.isArray(check.args) ? check.args.map(String) : [];
|
|
7749
|
+
const result = runProcess(check.command, args, projectRoot);
|
|
7750
|
+
results.push({
|
|
7751
|
+
command: check.command,
|
|
7752
|
+
args,
|
|
7753
|
+
exitCode: result.code
|
|
7754
|
+
});
|
|
7755
|
+
if (result.code !== 0) break;
|
|
7756
|
+
}
|
|
7757
|
+
return results;
|
|
7758
|
+
}
|
|
7759
|
+
function gitRun(cwd, args) {
|
|
7760
|
+
return runProcess("git", args, cwd);
|
|
7761
|
+
}
|
|
7762
|
+
function currentGitBranch(cwd) {
|
|
7763
|
+
return runGitCapture(["branch", "--show-current"], cwd) || runGitCapture(["rev-parse", "--abbrev-ref", "HEAD"], cwd) || null;
|
|
7764
|
+
}
|
|
7765
|
+
function resolveRef(cwd, ref) {
|
|
7766
|
+
return runGitCapture(["rev-parse", "--verify", ref], cwd) || null;
|
|
7767
|
+
}
|
|
7768
|
+
function resolveTree(cwd, commit) {
|
|
7769
|
+
return resolveRef(cwd, `${commit}^{tree}`);
|
|
7770
|
+
}
|
|
7771
|
+
function preserveLocalIntegrationEvidence(projectRoot, evidenceRef, featureTip) {
|
|
7772
|
+
return gitRun(projectRoot, ["update-ref", evidenceRef, featureTip]);
|
|
7773
|
+
}
|
|
7774
|
+
function isAncestor(cwd, ancestor, descendant) {
|
|
7775
|
+
return runProcess(
|
|
7776
|
+
"git",
|
|
7777
|
+
["merge-base", "--is-ancestor", ancestor, descendant],
|
|
7778
|
+
cwd
|
|
7779
|
+
).code === 0;
|
|
7780
|
+
}
|
|
7781
|
+
function resolveProjectRoot(config, feature) {
|
|
7782
|
+
if (config.docsRepo === "standalone") {
|
|
7783
|
+
const roots = resolveStandaloneProjectRoots(
|
|
7784
|
+
config,
|
|
7785
|
+
feature.type === "single" ? void 0 : feature.type
|
|
7786
|
+
);
|
|
7787
|
+
if (roots[0]) return path8.resolve(roots[0]);
|
|
7788
|
+
}
|
|
7789
|
+
return runGitCapture(
|
|
7790
|
+
["rev-parse", "--show-toplevel"],
|
|
7791
|
+
feature.git.projectGitCwd
|
|
7792
|
+
) || path8.resolve(feature.git.projectGitCwd);
|
|
7793
|
+
}
|
|
7794
|
+
function resolveStatePath(projectRoot, feature) {
|
|
7795
|
+
const commonDirRaw = runGitCapture(["rev-parse", "--git-common-dir"], projectRoot) || ".git";
|
|
7796
|
+
const commonDir = path8.isAbsolute(commonDirRaw) ? commonDirRaw : path8.resolve(projectRoot, commonDirRaw);
|
|
7797
|
+
const key = resolveFeatureStateKey(feature);
|
|
7798
|
+
return path8.join(
|
|
7799
|
+
commonDir,
|
|
7800
|
+
"lee-spec-kit",
|
|
7801
|
+
"local-integrations",
|
|
7802
|
+
`${key}.json`
|
|
7803
|
+
);
|
|
7804
|
+
}
|
|
7805
|
+
function resolveEvidenceRef(feature) {
|
|
7806
|
+
return `refs/lee-spec-kit/integrations/${resolveFeatureStateKey(feature)}`;
|
|
7807
|
+
}
|
|
7808
|
+
function resolveFeatureStateKey(feature) {
|
|
7809
|
+
return crypto.createHash("sha256").update(`${feature.type}:${feature.folderName}`).digest("hex").slice(0, 24);
|
|
7810
|
+
}
|
|
7811
|
+
async function readTasks(featureDir) {
|
|
7812
|
+
const tasksPath = path8.join(featureDir, "tasks.md");
|
|
7813
|
+
return await fs.pathExists(tasksPath) ? fs.readFile(tasksPath, "utf-8") : "";
|
|
7814
|
+
}
|
|
7815
|
+
function extractMetadataValue(content, labels) {
|
|
7816
|
+
for (const label of labels) {
|
|
7817
|
+
const match = content.match(
|
|
7818
|
+
new RegExp(`^\\s*-\\s*\\*\\*${label}\\*\\*:\\s*(.*?)\\s*$`, "mi")
|
|
7819
|
+
);
|
|
7820
|
+
const value = String(match?.[1] || "").trim().replace(/^`(.+)`$/, "$1");
|
|
7821
|
+
if (value && value !== "-") return value;
|
|
7822
|
+
}
|
|
7823
|
+
return null;
|
|
7824
|
+
}
|
|
7825
|
+
function assertValidBranch(cwd, branch, label) {
|
|
7826
|
+
if (runProcess("git", ["check-ref-format", "--branch", branch], cwd).code !== 0) {
|
|
7827
|
+
throw new Error(`Invalid ${label}: ${branch}`);
|
|
7828
|
+
}
|
|
7829
|
+
}
|
|
7830
|
+
function isClean(cwd) {
|
|
7831
|
+
return (runGitCapture(
|
|
7832
|
+
["status", "--porcelain", "--untracked-files=normal"],
|
|
7833
|
+
cwd
|
|
7834
|
+
) || "") === "";
|
|
7835
|
+
}
|
|
7836
|
+
function resolveRegisteredBranchWorktree(projectRoot, branch) {
|
|
7837
|
+
const output = runGitCapture(["worktree", "list", "--porcelain"], projectRoot) || "";
|
|
7838
|
+
let currentPath = null;
|
|
7839
|
+
for (const line of output.split(/\r?\n/u)) {
|
|
7840
|
+
if (line.startsWith("worktree ")) {
|
|
7841
|
+
currentPath = line.slice("worktree ".length).trim();
|
|
7842
|
+
continue;
|
|
7843
|
+
}
|
|
7844
|
+
if (line === `branch refs/heads/${branch}` && currentPath) {
|
|
7845
|
+
return currentPath;
|
|
7846
|
+
}
|
|
7847
|
+
}
|
|
7848
|
+
return null;
|
|
7849
|
+
}
|
|
7850
|
+
function normalizePostMergeChecks(value) {
|
|
7851
|
+
if (!Array.isArray(value)) return [];
|
|
7852
|
+
return value.filter(
|
|
7853
|
+
(entry) => !!entry && typeof entry === "object" && typeof entry.command === "string" && !!entry.command.trim()
|
|
7854
|
+
).map((entry) => ({
|
|
7855
|
+
command: entry.command.trim(),
|
|
7856
|
+
args: Array.isArray(entry.args) ? entry.args.map(String) : []
|
|
7857
|
+
}));
|
|
7858
|
+
}
|
|
7859
|
+
|
|
7860
|
+
// src/utils/workflow-stage.ts
|
|
7427
7861
|
var DOC_STATUS_LABELS = ["Doc Status", "\uBB38\uC11C \uC0C1\uD0DC"];
|
|
7428
7862
|
var ISSUE_LABELS = ["Issue", "Issue Number", "\uC774\uC288", "\uC774\uC288 \uBC88\uD638"];
|
|
7429
|
-
var
|
|
7863
|
+
var BRANCH_LABELS3 = ["Branch", "\uBE0C\uB79C\uCE58"];
|
|
7430
7864
|
var PR_LABELS = ["PR", "Pull Request"];
|
|
7431
7865
|
var PR_STATUS_LABELS = ["PR Status", "PR \uC0C1\uD0DC"];
|
|
7432
7866
|
var PRE_PR_REVIEW_LABELS = ["Pre-PR Review", "PR \uC804 \uB9AC\uBDF0"];
|
|
@@ -7434,12 +7868,14 @@ var PRE_PR_EVIDENCE_LABELS = ["Pre-PR Evidence", "PR \uC804 \uB9AC\uBDF0 Evidenc
|
|
|
7434
7868
|
var PRE_PR_DECISION_LABELS = ["Pre-PR Decision", "PR \uC804 \uB9AC\uBDF0 Decision"];
|
|
7435
7869
|
function resolveWorkflowRequirements(config) {
|
|
7436
7870
|
const workflow = config.workflow || {};
|
|
7437
|
-
const
|
|
7871
|
+
const hasCanonicalMode = workflow.mode === "github" || workflow.mode === "local";
|
|
7872
|
+
const workflowMode = hasCanonicalMode ? workflow.mode : workflow.preset === "local" ? "local" : "github";
|
|
7438
7873
|
const isLocalWorkflow = workflowMode === "local";
|
|
7874
|
+
const legacyStrictRequiresWorktree = !hasCanonicalMode && workflow.preset === "strict";
|
|
7439
7875
|
return {
|
|
7440
7876
|
requireIssue: workflow.requireIssue ?? !isLocalWorkflow,
|
|
7441
7877
|
requireBranch: workflow.requireBranch ?? true,
|
|
7442
|
-
requireWorktree: config.docsRepo === "standalone" ? true : workflow.requireWorktree ??
|
|
7878
|
+
requireWorktree: config.docsRepo === "standalone" ? true : workflow.requireWorktree ?? legacyStrictRequiresWorktree,
|
|
7443
7879
|
requirePr: workflow.requirePr ?? !isLocalWorkflow,
|
|
7444
7880
|
requireReview: workflow.requireReview ?? !isLocalWorkflow,
|
|
7445
7881
|
requireMerge: workflow.requireMerge ?? !isLocalWorkflow,
|
|
@@ -7488,7 +7924,7 @@ function parseTasksDoc(content) {
|
|
|
7488
7924
|
const issueRaw = extractFieldValue2(content, ISSUE_LABELS);
|
|
7489
7925
|
const issueNumberMatch = issueRaw?.match(/^#(\d+)$/);
|
|
7490
7926
|
const issueNumber = issueNumberMatch ? Number(issueNumberMatch[1]) : null;
|
|
7491
|
-
const branchRaw = extractFieldValue2(content,
|
|
7927
|
+
const branchRaw = extractFieldValue2(content, BRANCH_LABELS3);
|
|
7492
7928
|
const prRaw = extractFieldValue2(content, PR_LABELS);
|
|
7493
7929
|
const prePrDecision = extractFieldValue2(content, PRE_PR_DECISION_LABELS);
|
|
7494
7930
|
const tasks = [];
|
|
@@ -7769,8 +8205,13 @@ function resolveProjectCommitTopic(feature, tasks) {
|
|
|
7769
8205
|
}
|
|
7770
8206
|
function buildTaskCommitSummary(input) {
|
|
7771
8207
|
const { feature, tasks, effectiveProjectGitCwd, docsDirty, projectDirty, gateFailureReason } = input;
|
|
7772
|
-
const
|
|
7773
|
-
|
|
8208
|
+
const scope = resolveFeatureCommitScope({
|
|
8209
|
+
issueNumber: tasks.issueNumber,
|
|
8210
|
+
featureId: feature.id,
|
|
8211
|
+
workflowMode: tasks.issueNumber ? "github" : "local"
|
|
8212
|
+
}) || feature.id;
|
|
8213
|
+
const docsMessage = `git -C "${feature.git.docsGitCwd}" add "${feature.docs.featurePathFromDocs}" && git -C "${feature.git.docsGitCwd}" commit -m "docs(${scope}): ${feature.folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"`;
|
|
8214
|
+
const projectMessage = `Stage only the files touched by the just-finished task in "${effectiveProjectGitCwd}", then commit with: git -C "${effectiveProjectGitCwd}" commit -m "feat(${scope}): ${resolveProjectCommitTopic(feature, tasks)}"`;
|
|
7774
8215
|
const lines = ["Finish the task-level commit checkpoint before continuing."];
|
|
7775
8216
|
if (gateFailureReason) {
|
|
7776
8217
|
lines.push(`Current gate failure: ${gateFailureReason}`);
|
|
@@ -7982,8 +8423,13 @@ function nextTodoTask(tasks) {
|
|
|
7982
8423
|
function allTasksDone(tasks) {
|
|
7983
8424
|
return tasks.tasks.length > 0 && tasks.tasks.every((task) => task.status === "DONE");
|
|
7984
8425
|
}
|
|
7985
|
-
function prePrSatisfied(tasks) {
|
|
7986
|
-
return tasks.prePrReviewStatus === "done" &&
|
|
8426
|
+
function prePrSatisfied(config, feature, tasks) {
|
|
8427
|
+
return tasks.prePrReviewStatus === "done" && isPrePrEvidenceSatisfied({
|
|
8428
|
+
docsDir: config.docsDir,
|
|
8429
|
+
featureDir: feature.path,
|
|
8430
|
+
evidence: tasks.prePrEvidence,
|
|
8431
|
+
evidenceMode: config.workflow?.prePrReview?.evidenceMode
|
|
8432
|
+
}) && !!tasks.prePrDecision && tasks.prePrDecisionOutcome === "approve";
|
|
7987
8433
|
}
|
|
7988
8434
|
function issueExistsRemotely(issueNumber, feature) {
|
|
7989
8435
|
if (!issueNumber) return false;
|
|
@@ -8003,12 +8449,32 @@ function prExistsRemotely(prRef, feature) {
|
|
|
8003
8449
|
);
|
|
8004
8450
|
return result.code === 0;
|
|
8005
8451
|
}
|
|
8006
|
-
function buildAction(category, summary, approvalRequired, command = null) {
|
|
8452
|
+
function buildAction(category, summary, approvalRequired, command = null, reviewer) {
|
|
8007
8453
|
return {
|
|
8008
8454
|
category,
|
|
8009
8455
|
summary,
|
|
8010
8456
|
approvalRequired,
|
|
8011
|
-
command
|
|
8457
|
+
command,
|
|
8458
|
+
...reviewer ? {
|
|
8459
|
+
executor: reviewer.type,
|
|
8460
|
+
model: reviewer.model,
|
|
8461
|
+
reasoningEffort: reviewer.reasoningEffort,
|
|
8462
|
+
onUnavailable: reviewer.onUnavailable
|
|
8463
|
+
} : {}
|
|
8464
|
+
};
|
|
8465
|
+
}
|
|
8466
|
+
function resolvePrePrReviewer(config) {
|
|
8467
|
+
const defaults = createDefaultPrePrReviewerConfig();
|
|
8468
|
+
const configured = config.workflow?.prePrReview?.reviewer;
|
|
8469
|
+
const model = typeof configured?.model === "string" && configured.model.trim() ? configured.model.trim() : defaults.model;
|
|
8470
|
+
const reasoningEffort = PRE_PR_REVIEW_REASONING_EFFORTS.includes(
|
|
8471
|
+
configured?.reasoningEffort
|
|
8472
|
+
) ? configured?.reasoningEffort : defaults.reasoningEffort;
|
|
8473
|
+
return {
|
|
8474
|
+
type: "subagent",
|
|
8475
|
+
model,
|
|
8476
|
+
reasoningEffort,
|
|
8477
|
+
onUnavailable: configured?.onUnavailable === "error" ? "error" : defaults.onUnavailable
|
|
8012
8478
|
};
|
|
8013
8479
|
}
|
|
8014
8480
|
function buildStageOption(label, reply, category, summary, command = null) {
|
|
@@ -8030,7 +8496,9 @@ function resolveActionApprovalRequired(config, category, builtinRequiresUserChec
|
|
|
8030
8496
|
const requiredSteps = new Set(
|
|
8031
8497
|
(approval.requireCheckSteps ?? []).map((value) => typeof value === "number" ? value : Number(value)).filter((value) => Number.isFinite(value))
|
|
8032
8498
|
);
|
|
8033
|
-
const legacyStep =
|
|
8499
|
+
const legacyStep = LEGACY_APPROVAL_CATEGORY_STEPS.find(
|
|
8500
|
+
([legacyCategory]) => legacyCategory === category
|
|
8501
|
+
)?.[1];
|
|
8034
8502
|
return typeof legacyStep === "number" ? requiredSteps.has(legacyStep) : builtinRequiresUserCheck;
|
|
8035
8503
|
}
|
|
8036
8504
|
const requiredCategories = new Set(
|
|
@@ -8613,8 +9081,13 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
8613
9081
|
"implementation_approve",
|
|
8614
9082
|
true
|
|
8615
9083
|
);
|
|
9084
|
+
const localIntegrationApproval = config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) !== "none";
|
|
9085
|
+
const separateLocalMergeApproval = localIntegrationApproval && resolveActionApprovalRequired(config, "local_merge", false);
|
|
9086
|
+
const localBaseBranch = config.workflow?.baseBranch?.trim() || "main";
|
|
9087
|
+
const localIntegrationLabel = resolveLocalCompletionStrategy(config) === "local-squash" ? `squash integration into ${localBaseBranch}` : `fast-forward integration into ${localBaseBranch}`;
|
|
9088
|
+
const localCleanupSummary = config.workflow?.deleteFeatureBranchAfterMerge === false ? "remove any managed Feature worktree" : "remove any managed Feature worktree and delete the integrated local Feature branch";
|
|
8616
9089
|
const actionOptions = approvalRequired ? buildApprovalActionOptions({
|
|
8617
|
-
approveSummary: "Approve the completed implementation and continue to the pre-PR or PR preparation stage.",
|
|
9090
|
+
approveSummary: separateLocalMergeApproval ? `Approve the completed implementation and continue to the separate local merge approval before ${localIntegrationLabel}.` : localIntegrationApproval ? `Approve the completed implementation and authorize ${localIntegrationLabel}, post-merge verification, and cleanup that will ${localCleanupSummary}.` : "Approve the completed implementation and continue to the pre-PR or PR preparation stage.",
|
|
8618
9091
|
holdSummary: "Request implementation changes before the workflow continues."
|
|
8619
9092
|
}) : void 0;
|
|
8620
9093
|
return {
|
|
@@ -8625,7 +9098,7 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
8625
9098
|
stage: "implementation_approve",
|
|
8626
9099
|
nextAction: buildAction(
|
|
8627
9100
|
"implementation_approve",
|
|
8628
|
-
"Share the completed implementation, get user approval, and record the completion checkpoint in tasks.md.",
|
|
9101
|
+
separateLocalMergeApproval ? `Share the completed implementation and get user approval for the implementation itself. Record it in tasks.md; ${localIntegrationLabel} will require a separate local_merge approval.` : localIntegrationApproval ? `Share the completed implementation and get user approval for the remaining local completion flow: ${localIntegrationLabel}, 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.",
|
|
8629
9102
|
approvalRequired
|
|
8630
9103
|
),
|
|
8631
9104
|
approvalRequired,
|
|
@@ -8635,7 +9108,7 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
8635
9108
|
blockedReasonCode: "IMPLEMENTATION_APPROVAL_REQUIRED"
|
|
8636
9109
|
};
|
|
8637
9110
|
}
|
|
8638
|
-
if (requirements.prePrReviewEnabled && !prePrSatisfied(tasks)) {
|
|
9111
|
+
if (requirements.prePrReviewEnabled && !prePrSatisfied(config, feature, tasks)) {
|
|
8639
9112
|
return {
|
|
8640
9113
|
status: "ok",
|
|
8641
9114
|
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
@@ -8644,14 +9117,97 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
8644
9117
|
stage: "pre_pr_review",
|
|
8645
9118
|
nextAction: buildAction(
|
|
8646
9119
|
"pre_pr_review",
|
|
8647
|
-
"
|
|
8648
|
-
false
|
|
9120
|
+
"Delegate an independent read-only Pre-PR review to a fresh subagent and record its findings, decision, and reviewer metadata as evidence.",
|
|
9121
|
+
false,
|
|
9122
|
+
null,
|
|
9123
|
+
resolvePrePrReviewer(config)
|
|
8649
9124
|
),
|
|
8650
9125
|
approvalRequired: false,
|
|
8651
9126
|
implementationAllowed: false,
|
|
8652
9127
|
blockedReasonCode: "PRE_PR_REVIEW_NOT_APPROVED"
|
|
8653
9128
|
};
|
|
8654
9129
|
}
|
|
9130
|
+
if (config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) !== "none") {
|
|
9131
|
+
const localState = await resolveLocalIntegrationContext(config, feature);
|
|
9132
|
+
const localMergeBaseCommand = `npx lee-spec-kit local merge ${buildFeatureArgs(feature)} --json`;
|
|
9133
|
+
if (!localState.integrationComplete) {
|
|
9134
|
+
const approvalRequired = resolveActionApprovalRequired(
|
|
9135
|
+
config,
|
|
9136
|
+
"local_merge",
|
|
9137
|
+
false
|
|
9138
|
+
);
|
|
9139
|
+
const command = approvalRequired ? `${localMergeBaseCommand} --confirm OK` : localMergeBaseCommand;
|
|
9140
|
+
const actionOptions = approvalRequired ? buildApprovalActionOptions({
|
|
9141
|
+
approveSummary: localState.completionStrategy === "local-squash" ? `Squash ${localState.featureBranch} into ${localState.baseBranch}, preserve the source Feature tip, and run the configured post-merge checks.` : `Fast-forward ${localState.featureBranch} into ${localState.baseBranch} and run the configured post-merge checks.`,
|
|
9142
|
+
holdSummary: "Keep the completed Feature branch unmerged for now."
|
|
9143
|
+
}) : void 0;
|
|
9144
|
+
return {
|
|
9145
|
+
status: "ok",
|
|
9146
|
+
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
9147
|
+
docsDir: config.docsDir,
|
|
9148
|
+
featureRef: buildFeatureRef(feature),
|
|
9149
|
+
stage: "local_merge",
|
|
9150
|
+
nextAction: buildAction(
|
|
9151
|
+
"local_merge",
|
|
9152
|
+
localState.completionStrategy === "local-squash" ? `Create one squash commit from ${localState.featureBranch} on ${localState.baseBranch} and preserve the source Feature tip as internal integration evidence.` : `Fast-forward ${localState.featureBranch} into ${localState.baseBranch}; do not create a merge commit.`,
|
|
9153
|
+
approvalRequired,
|
|
9154
|
+
command
|
|
9155
|
+
),
|
|
9156
|
+
approvalRequired,
|
|
9157
|
+
implementationAllowed: false,
|
|
9158
|
+
primaryActionLabel: actionOptions ? "A" : void 0,
|
|
9159
|
+
actionOptions,
|
|
9160
|
+
blockedReasonCode: "LOCAL_MERGE_REQUIRED"
|
|
9161
|
+
};
|
|
9162
|
+
}
|
|
9163
|
+
if (!localState.state || !["verified", "cleaned"].includes(localState.state.status) || localState.state.featureTip !== localState.featureTip || localState.state.mergedBaseTip !== localState.baseTip) {
|
|
9164
|
+
return {
|
|
9165
|
+
status: "ok",
|
|
9166
|
+
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
9167
|
+
docsDir: config.docsDir,
|
|
9168
|
+
featureRef: buildFeatureRef(feature),
|
|
9169
|
+
stage: "local_verify",
|
|
9170
|
+
nextAction: buildAction(
|
|
9171
|
+
"local_verify",
|
|
9172
|
+
"Run the configured post-merge checks and record verification against the integrated Feature tip.",
|
|
9173
|
+
false,
|
|
9174
|
+
localMergeBaseCommand
|
|
9175
|
+
),
|
|
9176
|
+
approvalRequired: false,
|
|
9177
|
+
implementationAllowed: false,
|
|
9178
|
+
blockedReasonCode: "LOCAL_VERIFICATION_REQUIRED"
|
|
9179
|
+
};
|
|
9180
|
+
}
|
|
9181
|
+
if (!localCleanupComplete(localState)) {
|
|
9182
|
+
return {
|
|
9183
|
+
status: "ok",
|
|
9184
|
+
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
9185
|
+
docsDir: config.docsDir,
|
|
9186
|
+
featureRef: buildFeatureRef(feature),
|
|
9187
|
+
stage: "local_cleanup",
|
|
9188
|
+
nextAction: buildAction(
|
|
9189
|
+
"local_cleanup",
|
|
9190
|
+
"Remove the clean managed worktree and delete the local Feature branch according to workflow policy.",
|
|
9191
|
+
false,
|
|
9192
|
+
`npx lee-spec-kit local cleanup ${buildFeatureArgs(feature)} --json`
|
|
9193
|
+
),
|
|
9194
|
+
approvalRequired: false,
|
|
9195
|
+
implementationAllowed: false,
|
|
9196
|
+
blockedReasonCode: "LOCAL_CLEANUP_REQUIRED"
|
|
9197
|
+
};
|
|
9198
|
+
}
|
|
9199
|
+
return {
|
|
9200
|
+
status: "ok",
|
|
9201
|
+
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
9202
|
+
docsDir: config.docsDir,
|
|
9203
|
+
featureRef: buildFeatureRef(feature),
|
|
9204
|
+
stage: "done",
|
|
9205
|
+
nextAction: null,
|
|
9206
|
+
approvalRequired: false,
|
|
9207
|
+
implementationAllowed: false,
|
|
9208
|
+
blockedReasonCode: null
|
|
9209
|
+
};
|
|
9210
|
+
}
|
|
8655
9211
|
if (requirements.requirePr) {
|
|
8656
9212
|
const prReady = prDraft.status === "ready";
|
|
8657
9213
|
const prCreated = !!tasks.prLink && prExistsRemotely(tasks.prLink, feature);
|
|
@@ -9531,35 +10087,21 @@ var DEFAULT_MANAGED_DOC_FILES = [
|
|
|
9531
10087
|
".gitignore"
|
|
9532
10088
|
];
|
|
9533
10089
|
|
|
9534
|
-
// src/utils/commit-conventions.ts
|
|
9535
|
-
var PROJECT_COMMIT_PREFIX_PATTERN = /^(feat|fix|refactor|test|chore)\(#(\d+)\):\s+\S.+$/i;
|
|
9536
|
-
var DOCS_COMMIT_PREFIX_PATTERN = /^docs\(#(\d+)\):\s+\S.+$/i;
|
|
9537
|
-
function matchesProjectCommitConvention(message, issueNumber) {
|
|
9538
|
-
const normalized = String(message || "").trim();
|
|
9539
|
-
if (!normalized) return false;
|
|
9540
|
-
const match = normalized.match(PROJECT_COMMIT_PREFIX_PATTERN);
|
|
9541
|
-
if (!match) return false;
|
|
9542
|
-
return Number(match[2]) === issueNumber;
|
|
9543
|
-
}
|
|
9544
|
-
function matchesDocsCommitConvention(message, issueNumber) {
|
|
9545
|
-
const normalized = String(message || "").trim();
|
|
9546
|
-
if (!normalized) return false;
|
|
9547
|
-
const match = normalized.match(DOCS_COMMIT_PREFIX_PATTERN);
|
|
9548
|
-
if (!match) return false;
|
|
9549
|
-
return Number(match[1]) === issueNumber;
|
|
9550
|
-
}
|
|
9551
|
-
|
|
9552
10090
|
// src/commands/commit-audit.ts
|
|
9553
10091
|
var CANONICAL_FEATURE_DOC_PATTERN = /^features\/(?:[^/]+\/)?F\d{3,}[^/]*\/(spec|plan|tasks|decisions|issue|pr)\.md$/i;
|
|
9554
10092
|
var FEATURE_DOC_CANDIDATE_PATTERN = /^features\/(?:[^/]+\/)?F\d{3,}[^/]*\/(.+)$/i;
|
|
9555
10093
|
function commitAuditCommand(program2) {
|
|
9556
|
-
program2.command("commit-audit").description("Validate staged docs paths before commit").option("--json", "Output JSON for hooks and agents").option("--git-root <path>", "Override the git root used for staged-path inspection").option("--message <message>", "Validate a commit subject against the current workflow convention").action(async (options) => {
|
|
10094
|
+
program2.command("commit-audit").description("Validate staged docs paths and canonical commit subjects before commit").option("--json", "Output JSON for hooks and agents").option("--git-root <path>", "Override the git root used for staged-path inspection").option("--message <message>", "Validate a commit subject against the current workflow convention").option("--message-file <path>", "Read and validate the commit subject from a commit message file").option("--enforce", "Exit non-zero when commit-audit blocks the commit").action(async (options) => {
|
|
9557
10095
|
try {
|
|
10096
|
+
const commitMessage = await resolveCommitMessageInput(process.cwd(), options);
|
|
9558
10097
|
const payload = await collectCommitAudit(
|
|
9559
10098
|
process.cwd(),
|
|
9560
10099
|
options.gitRoot,
|
|
9561
|
-
|
|
10100
|
+
commitMessage
|
|
9562
10101
|
);
|
|
10102
|
+
if (options.enforce && payload.status === "blocked") {
|
|
10103
|
+
process.exitCode = 1;
|
|
10104
|
+
}
|
|
9563
10105
|
if (options.json) {
|
|
9564
10106
|
console.log(JSON.stringify(payload, null, 2));
|
|
9565
10107
|
return;
|
|
@@ -9586,6 +10128,7 @@ function commitAuditCommand(program2) {
|
|
|
9586
10128
|
2
|
|
9587
10129
|
)
|
|
9588
10130
|
);
|
|
10131
|
+
if (options.enforce) process.exitCode = 1;
|
|
9589
10132
|
return;
|
|
9590
10133
|
}
|
|
9591
10134
|
process.stderr.write(`[${cliError.code}] ${cliError.message}
|
|
@@ -9594,6 +10137,18 @@ function commitAuditCommand(program2) {
|
|
|
9594
10137
|
}
|
|
9595
10138
|
});
|
|
9596
10139
|
}
|
|
10140
|
+
async function resolveCommitMessageInput(cwd, options) {
|
|
10141
|
+
if (options.message && options.messageFile) {
|
|
10142
|
+
throw createCliError(
|
|
10143
|
+
"INVALID_ARGUMENT",
|
|
10144
|
+
"Use either --message or --message-file, not both."
|
|
10145
|
+
);
|
|
10146
|
+
}
|
|
10147
|
+
if (options.message) return options.message;
|
|
10148
|
+
if (!options.messageFile) return void 0;
|
|
10149
|
+
const content = await fs26.readFile(path8.resolve(cwd, options.messageFile), "utf-8");
|
|
10150
|
+
return content.split(/\r?\n/u).map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#"));
|
|
10151
|
+
}
|
|
9597
10152
|
async function collectCommitAudit(cwd, gitRootOverride, commitMessage) {
|
|
9598
10153
|
const config = await getConfig(cwd);
|
|
9599
10154
|
if (!config) {
|
|
@@ -9712,13 +10267,13 @@ function collectManagedWorktreeRepoRoots(config, projectRoot) {
|
|
|
9712
10267
|
const line = rawLine.trim();
|
|
9713
10268
|
if (!line.startsWith("worktree ")) continue;
|
|
9714
10269
|
const worktreePath = path8.resolve(line.slice("worktree ".length).trim());
|
|
9715
|
-
if (
|
|
10270
|
+
if (isSameOrWithin3(path8.resolve(managedRoot), worktreePath)) {
|
|
9716
10271
|
roots.add(worktreePath);
|
|
9717
10272
|
}
|
|
9718
10273
|
}
|
|
9719
10274
|
return [...roots];
|
|
9720
10275
|
}
|
|
9721
|
-
function
|
|
10276
|
+
function isSameOrWithin3(parentDir, candidateDir) {
|
|
9722
10277
|
const resolvedParent = path8.resolve(parentDir);
|
|
9723
10278
|
const resolvedCandidate = path8.resolve(candidateDir);
|
|
9724
10279
|
return resolvedParent === resolvedCandidate || resolvedCandidate.startsWith(`${resolvedParent}${path8.sep}`);
|
|
@@ -9739,10 +10294,10 @@ function parseStagedPaths(output) {
|
|
|
9739
10294
|
staged.set(`path:${normalizeSlashes3(parts[1])}`, `${status}:path`);
|
|
9740
10295
|
}
|
|
9741
10296
|
return [...staged.entries()].map(([encodedPath, encodedStatus]) => {
|
|
9742
|
-
const [role,
|
|
10297
|
+
const [role, path30] = encodedPath.split(":", 2);
|
|
9743
10298
|
const [status, entryRole] = encodedStatus.split(":", 2);
|
|
9744
10299
|
return {
|
|
9745
|
-
path:
|
|
10300
|
+
path: path30,
|
|
9746
10301
|
status,
|
|
9747
10302
|
role: entryRole || role || "path"
|
|
9748
10303
|
};
|
|
@@ -9817,10 +10372,17 @@ async function collectCommitMessageViolation(cwd, config, repoRoot, stagedEntrie
|
|
|
9817
10372
|
return null;
|
|
9818
10373
|
}
|
|
9819
10374
|
const selection = await resolveFeatureSelection(cwd);
|
|
9820
|
-
if (selection.status !== "selected" || !selection.matchedFeature
|
|
10375
|
+
if (selection.status !== "selected" || !selection.matchedFeature) {
|
|
10376
|
+
return null;
|
|
10377
|
+
}
|
|
10378
|
+
const scope = resolveFeatureCommitScope({
|
|
10379
|
+
issueNumber: selection.matchedFeature.issueNumber,
|
|
10380
|
+
featureId: selection.matchedFeature.id,
|
|
10381
|
+
workflowMode: config.workflow?.mode
|
|
10382
|
+
});
|
|
10383
|
+
if (!scope) {
|
|
9821
10384
|
return null;
|
|
9822
10385
|
}
|
|
9823
|
-
const issueNumber = selection.matchedFeature.issueNumber;
|
|
9824
10386
|
const docsRepoRoot = runGitCapture(["rev-parse", "--show-toplevel"], config.docsDir);
|
|
9825
10387
|
const normalizedRepoRoot = path8.resolve(repoRoot);
|
|
9826
10388
|
const normalizedDocsRepoRoot = docsRepoRoot ? path8.resolve(docsRepoRoot) : null;
|
|
@@ -9830,15 +10392,15 @@ async function collectCommitMessageViolation(cwd, config, repoRoot, stagedEntrie
|
|
|
9830
10392
|
return !!relativeToDocs && relativeToDocs !== "" && !relativeToDocs.startsWith("..");
|
|
9831
10393
|
});
|
|
9832
10394
|
const isDocsCommit = !!normalizedDocsRepoRoot && normalizedDocsRepoRoot === normalizedRepoRoot && (config.docsRepo === "standalone" || docsOnlyCommit);
|
|
9833
|
-
const valid = isDocsCommit ? matchesDocsCommitConvention(normalizedMessage,
|
|
10395
|
+
const valid = isDocsCommit ? matchesDocsCommitConvention(normalizedMessage, scope) : matchesProjectCommitConvention(normalizedMessage, scope);
|
|
9834
10396
|
if (valid) {
|
|
9835
10397
|
return null;
|
|
9836
10398
|
}
|
|
9837
|
-
const expected = isDocsCommit ? `docs(
|
|
10399
|
+
const expected = isDocsCommit ? `docs(${scope}): ...` : `type(${scope}): ... (feat/fix/refactor/test/style/chore)`;
|
|
9838
10400
|
return {
|
|
9839
10401
|
path: "(commit message)",
|
|
9840
10402
|
kind: "commit_message_policy",
|
|
9841
|
-
detail: `Commit subject must follow the
|
|
10403
|
+
detail: `Commit subject must follow the canonical feature-scoped convention. Expected ${expected}, received "${normalizedMessage}".`
|
|
9842
10404
|
};
|
|
9843
10405
|
}
|
|
9844
10406
|
function normalizeEntryName(value) {
|
|
@@ -9852,6 +10414,723 @@ function toAllowedSet(values, extras) {
|
|
|
9852
10414
|
function normalizeSlashes3(value) {
|
|
9853
10415
|
return value.replace(/\\/g, "/");
|
|
9854
10416
|
}
|
|
10417
|
+
var DESIGN_DOC_KINDS = [
|
|
10418
|
+
"ux-design",
|
|
10419
|
+
"design-system",
|
|
10420
|
+
"visual-reference"
|
|
10421
|
+
];
|
|
10422
|
+
var ALLOWED_DESIGN_KINDS = new Set(DESIGN_DOC_KINDS);
|
|
10423
|
+
var FILENAME_RULES = [
|
|
10424
|
+
{
|
|
10425
|
+
pattern: /(?:^|-)(?:system|backend|frontend)-architecture(?:-|$)|(?:^|-)architecture-overview(?:-|$)/i,
|
|
10426
|
+
classification: {
|
|
10427
|
+
kind: "architecture-overview",
|
|
10428
|
+
suggestedLocations: ["docs/prd/*-overview.md"]
|
|
10429
|
+
}
|
|
10430
|
+
},
|
|
10431
|
+
{
|
|
10432
|
+
pattern: /(?:^|-)(?:data-model|api-design|database-schema|implementation-plan|implementation-roadmap)(?:-|$)/i,
|
|
10433
|
+
classification: {
|
|
10434
|
+
kind: "feature-plan",
|
|
10435
|
+
suggestedLocations: [
|
|
10436
|
+
"docs/ideas/I###-*.md (before Feature promotion)",
|
|
10437
|
+
"the active Feature plan.md"
|
|
10438
|
+
]
|
|
10439
|
+
}
|
|
10440
|
+
},
|
|
10441
|
+
{
|
|
10442
|
+
pattern: /(?:^|-)(?:open-source-research|candidate-comparison|technology-comparison)(?:-|$)/i,
|
|
10443
|
+
classification: {
|
|
10444
|
+
kind: "idea",
|
|
10445
|
+
suggestedLocations: [
|
|
10446
|
+
"docs/ideas/I###-*.md (before Feature promotion)",
|
|
10447
|
+
"the active Feature decisions.md"
|
|
10448
|
+
]
|
|
10449
|
+
}
|
|
10450
|
+
},
|
|
10451
|
+
{
|
|
10452
|
+
pattern: /(?:^|-)(?:technical-decision|architecture-decision|tradeoff|adr)(?:-|$)/i,
|
|
10453
|
+
classification: {
|
|
10454
|
+
kind: "feature-decisions",
|
|
10455
|
+
suggestedLocations: [
|
|
10456
|
+
"docs/ideas/I###-*.md (before Feature promotion)",
|
|
10457
|
+
"the active Feature decisions.md"
|
|
10458
|
+
]
|
|
10459
|
+
}
|
|
10460
|
+
}
|
|
10461
|
+
];
|
|
10462
|
+
async function collectDocsTaxonomyViolations(docsDir) {
|
|
10463
|
+
const designsDir = path8.join(docsDir, "designs");
|
|
10464
|
+
if (!await fs.pathExists(designsDir)) return [];
|
|
10465
|
+
const markdownFiles = await collectMarkdownFiles(designsDir);
|
|
10466
|
+
const violations = [];
|
|
10467
|
+
for (const filePath of markdownFiles) {
|
|
10468
|
+
if (path8.basename(filePath).toLowerCase() === "readme.md") continue;
|
|
10469
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
10470
|
+
const displayPath = `docs/${normalizeSlashes4(path8.relative(docsDir, filePath))}`;
|
|
10471
|
+
const frontmatter = parseLeeSpecKitFrontmatter(content);
|
|
10472
|
+
if (frontmatter.present && !frontmatter.valid) {
|
|
10473
|
+
violations.push(
|
|
10474
|
+
createViolation(
|
|
10475
|
+
displayPath,
|
|
10476
|
+
"INVALID_DOC_FRONTMATTER",
|
|
10477
|
+
frontmatter.kind,
|
|
10478
|
+
null,
|
|
10479
|
+
"frontmatter",
|
|
10480
|
+
"lee-spec-kit frontmatter must contain a well-formed kind entry.",
|
|
10481
|
+
[]
|
|
10482
|
+
)
|
|
10483
|
+
);
|
|
10484
|
+
continue;
|
|
10485
|
+
}
|
|
10486
|
+
if (frontmatter.kind) {
|
|
10487
|
+
if (!isKnownDocKind(frontmatter.kind)) {
|
|
10488
|
+
violations.push(
|
|
10489
|
+
createViolation(
|
|
10490
|
+
displayPath,
|
|
10491
|
+
"INVALID_DOC_KIND",
|
|
10492
|
+
frontmatter.kind,
|
|
10493
|
+
null,
|
|
10494
|
+
"frontmatter",
|
|
10495
|
+
`Unknown lee-spec-kit document kind: ${frontmatter.kind}`,
|
|
10496
|
+
[]
|
|
10497
|
+
)
|
|
10498
|
+
);
|
|
10499
|
+
continue;
|
|
10500
|
+
}
|
|
10501
|
+
if (!ALLOWED_DESIGN_KINDS.has(frontmatter.kind)) {
|
|
10502
|
+
violations.push(
|
|
10503
|
+
createViolation(
|
|
10504
|
+
displayPath,
|
|
10505
|
+
"MISPLACED_MANAGED_DOC",
|
|
10506
|
+
frontmatter.kind,
|
|
10507
|
+
frontmatter.kind,
|
|
10508
|
+
"frontmatter",
|
|
10509
|
+
`Document kind "${frontmatter.kind}" does not belong in docs/designs/.`,
|
|
10510
|
+
suggestedLocationsForKind(frontmatter.kind)
|
|
10511
|
+
)
|
|
10512
|
+
);
|
|
10513
|
+
continue;
|
|
10514
|
+
}
|
|
10515
|
+
if (frontmatter.scope && frontmatter.scope !== "project") {
|
|
10516
|
+
violations.push(
|
|
10517
|
+
createViolation(
|
|
10518
|
+
displayPath,
|
|
10519
|
+
"INVALID_DOC_SCOPE",
|
|
10520
|
+
frontmatter.kind,
|
|
10521
|
+
frontmatter.kind,
|
|
10522
|
+
"frontmatter",
|
|
10523
|
+
"Documents in docs/designs/ must use lee-spec-kit scope: project.",
|
|
10524
|
+
[]
|
|
10525
|
+
)
|
|
10526
|
+
);
|
|
10527
|
+
}
|
|
10528
|
+
continue;
|
|
10529
|
+
}
|
|
10530
|
+
const inferred = classifyFilename(
|
|
10531
|
+
path8.basename(filePath, path8.extname(filePath))
|
|
10532
|
+
);
|
|
10533
|
+
if (inferred) {
|
|
10534
|
+
violations.push(
|
|
10535
|
+
createViolation(
|
|
10536
|
+
displayPath,
|
|
10537
|
+
"MISPLACED_MANAGED_DOC",
|
|
10538
|
+
null,
|
|
10539
|
+
inferred.kind,
|
|
10540
|
+
"filename",
|
|
10541
|
+
`The filename strongly suggests a ${inferred.kind} document, which does not belong in docs/designs/.`,
|
|
10542
|
+
inferred.suggestedLocations
|
|
10543
|
+
)
|
|
10544
|
+
);
|
|
10545
|
+
continue;
|
|
10546
|
+
}
|
|
10547
|
+
violations.push(
|
|
10548
|
+
createViolation(
|
|
10549
|
+
displayPath,
|
|
10550
|
+
"DOC_KIND_MISSING",
|
|
10551
|
+
null,
|
|
10552
|
+
null,
|
|
10553
|
+
null,
|
|
10554
|
+
"Add lee-spec-kit frontmatter with kind: ux-design, design-system, or visual-reference.",
|
|
10555
|
+
[]
|
|
10556
|
+
)
|
|
10557
|
+
);
|
|
10558
|
+
}
|
|
10559
|
+
return violations.sort((a, b) => a.path.localeCompare(b.path));
|
|
10560
|
+
}
|
|
10561
|
+
function createViolation(displayPath, violationCode, declaredKind, detectedKind, detectedBy, message, suggestedLocations) {
|
|
10562
|
+
return {
|
|
10563
|
+
path: displayPath,
|
|
10564
|
+
violationCode,
|
|
10565
|
+
declaredKind,
|
|
10566
|
+
detectedKind,
|
|
10567
|
+
detectedBy,
|
|
10568
|
+
confidence: detectedBy ? "high" : null,
|
|
10569
|
+
message,
|
|
10570
|
+
suggestedLocations
|
|
10571
|
+
};
|
|
10572
|
+
}
|
|
10573
|
+
async function collectMarkdownFiles(rootDir) {
|
|
10574
|
+
const files = [];
|
|
10575
|
+
const entries = await fs.readdir(rootDir, { withFileTypes: true });
|
|
10576
|
+
for (const entry of entries) {
|
|
10577
|
+
const absolutePath = path8.join(rootDir, entry.name);
|
|
10578
|
+
if (entry.isDirectory()) {
|
|
10579
|
+
if (entry.name.toLowerCase() === "assets") continue;
|
|
10580
|
+
files.push(...await collectMarkdownFiles(absolutePath));
|
|
10581
|
+
continue;
|
|
10582
|
+
}
|
|
10583
|
+
if (entry.isFile() && path8.extname(entry.name).toLowerCase() === ".md") {
|
|
10584
|
+
files.push(absolutePath);
|
|
10585
|
+
}
|
|
10586
|
+
}
|
|
10587
|
+
return files;
|
|
10588
|
+
}
|
|
10589
|
+
function parseLeeSpecKitFrontmatter(content) {
|
|
10590
|
+
const normalized = content.replace(/^\uFEFF/, "");
|
|
10591
|
+
if (!normalized.startsWith("---\n") && !normalized.startsWith("---\r\n")) {
|
|
10592
|
+
return { present: false, valid: true, kind: null, scope: null };
|
|
10593
|
+
}
|
|
10594
|
+
const lines = normalized.split(/\r?\n/);
|
|
10595
|
+
const closingIndex = lines.findIndex(
|
|
10596
|
+
(line, index) => index > 0 && line.trim() === "---"
|
|
10597
|
+
);
|
|
10598
|
+
if (closingIndex < 0) {
|
|
10599
|
+
return { present: true, valid: false, kind: null, scope: null };
|
|
10600
|
+
}
|
|
10601
|
+
const frontmatterLines = lines.slice(1, closingIndex);
|
|
10602
|
+
const rootIndex = frontmatterLines.findIndex(
|
|
10603
|
+
(line) => /^lee-spec-kit:\s*$/.test(line.trim())
|
|
10604
|
+
);
|
|
10605
|
+
if (rootIndex < 0) {
|
|
10606
|
+
return { present: true, valid: false, kind: null, scope: null };
|
|
10607
|
+
}
|
|
10608
|
+
let kind = null;
|
|
10609
|
+
let scope = null;
|
|
10610
|
+
for (const line of frontmatterLines.slice(rootIndex + 1)) {
|
|
10611
|
+
if (/^\S/.test(line)) break;
|
|
10612
|
+
const match = line.match(/^\s+(kind|scope):\s*(.*?)\s*$/);
|
|
10613
|
+
if (!match) continue;
|
|
10614
|
+
const value = stripYamlScalarQuotes(match[2]);
|
|
10615
|
+
if (match[1] === "kind") kind = value || null;
|
|
10616
|
+
if (match[1] === "scope") scope = value || null;
|
|
10617
|
+
}
|
|
10618
|
+
return { present: true, valid: !!kind, kind, scope };
|
|
10619
|
+
}
|
|
10620
|
+
function stripYamlScalarQuotes(value) {
|
|
10621
|
+
const trimmed = value.trim();
|
|
10622
|
+
if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
10623
|
+
return trimmed.slice(1, -1).trim();
|
|
10624
|
+
}
|
|
10625
|
+
return trimmed;
|
|
10626
|
+
}
|
|
10627
|
+
function classifyFilename(stem) {
|
|
10628
|
+
for (const rule of FILENAME_RULES) {
|
|
10629
|
+
if (rule.pattern.test(stem)) return rule.classification;
|
|
10630
|
+
}
|
|
10631
|
+
return null;
|
|
10632
|
+
}
|
|
10633
|
+
function isKnownDocKind(kind) {
|
|
10634
|
+
return (/* @__PURE__ */ new Set([
|
|
10635
|
+
...DESIGN_DOC_KINDS,
|
|
10636
|
+
"product-requirements",
|
|
10637
|
+
"architecture-overview",
|
|
10638
|
+
"idea",
|
|
10639
|
+
"feature-spec",
|
|
10640
|
+
"feature-plan",
|
|
10641
|
+
"feature-decisions"
|
|
10642
|
+
])).has(kind);
|
|
10643
|
+
}
|
|
10644
|
+
function suggestedLocationsForKind(kind) {
|
|
10645
|
+
if (kind === "architecture-overview") return ["docs/prd/*-overview.md"];
|
|
10646
|
+
if (kind === "idea") return ["docs/ideas/I###-*.md"];
|
|
10647
|
+
if (kind === "product-requirements") return ["docs/prd/"];
|
|
10648
|
+
if (kind === "feature-spec") return ["the active Feature spec.md"];
|
|
10649
|
+
if (kind === "feature-plan") return ["the active Feature plan.md"];
|
|
10650
|
+
if (kind === "feature-decisions") return ["the active Feature decisions.md"];
|
|
10651
|
+
return [];
|
|
10652
|
+
}
|
|
10653
|
+
function normalizeSlashes4(value) {
|
|
10654
|
+
return value.replace(/\\/g, "/");
|
|
10655
|
+
}
|
|
10656
|
+
|
|
10657
|
+
// src/commands/docs-audit.ts
|
|
10658
|
+
function docsAuditCommand(program2) {
|
|
10659
|
+
program2.command("docs-audit").description(
|
|
10660
|
+
"Audit whether managed documents are stored in the right location"
|
|
10661
|
+
).option("--json", "Output JSON for agents and automation").action(async (options) => {
|
|
10662
|
+
try {
|
|
10663
|
+
const payload = await collectDocsAudit(process.cwd());
|
|
10664
|
+
if (options.json) {
|
|
10665
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
10666
|
+
return;
|
|
10667
|
+
}
|
|
10668
|
+
console.log(`${payload.status}: ${payload.reasonCode}`);
|
|
10669
|
+
for (const violation of payload.violations) {
|
|
10670
|
+
console.log(`- ${violation.path}: ${violation.message}`);
|
|
10671
|
+
}
|
|
10672
|
+
} catch (error) {
|
|
10673
|
+
const cliError = toCliError(error);
|
|
10674
|
+
const payload = {
|
|
10675
|
+
status: "error",
|
|
10676
|
+
reasonCode: cliError.code === "CONFIG_NOT_FOUND" ? "CONFIG_NOT_FOUND" : "UNEXPECTED_ERROR",
|
|
10677
|
+
mode: "warn",
|
|
10678
|
+
docsDir: null,
|
|
10679
|
+
violations: []
|
|
10680
|
+
};
|
|
10681
|
+
if (options.json) {
|
|
10682
|
+
console.log(
|
|
10683
|
+
JSON.stringify({ ...payload, error: cliError.message }, null, 2)
|
|
10684
|
+
);
|
|
10685
|
+
} else {
|
|
10686
|
+
process.stderr.write(`[${cliError.code}] ${cliError.message}
|
|
10687
|
+
`);
|
|
10688
|
+
}
|
|
10689
|
+
process.exitCode = 1;
|
|
10690
|
+
}
|
|
10691
|
+
});
|
|
10692
|
+
}
|
|
10693
|
+
async function collectDocsAudit(cwd) {
|
|
10694
|
+
const config = await getConfig(cwd);
|
|
10695
|
+
if (!config) {
|
|
10696
|
+
throw createCliError(
|
|
10697
|
+
"CONFIG_NOT_FOUND",
|
|
10698
|
+
"Config file not found. Run `init` first."
|
|
10699
|
+
);
|
|
10700
|
+
}
|
|
10701
|
+
const violations = await collectDocsTaxonomyViolations(config.docsDir);
|
|
10702
|
+
return {
|
|
10703
|
+
status: violations.length > 0 ? "warning" : "ok",
|
|
10704
|
+
reasonCode: violations.length > 0 ? "DOCS_TAXONOMY_WARNING" : "DOCS_TAXONOMY_OK",
|
|
10705
|
+
mode: "warn",
|
|
10706
|
+
docsDir: config.docsDir,
|
|
10707
|
+
violations
|
|
10708
|
+
};
|
|
10709
|
+
}
|
|
10710
|
+
|
|
10711
|
+
// src/commands/local.ts
|
|
10712
|
+
function localCommand(program2) {
|
|
10713
|
+
const local = program2.command("local").description("Integrate and clean up local workflow feature branches");
|
|
10714
|
+
local.command("merge [feature-name]").description("Integrate a completed local feature and verify the result").option("--component <component>", "Component name for multi projects").option(
|
|
10715
|
+
"--confirm <token>",
|
|
10716
|
+
"Approval token when local_merge requires approval"
|
|
10717
|
+
).option("--json", "Output JSON for agents and hooks").action(
|
|
10718
|
+
async (featureName, options) => {
|
|
10719
|
+
await runLocalAction(
|
|
10720
|
+
options,
|
|
10721
|
+
() => runLocalMerge(featureName, options)
|
|
10722
|
+
);
|
|
10723
|
+
}
|
|
10724
|
+
);
|
|
10725
|
+
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(
|
|
10726
|
+
async (featureName, options) => {
|
|
10727
|
+
await runLocalAction(
|
|
10728
|
+
options,
|
|
10729
|
+
() => runLocalCleanup(featureName, options)
|
|
10730
|
+
);
|
|
10731
|
+
}
|
|
10732
|
+
);
|
|
10733
|
+
}
|
|
10734
|
+
async function runLocalAction(options, action) {
|
|
10735
|
+
try {
|
|
10736
|
+
const payload = await action();
|
|
10737
|
+
if (options.json) {
|
|
10738
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
10739
|
+
} else {
|
|
10740
|
+
console.log(`${payload.status}: ${payload.reasonCode}`);
|
|
10741
|
+
}
|
|
10742
|
+
if (payload.status !== "ok") process.exitCode = 1;
|
|
10743
|
+
} catch (error) {
|
|
10744
|
+
const cliError = toCliError(error);
|
|
10745
|
+
const payload = {
|
|
10746
|
+
status: "error",
|
|
10747
|
+
reasonCode: cliError.code,
|
|
10748
|
+
featureRef: null,
|
|
10749
|
+
error: cliError.message
|
|
10750
|
+
};
|
|
10751
|
+
if (options.json) {
|
|
10752
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
10753
|
+
} else {
|
|
10754
|
+
process.stderr.write(`[${cliError.code}] ${cliError.message}
|
|
10755
|
+
`);
|
|
10756
|
+
}
|
|
10757
|
+
process.exitCode = 1;
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
async function runLocalMerge(featureName, options) {
|
|
10761
|
+
const cwd = process.cwd();
|
|
10762
|
+
const stage = await collectWorkflowStage(cwd, featureName, options.component);
|
|
10763
|
+
if (stage.status !== "ok" || !stage.nextAction || !["local_merge", "local_verify"].includes(stage.nextAction.category)) {
|
|
10764
|
+
return blocked(
|
|
10765
|
+
"LOCAL_MERGE_STAGE_REQUIRED",
|
|
10766
|
+
stage.featureRef,
|
|
10767
|
+
"The active workflow is not at local_merge or local_verify."
|
|
10768
|
+
);
|
|
10769
|
+
}
|
|
10770
|
+
if (stage.approvalRequired && options.confirm !== "OK") {
|
|
10771
|
+
return blocked(
|
|
10772
|
+
"APPROVAL_REQUIRED",
|
|
10773
|
+
stage.featureRef,
|
|
10774
|
+
"Re-run with --confirm OK after the configured local_merge approval."
|
|
10775
|
+
);
|
|
10776
|
+
}
|
|
10777
|
+
const selection = await resolveFeatureSelection(
|
|
10778
|
+
cwd,
|
|
10779
|
+
featureName,
|
|
10780
|
+
options.component
|
|
10781
|
+
);
|
|
10782
|
+
if (selection.status !== "selected" || !selection.matchedFeature) {
|
|
10783
|
+
return blocked(
|
|
10784
|
+
"FEATURE_SELECTION_REQUIRED",
|
|
10785
|
+
null,
|
|
10786
|
+
"Select exactly one Feature."
|
|
10787
|
+
);
|
|
10788
|
+
}
|
|
10789
|
+
const config = await getConfig(cwd);
|
|
10790
|
+
if (!config || config.workflow?.mode !== "local") {
|
|
10791
|
+
return blocked(
|
|
10792
|
+
"LOCAL_WORKFLOW_REQUIRED",
|
|
10793
|
+
selection.matchedFeature.folderName
|
|
10794
|
+
);
|
|
10795
|
+
}
|
|
10796
|
+
const feature = selection.matchedFeature;
|
|
10797
|
+
let context = await resolveLocalIntegrationContext(config, feature);
|
|
10798
|
+
if (!context.featureTip || !context.baseTip) {
|
|
10799
|
+
return blocked(
|
|
10800
|
+
"LOCAL_MERGE_REF_NOT_FOUND",
|
|
10801
|
+
feature.folderName,
|
|
10802
|
+
"Both the configured base branch and Feature tip must exist."
|
|
10803
|
+
);
|
|
10804
|
+
}
|
|
10805
|
+
if (!context.featureWorktreeClean || !context.projectRootClean || !context.docsClean) {
|
|
10806
|
+
return blocked(
|
|
10807
|
+
"LOCAL_MERGE_DIRTY_WORKTREE",
|
|
10808
|
+
feature.folderName,
|
|
10809
|
+
"Feature worktree, base worktree, and docs repo must be clean."
|
|
10810
|
+
);
|
|
10811
|
+
}
|
|
10812
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10813
|
+
const previousStateStrategy = context.state?.strategy || "local-ff";
|
|
10814
|
+
let state = context.state && context.state.featureTip === context.featureTip && previousStateStrategy === context.completionStrategy ? context.state : {
|
|
10815
|
+
version: 1,
|
|
10816
|
+
featureRef: feature.folderName,
|
|
10817
|
+
component: feature.type,
|
|
10818
|
+
baseBranch: context.baseBranch,
|
|
10819
|
+
featureBranch: context.featureBranch,
|
|
10820
|
+
featureTip: context.featureTip,
|
|
10821
|
+
mergedBaseTip: context.baseTip,
|
|
10822
|
+
strategy: context.completionStrategy,
|
|
10823
|
+
status: "merged",
|
|
10824
|
+
mergedAt: now,
|
|
10825
|
+
verifiedAt: null,
|
|
10826
|
+
cleanedAt: null,
|
|
10827
|
+
verification: []
|
|
10828
|
+
};
|
|
10829
|
+
if (context.completionStrategy === "local-squash" && context.squashCommitMatchesSource && !context.squashEvidencePresent) {
|
|
10830
|
+
const evidence = preserveLocalIntegrationEvidence(
|
|
10831
|
+
context.projectRoot,
|
|
10832
|
+
context.evidenceRef,
|
|
10833
|
+
context.featureTip
|
|
10834
|
+
);
|
|
10835
|
+
if (evidence.code !== 0) {
|
|
10836
|
+
return blocked(
|
|
10837
|
+
"LOCAL_SQUASH_EVIDENCE_FAILED",
|
|
10838
|
+
feature.folderName,
|
|
10839
|
+
evidence.stderr
|
|
10840
|
+
);
|
|
10841
|
+
}
|
|
10842
|
+
context = await resolveLocalIntegrationContext(config, feature);
|
|
10843
|
+
}
|
|
10844
|
+
if (!context.featureTip || !context.baseTip) {
|
|
10845
|
+
return blocked("LOCAL_MERGE_REF_NOT_FOUND", feature.folderName);
|
|
10846
|
+
}
|
|
10847
|
+
if (!context.integrationComplete) {
|
|
10848
|
+
if (!isAncestor(
|
|
10849
|
+
context.projectRoot,
|
|
10850
|
+
`refs/heads/${context.baseBranch}`,
|
|
10851
|
+
context.featureTip
|
|
10852
|
+
)) {
|
|
10853
|
+
return blocked(
|
|
10854
|
+
context.completionStrategy === "local-squash" ? "LOCAL_SQUASH_BASE_NOT_ANCESTOR" : "LOCAL_MERGE_NOT_FAST_FORWARD",
|
|
10855
|
+
feature.folderName,
|
|
10856
|
+
`${context.baseBranch} is not an ancestor of ${context.featureBranch}.`
|
|
10857
|
+
);
|
|
10858
|
+
}
|
|
10859
|
+
if (currentGitBranch(context.projectRoot) !== context.baseBranch) {
|
|
10860
|
+
const checkout = gitRun(context.projectRoot, [
|
|
10861
|
+
"checkout",
|
|
10862
|
+
context.baseBranch
|
|
10863
|
+
]);
|
|
10864
|
+
if (checkout.code !== 0) {
|
|
10865
|
+
return blocked(
|
|
10866
|
+
"LOCAL_BASE_CHECKOUT_FAILED",
|
|
10867
|
+
feature.folderName,
|
|
10868
|
+
checkout.stderr
|
|
10869
|
+
);
|
|
10870
|
+
}
|
|
10871
|
+
}
|
|
10872
|
+
const integration = context.completionStrategy === "local-squash" ? integrateLocalSquash(context, feature.folderName, feature.id, feature.slug) : integrateLocalFastForward(context, feature.folderName);
|
|
10873
|
+
if (integration.status !== "ok") return integration.payload;
|
|
10874
|
+
const mergedBaseTip = resolveRef(
|
|
10875
|
+
context.projectRoot,
|
|
10876
|
+
`refs/heads/${context.baseBranch}`
|
|
10877
|
+
);
|
|
10878
|
+
const integratedTree = mergedBaseTip ? resolveTree(context.projectRoot, mergedBaseTip) : null;
|
|
10879
|
+
state = {
|
|
10880
|
+
...state,
|
|
10881
|
+
featureTip: context.featureTip,
|
|
10882
|
+
mergedBaseTip: mergedBaseTip || context.featureTip,
|
|
10883
|
+
strategy: context.completionStrategy,
|
|
10884
|
+
integratedCommit: mergedBaseTip || context.featureTip,
|
|
10885
|
+
...integratedTree ? { integratedTree } : {},
|
|
10886
|
+
...context.completionStrategy === "local-squash" ? { evidenceRef: context.evidenceRef } : {},
|
|
10887
|
+
status: "merged",
|
|
10888
|
+
mergedAt: now,
|
|
10889
|
+
verifiedAt: null,
|
|
10890
|
+
cleanedAt: null,
|
|
10891
|
+
verification: []
|
|
10892
|
+
};
|
|
10893
|
+
await writeLocalIntegrationState(context.projectRoot, feature, state);
|
|
10894
|
+
context = await resolveLocalIntegrationContext(config, feature);
|
|
10895
|
+
}
|
|
10896
|
+
if (!context.integrationComplete) {
|
|
10897
|
+
return blocked("LOCAL_MERGE_VERIFICATION_FAILED", feature.folderName);
|
|
10898
|
+
}
|
|
10899
|
+
const verification = runPostMergeChecks(
|
|
10900
|
+
context.projectRoot,
|
|
10901
|
+
context.postMergeChecks
|
|
10902
|
+
);
|
|
10903
|
+
const failedCheck = verification.find((entry) => entry.exitCode !== 0);
|
|
10904
|
+
if (failedCheck) {
|
|
10905
|
+
await writeLocalIntegrationState(context.projectRoot, feature, {
|
|
10906
|
+
...state,
|
|
10907
|
+
status: "merged",
|
|
10908
|
+
verification,
|
|
10909
|
+
verifiedAt: null
|
|
10910
|
+
});
|
|
10911
|
+
return {
|
|
10912
|
+
...blocked("LOCAL_POST_MERGE_CHECK_FAILED", feature.folderName),
|
|
10913
|
+
baseBranch: context.baseBranch,
|
|
10914
|
+
featureBranch: context.featureBranch,
|
|
10915
|
+
featureTip: context.featureTip,
|
|
10916
|
+
baseTip: resolveRef(
|
|
10917
|
+
context.projectRoot,
|
|
10918
|
+
`refs/heads/${context.baseBranch}`
|
|
10919
|
+
),
|
|
10920
|
+
verification
|
|
10921
|
+
};
|
|
10922
|
+
}
|
|
10923
|
+
const baseTip = resolveRef(
|
|
10924
|
+
context.projectRoot,
|
|
10925
|
+
`refs/heads/${context.baseBranch}`
|
|
10926
|
+
);
|
|
10927
|
+
const verifiedFeatureTip = context.featureTip || state.featureTip;
|
|
10928
|
+
const verifiedState = {
|
|
10929
|
+
...state,
|
|
10930
|
+
featureTip: verifiedFeatureTip,
|
|
10931
|
+
mergedBaseTip: baseTip || verifiedFeatureTip,
|
|
10932
|
+
status: "verified",
|
|
10933
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10934
|
+
verification
|
|
10935
|
+
};
|
|
10936
|
+
await writeLocalIntegrationState(context.projectRoot, feature, verifiedState);
|
|
10937
|
+
return {
|
|
10938
|
+
status: "ok",
|
|
10939
|
+
reasonCode: "LOCAL_MERGE_VERIFIED",
|
|
10940
|
+
featureRef: feature.folderName,
|
|
10941
|
+
baseBranch: context.baseBranch,
|
|
10942
|
+
featureBranch: context.featureBranch,
|
|
10943
|
+
featureTip: context.featureTip,
|
|
10944
|
+
baseTip,
|
|
10945
|
+
completionStrategy: context.completionStrategy,
|
|
10946
|
+
verification
|
|
10947
|
+
};
|
|
10948
|
+
}
|
|
10949
|
+
async function runLocalCleanup(featureName, options) {
|
|
10950
|
+
const cwd = process.cwd();
|
|
10951
|
+
const stage = await collectWorkflowStage(cwd, featureName, options.component);
|
|
10952
|
+
if (stage.status !== "ok" || stage.nextAction?.category !== "local_cleanup") {
|
|
10953
|
+
return blocked(
|
|
10954
|
+
"LOCAL_CLEANUP_STAGE_REQUIRED",
|
|
10955
|
+
stage.featureRef,
|
|
10956
|
+
"The active workflow is not at local_cleanup."
|
|
10957
|
+
);
|
|
10958
|
+
}
|
|
10959
|
+
const selection = await resolveFeatureSelection(
|
|
10960
|
+
cwd,
|
|
10961
|
+
featureName,
|
|
10962
|
+
options.component
|
|
10963
|
+
);
|
|
10964
|
+
if (selection.status !== "selected" || !selection.matchedFeature) {
|
|
10965
|
+
return blocked("FEATURE_SELECTION_REQUIRED", null);
|
|
10966
|
+
}
|
|
10967
|
+
const config = await getConfig(cwd);
|
|
10968
|
+
if (!config || config.workflow?.mode !== "local") {
|
|
10969
|
+
return blocked(
|
|
10970
|
+
"LOCAL_WORKFLOW_REQUIRED",
|
|
10971
|
+
selection.matchedFeature.folderName
|
|
10972
|
+
);
|
|
10973
|
+
}
|
|
10974
|
+
const feature = selection.matchedFeature;
|
|
10975
|
+
let context = await resolveLocalIntegrationContext(config, feature);
|
|
10976
|
+
if (!context.integrationComplete || !context.state || !["verified", "cleaned"].includes(context.state.status) || context.state.featureTip !== context.featureTip || context.state.mergedBaseTip !== context.baseTip) {
|
|
10977
|
+
return blocked("LOCAL_MERGE_NOT_VERIFIED", feature.folderName);
|
|
10978
|
+
}
|
|
10979
|
+
if (!context.featureWorktreeClean || !context.projectRootClean || !context.docsClean) {
|
|
10980
|
+
return blocked("LOCAL_CLEANUP_DIRTY_WORKTREE", feature.folderName);
|
|
10981
|
+
}
|
|
10982
|
+
if (currentGitBranch(context.projectRoot) !== context.baseBranch) {
|
|
10983
|
+
return blocked("LOCAL_BASE_NOT_CHECKED_OUT", feature.folderName);
|
|
10984
|
+
}
|
|
10985
|
+
if (context.managedFeatureWorktree) {
|
|
10986
|
+
const remove = gitRun(context.projectRoot, [
|
|
10987
|
+
"worktree",
|
|
10988
|
+
"remove",
|
|
10989
|
+
context.featureWorktree
|
|
10990
|
+
]);
|
|
10991
|
+
if (remove.code !== 0) {
|
|
10992
|
+
return blocked(
|
|
10993
|
+
"LOCAL_WORKTREE_REMOVE_FAILED",
|
|
10994
|
+
feature.folderName,
|
|
10995
|
+
remove.stderr
|
|
10996
|
+
);
|
|
10997
|
+
}
|
|
10998
|
+
}
|
|
10999
|
+
if (context.deleteFeatureBranchAfterMerge && context.featureBranchExists) {
|
|
11000
|
+
const removeBranch = gitRun(context.projectRoot, [
|
|
11001
|
+
"branch",
|
|
11002
|
+
context.completionStrategy === "local-squash" ? "-D" : "-d",
|
|
11003
|
+
context.featureBranch
|
|
11004
|
+
]);
|
|
11005
|
+
if (removeBranch.code !== 0) {
|
|
11006
|
+
return blocked(
|
|
11007
|
+
"LOCAL_BRANCH_DELETE_FAILED",
|
|
11008
|
+
feature.folderName,
|
|
11009
|
+
removeBranch.stderr
|
|
11010
|
+
);
|
|
11011
|
+
}
|
|
11012
|
+
}
|
|
11013
|
+
await writeLocalIntegrationState(context.projectRoot, feature, {
|
|
11014
|
+
...context.state,
|
|
11015
|
+
status: "cleaned",
|
|
11016
|
+
cleanedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11017
|
+
});
|
|
11018
|
+
context = await resolveLocalIntegrationContext(config, feature);
|
|
11019
|
+
return {
|
|
11020
|
+
status: "ok",
|
|
11021
|
+
reasonCode: "LOCAL_CLEANUP_COMPLETE",
|
|
11022
|
+
featureRef: feature.folderName,
|
|
11023
|
+
baseBranch: context.baseBranch,
|
|
11024
|
+
featureBranch: context.featureBranch,
|
|
11025
|
+
featureTip: context.featureTip,
|
|
11026
|
+
baseTip: context.baseTip,
|
|
11027
|
+
completionStrategy: context.completionStrategy
|
|
11028
|
+
};
|
|
11029
|
+
}
|
|
11030
|
+
function integrateLocalFastForward(context, featureRef) {
|
|
11031
|
+
const merge = gitRun(context.projectRoot, [
|
|
11032
|
+
"merge",
|
|
11033
|
+
"--ff-only",
|
|
11034
|
+
context.featureBranch
|
|
11035
|
+
]);
|
|
11036
|
+
if (merge.code !== 0) {
|
|
11037
|
+
return {
|
|
11038
|
+
status: "blocked",
|
|
11039
|
+
payload: blocked(
|
|
11040
|
+
"LOCAL_MERGE_NOT_FAST_FORWARD",
|
|
11041
|
+
featureRef,
|
|
11042
|
+
merge.stderr
|
|
11043
|
+
)
|
|
11044
|
+
};
|
|
11045
|
+
}
|
|
11046
|
+
return { status: "ok" };
|
|
11047
|
+
}
|
|
11048
|
+
function integrateLocalSquash(context, featureRef, featureId, featureSlug) {
|
|
11049
|
+
const originalBaseTip = context.baseTip;
|
|
11050
|
+
if (!originalBaseTip || !context.featureTip) {
|
|
11051
|
+
return {
|
|
11052
|
+
status: "blocked",
|
|
11053
|
+
payload: blocked("LOCAL_MERGE_REF_NOT_FOUND", featureRef)
|
|
11054
|
+
};
|
|
11055
|
+
}
|
|
11056
|
+
const squash = gitRun(context.projectRoot, [
|
|
11057
|
+
"merge",
|
|
11058
|
+
"--squash",
|
|
11059
|
+
context.featureBranch
|
|
11060
|
+
]);
|
|
11061
|
+
if (squash.code !== 0) {
|
|
11062
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11063
|
+
return {
|
|
11064
|
+
status: "blocked",
|
|
11065
|
+
payload: blocked("LOCAL_SQUASH_FAILED", featureRef, squash.stderr)
|
|
11066
|
+
};
|
|
11067
|
+
}
|
|
11068
|
+
const scope = resolveFeatureCommitScope({
|
|
11069
|
+
featureId,
|
|
11070
|
+
workflowMode: "local"
|
|
11071
|
+
});
|
|
11072
|
+
if (!scope) {
|
|
11073
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11074
|
+
return {
|
|
11075
|
+
status: "blocked",
|
|
11076
|
+
payload: blocked(
|
|
11077
|
+
"LOCAL_SQUASH_COMMIT_SCOPE_INVALID",
|
|
11078
|
+
featureRef,
|
|
11079
|
+
`Could not resolve a canonical local commit scope from ${featureId}.`
|
|
11080
|
+
)
|
|
11081
|
+
};
|
|
11082
|
+
}
|
|
11083
|
+
const commit = gitRun(context.projectRoot, [
|
|
11084
|
+
"commit",
|
|
11085
|
+
"-m",
|
|
11086
|
+
`feat(${scope}): integrate ${featureSlug}`
|
|
11087
|
+
]);
|
|
11088
|
+
if (commit.code !== 0) {
|
|
11089
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11090
|
+
return {
|
|
11091
|
+
status: "blocked",
|
|
11092
|
+
payload: blocked("LOCAL_SQUASH_COMMIT_FAILED", featureRef, commit.stderr)
|
|
11093
|
+
};
|
|
11094
|
+
}
|
|
11095
|
+
const integratedCommit = resolveRef(context.projectRoot, "HEAD");
|
|
11096
|
+
const sourceTree = resolveTree(context.projectRoot, context.featureTip);
|
|
11097
|
+
const integratedTree = integratedCommit ? resolveTree(context.projectRoot, integratedCommit) : null;
|
|
11098
|
+
if (!integratedCommit || !sourceTree || integratedTree !== sourceTree) {
|
|
11099
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11100
|
+
return {
|
|
11101
|
+
status: "blocked",
|
|
11102
|
+
payload: blocked("LOCAL_SQUASH_TREE_MISMATCH", featureRef)
|
|
11103
|
+
};
|
|
11104
|
+
}
|
|
11105
|
+
const evidence = preserveLocalIntegrationEvidence(
|
|
11106
|
+
context.projectRoot,
|
|
11107
|
+
context.evidenceRef,
|
|
11108
|
+
context.featureTip
|
|
11109
|
+
);
|
|
11110
|
+
if (evidence.code !== 0) {
|
|
11111
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11112
|
+
return {
|
|
11113
|
+
status: "blocked",
|
|
11114
|
+
payload: blocked(
|
|
11115
|
+
"LOCAL_SQUASH_EVIDENCE_FAILED",
|
|
11116
|
+
featureRef,
|
|
11117
|
+
evidence.stderr
|
|
11118
|
+
)
|
|
11119
|
+
};
|
|
11120
|
+
}
|
|
11121
|
+
return { status: "ok" };
|
|
11122
|
+
}
|
|
11123
|
+
function rollbackSquash(projectRoot, originalBaseTip) {
|
|
11124
|
+
gitRun(projectRoot, ["reset", "--hard", originalBaseTip]);
|
|
11125
|
+
}
|
|
11126
|
+
function blocked(reasonCode, featureRef, error) {
|
|
11127
|
+
return {
|
|
11128
|
+
status: "blocked",
|
|
11129
|
+
reasonCode,
|
|
11130
|
+
featureRef,
|
|
11131
|
+
...error ? { error } : {}
|
|
11132
|
+
};
|
|
11133
|
+
}
|
|
9855
11134
|
function isBannerDisabled() {
|
|
9856
11135
|
const v = (process.env.LEE_SPEC_KIT_NO_BANNER || "").trim();
|
|
9857
11136
|
return v === "1";
|
|
@@ -10023,8 +11302,10 @@ function configureRootCommandSurface() {
|
|
|
10023
11302
|
["detect", "Workflow Policy Commands:"],
|
|
10024
11303
|
["github", "Workflow Policy Commands:"],
|
|
10025
11304
|
["workflow-stage", "Workflow Policy Commands:"],
|
|
11305
|
+
["local", "Workflow Policy Commands:"],
|
|
10026
11306
|
["integrations", "Codex Integration Commands:"],
|
|
10027
11307
|
["commit-audit", "Codex Integration Commands:"],
|
|
11308
|
+
["docs-audit", "Codex Integration Commands:"],
|
|
10028
11309
|
["workflow-audit", "Codex Integration Commands:"]
|
|
10029
11310
|
]);
|
|
10030
11311
|
for (const command of program.commands) {
|
|
@@ -10037,7 +11318,9 @@ function configureRootCommandSurface() {
|
|
|
10037
11318
|
}
|
|
10038
11319
|
}
|
|
10039
11320
|
var cliVersion = getCliVersion();
|
|
10040
|
-
program.name("lee-spec-kit").description(
|
|
11321
|
+
program.name("lee-spec-kit").description(
|
|
11322
|
+
"Document-centered harness engineering toolkit for AI agent development"
|
|
11323
|
+
).version(cliVersion).option("--no-banner", "Hide banner in help output");
|
|
10041
11324
|
if (shouldShowBanner()) {
|
|
10042
11325
|
program.addHelpText("beforeAll", getBanner({ version: cliVersion }));
|
|
10043
11326
|
}
|
|
@@ -10055,6 +11338,8 @@ workflowStageCommand(program);
|
|
|
10055
11338
|
integrationsCommand(program);
|
|
10056
11339
|
workflowAuditCommand(program);
|
|
10057
11340
|
commitAuditCommand(program);
|
|
11341
|
+
docsAuditCommand(program);
|
|
11342
|
+
localCommand(program);
|
|
10058
11343
|
configureRootCommandSurface();
|
|
10059
11344
|
await program.parseAsync();
|
|
10060
11345
|
//# sourceMappingURL=index.js.map
|