lee-spec-kit 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/dist/index.js +339 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/en/common/README.md +3 -1
- package/templates/en/common/agents/agents.md +1 -1
- package/templates/en/common/features/README.md +5 -1
- package/templates/en/common/features/feature-base/tasks.md +3 -3
- package/templates/ko/common/README.md +3 -1
- package/templates/ko/common/agents/agents.md +1 -1
- package/templates/ko/common/features/README.md +5 -1
- package/templates/ko/common/features/feature-base/tasks.md +3 -3
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
|
|
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
|
|
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
|
|
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,9 +7721,17 @@ 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 &&
|
|
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;
|
|
7727
|
+
const recordedStrategy = state?.strategy || "local-ff";
|
|
7728
|
+
const recordedIntegratedCommit = state?.integratedCommit || state?.mergedBaseTip || null;
|
|
7729
|
+
const baseContainsRecordedIntegration = !!recordedIntegratedCommit && isAncestor(
|
|
7730
|
+
projectRoot,
|
|
7731
|
+
recordedIntegratedCommit,
|
|
7732
|
+
`refs/heads/${baseBranch}`
|
|
7733
|
+
);
|
|
7734
|
+
const cleanedIntegrationStillValid = state?.status === "cleaned" && recordedStrategy === completionStrategy && state.featureTip === featureTip && baseContainsRecordedIntegration && (completionStrategy === "local-squash" ? state.integratedCommit === recordedIntegratedCommit && state.integratedTree === featureTree && squashEvidencePresent : state.mergedBaseTip === featureTip && baseContainsFeature);
|
|
7699
7735
|
return {
|
|
7700
7736
|
projectRoot,
|
|
7701
7737
|
featureWorktree: effectiveFeatureWorktree,
|
|
@@ -7703,12 +7739,14 @@ async function resolveLocalIntegrationContext(config, feature) {
|
|
|
7703
7739
|
baseBranch,
|
|
7704
7740
|
featureBranch,
|
|
7705
7741
|
featureTip,
|
|
7742
|
+
featureTree,
|
|
7706
7743
|
baseTip,
|
|
7707
7744
|
currentBranch,
|
|
7708
7745
|
state,
|
|
7709
7746
|
completionStrategy,
|
|
7710
7747
|
baseContainsFeature,
|
|
7711
|
-
integrationComplete: completionStrategy === "local-squash" ? squashIntegrationComplete :
|
|
7748
|
+
integrationComplete: completionStrategy === "local-squash" ? squashIntegrationComplete : !!featureTip && baseTip === featureTip,
|
|
7749
|
+
cleanedIntegrationStillValid,
|
|
7712
7750
|
evidenceRef,
|
|
7713
7751
|
squashCommitMatchesSource,
|
|
7714
7752
|
squashEvidencePresent,
|
|
@@ -7717,7 +7755,10 @@ async function resolveLocalIntegrationContext(config, feature) {
|
|
|
7717
7755
|
docsClean: isClean(feature.git.docsGitCwd),
|
|
7718
7756
|
managedFeatureWorktree,
|
|
7719
7757
|
featureBranchExists: !!featureBranchTip,
|
|
7720
|
-
|
|
7758
|
+
featureChecks: Array.isArray(config.workflow?.featureChecks) ? normalizeWorkflowChecks(config.workflow.featureChecks) : normalizeWorkflowChecks(config.workflow?.postMergeChecks),
|
|
7759
|
+
// Compatibility: pre-0.9.2 postMergeChecks are treated as Feature checks.
|
|
7760
|
+
// Projects can opt into true post-integration checks by defining featureChecks.
|
|
7761
|
+
postMergeChecks: Array.isArray(config.workflow?.featureChecks) ? normalizeWorkflowChecks(config.workflow?.postMergeChecks) : [],
|
|
7721
7762
|
deleteFeatureBranchAfterMerge: config.workflow?.deleteFeatureBranchAfterMerge !== false
|
|
7722
7763
|
};
|
|
7723
7764
|
}
|
|
@@ -7742,20 +7783,69 @@ async function readLocalIntegrationState(projectRoot, feature) {
|
|
|
7742
7783
|
function localCleanupComplete(context) {
|
|
7743
7784
|
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
7785
|
}
|
|
7745
|
-
function
|
|
7786
|
+
function runLocalChecks(cwd, checks, targetSha, logDir, phase) {
|
|
7746
7787
|
const results = [];
|
|
7788
|
+
fs.ensureDirSync(logDir);
|
|
7747
7789
|
for (const check of checks) {
|
|
7748
7790
|
const args = Array.isArray(check.args) ? check.args.map(String) : [];
|
|
7749
|
-
const
|
|
7791
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
7792
|
+
const result = runProcess(check.command, args, cwd);
|
|
7793
|
+
const finishedAt = /* @__PURE__ */ new Date();
|
|
7794
|
+
const runtime = {
|
|
7795
|
+
node: process.version,
|
|
7796
|
+
platform: process.platform,
|
|
7797
|
+
arch: process.arch,
|
|
7798
|
+
path: process.env.PATH || ""
|
|
7799
|
+
};
|
|
7800
|
+
const index = results.length + 1;
|
|
7801
|
+
const logPath = path8.join(logDir, `${phase}-${String(index).padStart(2, "0")}.log`);
|
|
7802
|
+
const log = [
|
|
7803
|
+
`phase: ${phase}`,
|
|
7804
|
+
`targetSha: ${targetSha}`,
|
|
7805
|
+
`cwd: ${cwd}`,
|
|
7806
|
+
`command: ${JSON.stringify([check.command, ...args])}`,
|
|
7807
|
+
`startedAt: ${startedAt.toISOString()}`,
|
|
7808
|
+
`finishedAt: ${finishedAt.toISOString()}`,
|
|
7809
|
+
`exitCode: ${result.code}`,
|
|
7810
|
+
`runtime: ${JSON.stringify(runtime)}`,
|
|
7811
|
+
"",
|
|
7812
|
+
"--- stdout ---",
|
|
7813
|
+
result.stdout,
|
|
7814
|
+
"--- stderr ---",
|
|
7815
|
+
result.stderr
|
|
7816
|
+
].join("\n");
|
|
7817
|
+
fs.writeFileSync(logPath, log, { encoding: "utf-8", mode: 384 });
|
|
7750
7818
|
results.push({
|
|
7751
7819
|
command: check.command,
|
|
7752
7820
|
args,
|
|
7753
|
-
|
|
7821
|
+
cwd,
|
|
7822
|
+
targetSha,
|
|
7823
|
+
startedAt: startedAt.toISOString(),
|
|
7824
|
+
finishedAt: finishedAt.toISOString(),
|
|
7825
|
+
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
7826
|
+
exitCode: result.code,
|
|
7827
|
+
runtime,
|
|
7828
|
+
...preview(result.stdout) ? { stdoutPreview: preview(result.stdout) } : {},
|
|
7829
|
+
...preview(result.stderr) ? { stderrPreview: preview(result.stderr) } : {},
|
|
7830
|
+
logPath
|
|
7754
7831
|
});
|
|
7755
7832
|
if (result.code !== 0) break;
|
|
7756
7833
|
}
|
|
7757
7834
|
return results;
|
|
7758
7835
|
}
|
|
7836
|
+
function resolveLocalIntegrationLogDir(projectRoot, feature, targetSha) {
|
|
7837
|
+
return path8.join(
|
|
7838
|
+
path8.dirname(resolveStatePath(projectRoot, feature)),
|
|
7839
|
+
"logs",
|
|
7840
|
+
resolveFeatureStateKey(feature),
|
|
7841
|
+
targetSha.slice(0, 12)
|
|
7842
|
+
);
|
|
7843
|
+
}
|
|
7844
|
+
function preview(value) {
|
|
7845
|
+
const trimmed = value.trim();
|
|
7846
|
+
return trimmed.length > 2e3 ? `${trimmed.slice(0, 2e3)}
|
|
7847
|
+
\u2026[truncated]` : trimmed;
|
|
7848
|
+
}
|
|
7759
7849
|
function gitRun(cwd, args) {
|
|
7760
7850
|
return runProcess("git", args, cwd);
|
|
7761
7851
|
}
|
|
@@ -7847,7 +7937,7 @@ function resolveRegisteredBranchWorktree(projectRoot, branch) {
|
|
|
7847
7937
|
}
|
|
7848
7938
|
return null;
|
|
7849
7939
|
}
|
|
7850
|
-
function
|
|
7940
|
+
function normalizeWorkflowChecks(value) {
|
|
7851
7941
|
if (!Array.isArray(value)) return [];
|
|
7852
7942
|
return value.filter(
|
|
7853
7943
|
(entry) => !!entry && typeof entry === "object" && typeof entry.command === "string" && !!entry.command.trim()
|
|
@@ -7866,6 +7956,11 @@ var PR_STATUS_LABELS = ["PR Status", "PR \uC0C1\uD0DC"];
|
|
|
7866
7956
|
var PRE_PR_REVIEW_LABELS = ["Pre-PR Review", "PR \uC804 \uB9AC\uBDF0"];
|
|
7867
7957
|
var PRE_PR_EVIDENCE_LABELS = ["Pre-PR Evidence", "PR \uC804 \uB9AC\uBDF0 Evidence"];
|
|
7868
7958
|
var PRE_PR_DECISION_LABELS = ["Pre-PR Decision", "PR \uC804 \uB9AC\uBDF0 Decision"];
|
|
7959
|
+
var COMPLETION_MARKERS = {
|
|
7960
|
+
allTasks: "lee-spec-kit:completion:all-tasks",
|
|
7961
|
+
tests: "lee-spec-kit:completion:tests",
|
|
7962
|
+
finalOutcome: "lee-spec-kit:completion:final-outcome"
|
|
7963
|
+
};
|
|
7869
7964
|
function resolveWorkflowRequirements(config) {
|
|
7870
7965
|
const workflow = config.workflow || {};
|
|
7871
7966
|
const hasCanonicalMode = workflow.mode === "github" || workflow.mode === "local";
|
|
@@ -7920,6 +8015,16 @@ function withoutFencedCodeBlocks(content) {
|
|
|
7920
8015
|
}
|
|
7921
8016
|
return lines;
|
|
7922
8017
|
}
|
|
8018
|
+
function parseCompletionCheckbox(lines, marker, legacyPattern) {
|
|
8019
|
+
const markerToken = `<!-- ${marker} -->`;
|
|
8020
|
+
const markedLines = lines.filter((line) => line.includes(markerToken));
|
|
8021
|
+
if (markedLines.length > 0) {
|
|
8022
|
+
return markedLines.length === 1 && parseMarkdownCheckbox(markedLines[0]) === true;
|
|
8023
|
+
}
|
|
8024
|
+
return lines.some(
|
|
8025
|
+
(line) => legacyPattern.test(line) && parseMarkdownCheckbox(line) === true
|
|
8026
|
+
);
|
|
8027
|
+
}
|
|
7923
8028
|
function parseTasksDoc(content) {
|
|
7924
8029
|
const issueRaw = extractFieldValue2(content, ISSUE_LABELS);
|
|
7925
8030
|
const issueNumberMatch = issueRaw?.match(/^#(\d+)$/);
|
|
@@ -7940,16 +8045,20 @@ function parseTasksDoc(content) {
|
|
|
7940
8045
|
title: match[2].trim()
|
|
7941
8046
|
});
|
|
7942
8047
|
}
|
|
7943
|
-
const allTasksChecked =
|
|
7944
|
-
|
|
8048
|
+
const allTasksChecked = parseCompletionCheckbox(
|
|
8049
|
+
nonCodeLines,
|
|
8050
|
+
COMPLETION_MARKERS.allTasks,
|
|
8051
|
+
/(All tasks are|모든 태스크가)/i
|
|
7945
8052
|
);
|
|
7946
|
-
const testsChecked =
|
|
7947
|
-
|
|
8053
|
+
const testsChecked = parseCompletionCheckbox(
|
|
8054
|
+
nonCodeLines,
|
|
8055
|
+
COMPLETION_MARKERS.tests,
|
|
8056
|
+
/(Tests executed and passing|테스트 실행 및 통과)/i
|
|
7948
8057
|
);
|
|
7949
|
-
const finalOutcomeChecked =
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
8058
|
+
const finalOutcomeChecked = parseCompletionCheckbox(
|
|
8059
|
+
nonCodeLines,
|
|
8060
|
+
COMPLETION_MARKERS.finalOutcome,
|
|
8061
|
+
/(Final outcome shared and any required user confirmation recorded|Final user approval|최종 결과를 공유했고, 필요한 사용자 확인을 문서화된 workflow checkpoint 기준으로 기록함)/i
|
|
7953
8062
|
);
|
|
7954
8063
|
const prStatus = (() => {
|
|
7955
8064
|
const value = (extractFieldValue2(content, PR_STATUS_LABELS) || "").trim().toLowerCase();
|
|
@@ -9129,8 +9238,62 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
9129
9238
|
}
|
|
9130
9239
|
if (config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) !== "none") {
|
|
9131
9240
|
const localState = await resolveLocalIntegrationContext(config, feature);
|
|
9241
|
+
const localVerifyCommand = `npx lee-spec-kit local verify ${buildFeatureArgs(feature)} --json`;
|
|
9132
9242
|
const localMergeBaseCommand = `npx lee-spec-kit local merge ${buildFeatureArgs(feature)} --json`;
|
|
9243
|
+
if (localState.cleanedIntegrationStillValid) {
|
|
9244
|
+
return {
|
|
9245
|
+
status: "ok",
|
|
9246
|
+
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
9247
|
+
docsDir: config.docsDir,
|
|
9248
|
+
featureRef: buildFeatureRef(feature),
|
|
9249
|
+
stage: "done",
|
|
9250
|
+
nextAction: null,
|
|
9251
|
+
approvalRequired: false,
|
|
9252
|
+
implementationAllowed: false,
|
|
9253
|
+
blockedReasonCode: null
|
|
9254
|
+
};
|
|
9255
|
+
}
|
|
9133
9256
|
if (!localState.integrationComplete) {
|
|
9257
|
+
const featureVerified = !!localState.state && ["feature_verified", "merged", "verified", "cleaned"].includes(
|
|
9258
|
+
localState.state.status
|
|
9259
|
+
) && localState.state.verifiedFeatureTip === localState.featureTip && localState.state.verifiedFeatureTree === localState.featureTree;
|
|
9260
|
+
const featureVerificationFailed = localState.state?.status === "feature_failed" && localState.state.featureTip === localState.featureTip;
|
|
9261
|
+
if (featureVerificationFailed) {
|
|
9262
|
+
return {
|
|
9263
|
+
status: "ok",
|
|
9264
|
+
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
9265
|
+
docsDir: config.docsDir,
|
|
9266
|
+
featureRef: buildFeatureRef(feature),
|
|
9267
|
+
stage: "feature_remediation",
|
|
9268
|
+
nextAction: buildAction(
|
|
9269
|
+
"feature_remediation",
|
|
9270
|
+
`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.`,
|
|
9271
|
+
false,
|
|
9272
|
+
localVerifyCommand
|
|
9273
|
+
),
|
|
9274
|
+
approvalRequired: false,
|
|
9275
|
+
implementationAllowed: true,
|
|
9276
|
+
blockedReasonCode: "FEATURE_REMEDIATION_REQUIRED"
|
|
9277
|
+
};
|
|
9278
|
+
}
|
|
9279
|
+
if (!featureVerified) {
|
|
9280
|
+
return {
|
|
9281
|
+
status: "ok",
|
|
9282
|
+
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
9283
|
+
docsDir: config.docsDir,
|
|
9284
|
+
featureRef: buildFeatureRef(feature),
|
|
9285
|
+
stage: "feature_verify",
|
|
9286
|
+
nextAction: buildAction(
|
|
9287
|
+
"feature_verify",
|
|
9288
|
+
`Run the configured Feature checks in ${localState.featureBranch} before integration and bind the result to its exact commit and tree.`,
|
|
9289
|
+
false,
|
|
9290
|
+
localVerifyCommand
|
|
9291
|
+
),
|
|
9292
|
+
approvalRequired: false,
|
|
9293
|
+
implementationAllowed: false,
|
|
9294
|
+
blockedReasonCode: "FEATURE_VERIFICATION_REQUIRED"
|
|
9295
|
+
};
|
|
9296
|
+
}
|
|
9134
9297
|
const approvalRequired = resolveActionApprovalRequired(
|
|
9135
9298
|
config,
|
|
9136
9299
|
"local_merge",
|
|
@@ -10711,6 +10874,14 @@ async function collectDocsAudit(cwd) {
|
|
|
10711
10874
|
// src/commands/local.ts
|
|
10712
10875
|
function localCommand(program2) {
|
|
10713
10876
|
const local = program2.command("local").description("Integrate and clean up local workflow feature branches");
|
|
10877
|
+
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(
|
|
10878
|
+
async (featureName, options) => {
|
|
10879
|
+
await runLocalAction(
|
|
10880
|
+
options,
|
|
10881
|
+
() => runLocalVerify(featureName, options)
|
|
10882
|
+
);
|
|
10883
|
+
}
|
|
10884
|
+
);
|
|
10714
10885
|
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
10886
|
"--confirm <token>",
|
|
10716
10887
|
"Approval token when local_merge requires approval"
|
|
@@ -10731,6 +10902,111 @@ function localCommand(program2) {
|
|
|
10731
10902
|
}
|
|
10732
10903
|
);
|
|
10733
10904
|
}
|
|
10905
|
+
async function runLocalVerify(featureName, options) {
|
|
10906
|
+
const cwd = process.cwd();
|
|
10907
|
+
const stage = await collectWorkflowStage(cwd, featureName, options.component);
|
|
10908
|
+
if (stage.status !== "ok" || !stage.nextAction || !["feature_verify", "feature_remediation"].includes(stage.nextAction.category)) {
|
|
10909
|
+
return blocked(
|
|
10910
|
+
"LOCAL_FEATURE_VERIFY_STAGE_REQUIRED",
|
|
10911
|
+
stage.featureRef,
|
|
10912
|
+
"The active workflow is not at feature_verify or feature_remediation."
|
|
10913
|
+
);
|
|
10914
|
+
}
|
|
10915
|
+
const selection = await resolveFeatureSelection(
|
|
10916
|
+
cwd,
|
|
10917
|
+
featureName,
|
|
10918
|
+
options.component
|
|
10919
|
+
);
|
|
10920
|
+
if (selection.status !== "selected" || !selection.matchedFeature) {
|
|
10921
|
+
return blocked("FEATURE_SELECTION_REQUIRED", null);
|
|
10922
|
+
}
|
|
10923
|
+
const config = await getConfig(cwd);
|
|
10924
|
+
if (!config || config.workflow?.mode !== "local") {
|
|
10925
|
+
return blocked("LOCAL_WORKFLOW_REQUIRED", selection.matchedFeature.folderName);
|
|
10926
|
+
}
|
|
10927
|
+
const feature = selection.matchedFeature;
|
|
10928
|
+
const context = await resolveLocalIntegrationContext(config, feature);
|
|
10929
|
+
if (!context.featureTip || !context.featureTree) {
|
|
10930
|
+
return blocked("LOCAL_MERGE_REF_NOT_FOUND", feature.folderName);
|
|
10931
|
+
}
|
|
10932
|
+
if (!context.featureWorktreeClean || !context.docsClean) {
|
|
10933
|
+
return blocked(
|
|
10934
|
+
"LOCAL_FEATURE_VERIFY_DIRTY_WORKTREE",
|
|
10935
|
+
feature.folderName,
|
|
10936
|
+
"Feature worktree and docs repo must be clean before verification."
|
|
10937
|
+
);
|
|
10938
|
+
}
|
|
10939
|
+
const targetTip = context.featureTip;
|
|
10940
|
+
const targetTree = context.featureTree;
|
|
10941
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10942
|
+
const logDir = resolveLocalIntegrationLogDir(
|
|
10943
|
+
context.projectRoot,
|
|
10944
|
+
feature,
|
|
10945
|
+
targetTip
|
|
10946
|
+
);
|
|
10947
|
+
const verification = runLocalChecks(
|
|
10948
|
+
context.featureWorktree,
|
|
10949
|
+
context.featureChecks,
|
|
10950
|
+
targetTip,
|
|
10951
|
+
logDir,
|
|
10952
|
+
"feature"
|
|
10953
|
+
);
|
|
10954
|
+
const refreshed = await resolveLocalIntegrationContext(config, feature);
|
|
10955
|
+
const changedDuringVerification = refreshed.featureTip !== targetTip || refreshed.featureTree !== targetTree || !refreshed.featureWorktreeClean;
|
|
10956
|
+
const failed = verification.some((entry) => entry.exitCode !== 0);
|
|
10957
|
+
const baseState = {
|
|
10958
|
+
version: 1,
|
|
10959
|
+
featureRef: feature.folderName,
|
|
10960
|
+
component: feature.type,
|
|
10961
|
+
baseBranch: context.baseBranch,
|
|
10962
|
+
featureBranch: context.featureBranch,
|
|
10963
|
+
featureTip: targetTip,
|
|
10964
|
+
mergedBaseTip: context.baseTip || targetTip,
|
|
10965
|
+
strategy: context.completionStrategy,
|
|
10966
|
+
status: failed || changedDuringVerification ? "feature_failed" : "feature_verified",
|
|
10967
|
+
mergedAt: context.state?.mergedAt || now,
|
|
10968
|
+
featureVerifiedAt: failed || changedDuringVerification ? null : now,
|
|
10969
|
+
verifiedAt: null,
|
|
10970
|
+
cleanedAt: null,
|
|
10971
|
+
...failed || changedDuringVerification ? {} : {
|
|
10972
|
+
verifiedFeatureTip: targetTip,
|
|
10973
|
+
verifiedFeatureTree: targetTree
|
|
10974
|
+
},
|
|
10975
|
+
featureVerification: verification,
|
|
10976
|
+
postMergeVerification: [],
|
|
10977
|
+
verification
|
|
10978
|
+
};
|
|
10979
|
+
await writeLocalIntegrationState(context.projectRoot, feature, baseState);
|
|
10980
|
+
if (changedDuringVerification) {
|
|
10981
|
+
return {
|
|
10982
|
+
...blocked(
|
|
10983
|
+
"LOCAL_FEATURE_CHANGED_DURING_VERIFICATION",
|
|
10984
|
+
feature.folderName,
|
|
10985
|
+
"The Feature tip, tree, or worktree changed while checks were running."
|
|
10986
|
+
),
|
|
10987
|
+
featureTip: refreshed.featureTip,
|
|
10988
|
+
verification
|
|
10989
|
+
};
|
|
10990
|
+
}
|
|
10991
|
+
if (failed) {
|
|
10992
|
+
return {
|
|
10993
|
+
...blocked("LOCAL_FEATURE_CHECK_FAILED", feature.folderName),
|
|
10994
|
+
featureTip: targetTip,
|
|
10995
|
+
verification
|
|
10996
|
+
};
|
|
10997
|
+
}
|
|
10998
|
+
return {
|
|
10999
|
+
status: "ok",
|
|
11000
|
+
reasonCode: "LOCAL_FEATURE_VERIFIED",
|
|
11001
|
+
featureRef: feature.folderName,
|
|
11002
|
+
baseBranch: context.baseBranch,
|
|
11003
|
+
featureBranch: context.featureBranch,
|
|
11004
|
+
featureTip: targetTip,
|
|
11005
|
+
baseTip: context.baseTip,
|
|
11006
|
+
completionStrategy: context.completionStrategy,
|
|
11007
|
+
verification
|
|
11008
|
+
};
|
|
11009
|
+
}
|
|
10734
11010
|
async function runLocalAction(options, action) {
|
|
10735
11011
|
try {
|
|
10736
11012
|
const payload = await action();
|
|
@@ -10802,6 +11078,15 @@ async function runLocalMerge(featureName, options) {
|
|
|
10802
11078
|
"Both the configured base branch and Feature tip must exist."
|
|
10803
11079
|
);
|
|
10804
11080
|
}
|
|
11081
|
+
if (!context.integrationComplete && (!context.state || !["feature_verified", "merged", "verified", "cleaned"].includes(
|
|
11082
|
+
context.state.status
|
|
11083
|
+
) || context.state.verifiedFeatureTip !== context.featureTip || context.state.verifiedFeatureTree !== context.featureTree)) {
|
|
11084
|
+
return blocked(
|
|
11085
|
+
"LOCAL_FEATURE_VERIFICATION_REQUIRED",
|
|
11086
|
+
feature.folderName,
|
|
11087
|
+
"Verify the exact Feature tip before local integration."
|
|
11088
|
+
);
|
|
11089
|
+
}
|
|
10805
11090
|
if (!context.featureWorktreeClean || !context.projectRootClean || !context.docsClean) {
|
|
10806
11091
|
return blocked(
|
|
10807
11092
|
"LOCAL_MERGE_DIRTY_WORKTREE",
|
|
@@ -10810,6 +11095,7 @@ async function runLocalMerge(featureName, options) {
|
|
|
10810
11095
|
);
|
|
10811
11096
|
}
|
|
10812
11097
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11098
|
+
const originalBaseTip = context.baseTip;
|
|
10813
11099
|
const previousStateStrategy = context.state?.strategy || "local-ff";
|
|
10814
11100
|
let state = context.state && context.state.featureTip === context.featureTip && previousStateStrategy === context.completionStrategy ? context.state : {
|
|
10815
11101
|
version: 1,
|
|
@@ -10882,6 +11168,7 @@ async function runLocalMerge(featureName, options) {
|
|
|
10882
11168
|
mergedBaseTip: mergedBaseTip || context.featureTip,
|
|
10883
11169
|
strategy: context.completionStrategy,
|
|
10884
11170
|
integratedCommit: mergedBaseTip || context.featureTip,
|
|
11171
|
+
originalBaseTip,
|
|
10885
11172
|
...integratedTree ? { integratedTree } : {},
|
|
10886
11173
|
...context.completionStrategy === "local-squash" ? { evidenceRef: context.evidenceRef } : {},
|
|
10887
11174
|
status: "merged",
|
|
@@ -10896,16 +11183,36 @@ async function runLocalMerge(featureName, options) {
|
|
|
10896
11183
|
if (!context.integrationComplete) {
|
|
10897
11184
|
return blocked("LOCAL_MERGE_VERIFICATION_FAILED", feature.folderName);
|
|
10898
11185
|
}
|
|
10899
|
-
const
|
|
11186
|
+
const integratedTip = resolveRef(
|
|
10900
11187
|
context.projectRoot,
|
|
10901
|
-
context.
|
|
11188
|
+
`refs/heads/${context.baseBranch}`
|
|
10902
11189
|
);
|
|
10903
|
-
const
|
|
11190
|
+
const verificationTarget = integratedTip || state.featureTip;
|
|
11191
|
+
const postMergeVerification = runLocalChecks(
|
|
11192
|
+
context.projectRoot,
|
|
11193
|
+
context.postMergeChecks,
|
|
11194
|
+
verificationTarget,
|
|
11195
|
+
resolveLocalIntegrationLogDir(
|
|
11196
|
+
context.projectRoot,
|
|
11197
|
+
feature,
|
|
11198
|
+
verificationTarget
|
|
11199
|
+
),
|
|
11200
|
+
"post-merge"
|
|
11201
|
+
);
|
|
11202
|
+
const featureVerification = state.featureVerification || context.state?.featureVerification || [];
|
|
11203
|
+
const verification = [...featureVerification, ...postMergeVerification];
|
|
11204
|
+
const failedCheck = postMergeVerification.find((entry) => entry.exitCode !== 0);
|
|
10904
11205
|
if (failedCheck) {
|
|
11206
|
+
rollbackLocalIntegration(context, originalBaseTip);
|
|
10905
11207
|
await writeLocalIntegrationState(context.projectRoot, feature, {
|
|
10906
11208
|
...state,
|
|
10907
|
-
status: "
|
|
11209
|
+
status: "feature_failed",
|
|
11210
|
+
mergedBaseTip: originalBaseTip,
|
|
11211
|
+
integratedCommit: void 0,
|
|
11212
|
+
integratedTree: void 0,
|
|
10908
11213
|
verification,
|
|
11214
|
+
featureVerification,
|
|
11215
|
+
postMergeVerification,
|
|
10909
11216
|
verifiedAt: null
|
|
10910
11217
|
});
|
|
10911
11218
|
return {
|
|
@@ -10931,7 +11238,9 @@ async function runLocalMerge(featureName, options) {
|
|
|
10931
11238
|
mergedBaseTip: baseTip || verifiedFeatureTip,
|
|
10932
11239
|
status: "verified",
|
|
10933
11240
|
verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10934
|
-
verification
|
|
11241
|
+
verification,
|
|
11242
|
+
featureVerification,
|
|
11243
|
+
postMergeVerification
|
|
10935
11244
|
};
|
|
10936
11245
|
await writeLocalIntegrationState(context.projectRoot, feature, verifiedState);
|
|
10937
11246
|
return {
|
|
@@ -10946,6 +11255,15 @@ async function runLocalMerge(featureName, options) {
|
|
|
10946
11255
|
verification
|
|
10947
11256
|
};
|
|
10948
11257
|
}
|
|
11258
|
+
function rollbackLocalIntegration(context, originalBaseTip) {
|
|
11259
|
+
gitRun(context.projectRoot, ["reset", "--hard", originalBaseTip]);
|
|
11260
|
+
if (context.completionStrategy === "local-squash") {
|
|
11261
|
+
gitRun(context.projectRoot, ["update-ref", "-d", context.evidenceRef]);
|
|
11262
|
+
}
|
|
11263
|
+
if (!context.managedFeatureWorktree) {
|
|
11264
|
+
gitRun(context.projectRoot, ["checkout", context.featureBranch]);
|
|
11265
|
+
}
|
|
11266
|
+
}
|
|
10949
11267
|
async function runLocalCleanup(featureName, options) {
|
|
10950
11268
|
const cwd = process.cwd();
|
|
10951
11269
|
const stage = await collectWorkflowStage(cwd, featureName, options.component);
|