lee-spec-kit 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -1
- package/README.md +2 -2
- package/dist/bootstrap-Q77MTW3Q.js +0 -0
- package/dist/chunk-3AFCPGGS.js +0 -0
- package/dist/chunk-7V7RMGEU.js +0 -0
- package/dist/chunk-GR7JQBWF.js +0 -0
- package/dist/hooks-373Z6JG2.js +0 -0
- package/dist/index.js +285 -70
- package/dist/index.js.map +1 -1
- package/package.json +13 -15
- package/templates/en/common/README.md +8 -8
- package/templates/en/common/agents/agents.md +8 -2
- package/templates/en/common/agents/git-workflow.md +27 -8
- package/templates/en/common/agents/skills/split-feature.md +2 -1
- package/templates/en/common/agents/ui-ux-design.md +128 -0
- package/templates/en/common/designs/README.md +39 -1
- package/templates/en/common/features/README.md +2 -2
- package/templates/en/common/features/feature-base/decisions.md +3 -1
- package/templates/en/common/features/feature-base/spec.md +3 -0
- package/templates/en/common/features/feature-base/tasks.md +1 -0
- package/templates/ko/common/README.md +8 -8
- package/templates/ko/common/agents/agents.md +8 -2
- package/templates/ko/common/agents/git-workflow.md +27 -8
- package/templates/ko/common/agents/skills/split-feature.md +4 -3
- package/templates/ko/common/agents/ui-ux-design.md +128 -0
- package/templates/ko/common/designs/README.md +39 -1
- package/templates/ko/common/features/README.md +2 -2
- package/templates/ko/common/features/feature-base/decisions.md +3 -1
- package/templates/ko/common/features/feature-base/spec.md +3 -0
- package/templates/ko/common/features/feature-base/tasks.md +1 -0
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { spawn, execFileSync, spawnSync } from 'child_process';
|
|
|
12
12
|
import os from 'os';
|
|
13
13
|
import crypto, { createHash } from 'crypto';
|
|
14
14
|
import fs21 from 'fs';
|
|
15
|
+
import fs26 from 'fs/promises';
|
|
15
16
|
|
|
16
17
|
async function walkFiles(fsAdapter, rootDir, options = {}) {
|
|
17
18
|
const out = [];
|
|
@@ -1231,7 +1232,8 @@ var ENGINE_MANAGED_AGENT_FILES = [
|
|
|
1231
1232
|
"agents.md",
|
|
1232
1233
|
"git-workflow.md",
|
|
1233
1234
|
"issue-template.md",
|
|
1234
|
-
"pr-template.md"
|
|
1235
|
+
"pr-template.md",
|
|
1236
|
+
"ui-ux-design.md"
|
|
1235
1237
|
];
|
|
1236
1238
|
var ENGINE_MANAGED_AGENT_DIRS = ["skills"];
|
|
1237
1239
|
var ENGINE_MANAGED_FEATURE_PATH = path8.join(
|
|
@@ -1307,7 +1309,7 @@ Before taking the next workflow step:
|
|
|
1307
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
|
|
1308
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
|
|
1309
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\`
|
|
1310
|
-
16. In a \`local-ff\` workflow,
|
|
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
|
|
1311
1313
|
|
|
1312
1314
|
Approval and remote actions:
|
|
1313
1315
|
|
|
@@ -1315,14 +1317,20 @@ Approval and remote actions:
|
|
|
1315
1317
|
- If \`workflow-stage --json\` reports \`approvalRequired === true\`, stop at that boundary and ask the user before proceeding
|
|
1316
1318
|
- If \`workflow-stage --json\` returns labeled \`actionOptions\` at any approval boundary, keep the same option labels and exact \`reply\` tokens in the user prompt and do not improvise different reply formats
|
|
1317
1319
|
- If \`workflow-stage --json\` reports \`nextAction.category === "task_commit"\`, make the docs commit and project commit for the just-finished task before starting the next task or moving to the next stage
|
|
1318
|
-
- Before \`git commit\`, prefer \`npx lee-spec-kit commit-audit --json\` when
|
|
1320
|
+
- Before \`git commit\`, prefer \`npx lee-spec-kit commit-audit --json\`; Feature-scoped commits use \`#123\` when an Issue is linked and the stable Feature ID such as \`F027\` for issue-less local workflows
|
|
1319
1321
|
- Before remote GitHub actions, share the plan or artifact being sent
|
|
1320
1322
|
- Respect repo policy from docs and config first; hooks only enforce guardrails and continuation checks
|
|
1321
1323
|
|
|
1322
1324
|
Validation:
|
|
1323
1325
|
|
|
1324
|
-
- Prefer \`npx lee-spec-kit commit-audit --json\` for commit-time staged docs path validation
|
|
1326
|
+
- Prefer \`npx lee-spec-kit commit-audit --json\` for commit-time staged docs path validation and canonical commit-subject validation
|
|
1325
1327
|
- Prefer \`npx lee-spec-kit workflow-audit --json\` as the default docs-sync validator for Codex hooks and end-of-turn checks; it expects the active feature docs to carry one fresh \`lee-spec-kit:workflow-sync\` marker after meaningful code/doc sync
|
|
1328
|
+
|
|
1329
|
+
Optional UI/UX design policy:
|
|
1330
|
+
|
|
1331
|
+
- Only when the user request explicitly mentions a design system, UI/visual redesign, design consistency, shared UI/component-library consolidation, branding/theme/token redesign, or implementation from Figma/design images, read and apply \`npx lee-spec-kit docs get ui-ux-design --json\`
|
|
1332
|
+
- Do not apply that policy merely because the target is web/frontend, to a non-UI/backend Feature, or to a simple bug fix unrelated to durable design rules
|
|
1333
|
+
- Treat it as optional guidance, not a \`requiredDocs\` entry or workflow approval gate
|
|
1326
1334
|
`;
|
|
1327
1335
|
function renderManagedSegment(lang, docsRepo) {
|
|
1328
1336
|
return `${LEE_SPEC_KIT_AGENTS_BEGIN}
|
|
@@ -1887,7 +1895,8 @@ async function getConfig(cwd) {
|
|
|
1887
1895
|
// src/config/types.ts
|
|
1888
1896
|
var DEFAULT_APPROVAL_REQUIRE_CHECK_CATEGORIES = [
|
|
1889
1897
|
"spec_approve",
|
|
1890
|
-
"implementation_approve"
|
|
1898
|
+
"implementation_approve",
|
|
1899
|
+
"local_merge"
|
|
1891
1900
|
];
|
|
1892
1901
|
var PRE_PR_REVIEW_REASONING_EFFORTS = [
|
|
1893
1902
|
"low",
|
|
@@ -3481,6 +3490,11 @@ function isLegacyGeneratedApprovalConfig(approval) {
|
|
|
3481
3490
|
];
|
|
3482
3491
|
return !overrideKeys.some((key) => hasOwnKey2(approval, key));
|
|
3483
3492
|
}
|
|
3493
|
+
function isPreviousDefaultApprovalConfig(approval) {
|
|
3494
|
+
const keys = Object.keys(approval).sort();
|
|
3495
|
+
const categories = Array.isArray(approval.requireCheckCategories) ? approval.requireCheckCategories : [];
|
|
3496
|
+
return JSON.stringify(keys) === JSON.stringify(["default", "mode", "requireCheckCategories"]) && approval.mode === "category" && approval.default === "skip" && categories.length === 2 && categories[0] === "spec_approve" && categories[1] === "implementation_approve";
|
|
3497
|
+
}
|
|
3484
3498
|
function updateCommand(program2) {
|
|
3485
3499
|
program2.command("update").description("Update docs templates to the latest version").option("--agents", "Update agents/ folder only").option("--agents-md", "Sync project-scoped AGENTS.md entrypoint").option(
|
|
3486
3500
|
"-f, --force",
|
|
@@ -3720,7 +3734,7 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
|
3720
3734
|
"none",
|
|
3721
3735
|
"workflow.completionStrategy"
|
|
3722
3736
|
);
|
|
3723
|
-
if (workflow.completionStrategy !== "local-ff" && workflow.completionStrategy !== "none") {
|
|
3737
|
+
if (workflow.completionStrategy !== "local-ff" && workflow.completionStrategy !== "local-squash" && workflow.completionStrategy !== "none") {
|
|
3724
3738
|
workflow.completionStrategy = "none";
|
|
3725
3739
|
changedPaths.push("workflow.completionStrategy");
|
|
3726
3740
|
}
|
|
@@ -3822,7 +3836,7 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
|
3822
3836
|
const migration = migrateLegacyApprovalSettings(approval);
|
|
3823
3837
|
raw.approval = migration.approval;
|
|
3824
3838
|
changedPaths.push(...migration.changedPaths);
|
|
3825
|
-
if (isPlainObject(raw.approval) && isLegacyGeneratedApprovalConfig(raw.approval)) {
|
|
3839
|
+
if (isPlainObject(raw.approval) && (isLegacyGeneratedApprovalConfig(raw.approval) || isPreviousDefaultApprovalConfig(raw.approval))) {
|
|
3826
3840
|
raw.approval = createDefaultApprovalConfig();
|
|
3827
3841
|
changedPaths.push("approval");
|
|
3828
3842
|
}
|
|
@@ -6697,6 +6711,14 @@ var BUILTIN_DOC_DEFINITIONS = [
|
|
|
6697
6711
|
id: "split-feature",
|
|
6698
6712
|
title: { ko: "feature \uBD84\uD560 \uAC00\uC774\uB4DC", en: "feature split guide" },
|
|
6699
6713
|
relativePath: (_, lang) => path8.join(lang, "common", "agents", "skills", "split-feature.md")
|
|
6714
|
+
},
|
|
6715
|
+
{
|
|
6716
|
+
id: "ui-ux-design",
|
|
6717
|
+
title: {
|
|
6718
|
+
ko: "\uC120\uD0DD\uC801 UI/UX \uB514\uC790\uC778 \uBB38\uC11C \uC815\uCC45",
|
|
6719
|
+
en: "Optional UI/UX Design Documentation Policy"
|
|
6720
|
+
},
|
|
6721
|
+
relativePath: (_, lang) => path8.join(lang, "common", "agents", "ui-ux-design.md")
|
|
6700
6722
|
}
|
|
6701
6723
|
];
|
|
6702
6724
|
var DOC_FOLLOWUPS = {
|
|
@@ -6717,7 +6739,8 @@ var DOC_FOLLOWUPS = {
|
|
|
6717
6739
|
"execute-task": ["git-workflow", "split-feature"],
|
|
6718
6740
|
"create-issue": ["issue-doc"],
|
|
6719
6741
|
"create-pr": ["pr-doc"],
|
|
6720
|
-
"split-feature": []
|
|
6742
|
+
"split-feature": [],
|
|
6743
|
+
"ui-ux-design": []
|
|
6721
6744
|
};
|
|
6722
6745
|
function getBuiltinDocIds() {
|
|
6723
6746
|
return BUILTIN_DOC_DEFINITIONS.map((doc) => doc.id);
|
|
@@ -6736,6 +6759,7 @@ function normalizeBuiltinDocId(input) {
|
|
|
6736
6759
|
if (normalized === "split-feature") {
|
|
6737
6760
|
return "split-feature";
|
|
6738
6761
|
}
|
|
6762
|
+
if (normalized === "ui-ux-design") return "ui-ux-design";
|
|
6739
6763
|
if (normalized === "agents") return "agents";
|
|
6740
6764
|
return null;
|
|
6741
6765
|
}
|
|
@@ -7197,11 +7221,9 @@ function findPlaceholderDecisionRange(content) {
|
|
|
7197
7221
|
const block = content.slice(match.index, end);
|
|
7198
7222
|
const isPlaceholder = /\{(?:Decision Title|결정 제목)\}/.test(match[0]) || /-\s+\*\*Decision\*\*:\s*(Final choice|최종 선택)/.test(block);
|
|
7199
7223
|
if (!isPlaceholder) return null;
|
|
7200
|
-
const sequence = Number(match[1] || "1");
|
|
7201
7224
|
return {
|
|
7202
7225
|
start: match.index,
|
|
7203
|
-
end
|
|
7204
|
-
sequence: Number.isFinite(sequence) ? sequence : 1
|
|
7226
|
+
end
|
|
7205
7227
|
};
|
|
7206
7228
|
}
|
|
7207
7229
|
function formatOptions(options) {
|
|
@@ -7242,7 +7264,7 @@ async function runDecisionAdd(featureName, options) {
|
|
|
7242
7264
|
const optionItems = (options.option || []).map((value) => value.trim()).filter(Boolean);
|
|
7243
7265
|
const content = await fs.readFile(target.path, "utf-8");
|
|
7244
7266
|
const placeholderRange = findPlaceholderDecisionRange(content);
|
|
7245
|
-
const decisionSequence = placeholderRange
|
|
7267
|
+
const decisionSequence = placeholderRange ? 1 : getNextDecisionSequence(content);
|
|
7246
7268
|
const decisionId = `D${String(decisionSequence).padStart(3, "0")}`;
|
|
7247
7269
|
const recordedAt = localDate();
|
|
7248
7270
|
const block = formatDecisionBlock({
|
|
@@ -7551,6 +7573,44 @@ function integrationsCommand(program2) {
|
|
|
7551
7573
|
registerCodexIntegration(integrations);
|
|
7552
7574
|
registerCodexHooksIntegration(integrations);
|
|
7553
7575
|
}
|
|
7576
|
+
|
|
7577
|
+
// src/utils/commit-conventions.ts
|
|
7578
|
+
var PROJECT_COMMIT_TYPES = [
|
|
7579
|
+
"feat",
|
|
7580
|
+
"fix",
|
|
7581
|
+
"refactor",
|
|
7582
|
+
"test",
|
|
7583
|
+
"style",
|
|
7584
|
+
"chore"
|
|
7585
|
+
];
|
|
7586
|
+
function resolveFeatureCommitScope(input) {
|
|
7587
|
+
const issueNumber = Number(input.issueNumber || 0);
|
|
7588
|
+
if (Number.isInteger(issueNumber) && issueNumber > 0) {
|
|
7589
|
+
return `#${issueNumber}`;
|
|
7590
|
+
}
|
|
7591
|
+
if (input.workflowMode !== "local") {
|
|
7592
|
+
return null;
|
|
7593
|
+
}
|
|
7594
|
+
const featureId = String(input.featureId || "").trim().toUpperCase();
|
|
7595
|
+
return /^F\d{3,}$/.test(featureId) ? featureId : null;
|
|
7596
|
+
}
|
|
7597
|
+
function matchesProjectCommitConvention(message, scope) {
|
|
7598
|
+
return matchesCommitConvention(message, scope, PROJECT_COMMIT_TYPES);
|
|
7599
|
+
}
|
|
7600
|
+
function matchesDocsCommitConvention(message, scope) {
|
|
7601
|
+
return matchesCommitConvention(message, scope, ["docs"]);
|
|
7602
|
+
}
|
|
7603
|
+
function matchesCommitConvention(message, scope, types) {
|
|
7604
|
+
const normalized = String(message || "").trim();
|
|
7605
|
+
const normalizedScope = String(scope || "").trim();
|
|
7606
|
+
if (!normalized || !normalizedScope) return false;
|
|
7607
|
+
const typePattern = types.join("|");
|
|
7608
|
+
const match = normalized.match(
|
|
7609
|
+
new RegExp(`^(${typePattern})\\((#[0-9]+|F[0-9]{3,})\\):\\s+\\S.+$`, "i")
|
|
7610
|
+
);
|
|
7611
|
+
if (!match) return false;
|
|
7612
|
+
return match[2].toLowerCase() === normalizedScope.toLowerCase();
|
|
7613
|
+
}
|
|
7554
7614
|
function isSameOrWithin2(parentDir, candidatePath) {
|
|
7555
7615
|
const relative = path8.relative(parentDir, candidatePath);
|
|
7556
7616
|
return relative === "" || relative !== ".." && !relative.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relative);
|
|
@@ -7591,7 +7651,8 @@ function isPrePrEvidenceSatisfied(check) {
|
|
|
7591
7651
|
}
|
|
7592
7652
|
var BRANCH_LABELS2 = ["Branch", "\uBE0C\uB79C\uCE58"];
|
|
7593
7653
|
function resolveLocalCompletionStrategy(config) {
|
|
7594
|
-
|
|
7654
|
+
const strategy = config.workflow?.completionStrategy;
|
|
7655
|
+
return strategy === "local-ff" || strategy === "local-squash" ? strategy : "none";
|
|
7595
7656
|
}
|
|
7596
7657
|
async function resolveLocalIntegrationContext(config, feature) {
|
|
7597
7658
|
const projectRoot = resolveProjectRoot(config, feature);
|
|
@@ -7611,6 +7672,9 @@ async function resolveLocalIntegrationContext(config, feature) {
|
|
|
7611
7672
|
);
|
|
7612
7673
|
}
|
|
7613
7674
|
const state = await readLocalIntegrationState(projectRoot, feature);
|
|
7675
|
+
const configuredStrategy = resolveLocalCompletionStrategy(config);
|
|
7676
|
+
const completionStrategy = configuredStrategy === "local-squash" ? "local-squash" : "local-ff";
|
|
7677
|
+
const evidenceRef = resolveEvidenceRef(feature);
|
|
7614
7678
|
const featureBranchTip = resolveRef(
|
|
7615
7679
|
projectRoot,
|
|
7616
7680
|
`refs/heads/${featureBranch}`
|
|
@@ -7624,6 +7688,14 @@ async function resolveLocalIntegrationContext(config, feature) {
|
|
|
7624
7688
|
);
|
|
7625
7689
|
const managedFeatureWorktree = !!registeredWorktree && path8.resolve(registeredWorktree) !== path8.resolve(projectRoot);
|
|
7626
7690
|
const effectiveFeatureWorktree = registeredWorktree || (fs.existsSync(featureWorktree) ? featureWorktree : projectRoot);
|
|
7691
|
+
const baseContainsFeature = !!featureTip && !!baseTip && isAncestor(projectRoot, featureTip, `refs/heads/${baseBranch}`);
|
|
7692
|
+
const integratedCommit = state?.integratedCommit || state?.mergedBaseTip;
|
|
7693
|
+
const evidenceTip = resolveRef(projectRoot, evidenceRef);
|
|
7694
|
+
const featureTree = featureTip ? resolveTree(projectRoot, featureTip) : null;
|
|
7695
|
+
const integratedTree = integratedCommit ? resolveTree(projectRoot, integratedCommit) : null;
|
|
7696
|
+
const squashCommitMatchesSource = completionStrategy === "local-squash" && state?.strategy === "local-squash" && !!featureTip && state.featureTip === featureTip && !!integratedCommit && !!baseTip && isAncestor(projectRoot, integratedCommit, `refs/heads/${baseBranch}`) && !!featureTree && integratedTree === featureTree && state.integratedTree === featureTree;
|
|
7697
|
+
const squashEvidencePresent = !!featureTip && evidenceTip === featureTip;
|
|
7698
|
+
const squashIntegrationComplete = squashCommitMatchesSource && squashEvidencePresent;
|
|
7627
7699
|
return {
|
|
7628
7700
|
projectRoot,
|
|
7629
7701
|
featureWorktree: effectiveFeatureWorktree,
|
|
@@ -7634,7 +7706,12 @@ async function resolveLocalIntegrationContext(config, feature) {
|
|
|
7634
7706
|
baseTip,
|
|
7635
7707
|
currentBranch,
|
|
7636
7708
|
state,
|
|
7637
|
-
|
|
7709
|
+
completionStrategy,
|
|
7710
|
+
baseContainsFeature,
|
|
7711
|
+
integrationComplete: completionStrategy === "local-squash" ? squashIntegrationComplete : baseContainsFeature,
|
|
7712
|
+
evidenceRef,
|
|
7713
|
+
squashCommitMatchesSource,
|
|
7714
|
+
squashEvidencePresent,
|
|
7638
7715
|
projectRootClean: isClean(projectRoot),
|
|
7639
7716
|
featureWorktreeClean: isClean(effectiveFeatureWorktree),
|
|
7640
7717
|
docsClean: isClean(feature.git.docsGitCwd),
|
|
@@ -7663,7 +7740,7 @@ async function readLocalIntegrationState(projectRoot, feature) {
|
|
|
7663
7740
|
}
|
|
7664
7741
|
}
|
|
7665
7742
|
function localCleanupComplete(context) {
|
|
7666
|
-
return context.
|
|
7743
|
+
return context.integrationComplete && context.currentBranch === context.baseBranch && !context.managedFeatureWorktree && (!context.deleteFeatureBranchAfterMerge || !context.featureBranchExists) && context.projectRootClean && context.docsClean && context.state?.status === "cleaned" && context.state.featureTip === context.featureTip && context.state.mergedBaseTip === context.baseTip;
|
|
7667
7744
|
}
|
|
7668
7745
|
function runPostMergeChecks(projectRoot, checks) {
|
|
7669
7746
|
const results = [];
|
|
@@ -7688,6 +7765,12 @@ function currentGitBranch(cwd) {
|
|
|
7688
7765
|
function resolveRef(cwd, ref) {
|
|
7689
7766
|
return runGitCapture(["rev-parse", "--verify", ref], cwd) || null;
|
|
7690
7767
|
}
|
|
7768
|
+
function resolveTree(cwd, commit) {
|
|
7769
|
+
return resolveRef(cwd, `${commit}^{tree}`);
|
|
7770
|
+
}
|
|
7771
|
+
function preserveLocalIntegrationEvidence(projectRoot, evidenceRef, featureTip) {
|
|
7772
|
+
return gitRun(projectRoot, ["update-ref", evidenceRef, featureTip]);
|
|
7773
|
+
}
|
|
7691
7774
|
function isAncestor(cwd, ancestor, descendant) {
|
|
7692
7775
|
return runProcess(
|
|
7693
7776
|
"git",
|
|
@@ -7711,7 +7794,7 @@ function resolveProjectRoot(config, feature) {
|
|
|
7711
7794
|
function resolveStatePath(projectRoot, feature) {
|
|
7712
7795
|
const commonDirRaw = runGitCapture(["rev-parse", "--git-common-dir"], projectRoot) || ".git";
|
|
7713
7796
|
const commonDir = path8.isAbsolute(commonDirRaw) ? commonDirRaw : path8.resolve(projectRoot, commonDirRaw);
|
|
7714
|
-
const key =
|
|
7797
|
+
const key = resolveFeatureStateKey(feature);
|
|
7715
7798
|
return path8.join(
|
|
7716
7799
|
commonDir,
|
|
7717
7800
|
"lee-spec-kit",
|
|
@@ -7719,6 +7802,12 @@ function resolveStatePath(projectRoot, feature) {
|
|
|
7719
7802
|
`${key}.json`
|
|
7720
7803
|
);
|
|
7721
7804
|
}
|
|
7805
|
+
function resolveEvidenceRef(feature) {
|
|
7806
|
+
return `refs/lee-spec-kit/integrations/${resolveFeatureStateKey(feature)}`;
|
|
7807
|
+
}
|
|
7808
|
+
function resolveFeatureStateKey(feature) {
|
|
7809
|
+
return crypto.createHash("sha256").update(`${feature.type}:${feature.folderName}`).digest("hex").slice(0, 24);
|
|
7810
|
+
}
|
|
7722
7811
|
async function readTasks(featureDir) {
|
|
7723
7812
|
const tasksPath = path8.join(featureDir, "tasks.md");
|
|
7724
7813
|
return await fs.pathExists(tasksPath) ? fs.readFile(tasksPath, "utf-8") : "";
|
|
@@ -8116,8 +8205,13 @@ function resolveProjectCommitTopic(feature, tasks) {
|
|
|
8116
8205
|
}
|
|
8117
8206
|
function buildTaskCommitSummary(input) {
|
|
8118
8207
|
const { feature, tasks, effectiveProjectGitCwd, docsDirty, projectDirty, gateFailureReason } = input;
|
|
8119
|
-
const
|
|
8120
|
-
|
|
8208
|
+
const scope = resolveFeatureCommitScope({
|
|
8209
|
+
issueNumber: tasks.issueNumber,
|
|
8210
|
+
featureId: feature.id,
|
|
8211
|
+
workflowMode: tasks.issueNumber ? "github" : "local"
|
|
8212
|
+
}) || feature.id;
|
|
8213
|
+
const docsMessage = `git -C "${feature.git.docsGitCwd}" add "${feature.docs.featurePathFromDocs}" && git -C "${feature.git.docsGitCwd}" commit -m "docs(${scope}): ${feature.folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"`;
|
|
8214
|
+
const projectMessage = `Stage only the files touched by the just-finished task in "${effectiveProjectGitCwd}", then commit with: git -C "${effectiveProjectGitCwd}" commit -m "feat(${scope}): ${resolveProjectCommitTopic(feature, tasks)}"`;
|
|
8121
8215
|
const lines = ["Finish the task-level commit checkpoint before continuing."];
|
|
8122
8216
|
if (gateFailureReason) {
|
|
8123
8217
|
lines.push(`Current gate failure: ${gateFailureReason}`);
|
|
@@ -8987,11 +9081,13 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
8987
9081
|
"implementation_approve",
|
|
8988
9082
|
true
|
|
8989
9083
|
);
|
|
8990
|
-
const localIntegrationApproval = config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config)
|
|
9084
|
+
const localIntegrationApproval = config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) !== "none";
|
|
9085
|
+
const separateLocalMergeApproval = localIntegrationApproval && resolveActionApprovalRequired(config, "local_merge", false);
|
|
8991
9086
|
const localBaseBranch = config.workflow?.baseBranch?.trim() || "main";
|
|
9087
|
+
const localIntegrationLabel = resolveLocalCompletionStrategy(config) === "local-squash" ? `squash integration into ${localBaseBranch}` : `fast-forward integration into ${localBaseBranch}`;
|
|
8992
9088
|
const localCleanupSummary = config.workflow?.deleteFeatureBranchAfterMerge === false ? "remove any managed Feature worktree" : "remove any managed Feature worktree and delete the integrated local Feature branch";
|
|
8993
9089
|
const actionOptions = approvalRequired ? buildApprovalActionOptions({
|
|
8994
|
-
approveSummary:
|
|
9090
|
+
approveSummary: separateLocalMergeApproval ? `Approve the completed implementation and continue to the separate local merge approval before ${localIntegrationLabel}.` : localIntegrationApproval ? `Approve the completed implementation and authorize ${localIntegrationLabel}, post-merge verification, and cleanup that will ${localCleanupSummary}.` : "Approve the completed implementation and continue to the pre-PR or PR preparation stage.",
|
|
8995
9091
|
holdSummary: "Request implementation changes before the workflow continues."
|
|
8996
9092
|
}) : void 0;
|
|
8997
9093
|
return {
|
|
@@ -9002,7 +9098,7 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
9002
9098
|
stage: "implementation_approve",
|
|
9003
9099
|
nextAction: buildAction(
|
|
9004
9100
|
"implementation_approve",
|
|
9005
|
-
localIntegrationApproval ? `Share the completed implementation and get user approval for the remaining local completion flow:
|
|
9101
|
+
separateLocalMergeApproval ? `Share the completed implementation and get user approval for the implementation itself. Record it in tasks.md; ${localIntegrationLabel} will require a separate local_merge approval.` : localIntegrationApproval ? `Share the completed implementation and get user approval for the remaining local completion flow: ${localIntegrationLabel}, run post-merge checks, then ${localCleanupSummary}. Record that approval in tasks.md.` : "Share the completed implementation, get user approval, and record the completion checkpoint in tasks.md.",
|
|
9006
9102
|
approvalRequired
|
|
9007
9103
|
),
|
|
9008
9104
|
approvalRequired,
|
|
@@ -9031,10 +9127,10 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
9031
9127
|
blockedReasonCode: "PRE_PR_REVIEW_NOT_APPROVED"
|
|
9032
9128
|
};
|
|
9033
9129
|
}
|
|
9034
|
-
if (config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config)
|
|
9130
|
+
if (config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) !== "none") {
|
|
9035
9131
|
const localState = await resolveLocalIntegrationContext(config, feature);
|
|
9036
9132
|
const localMergeBaseCommand = `npx lee-spec-kit local merge ${buildFeatureArgs(feature)} --json`;
|
|
9037
|
-
if (!localState.
|
|
9133
|
+
if (!localState.integrationComplete) {
|
|
9038
9134
|
const approvalRequired = resolveActionApprovalRequired(
|
|
9039
9135
|
config,
|
|
9040
9136
|
"local_merge",
|
|
@@ -9042,7 +9138,7 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
9042
9138
|
);
|
|
9043
9139
|
const command = approvalRequired ? `${localMergeBaseCommand} --confirm OK` : localMergeBaseCommand;
|
|
9044
9140
|
const actionOptions = approvalRequired ? buildApprovalActionOptions({
|
|
9045
|
-
approveSummary: `Fast-forward ${localState.featureBranch} into ${localState.baseBranch} and run the configured post-merge checks.`,
|
|
9141
|
+
approveSummary: localState.completionStrategy === "local-squash" ? `Squash ${localState.featureBranch} into ${localState.baseBranch}, preserve the source Feature tip, and run the configured post-merge checks.` : `Fast-forward ${localState.featureBranch} into ${localState.baseBranch} and run the configured post-merge checks.`,
|
|
9046
9142
|
holdSummary: "Keep the completed Feature branch unmerged for now."
|
|
9047
9143
|
}) : void 0;
|
|
9048
9144
|
return {
|
|
@@ -9053,7 +9149,7 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
9053
9149
|
stage: "local_merge",
|
|
9054
9150
|
nextAction: buildAction(
|
|
9055
9151
|
"local_merge",
|
|
9056
|
-
`Fast-forward ${localState.featureBranch} into ${localState.baseBranch}; do not create a merge commit.`,
|
|
9152
|
+
localState.completionStrategy === "local-squash" ? `Create one squash commit from ${localState.featureBranch} on ${localState.baseBranch} and preserve the source Feature tip as internal integration evidence.` : `Fast-forward ${localState.featureBranch} into ${localState.baseBranch}; do not create a merge commit.`,
|
|
9057
9153
|
approvalRequired,
|
|
9058
9154
|
command
|
|
9059
9155
|
),
|
|
@@ -9991,35 +10087,21 @@ var DEFAULT_MANAGED_DOC_FILES = [
|
|
|
9991
10087
|
".gitignore"
|
|
9992
10088
|
];
|
|
9993
10089
|
|
|
9994
|
-
// src/utils/commit-conventions.ts
|
|
9995
|
-
var PROJECT_COMMIT_PREFIX_PATTERN = /^(feat|fix|refactor|test|chore)\(#(\d+)\):\s+\S.+$/i;
|
|
9996
|
-
var DOCS_COMMIT_PREFIX_PATTERN = /^docs\(#(\d+)\):\s+\S.+$/i;
|
|
9997
|
-
function matchesProjectCommitConvention(message, issueNumber) {
|
|
9998
|
-
const normalized = String(message || "").trim();
|
|
9999
|
-
if (!normalized) return false;
|
|
10000
|
-
const match = normalized.match(PROJECT_COMMIT_PREFIX_PATTERN);
|
|
10001
|
-
if (!match) return false;
|
|
10002
|
-
return Number(match[2]) === issueNumber;
|
|
10003
|
-
}
|
|
10004
|
-
function matchesDocsCommitConvention(message, issueNumber) {
|
|
10005
|
-
const normalized = String(message || "").trim();
|
|
10006
|
-
if (!normalized) return false;
|
|
10007
|
-
const match = normalized.match(DOCS_COMMIT_PREFIX_PATTERN);
|
|
10008
|
-
if (!match) return false;
|
|
10009
|
-
return Number(match[1]) === issueNumber;
|
|
10010
|
-
}
|
|
10011
|
-
|
|
10012
10090
|
// src/commands/commit-audit.ts
|
|
10013
10091
|
var CANONICAL_FEATURE_DOC_PATTERN = /^features\/(?:[^/]+\/)?F\d{3,}[^/]*\/(spec|plan|tasks|decisions|issue|pr)\.md$/i;
|
|
10014
10092
|
var FEATURE_DOC_CANDIDATE_PATTERN = /^features\/(?:[^/]+\/)?F\d{3,}[^/]*\/(.+)$/i;
|
|
10015
10093
|
function commitAuditCommand(program2) {
|
|
10016
|
-
program2.command("commit-audit").description("Validate staged docs paths before commit").option("--json", "Output JSON for hooks and agents").option("--git-root <path>", "Override the git root used for staged-path inspection").option("--message <message>", "Validate a commit subject against the current workflow convention").action(async (options) => {
|
|
10094
|
+
program2.command("commit-audit").description("Validate staged docs paths and canonical commit subjects before commit").option("--json", "Output JSON for hooks and agents").option("--git-root <path>", "Override the git root used for staged-path inspection").option("--message <message>", "Validate a commit subject against the current workflow convention").option("--message-file <path>", "Read and validate the commit subject from a commit message file").option("--enforce", "Exit non-zero when commit-audit blocks the commit").action(async (options) => {
|
|
10017
10095
|
try {
|
|
10096
|
+
const commitMessage = await resolveCommitMessageInput(process.cwd(), options);
|
|
10018
10097
|
const payload = await collectCommitAudit(
|
|
10019
10098
|
process.cwd(),
|
|
10020
10099
|
options.gitRoot,
|
|
10021
|
-
|
|
10100
|
+
commitMessage
|
|
10022
10101
|
);
|
|
10102
|
+
if (options.enforce && payload.status === "blocked") {
|
|
10103
|
+
process.exitCode = 1;
|
|
10104
|
+
}
|
|
10023
10105
|
if (options.json) {
|
|
10024
10106
|
console.log(JSON.stringify(payload, null, 2));
|
|
10025
10107
|
return;
|
|
@@ -10046,6 +10128,7 @@ function commitAuditCommand(program2) {
|
|
|
10046
10128
|
2
|
|
10047
10129
|
)
|
|
10048
10130
|
);
|
|
10131
|
+
if (options.enforce) process.exitCode = 1;
|
|
10049
10132
|
return;
|
|
10050
10133
|
}
|
|
10051
10134
|
process.stderr.write(`[${cliError.code}] ${cliError.message}
|
|
@@ -10054,6 +10137,18 @@ function commitAuditCommand(program2) {
|
|
|
10054
10137
|
}
|
|
10055
10138
|
});
|
|
10056
10139
|
}
|
|
10140
|
+
async function resolveCommitMessageInput(cwd, options) {
|
|
10141
|
+
if (options.message && options.messageFile) {
|
|
10142
|
+
throw createCliError(
|
|
10143
|
+
"INVALID_ARGUMENT",
|
|
10144
|
+
"Use either --message or --message-file, not both."
|
|
10145
|
+
);
|
|
10146
|
+
}
|
|
10147
|
+
if (options.message) return options.message;
|
|
10148
|
+
if (!options.messageFile) return void 0;
|
|
10149
|
+
const content = await fs26.readFile(path8.resolve(cwd, options.messageFile), "utf-8");
|
|
10150
|
+
return content.split(/\r?\n/u).map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#"));
|
|
10151
|
+
}
|
|
10057
10152
|
async function collectCommitAudit(cwd, gitRootOverride, commitMessage) {
|
|
10058
10153
|
const config = await getConfig(cwd);
|
|
10059
10154
|
if (!config) {
|
|
@@ -10277,10 +10372,17 @@ async function collectCommitMessageViolation(cwd, config, repoRoot, stagedEntrie
|
|
|
10277
10372
|
return null;
|
|
10278
10373
|
}
|
|
10279
10374
|
const selection = await resolveFeatureSelection(cwd);
|
|
10280
|
-
if (selection.status !== "selected" || !selection.matchedFeature
|
|
10375
|
+
if (selection.status !== "selected" || !selection.matchedFeature) {
|
|
10376
|
+
return null;
|
|
10377
|
+
}
|
|
10378
|
+
const scope = resolveFeatureCommitScope({
|
|
10379
|
+
issueNumber: selection.matchedFeature.issueNumber,
|
|
10380
|
+
featureId: selection.matchedFeature.id,
|
|
10381
|
+
workflowMode: config.workflow?.mode
|
|
10382
|
+
});
|
|
10383
|
+
if (!scope) {
|
|
10281
10384
|
return null;
|
|
10282
10385
|
}
|
|
10283
|
-
const issueNumber = selection.matchedFeature.issueNumber;
|
|
10284
10386
|
const docsRepoRoot = runGitCapture(["rev-parse", "--show-toplevel"], config.docsDir);
|
|
10285
10387
|
const normalizedRepoRoot = path8.resolve(repoRoot);
|
|
10286
10388
|
const normalizedDocsRepoRoot = docsRepoRoot ? path8.resolve(docsRepoRoot) : null;
|
|
@@ -10290,15 +10392,15 @@ async function collectCommitMessageViolation(cwd, config, repoRoot, stagedEntrie
|
|
|
10290
10392
|
return !!relativeToDocs && relativeToDocs !== "" && !relativeToDocs.startsWith("..");
|
|
10291
10393
|
});
|
|
10292
10394
|
const isDocsCommit = !!normalizedDocsRepoRoot && normalizedDocsRepoRoot === normalizedRepoRoot && (config.docsRepo === "standalone" || docsOnlyCommit);
|
|
10293
|
-
const valid = isDocsCommit ? matchesDocsCommitConvention(normalizedMessage,
|
|
10395
|
+
const valid = isDocsCommit ? matchesDocsCommitConvention(normalizedMessage, scope) : matchesProjectCommitConvention(normalizedMessage, scope);
|
|
10294
10396
|
if (valid) {
|
|
10295
10397
|
return null;
|
|
10296
10398
|
}
|
|
10297
|
-
const expected = isDocsCommit ? `docs(
|
|
10399
|
+
const expected = isDocsCommit ? `docs(${scope}): ...` : `type(${scope}): ... (feat/fix/refactor/test/style/chore)`;
|
|
10298
10400
|
return {
|
|
10299
10401
|
path: "(commit message)",
|
|
10300
10402
|
kind: "commit_message_policy",
|
|
10301
|
-
detail: `Commit subject must follow the
|
|
10403
|
+
detail: `Commit subject must follow the canonical feature-scoped convention. Expected ${expected}, received "${normalizedMessage}".`
|
|
10302
10404
|
};
|
|
10303
10405
|
}
|
|
10304
10406
|
function normalizeEntryName(value) {
|
|
@@ -10609,7 +10711,7 @@ async function collectDocsAudit(cwd) {
|
|
|
10609
10711
|
// src/commands/local.ts
|
|
10610
10712
|
function localCommand(program2) {
|
|
10611
10713
|
const local = program2.command("local").description("Integrate and clean up local workflow feature branches");
|
|
10612
|
-
local.command("merge [feature-name]").description("
|
|
10714
|
+
local.command("merge [feature-name]").description("Integrate a completed local feature and verify the result").option("--component <component>", "Component name for multi projects").option(
|
|
10613
10715
|
"--confirm <token>",
|
|
10614
10716
|
"Approval token when local_merge requires approval"
|
|
10615
10717
|
).option("--json", "Output JSON for agents and hooks").action(
|
|
@@ -10708,7 +10810,8 @@ async function runLocalMerge(featureName, options) {
|
|
|
10708
10810
|
);
|
|
10709
10811
|
}
|
|
10710
10812
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10711
|
-
|
|
10813
|
+
const previousStateStrategy = context.state?.strategy || "local-ff";
|
|
10814
|
+
let state = context.state && context.state.featureTip === context.featureTip && previousStateStrategy === context.completionStrategy ? context.state : {
|
|
10712
10815
|
version: 1,
|
|
10713
10816
|
featureRef: feature.folderName,
|
|
10714
10817
|
component: feature.type,
|
|
@@ -10716,20 +10819,39 @@ async function runLocalMerge(featureName, options) {
|
|
|
10716
10819
|
featureBranch: context.featureBranch,
|
|
10717
10820
|
featureTip: context.featureTip,
|
|
10718
10821
|
mergedBaseTip: context.baseTip,
|
|
10822
|
+
strategy: context.completionStrategy,
|
|
10719
10823
|
status: "merged",
|
|
10720
10824
|
mergedAt: now,
|
|
10721
10825
|
verifiedAt: null,
|
|
10722
10826
|
cleanedAt: null,
|
|
10723
10827
|
verification: []
|
|
10724
10828
|
};
|
|
10725
|
-
if (!context.
|
|
10829
|
+
if (context.completionStrategy === "local-squash" && context.squashCommitMatchesSource && !context.squashEvidencePresent) {
|
|
10830
|
+
const evidence = preserveLocalIntegrationEvidence(
|
|
10831
|
+
context.projectRoot,
|
|
10832
|
+
context.evidenceRef,
|
|
10833
|
+
context.featureTip
|
|
10834
|
+
);
|
|
10835
|
+
if (evidence.code !== 0) {
|
|
10836
|
+
return blocked(
|
|
10837
|
+
"LOCAL_SQUASH_EVIDENCE_FAILED",
|
|
10838
|
+
feature.folderName,
|
|
10839
|
+
evidence.stderr
|
|
10840
|
+
);
|
|
10841
|
+
}
|
|
10842
|
+
context = await resolveLocalIntegrationContext(config, feature);
|
|
10843
|
+
}
|
|
10844
|
+
if (!context.featureTip || !context.baseTip) {
|
|
10845
|
+
return blocked("LOCAL_MERGE_REF_NOT_FOUND", feature.folderName);
|
|
10846
|
+
}
|
|
10847
|
+
if (!context.integrationComplete) {
|
|
10726
10848
|
if (!isAncestor(
|
|
10727
10849
|
context.projectRoot,
|
|
10728
10850
|
`refs/heads/${context.baseBranch}`,
|
|
10729
10851
|
context.featureTip
|
|
10730
10852
|
)) {
|
|
10731
10853
|
return blocked(
|
|
10732
|
-
"LOCAL_MERGE_NOT_FAST_FORWARD",
|
|
10854
|
+
context.completionStrategy === "local-squash" ? "LOCAL_SQUASH_BASE_NOT_ANCESTOR" : "LOCAL_MERGE_NOT_FAST_FORWARD",
|
|
10733
10855
|
feature.folderName,
|
|
10734
10856
|
`${context.baseBranch} is not an ancestor of ${context.featureBranch}.`
|
|
10735
10857
|
);
|
|
@@ -10747,26 +10869,21 @@ async function runLocalMerge(featureName, options) {
|
|
|
10747
10869
|
);
|
|
10748
10870
|
}
|
|
10749
10871
|
}
|
|
10750
|
-
const
|
|
10751
|
-
|
|
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
|
-
}
|
|
10872
|
+
const integration = context.completionStrategy === "local-squash" ? integrateLocalSquash(context, feature.folderName, feature.id, feature.slug) : integrateLocalFastForward(context, feature.folderName);
|
|
10873
|
+
if (integration.status !== "ok") return integration.payload;
|
|
10762
10874
|
const mergedBaseTip = resolveRef(
|
|
10763
10875
|
context.projectRoot,
|
|
10764
10876
|
`refs/heads/${context.baseBranch}`
|
|
10765
10877
|
);
|
|
10878
|
+
const integratedTree = mergedBaseTip ? resolveTree(context.projectRoot, mergedBaseTip) : null;
|
|
10766
10879
|
state = {
|
|
10767
10880
|
...state,
|
|
10768
10881
|
featureTip: context.featureTip,
|
|
10769
10882
|
mergedBaseTip: mergedBaseTip || context.featureTip,
|
|
10883
|
+
strategy: context.completionStrategy,
|
|
10884
|
+
integratedCommit: mergedBaseTip || context.featureTip,
|
|
10885
|
+
...integratedTree ? { integratedTree } : {},
|
|
10886
|
+
...context.completionStrategy === "local-squash" ? { evidenceRef: context.evidenceRef } : {},
|
|
10770
10887
|
status: "merged",
|
|
10771
10888
|
mergedAt: now,
|
|
10772
10889
|
verifiedAt: null,
|
|
@@ -10776,7 +10893,7 @@ async function runLocalMerge(featureName, options) {
|
|
|
10776
10893
|
await writeLocalIntegrationState(context.projectRoot, feature, state);
|
|
10777
10894
|
context = await resolveLocalIntegrationContext(config, feature);
|
|
10778
10895
|
}
|
|
10779
|
-
if (!context.
|
|
10896
|
+
if (!context.integrationComplete) {
|
|
10780
10897
|
return blocked("LOCAL_MERGE_VERIFICATION_FAILED", feature.folderName);
|
|
10781
10898
|
}
|
|
10782
10899
|
const verification = runPostMergeChecks(
|
|
@@ -10825,6 +10942,7 @@ async function runLocalMerge(featureName, options) {
|
|
|
10825
10942
|
featureBranch: context.featureBranch,
|
|
10826
10943
|
featureTip: context.featureTip,
|
|
10827
10944
|
baseTip,
|
|
10945
|
+
completionStrategy: context.completionStrategy,
|
|
10828
10946
|
verification
|
|
10829
10947
|
};
|
|
10830
10948
|
}
|
|
@@ -10855,7 +10973,7 @@ async function runLocalCleanup(featureName, options) {
|
|
|
10855
10973
|
}
|
|
10856
10974
|
const feature = selection.matchedFeature;
|
|
10857
10975
|
let context = await resolveLocalIntegrationContext(config, feature);
|
|
10858
|
-
if (!context.
|
|
10976
|
+
if (!context.integrationComplete || !context.state || !["verified", "cleaned"].includes(context.state.status) || context.state.featureTip !== context.featureTip || context.state.mergedBaseTip !== context.baseTip) {
|
|
10859
10977
|
return blocked("LOCAL_MERGE_NOT_VERIFIED", feature.folderName);
|
|
10860
10978
|
}
|
|
10861
10979
|
if (!context.featureWorktreeClean || !context.projectRootClean || !context.docsClean) {
|
|
@@ -10881,7 +10999,7 @@ async function runLocalCleanup(featureName, options) {
|
|
|
10881
10999
|
if (context.deleteFeatureBranchAfterMerge && context.featureBranchExists) {
|
|
10882
11000
|
const removeBranch = gitRun(context.projectRoot, [
|
|
10883
11001
|
"branch",
|
|
10884
|
-
"-d",
|
|
11002
|
+
context.completionStrategy === "local-squash" ? "-D" : "-d",
|
|
10885
11003
|
context.featureBranch
|
|
10886
11004
|
]);
|
|
10887
11005
|
if (removeBranch.code !== 0) {
|
|
@@ -10905,9 +11023,106 @@ async function runLocalCleanup(featureName, options) {
|
|
|
10905
11023
|
baseBranch: context.baseBranch,
|
|
10906
11024
|
featureBranch: context.featureBranch,
|
|
10907
11025
|
featureTip: context.featureTip,
|
|
10908
|
-
baseTip: context.baseTip
|
|
11026
|
+
baseTip: context.baseTip,
|
|
11027
|
+
completionStrategy: context.completionStrategy
|
|
10909
11028
|
};
|
|
10910
11029
|
}
|
|
11030
|
+
function integrateLocalFastForward(context, featureRef) {
|
|
11031
|
+
const merge = gitRun(context.projectRoot, [
|
|
11032
|
+
"merge",
|
|
11033
|
+
"--ff-only",
|
|
11034
|
+
context.featureBranch
|
|
11035
|
+
]);
|
|
11036
|
+
if (merge.code !== 0) {
|
|
11037
|
+
return {
|
|
11038
|
+
status: "blocked",
|
|
11039
|
+
payload: blocked(
|
|
11040
|
+
"LOCAL_MERGE_NOT_FAST_FORWARD",
|
|
11041
|
+
featureRef,
|
|
11042
|
+
merge.stderr
|
|
11043
|
+
)
|
|
11044
|
+
};
|
|
11045
|
+
}
|
|
11046
|
+
return { status: "ok" };
|
|
11047
|
+
}
|
|
11048
|
+
function integrateLocalSquash(context, featureRef, featureId, featureSlug) {
|
|
11049
|
+
const originalBaseTip = context.baseTip;
|
|
11050
|
+
if (!originalBaseTip || !context.featureTip) {
|
|
11051
|
+
return {
|
|
11052
|
+
status: "blocked",
|
|
11053
|
+
payload: blocked("LOCAL_MERGE_REF_NOT_FOUND", featureRef)
|
|
11054
|
+
};
|
|
11055
|
+
}
|
|
11056
|
+
const squash = gitRun(context.projectRoot, [
|
|
11057
|
+
"merge",
|
|
11058
|
+
"--squash",
|
|
11059
|
+
context.featureBranch
|
|
11060
|
+
]);
|
|
11061
|
+
if (squash.code !== 0) {
|
|
11062
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11063
|
+
return {
|
|
11064
|
+
status: "blocked",
|
|
11065
|
+
payload: blocked("LOCAL_SQUASH_FAILED", featureRef, squash.stderr)
|
|
11066
|
+
};
|
|
11067
|
+
}
|
|
11068
|
+
const scope = resolveFeatureCommitScope({
|
|
11069
|
+
featureId,
|
|
11070
|
+
workflowMode: "local"
|
|
11071
|
+
});
|
|
11072
|
+
if (!scope) {
|
|
11073
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11074
|
+
return {
|
|
11075
|
+
status: "blocked",
|
|
11076
|
+
payload: blocked(
|
|
11077
|
+
"LOCAL_SQUASH_COMMIT_SCOPE_INVALID",
|
|
11078
|
+
featureRef,
|
|
11079
|
+
`Could not resolve a canonical local commit scope from ${featureId}.`
|
|
11080
|
+
)
|
|
11081
|
+
};
|
|
11082
|
+
}
|
|
11083
|
+
const commit = gitRun(context.projectRoot, [
|
|
11084
|
+
"commit",
|
|
11085
|
+
"-m",
|
|
11086
|
+
`feat(${scope}): integrate ${featureSlug}`
|
|
11087
|
+
]);
|
|
11088
|
+
if (commit.code !== 0) {
|
|
11089
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11090
|
+
return {
|
|
11091
|
+
status: "blocked",
|
|
11092
|
+
payload: blocked("LOCAL_SQUASH_COMMIT_FAILED", featureRef, commit.stderr)
|
|
11093
|
+
};
|
|
11094
|
+
}
|
|
11095
|
+
const integratedCommit = resolveRef(context.projectRoot, "HEAD");
|
|
11096
|
+
const sourceTree = resolveTree(context.projectRoot, context.featureTip);
|
|
11097
|
+
const integratedTree = integratedCommit ? resolveTree(context.projectRoot, integratedCommit) : null;
|
|
11098
|
+
if (!integratedCommit || !sourceTree || integratedTree !== sourceTree) {
|
|
11099
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11100
|
+
return {
|
|
11101
|
+
status: "blocked",
|
|
11102
|
+
payload: blocked("LOCAL_SQUASH_TREE_MISMATCH", featureRef)
|
|
11103
|
+
};
|
|
11104
|
+
}
|
|
11105
|
+
const evidence = preserveLocalIntegrationEvidence(
|
|
11106
|
+
context.projectRoot,
|
|
11107
|
+
context.evidenceRef,
|
|
11108
|
+
context.featureTip
|
|
11109
|
+
);
|
|
11110
|
+
if (evidence.code !== 0) {
|
|
11111
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11112
|
+
return {
|
|
11113
|
+
status: "blocked",
|
|
11114
|
+
payload: blocked(
|
|
11115
|
+
"LOCAL_SQUASH_EVIDENCE_FAILED",
|
|
11116
|
+
featureRef,
|
|
11117
|
+
evidence.stderr
|
|
11118
|
+
)
|
|
11119
|
+
};
|
|
11120
|
+
}
|
|
11121
|
+
return { status: "ok" };
|
|
11122
|
+
}
|
|
11123
|
+
function rollbackSquash(projectRoot, originalBaseTip) {
|
|
11124
|
+
gitRun(projectRoot, ["reset", "--hard", originalBaseTip]);
|
|
11125
|
+
}
|
|
10911
11126
|
function blocked(reasonCode, featureRef, error) {
|
|
10912
11127
|
return {
|
|
10913
11128
|
status: "blocked",
|