lee-spec-kit 0.9.1 → 0.9.2

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 CHANGED
@@ -79,7 +79,8 @@ The overall approach is influenced by [spec-kit](https://github.com/github/spec-
79
79
  - `integrations codex`: install/remove the optional global `[features].hooks` setting
80
80
  - `commit-audit --json`
81
81
  - `workflow-audit --json`
82
- - `local merge <feature-ref> --json`: integrate a completed local Feature using its configured fast-forward or squash strategy and run post-merge checks
82
+ - `local verify <feature-ref> --json`: run checks in the local Feature worktree and bind the result to its exact tip/tree
83
+ - `local merge <feature-ref> --json`: integrate a verified local Feature using its configured fast-forward or squash strategy
83
84
  - `local cleanup <feature-ref> --json`: remove the managed worktree and optionally delete the integrated Feature branch
84
85
 
85
86
  Supported modes:
package/README.md CHANGED
@@ -79,7 +79,8 @@ npx lee-spec-kit feature user-auth
79
79
  - `integrations codex`: 선택적 전역 `[features].hooks` 설정 설치/제거
80
80
  - `commit-audit --json`: hooks용 commit-time docs path + canonical commit subject validator
81
81
  - `workflow-audit --json`: hooks용 docs sync validator
82
- - `local merge <feature-ref> --json`: 완료된 local Feature 설정된 fast-forward 또는 squash 전략으로 base branch통합하고 사후 검증 실행
82
+ - `local verify <feature-ref> --json`: local Feature worktree에서 검사를 실행하고 결과를 정확한 tip/tree결속
83
+ - `local merge <feature-ref> --json`: 검증된 local Feature를 설정된 fast-forward 또는 squash 전략으로 base branch에 통합
83
84
  - `local cleanup <feature-ref> --json`: managed worktree 제거 및 설정에 따른 통합 완료 Feature 브랜치 삭제
84
85
 
85
86
  지원 모드:
package/dist/index.js CHANGED
@@ -1308,7 +1308,7 @@ Before taking the next workflow step:
1308
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
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
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\`
1311
+ 15. For a local workflow, do not report completion directly after implementation approval; follow the exact returned \`local verify\` / \`local merge\` / \`local cleanup\` commands until \`workflow-stage\` proves verification, integration, and cleanup and returns \`done\`; \`feature_remediation\` explicitly permits Feature worktree fixes
1312
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
1313
1313
 
1314
1314
  Approval and remote actions:
@@ -2543,6 +2543,7 @@ async function runInit(options) {
2543
2543
  baseBranch: resolveInitialBaseBranch(cwd, docsRepo, projectRoot),
2544
2544
  completionStrategy: "local-ff",
2545
2545
  deleteFeatureBranchAfterMerge: true,
2546
+ featureChecks: [],
2546
2547
  postMergeChecks: []
2547
2548
  } : {},
2548
2549
  prePrReview: {
@@ -3748,6 +3749,33 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
3748
3749
  workflow.deleteFeatureBranchAfterMerge = true;
3749
3750
  changedPaths.push("workflow.deleteFeatureBranchAfterMerge");
3750
3751
  }
3752
+ if (!Object.prototype.hasOwnProperty.call(workflow, "featureChecks")) {
3753
+ workflow.featureChecks = Array.isArray(workflow.postMergeChecks) ? workflow.postMergeChecks : [];
3754
+ workflow.postMergeChecks = [];
3755
+ changedPaths.push("workflow.featureChecks");
3756
+ changedPaths.push("workflow.postMergeChecks");
3757
+ }
3758
+ setIfMissing(workflow, "featureChecks", [], "workflow.featureChecks");
3759
+ if (!Array.isArray(workflow.featureChecks)) {
3760
+ workflow.featureChecks = [];
3761
+ changedPaths.push("workflow.featureChecks");
3762
+ } else {
3763
+ const normalizedFeatureChecks = workflow.featureChecks.flatMap(
3764
+ (value) => {
3765
+ if (!isPlainObject(value) || typeof value.command !== "string") {
3766
+ return [];
3767
+ }
3768
+ const command = value.command.trim();
3769
+ if (!command) return [];
3770
+ const args = Array.isArray(value.args) ? value.args.filter((arg) => typeof arg === "string") : [];
3771
+ return [{ command, ...args.length > 0 ? { args } : {} }];
3772
+ }
3773
+ );
3774
+ if (JSON.stringify(normalizedFeatureChecks) !== JSON.stringify(workflow.featureChecks)) {
3775
+ workflow.featureChecks = normalizedFeatureChecks;
3776
+ changedPaths.push("workflow.featureChecks");
3777
+ }
3778
+ }
3751
3779
  setIfMissing(workflow, "postMergeChecks", [], "workflow.postMergeChecks");
3752
3780
  if (!Array.isArray(workflow.postMergeChecks)) {
3753
3781
  workflow.postMergeChecks = [];
@@ -7693,7 +7721,7 @@ async function resolveLocalIntegrationContext(config, feature) {
7693
7721
  const evidenceTip = resolveRef(projectRoot, evidenceRef);
7694
7722
  const featureTree = featureTip ? resolveTree(projectRoot, featureTip) : null;
7695
7723
  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;
7724
+ const squashCommitMatchesSource = completionStrategy === "local-squash" && state?.strategy === "local-squash" && !!featureTip && state.featureTip === featureTip && !!integratedCommit && !!baseTip && integratedCommit === baseTip && !!featureTree && integratedTree === featureTree && state.integratedTree === featureTree;
7697
7725
  const squashEvidencePresent = !!featureTip && evidenceTip === featureTip;
7698
7726
  const squashIntegrationComplete = squashCommitMatchesSource && squashEvidencePresent;
7699
7727
  return {
@@ -7703,12 +7731,13 @@ async function resolveLocalIntegrationContext(config, feature) {
7703
7731
  baseBranch,
7704
7732
  featureBranch,
7705
7733
  featureTip,
7734
+ featureTree,
7706
7735
  baseTip,
7707
7736
  currentBranch,
7708
7737
  state,
7709
7738
  completionStrategy,
7710
7739
  baseContainsFeature,
7711
- integrationComplete: completionStrategy === "local-squash" ? squashIntegrationComplete : baseContainsFeature,
7740
+ integrationComplete: completionStrategy === "local-squash" ? squashIntegrationComplete : !!featureTip && baseTip === featureTip,
7712
7741
  evidenceRef,
7713
7742
  squashCommitMatchesSource,
7714
7743
  squashEvidencePresent,
@@ -7717,7 +7746,10 @@ async function resolveLocalIntegrationContext(config, feature) {
7717
7746
  docsClean: isClean(feature.git.docsGitCwd),
7718
7747
  managedFeatureWorktree,
7719
7748
  featureBranchExists: !!featureBranchTip,
7720
- postMergeChecks: normalizePostMergeChecks(config.workflow?.postMergeChecks),
7749
+ featureChecks: Array.isArray(config.workflow?.featureChecks) ? normalizeWorkflowChecks(config.workflow.featureChecks) : normalizeWorkflowChecks(config.workflow?.postMergeChecks),
7750
+ // Compatibility: pre-0.9.2 postMergeChecks are treated as Feature checks.
7751
+ // Projects can opt into true post-integration checks by defining featureChecks.
7752
+ postMergeChecks: Array.isArray(config.workflow?.featureChecks) ? normalizeWorkflowChecks(config.workflow?.postMergeChecks) : [],
7721
7753
  deleteFeatureBranchAfterMerge: config.workflow?.deleteFeatureBranchAfterMerge !== false
7722
7754
  };
7723
7755
  }
@@ -7742,20 +7774,69 @@ async function readLocalIntegrationState(projectRoot, feature) {
7742
7774
  function localCleanupComplete(context) {
7743
7775
  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
7776
  }
7745
- function runPostMergeChecks(projectRoot, checks) {
7777
+ function runLocalChecks(cwd, checks, targetSha, logDir, phase) {
7746
7778
  const results = [];
7779
+ fs.ensureDirSync(logDir);
7747
7780
  for (const check of checks) {
7748
7781
  const args = Array.isArray(check.args) ? check.args.map(String) : [];
7749
- const result = runProcess(check.command, args, projectRoot);
7782
+ const startedAt = /* @__PURE__ */ new Date();
7783
+ const result = runProcess(check.command, args, cwd);
7784
+ const finishedAt = /* @__PURE__ */ new Date();
7785
+ const runtime = {
7786
+ node: process.version,
7787
+ platform: process.platform,
7788
+ arch: process.arch,
7789
+ path: process.env.PATH || ""
7790
+ };
7791
+ const index = results.length + 1;
7792
+ const logPath = path8.join(logDir, `${phase}-${String(index).padStart(2, "0")}.log`);
7793
+ const log = [
7794
+ `phase: ${phase}`,
7795
+ `targetSha: ${targetSha}`,
7796
+ `cwd: ${cwd}`,
7797
+ `command: ${JSON.stringify([check.command, ...args])}`,
7798
+ `startedAt: ${startedAt.toISOString()}`,
7799
+ `finishedAt: ${finishedAt.toISOString()}`,
7800
+ `exitCode: ${result.code}`,
7801
+ `runtime: ${JSON.stringify(runtime)}`,
7802
+ "",
7803
+ "--- stdout ---",
7804
+ result.stdout,
7805
+ "--- stderr ---",
7806
+ result.stderr
7807
+ ].join("\n");
7808
+ fs.writeFileSync(logPath, log, { encoding: "utf-8", mode: 384 });
7750
7809
  results.push({
7751
7810
  command: check.command,
7752
7811
  args,
7753
- exitCode: result.code
7812
+ cwd,
7813
+ targetSha,
7814
+ startedAt: startedAt.toISOString(),
7815
+ finishedAt: finishedAt.toISOString(),
7816
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
7817
+ exitCode: result.code,
7818
+ runtime,
7819
+ ...preview(result.stdout) ? { stdoutPreview: preview(result.stdout) } : {},
7820
+ ...preview(result.stderr) ? { stderrPreview: preview(result.stderr) } : {},
7821
+ logPath
7754
7822
  });
7755
7823
  if (result.code !== 0) break;
7756
7824
  }
7757
7825
  return results;
7758
7826
  }
7827
+ function resolveLocalIntegrationLogDir(projectRoot, feature, targetSha) {
7828
+ return path8.join(
7829
+ path8.dirname(resolveStatePath(projectRoot, feature)),
7830
+ "logs",
7831
+ resolveFeatureStateKey(feature),
7832
+ targetSha.slice(0, 12)
7833
+ );
7834
+ }
7835
+ function preview(value) {
7836
+ const trimmed = value.trim();
7837
+ return trimmed.length > 2e3 ? `${trimmed.slice(0, 2e3)}
7838
+ \u2026[truncated]` : trimmed;
7839
+ }
7759
7840
  function gitRun(cwd, args) {
7760
7841
  return runProcess("git", args, cwd);
7761
7842
  }
@@ -7847,7 +7928,7 @@ function resolveRegisteredBranchWorktree(projectRoot, branch) {
7847
7928
  }
7848
7929
  return null;
7849
7930
  }
7850
- function normalizePostMergeChecks(value) {
7931
+ function normalizeWorkflowChecks(value) {
7851
7932
  if (!Array.isArray(value)) return [];
7852
7933
  return value.filter(
7853
7934
  (entry) => !!entry && typeof entry === "object" && typeof entry.command === "string" && !!entry.command.trim()
@@ -7866,6 +7947,11 @@ var PR_STATUS_LABELS = ["PR Status", "PR \uC0C1\uD0DC"];
7866
7947
  var PRE_PR_REVIEW_LABELS = ["Pre-PR Review", "PR \uC804 \uB9AC\uBDF0"];
7867
7948
  var PRE_PR_EVIDENCE_LABELS = ["Pre-PR Evidence", "PR \uC804 \uB9AC\uBDF0 Evidence"];
7868
7949
  var PRE_PR_DECISION_LABELS = ["Pre-PR Decision", "PR \uC804 \uB9AC\uBDF0 Decision"];
7950
+ var COMPLETION_MARKERS = {
7951
+ allTasks: "lee-spec-kit:completion:all-tasks",
7952
+ tests: "lee-spec-kit:completion:tests",
7953
+ finalOutcome: "lee-spec-kit:completion:final-outcome"
7954
+ };
7869
7955
  function resolveWorkflowRequirements(config) {
7870
7956
  const workflow = config.workflow || {};
7871
7957
  const hasCanonicalMode = workflow.mode === "github" || workflow.mode === "local";
@@ -7920,6 +8006,16 @@ function withoutFencedCodeBlocks(content) {
7920
8006
  }
7921
8007
  return lines;
7922
8008
  }
8009
+ function parseCompletionCheckbox(lines, marker, legacyPattern) {
8010
+ const markerToken = `<!-- ${marker} -->`;
8011
+ const markedLines = lines.filter((line) => line.includes(markerToken));
8012
+ if (markedLines.length > 0) {
8013
+ return markedLines.length === 1 && parseMarkdownCheckbox(markedLines[0]) === true;
8014
+ }
8015
+ return lines.some(
8016
+ (line) => legacyPattern.test(line) && parseMarkdownCheckbox(line) === true
8017
+ );
8018
+ }
7923
8019
  function parseTasksDoc(content) {
7924
8020
  const issueRaw = extractFieldValue2(content, ISSUE_LABELS);
7925
8021
  const issueNumberMatch = issueRaw?.match(/^#(\d+)$/);
@@ -7940,16 +8036,20 @@ function parseTasksDoc(content) {
7940
8036
  title: match[2].trim()
7941
8037
  });
7942
8038
  }
7943
- const allTasksChecked = nonCodeLines.some(
7944
- (line) => /(All tasks are|모든 태스크가)/i.test(line) && parseMarkdownCheckbox(line) === true
8039
+ const allTasksChecked = parseCompletionCheckbox(
8040
+ nonCodeLines,
8041
+ COMPLETION_MARKERS.allTasks,
8042
+ /(All tasks are|모든 태스크가)/i
7945
8043
  );
7946
- const testsChecked = nonCodeLines.some(
7947
- (line) => /(Tests executed and passing|테스트 실행 및 통과)/i.test(line) && parseMarkdownCheckbox(line) === true
8044
+ const testsChecked = parseCompletionCheckbox(
8045
+ nonCodeLines,
8046
+ COMPLETION_MARKERS.tests,
8047
+ /(Tests executed and passing|테스트 실행 및 통과)/i
7948
8048
  );
7949
- const finalOutcomeChecked = nonCodeLines.some(
7950
- (line) => /(Final outcome shared and any required user confirmation recorded|Final user approval|최종 결과를 공유했고, 필요한 사용자 확인을 문서화된 workflow checkpoint 기준으로 기록함)/i.test(
7951
- line
7952
- ) && parseMarkdownCheckbox(line) === true
8049
+ const finalOutcomeChecked = parseCompletionCheckbox(
8050
+ nonCodeLines,
8051
+ COMPLETION_MARKERS.finalOutcome,
8052
+ /(Final outcome shared and any required user confirmation recorded|Final user approval|최종 결과를 공유했고, 필요한 사용자 확인을 문서화된 workflow checkpoint 기준으로 기록함)/i
7953
8053
  );
7954
8054
  const prStatus = (() => {
7955
8055
  const value = (extractFieldValue2(content, PR_STATUS_LABELS) || "").trim().toLowerCase();
@@ -9129,8 +9229,49 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
9129
9229
  }
9130
9230
  if (config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) !== "none") {
9131
9231
  const localState = await resolveLocalIntegrationContext(config, feature);
9232
+ const localVerifyCommand = `npx lee-spec-kit local verify ${buildFeatureArgs(feature)} --json`;
9132
9233
  const localMergeBaseCommand = `npx lee-spec-kit local merge ${buildFeatureArgs(feature)} --json`;
9133
9234
  if (!localState.integrationComplete) {
9235
+ const featureVerified = !!localState.state && ["feature_verified", "merged", "verified", "cleaned"].includes(
9236
+ localState.state.status
9237
+ ) && localState.state.verifiedFeatureTip === localState.featureTip && localState.state.verifiedFeatureTree === localState.featureTree;
9238
+ const featureVerificationFailed = localState.state?.status === "feature_failed" && localState.state.featureTip === localState.featureTip;
9239
+ if (featureVerificationFailed) {
9240
+ return {
9241
+ status: "ok",
9242
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
9243
+ docsDir: config.docsDir,
9244
+ featureRef: buildFeatureRef(feature),
9245
+ stage: "feature_remediation",
9246
+ nextAction: buildAction(
9247
+ "feature_remediation",
9248
+ `Fix the failed checks in ${localState.featureBranch} at ${localState.featureWorktree}, commit the remediation, then verify the new Feature tip. Re-run the command without code changes to retry an environmental failure.`,
9249
+ false,
9250
+ localVerifyCommand
9251
+ ),
9252
+ approvalRequired: false,
9253
+ implementationAllowed: true,
9254
+ blockedReasonCode: "FEATURE_REMEDIATION_REQUIRED"
9255
+ };
9256
+ }
9257
+ if (!featureVerified) {
9258
+ return {
9259
+ status: "ok",
9260
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
9261
+ docsDir: config.docsDir,
9262
+ featureRef: buildFeatureRef(feature),
9263
+ stage: "feature_verify",
9264
+ nextAction: buildAction(
9265
+ "feature_verify",
9266
+ `Run the configured Feature checks in ${localState.featureBranch} before integration and bind the result to its exact commit and tree.`,
9267
+ false,
9268
+ localVerifyCommand
9269
+ ),
9270
+ approvalRequired: false,
9271
+ implementationAllowed: false,
9272
+ blockedReasonCode: "FEATURE_VERIFICATION_REQUIRED"
9273
+ };
9274
+ }
9134
9275
  const approvalRequired = resolveActionApprovalRequired(
9135
9276
  config,
9136
9277
  "local_merge",
@@ -10711,6 +10852,14 @@ async function collectDocsAudit(cwd) {
10711
10852
  // src/commands/local.ts
10712
10853
  function localCommand(program2) {
10713
10854
  const local = program2.command("local").description("Integrate and clean up local workflow feature branches");
10855
+ local.command("verify [feature-name]").description("Verify the exact Feature tip before local integration").option("--component <component>", "Component name for multi projects").option("--json", "Output JSON for agents and hooks").action(
10856
+ async (featureName, options) => {
10857
+ await runLocalAction(
10858
+ options,
10859
+ () => runLocalVerify(featureName, options)
10860
+ );
10861
+ }
10862
+ );
10714
10863
  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
10864
  "--confirm <token>",
10716
10865
  "Approval token when local_merge requires approval"
@@ -10731,6 +10880,111 @@ function localCommand(program2) {
10731
10880
  }
10732
10881
  );
10733
10882
  }
10883
+ async function runLocalVerify(featureName, options) {
10884
+ const cwd = process.cwd();
10885
+ const stage = await collectWorkflowStage(cwd, featureName, options.component);
10886
+ if (stage.status !== "ok" || !stage.nextAction || !["feature_verify", "feature_remediation"].includes(stage.nextAction.category)) {
10887
+ return blocked(
10888
+ "LOCAL_FEATURE_VERIFY_STAGE_REQUIRED",
10889
+ stage.featureRef,
10890
+ "The active workflow is not at feature_verify or feature_remediation."
10891
+ );
10892
+ }
10893
+ const selection = await resolveFeatureSelection(
10894
+ cwd,
10895
+ featureName,
10896
+ options.component
10897
+ );
10898
+ if (selection.status !== "selected" || !selection.matchedFeature) {
10899
+ return blocked("FEATURE_SELECTION_REQUIRED", null);
10900
+ }
10901
+ const config = await getConfig(cwd);
10902
+ if (!config || config.workflow?.mode !== "local") {
10903
+ return blocked("LOCAL_WORKFLOW_REQUIRED", selection.matchedFeature.folderName);
10904
+ }
10905
+ const feature = selection.matchedFeature;
10906
+ const context = await resolveLocalIntegrationContext(config, feature);
10907
+ if (!context.featureTip || !context.featureTree) {
10908
+ return blocked("LOCAL_MERGE_REF_NOT_FOUND", feature.folderName);
10909
+ }
10910
+ if (!context.featureWorktreeClean || !context.docsClean) {
10911
+ return blocked(
10912
+ "LOCAL_FEATURE_VERIFY_DIRTY_WORKTREE",
10913
+ feature.folderName,
10914
+ "Feature worktree and docs repo must be clean before verification."
10915
+ );
10916
+ }
10917
+ const targetTip = context.featureTip;
10918
+ const targetTree = context.featureTree;
10919
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10920
+ const logDir = resolveLocalIntegrationLogDir(
10921
+ context.projectRoot,
10922
+ feature,
10923
+ targetTip
10924
+ );
10925
+ const verification = runLocalChecks(
10926
+ context.featureWorktree,
10927
+ context.featureChecks,
10928
+ targetTip,
10929
+ logDir,
10930
+ "feature"
10931
+ );
10932
+ const refreshed = await resolveLocalIntegrationContext(config, feature);
10933
+ const changedDuringVerification = refreshed.featureTip !== targetTip || refreshed.featureTree !== targetTree || !refreshed.featureWorktreeClean;
10934
+ const failed = verification.some((entry) => entry.exitCode !== 0);
10935
+ const baseState = {
10936
+ version: 1,
10937
+ featureRef: feature.folderName,
10938
+ component: feature.type,
10939
+ baseBranch: context.baseBranch,
10940
+ featureBranch: context.featureBranch,
10941
+ featureTip: targetTip,
10942
+ mergedBaseTip: context.baseTip || targetTip,
10943
+ strategy: context.completionStrategy,
10944
+ status: failed || changedDuringVerification ? "feature_failed" : "feature_verified",
10945
+ mergedAt: context.state?.mergedAt || now,
10946
+ featureVerifiedAt: failed || changedDuringVerification ? null : now,
10947
+ verifiedAt: null,
10948
+ cleanedAt: null,
10949
+ ...failed || changedDuringVerification ? {} : {
10950
+ verifiedFeatureTip: targetTip,
10951
+ verifiedFeatureTree: targetTree
10952
+ },
10953
+ featureVerification: verification,
10954
+ postMergeVerification: [],
10955
+ verification
10956
+ };
10957
+ await writeLocalIntegrationState(context.projectRoot, feature, baseState);
10958
+ if (changedDuringVerification) {
10959
+ return {
10960
+ ...blocked(
10961
+ "LOCAL_FEATURE_CHANGED_DURING_VERIFICATION",
10962
+ feature.folderName,
10963
+ "The Feature tip, tree, or worktree changed while checks were running."
10964
+ ),
10965
+ featureTip: refreshed.featureTip,
10966
+ verification
10967
+ };
10968
+ }
10969
+ if (failed) {
10970
+ return {
10971
+ ...blocked("LOCAL_FEATURE_CHECK_FAILED", feature.folderName),
10972
+ featureTip: targetTip,
10973
+ verification
10974
+ };
10975
+ }
10976
+ return {
10977
+ status: "ok",
10978
+ reasonCode: "LOCAL_FEATURE_VERIFIED",
10979
+ featureRef: feature.folderName,
10980
+ baseBranch: context.baseBranch,
10981
+ featureBranch: context.featureBranch,
10982
+ featureTip: targetTip,
10983
+ baseTip: context.baseTip,
10984
+ completionStrategy: context.completionStrategy,
10985
+ verification
10986
+ };
10987
+ }
10734
10988
  async function runLocalAction(options, action) {
10735
10989
  try {
10736
10990
  const payload = await action();
@@ -10802,6 +11056,15 @@ async function runLocalMerge(featureName, options) {
10802
11056
  "Both the configured base branch and Feature tip must exist."
10803
11057
  );
10804
11058
  }
11059
+ if (!context.integrationComplete && (!context.state || !["feature_verified", "merged", "verified", "cleaned"].includes(
11060
+ context.state.status
11061
+ ) || context.state.verifiedFeatureTip !== context.featureTip || context.state.verifiedFeatureTree !== context.featureTree)) {
11062
+ return blocked(
11063
+ "LOCAL_FEATURE_VERIFICATION_REQUIRED",
11064
+ feature.folderName,
11065
+ "Verify the exact Feature tip before local integration."
11066
+ );
11067
+ }
10805
11068
  if (!context.featureWorktreeClean || !context.projectRootClean || !context.docsClean) {
10806
11069
  return blocked(
10807
11070
  "LOCAL_MERGE_DIRTY_WORKTREE",
@@ -10810,6 +11073,7 @@ async function runLocalMerge(featureName, options) {
10810
11073
  );
10811
11074
  }
10812
11075
  const now = (/* @__PURE__ */ new Date()).toISOString();
11076
+ const originalBaseTip = context.baseTip;
10813
11077
  const previousStateStrategy = context.state?.strategy || "local-ff";
10814
11078
  let state = context.state && context.state.featureTip === context.featureTip && previousStateStrategy === context.completionStrategy ? context.state : {
10815
11079
  version: 1,
@@ -10882,6 +11146,7 @@ async function runLocalMerge(featureName, options) {
10882
11146
  mergedBaseTip: mergedBaseTip || context.featureTip,
10883
11147
  strategy: context.completionStrategy,
10884
11148
  integratedCommit: mergedBaseTip || context.featureTip,
11149
+ originalBaseTip,
10885
11150
  ...integratedTree ? { integratedTree } : {},
10886
11151
  ...context.completionStrategy === "local-squash" ? { evidenceRef: context.evidenceRef } : {},
10887
11152
  status: "merged",
@@ -10896,16 +11161,36 @@ async function runLocalMerge(featureName, options) {
10896
11161
  if (!context.integrationComplete) {
10897
11162
  return blocked("LOCAL_MERGE_VERIFICATION_FAILED", feature.folderName);
10898
11163
  }
10899
- const verification = runPostMergeChecks(
11164
+ const integratedTip = resolveRef(
11165
+ context.projectRoot,
11166
+ `refs/heads/${context.baseBranch}`
11167
+ );
11168
+ const verificationTarget = integratedTip || state.featureTip;
11169
+ const postMergeVerification = runLocalChecks(
10900
11170
  context.projectRoot,
10901
- context.postMergeChecks
11171
+ context.postMergeChecks,
11172
+ verificationTarget,
11173
+ resolveLocalIntegrationLogDir(
11174
+ context.projectRoot,
11175
+ feature,
11176
+ verificationTarget
11177
+ ),
11178
+ "post-merge"
10902
11179
  );
10903
- const failedCheck = verification.find((entry) => entry.exitCode !== 0);
11180
+ const featureVerification = state.featureVerification || context.state?.featureVerification || [];
11181
+ const verification = [...featureVerification, ...postMergeVerification];
11182
+ const failedCheck = postMergeVerification.find((entry) => entry.exitCode !== 0);
10904
11183
  if (failedCheck) {
11184
+ rollbackLocalIntegration(context, originalBaseTip);
10905
11185
  await writeLocalIntegrationState(context.projectRoot, feature, {
10906
11186
  ...state,
10907
- status: "merged",
11187
+ status: "feature_failed",
11188
+ mergedBaseTip: originalBaseTip,
11189
+ integratedCommit: void 0,
11190
+ integratedTree: void 0,
10908
11191
  verification,
11192
+ featureVerification,
11193
+ postMergeVerification,
10909
11194
  verifiedAt: null
10910
11195
  });
10911
11196
  return {
@@ -10931,7 +11216,9 @@ async function runLocalMerge(featureName, options) {
10931
11216
  mergedBaseTip: baseTip || verifiedFeatureTip,
10932
11217
  status: "verified",
10933
11218
  verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
10934
- verification
11219
+ verification,
11220
+ featureVerification,
11221
+ postMergeVerification
10935
11222
  };
10936
11223
  await writeLocalIntegrationState(context.projectRoot, feature, verifiedState);
10937
11224
  return {
@@ -10946,6 +11233,15 @@ async function runLocalMerge(featureName, options) {
10946
11233
  verification
10947
11234
  };
10948
11235
  }
11236
+ function rollbackLocalIntegration(context, originalBaseTip) {
11237
+ gitRun(context.projectRoot, ["reset", "--hard", originalBaseTip]);
11238
+ if (context.completionStrategy === "local-squash") {
11239
+ gitRun(context.projectRoot, ["update-ref", "-d", context.evidenceRef]);
11240
+ }
11241
+ if (!context.managedFeatureWorktree) {
11242
+ gitRun(context.projectRoot, ["checkout", context.featureBranch]);
11243
+ }
11244
+ }
10949
11245
  async function runLocalCleanup(featureName, options) {
10950
11246
  const cwd = process.cwd();
10951
11247
  const stage = await collectWorkflowStage(cwd, featureName, options.component);