lee-spec-kit 0.8.8 → 0.9.0

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