@magnusekdahl/parallix 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +190 -185
- package/lib/agents/agents.js +737 -838
- package/lib/agents/agents.ts +930 -0
- package/lib/agents/claude-telemetry.js +149 -154
- package/lib/agents/claude-telemetry.ts +233 -0
- package/lib/agents/claude.js +121 -131
- package/lib/agents/claude.ts +156 -0
- package/lib/agents/codex-telemetry.js +176 -164
- package/lib/agents/codex-telemetry.ts +205 -0
- package/lib/agents/codex.js +168 -201
- package/lib/agents/codex.ts +236 -0
- package/lib/agents/limit-hit.js +216 -226
- package/lib/agents/limit-hit.ts +262 -0
- package/lib/agents/mistral-telemetry.js +7 -10
- package/lib/agents/mistral-telemetry.ts +44 -0
- package/lib/agents/mistral.js +40 -58
- package/lib/agents/mistral.ts +84 -0
- package/lib/agents/opencode-export.js +126 -127
- package/lib/agents/opencode-export.ts +160 -0
- package/lib/agents/opencode-telemetry.js +282 -261
- package/lib/agents/opencode-telemetry.ts +350 -0
- package/lib/agents/opencode.js +263 -305
- package/lib/agents/opencode.ts +370 -0
- package/lib/agents/stage-telemetry.js +23 -24
- package/lib/agents/stage-telemetry.ts +47 -0
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1332 -1463
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/fmt.js +140 -211
- package/lib/core/fmt.ts +189 -0
- package/lib/core/git.js +121 -102
- package/lib/core/git.ts +146 -0
- package/lib/core/gitignore.js +59 -106
- package/lib/core/gitignore.ts +103 -0
- package/lib/core/mission-utils.js +833 -875
- package/lib/core/mission-utils.ts +998 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/persistent-data-migration.js +247 -205
- package/lib/core/persistent-data-migration.ts +255 -0
- package/lib/core/product-config.js +400 -476
- package/lib/core/product-config.ts +518 -0
- package/lib/core/runtime-matrix.js +41 -87
- package/lib/core/runtime-matrix.ts +84 -0
- package/lib/core/spawn-tee.js +152 -192
- package/lib/core/spawn-tee.ts +202 -0
- package/lib/core/state-map.js +92 -92
- package/lib/core/state-map.ts +115 -0
- package/lib/core/storage.js +101 -129
- package/lib/core/storage.ts +182 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/core/verification.js +148 -138
- package/lib/core/verification.ts +200 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +7 -5
- package/prompts/draft.md +1 -0
- package/prompts/portfolio.md +1 -1
- package/px.js +226 -210
|
@@ -1,196 +1,264 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.SUPPORTED_VERIFY_AREAS = void 0;
|
|
40
|
+
exports.missionBaseDir = missionBaseDir;
|
|
41
|
+
exports.missionUsesYearTier = missionUsesYearTier;
|
|
42
|
+
exports.missionBranchPrefix = missionBranchPrefix;
|
|
43
|
+
exports.missionBranchName = missionBranchName;
|
|
44
|
+
exports.missionBranchRef = missionBranchRef;
|
|
45
|
+
exports.isMissionSlugCandidate = isMissionSlugCandidate;
|
|
46
|
+
exports.extractSlugFromBranch = extractSlugFromBranch;
|
|
47
|
+
exports.getPrimaryBranch = getPrimaryBranch;
|
|
48
|
+
exports.resolveMainRepo = resolveMainRepo;
|
|
49
|
+
exports.getPrimaryWorktree = getPrimaryWorktree;
|
|
50
|
+
exports.conventionalWorktreePath = conventionalWorktreePath;
|
|
51
|
+
exports.conventionalBaseWorktreePath = conventionalBaseWorktreePath;
|
|
52
|
+
exports.detectLaunchBaseBranch = detectLaunchBaseBranch;
|
|
53
|
+
exports.parseBaseBranchLine = parseBaseBranchLine;
|
|
54
|
+
exports.readRecordedBaseBranch = readRecordedBaseBranch;
|
|
55
|
+
exports.resolveMissionBaseBranch = resolveMissionBaseBranch;
|
|
56
|
+
exports.resolveBaseWorktree = resolveBaseWorktree;
|
|
57
|
+
exports.getMissionYear = getMissionYear;
|
|
58
|
+
exports.findMissionDir = findMissionDir;
|
|
59
|
+
exports.resolveWorktree = resolveWorktree;
|
|
60
|
+
exports.findCheckpoints = findCheckpoints;
|
|
61
|
+
exports.compareCheckpointFiles = compareCheckpointFiles;
|
|
62
|
+
exports.checkpointOrder = checkpointOrder;
|
|
63
|
+
exports.getFirstLine = getFirstLine;
|
|
64
|
+
exports.missionTitle = missionTitle;
|
|
65
|
+
exports.normalizeVerifyArea = normalizeVerifyArea;
|
|
66
|
+
exports.detectMissionAreaFromContent = detectMissionAreaFromContent;
|
|
67
|
+
exports.findMissionArea = findMissionArea;
|
|
68
|
+
exports.graphifyAvailable = graphifyAvailable;
|
|
69
|
+
exports.graphifyCommandCandidates = graphifyCommandCandidates;
|
|
70
|
+
exports.probeGraphifyAvailability = probeGraphifyAvailability;
|
|
71
|
+
exports.updateGraphifyKnowledgeGraph = updateGraphifyKnowledgeGraph;
|
|
72
|
+
exports.missionPathForSlug = missionPathForSlug;
|
|
73
|
+
exports.missionDirForSlug = missionDirForSlug;
|
|
74
|
+
exports.inferSlug = inferSlug;
|
|
75
|
+
exports.parseConflictFilesFromMergeOutput = parseConflictFilesFromMergeOutput;
|
|
76
|
+
exports.getConflictFiles = getConflictFiles;
|
|
77
|
+
exports.findLastNonNoiseCommit = findLastNonNoiseCommit;
|
|
78
|
+
exports.squashTrailingBacklogNoiseIntoPreviousMission = squashTrailingBacklogNoiseIntoPreviousMission;
|
|
79
|
+
exports.softResetTrailingBacklogNoise = softResetTrailingBacklogNoise;
|
|
80
|
+
exports.findMissionDocInBranches = findMissionDocInBranches;
|
|
81
|
+
exports.isMissionArtifact = isMissionArtifact;
|
|
82
|
+
exports.isWorkflowGeneratedArtifact = isWorkflowGeneratedArtifact;
|
|
83
|
+
const fs = __importStar(require("node:fs"));
|
|
84
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
85
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
86
|
+
const fmt = __importStar(require("./fmt.js"));
|
|
87
|
+
const product_config_js_1 = require("./product-config.js");
|
|
88
|
+
const gitModule = __importStar(require("./git.js"));
|
|
7
89
|
/** @param {string} prefix */
|
|
8
90
|
function normalizeBranchPrefix(prefix) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
91
|
+
if (typeof prefix !== 'string' || !prefix.trim()) {
|
|
92
|
+
return 'mission/';
|
|
93
|
+
}
|
|
94
|
+
return prefix.endsWith('/') ? prefix : prefix + '/';
|
|
13
95
|
}
|
|
14
|
-
|
|
15
96
|
function missionAdapterDefaults() {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
97
|
+
return {
|
|
98
|
+
baseDir: 'missions',
|
|
99
|
+
branchPrefix: 'mission/',
|
|
100
|
+
worktreePattern: '../<repo>-<slug>',
|
|
101
|
+
};
|
|
21
102
|
}
|
|
22
|
-
|
|
23
103
|
function resolveMissionAdapter(rootDir = process.cwd()) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
104
|
+
const adapters = (0, product_config_js_1.loadAdapterConfig)(rootDir);
|
|
105
|
+
const missions = adapters.missions || {};
|
|
106
|
+
const defaults = missionAdapterDefaults();
|
|
107
|
+
return {
|
|
108
|
+
baseDir: typeof missions.baseDir === 'string' && missions.baseDir.trim()
|
|
109
|
+
? missions.baseDir
|
|
110
|
+
: defaults.baseDir,
|
|
111
|
+
branchPrefix: normalizeBranchPrefix(missions.branchPrefix || defaults.branchPrefix),
|
|
112
|
+
worktreePattern: typeof missions.worktreePattern === 'string' && missions.worktreePattern.trim()
|
|
113
|
+
? missions.worktreePattern
|
|
114
|
+
: defaults.worktreePattern,
|
|
115
|
+
};
|
|
36
116
|
}
|
|
37
|
-
|
|
38
117
|
function missionBaseDir(rootDir = process.cwd()) {
|
|
39
|
-
|
|
118
|
+
return node_path_1.default.resolve(rootDir, resolveMissionAdapter(rootDir).baseDir);
|
|
40
119
|
}
|
|
41
|
-
|
|
42
120
|
function missionUsesYearTier(rootDir = process.cwd()) {
|
|
43
|
-
|
|
121
|
+
return resolveMissionAdapter(rootDir).baseDir !== missionAdapterDefaults().baseDir;
|
|
44
122
|
}
|
|
45
|
-
|
|
46
123
|
function missionBranchPrefix(rootDir = process.cwd()) {
|
|
47
|
-
|
|
124
|
+
return resolveMissionAdapter(rootDir).branchPrefix;
|
|
48
125
|
}
|
|
49
|
-
|
|
50
126
|
/** @param {string} slug @param {string} [rootDir] */
|
|
51
127
|
function missionBranchName(slug, rootDir = process.cwd()) {
|
|
52
|
-
|
|
128
|
+
return missionBranchPrefix(rootDir) + slug;
|
|
53
129
|
}
|
|
54
|
-
|
|
55
130
|
/** @param {string} slug @param {string} [rootDir] */
|
|
56
131
|
function missionBranchRef(slug, rootDir = process.cwd()) {
|
|
57
|
-
|
|
132
|
+
return 'refs/heads/' + missionBranchName(slug, rootDir);
|
|
58
133
|
}
|
|
59
|
-
|
|
60
134
|
/** @param {unknown} value */
|
|
61
135
|
function isMissionSlugCandidate(value) {
|
|
62
|
-
|
|
136
|
+
return typeof value === 'string' && /^(task|adhoc)-[a-z0-9][a-z0-9-]*$/i.test(value.trim());
|
|
63
137
|
}
|
|
64
|
-
|
|
65
138
|
/** @param {string} branch @param {string} [rootDir] */
|
|
66
139
|
function extractSlugFromBranch(branch, rootDir = process.cwd()) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
140
|
+
const prefix = missionBranchPrefix(rootDir);
|
|
141
|
+
if (!branch || !branch.startsWith(prefix)) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return branch.slice(prefix.length).toLowerCase();
|
|
70
145
|
}
|
|
71
|
-
|
|
72
146
|
/** @param {string|Function} rootDirOrGitFn @param {Function|null} [maybeGitFn] */
|
|
73
147
|
function getPrimaryBranch(rootDirOrGitFn = process.cwd(), maybeGitFn = null) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
148
|
+
const rootDir = typeof rootDirOrGitFn === 'function' ? process.cwd() : rootDirOrGitFn;
|
|
149
|
+
/** @type {Function} */
|
|
150
|
+
const runner = typeof rootDirOrGitFn === 'function' ? rootDirOrGitFn : (maybeGitFn || gitModule.git);
|
|
151
|
+
const listLocalBranches = () => {
|
|
152
|
+
const result = runner(['-C', rootDir, 'branch', '--list', '--format=%(refname:short)', 'main', 'master']);
|
|
153
|
+
return (result.stdout || '').split('\n').map((b) => b.trim()).filter(Boolean);
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
const config = (0, product_config_js_1.loadAdapterConfig)(rootDir);
|
|
157
|
+
const missions = config.missions || {};
|
|
158
|
+
if (typeof missions.primaryBranch === 'string' && missions.primaryBranch.trim()) {
|
|
159
|
+
const configured = missions.primaryBranch.trim();
|
|
160
|
+
const branches = listLocalBranches();
|
|
161
|
+
if (branches.includes(configured)) {
|
|
162
|
+
return configured;
|
|
163
|
+
}
|
|
164
|
+
if (configured !== 'main' && configured !== 'master') {
|
|
165
|
+
return configured;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (_) {
|
|
170
|
+
// fall through to git-based detection
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const branches = listLocalBranches();
|
|
174
|
+
if (branches.includes('main')) {
|
|
175
|
+
return 'main';
|
|
176
|
+
}
|
|
177
|
+
if (branches.includes('master')) {
|
|
178
|
+
return 'master';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (_) {
|
|
182
|
+
// fall through
|
|
183
|
+
}
|
|
184
|
+
throw new Error("Could not detect primary branch. Neither 'main' nor 'master' exists as a local branch.");
|
|
108
185
|
}
|
|
109
|
-
|
|
110
186
|
function resolveMainRepo() {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
throw new Error(
|
|
175
|
-
`Could not resolve primary repository. No worktree on '${primaryBranch}' branch found and PRIMARY_WORKTREE is not set. ` +
|
|
176
|
-
"Verify your worktree setup or set the PRIMARY_WORKTREE environment variable."
|
|
177
|
-
);
|
|
187
|
+
if (process.env.PRIMARY_WORKTREE) {
|
|
188
|
+
return process.env.PRIMARY_WORKTREE;
|
|
189
|
+
}
|
|
190
|
+
const cwd = process.cwd();
|
|
191
|
+
const primaryBranch = getPrimaryBranch(process.cwd(), gitModule.git);
|
|
192
|
+
try {
|
|
193
|
+
const lines = gitModule.git(['worktree', 'list', '--porcelain']).stdout.split('\n');
|
|
194
|
+
const worktrees = [];
|
|
195
|
+
let current = null;
|
|
196
|
+
for (const line of lines) {
|
|
197
|
+
if (line.startsWith('worktree ')) {
|
|
198
|
+
if (current) {
|
|
199
|
+
worktrees.push(current);
|
|
200
|
+
}
|
|
201
|
+
current = { path: line.slice('worktree '.length).trim(), branch: null, bare: false };
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (!current) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (line.startsWith('branch ')) {
|
|
208
|
+
current.branch = line.slice('branch '.length).trim();
|
|
209
|
+
}
|
|
210
|
+
else if (line === 'bare') {
|
|
211
|
+
current.bare = true;
|
|
212
|
+
}
|
|
213
|
+
else if (line === '') {
|
|
214
|
+
worktrees.push(current);
|
|
215
|
+
current = null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (current) {
|
|
219
|
+
worktrees.push(current);
|
|
220
|
+
}
|
|
221
|
+
const primaryWorktree = worktrees.find(wt => !wt.bare && wt.branch === `refs/heads/${primaryBranch}`);
|
|
222
|
+
if (primaryWorktree) {
|
|
223
|
+
return primaryWorktree.path;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (_) {
|
|
227
|
+
// ignore git errors, fall through to error
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const currentBranch = gitModule.getCurrentBranch(cwd);
|
|
231
|
+
const toplevel = gitModule.git(['-C', cwd, 'rev-parse', '--show-toplevel']);
|
|
232
|
+
const repoRoot = (toplevel.stdout || '').trim();
|
|
233
|
+
if (toplevel.status === 0 && repoRoot) {
|
|
234
|
+
if (currentBranch === primaryBranch) {
|
|
235
|
+
return repoRoot;
|
|
236
|
+
}
|
|
237
|
+
if (node_path_1.default.resolve(repoRoot) === node_path_1.default.resolve(cwd) &&
|
|
238
|
+
(0, product_config_js_1.isStandaloneWorkflowLayout)(repoRoot) &&
|
|
239
|
+
!currentBranch) {
|
|
240
|
+
return repoRoot;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (_) {
|
|
245
|
+
// ignore git errors, fall through to error
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`Could not resolve primary repository. No worktree on '${primaryBranch}' branch found and PRIMARY_WORKTREE is not set. ` +
|
|
248
|
+
"Verify your worktree setup or set the PRIMARY_WORKTREE environment variable.");
|
|
178
249
|
}
|
|
179
|
-
|
|
180
250
|
function getPrimaryWorktree() {
|
|
181
|
-
|
|
251
|
+
return resolveMainRepo();
|
|
182
252
|
}
|
|
183
|
-
|
|
184
253
|
/** @param {string} slug @param {string} [mainRepo] */
|
|
185
254
|
function conventionalWorktreePath(slug, mainRepo = getPrimaryWorktree()) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
255
|
+
const projectName = node_path_1.default.basename(mainRepo);
|
|
256
|
+
const pattern = resolveMissionAdapter(mainRepo).worktreePattern;
|
|
257
|
+
const rendered = pattern
|
|
258
|
+
.replaceAll('<repo>', projectName)
|
|
259
|
+
.replaceAll('<slug>', slug);
|
|
260
|
+
return node_path_1.default.resolve(mainRepo, rendered);
|
|
192
261
|
}
|
|
193
|
-
|
|
194
262
|
/**
|
|
195
263
|
* Conventional path for an auto-created *base* feature-branch worktree.
|
|
196
264
|
*
|
|
@@ -201,10 +269,9 @@ function conventionalWorktreePath(slug, mainRepo = getPrimaryWorktree()) {
|
|
|
201
269
|
*/
|
|
202
270
|
/** @param {string} baseBranch @param {string} [mainRepo] */
|
|
203
271
|
function conventionalBaseWorktreePath(baseBranch, mainRepo = getPrimaryWorktree()) {
|
|
204
|
-
|
|
205
|
-
|
|
272
|
+
const safeName = String(baseBranch).replace(/[\\/]+/g, '-');
|
|
273
|
+
return conventionalWorktreePath(`base-${safeName}`, mainRepo);
|
|
206
274
|
}
|
|
207
|
-
|
|
208
275
|
/**
|
|
209
276
|
* Detect the branch HEAD is on at draft time, to be used as the mission base.
|
|
210
277
|
*
|
|
@@ -219,31 +286,28 @@ function conventionalBaseWorktreePath(baseBranch, mainRepo = getPrimaryWorktree(
|
|
|
219
286
|
*/
|
|
220
287
|
/** @param {string} [cwd] @param {{gitFn?: Function}} [options] */
|
|
221
288
|
function detectLaunchBaseBranch(cwd = process.cwd(), options = {}) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
return branch;
|
|
289
|
+
/** @type {Function} */
|
|
290
|
+
const runner = options.gitFn || gitModule.git;
|
|
291
|
+
const result = runner(['-C', cwd, 'branch', '--show-current']);
|
|
292
|
+
const branch = ((result && result.stdout) || '').trim();
|
|
293
|
+
if (!branch) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const prefix = missionBranchPrefix(cwd);
|
|
297
|
+
if (branch.startsWith(prefix)) {
|
|
298
|
+
throw new Error(`Cannot launch a mission from mission branch '${branch}': the '${prefix}' prefix is reserved. ` +
|
|
299
|
+
'Check out the feature branch or primary branch you want as the base before running draft.');
|
|
300
|
+
}
|
|
301
|
+
return branch;
|
|
238
302
|
}
|
|
239
|
-
|
|
240
303
|
/** @param {string} content */
|
|
241
304
|
function parseBaseBranchLine(content) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
305
|
+
if (!content) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const match = content.match(/^Base-Branch:\s*(\S+)\s*$/m);
|
|
309
|
+
return match ? match[1].trim() : null;
|
|
245
310
|
}
|
|
246
|
-
|
|
247
311
|
/**
|
|
248
312
|
* Read the `Base-Branch:` line recorded in the mission's MISSION.md.
|
|
249
313
|
*
|
|
@@ -251,48 +315,47 @@ function parseBaseBranchLine(content) {
|
|
|
251
315
|
* is not on disk, falls back to reading it from the mission branch via
|
|
252
316
|
* `git show <branch>:<path>`. Returns `null` when no `Base-Branch:` line exists
|
|
253
317
|
* (every pre-existing mission), so callers fall back to the primary branch.
|
|
254
|
-
|
|
318
|
+
*/
|
|
255
319
|
/** @param {string} slug @param {string} [rootDir] @param {{gitFn?: Function}} [options] */
|
|
256
320
|
function readRecordedBaseBranch(slug, rootDir = process.cwd(), options = {}) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
321
|
+
/** @type {Function | null} */
|
|
322
|
+
const gitFn = options.gitFn ?? null;
|
|
323
|
+
if (!slug) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const missionDir = findMissionDir(slug, rootDir);
|
|
327
|
+
if (missionDir) {
|
|
328
|
+
const missionPath = node_path_1.default.join(missionDir, 'MISSION.md');
|
|
329
|
+
if (fs.existsSync(missionPath)) {
|
|
330
|
+
return parseBaseBranchLine(fs.readFileSync(missionPath, 'utf8'));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const runner = gitFn || gitModule.git;
|
|
334
|
+
const branch = missionBranchName(slug, rootDir);
|
|
335
|
+
const baseSlugMatch = slug.match(/^(task-\d+)/i);
|
|
336
|
+
const baseSlug = baseSlugMatch ? baseSlugMatch[1].toLowerCase() : slug;
|
|
337
|
+
const year = getMissionYear(slug, rootDir);
|
|
338
|
+
const adapterBaseDir = (node_path_1.default.relative(rootDir, missionBaseDir(rootDir)) || '.').split(node_path_1.default.sep).join('/');
|
|
339
|
+
const files = Array.from(new Set([
|
|
340
|
+
node_path_1.default.posix.join(adapterBaseDir, year, slug, 'MISSION.md'),
|
|
341
|
+
node_path_1.default.posix.join(adapterBaseDir, year, baseSlug, 'MISSION.md')
|
|
342
|
+
]));
|
|
343
|
+
for (const f of files) {
|
|
344
|
+
try {
|
|
345
|
+
const res = runner(['-C', rootDir, 'show', `${branch}:${f}`]);
|
|
346
|
+
if (res && res.status === 0) {
|
|
347
|
+
const parsed = parseBaseBranchLine(res.stdout);
|
|
348
|
+
if (parsed) {
|
|
349
|
+
return parsed;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch (_) {
|
|
354
|
+
// ignore and try the next candidate
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
294
358
|
}
|
|
295
|
-
|
|
296
359
|
/**
|
|
297
360
|
* Resolve the base branch a mission was drafted from.
|
|
298
361
|
*
|
|
@@ -301,34 +364,41 @@ function readRecordedBaseBranch(slug, rootDir = process.cwd(), options = {}) {
|
|
|
301
364
|
*/
|
|
302
365
|
/** @param {string} slug @param {string} [rootDir] @param {{gitFn?: Function}} [options] */
|
|
303
366
|
function resolveMissionBaseBranch(slug, rootDir = process.cwd(), options = {}) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
367
|
+
/** @type {Function | null} */
|
|
368
|
+
const gitFn = options.gitFn ?? null;
|
|
369
|
+
const recorded = readRecordedBaseBranch(slug, rootDir, { gitFn: gitFn });
|
|
370
|
+
if (recorded) {
|
|
371
|
+
return recorded;
|
|
372
|
+
}
|
|
373
|
+
return gitFn ? getPrimaryBranch(rootDir, gitFn) : getPrimaryBranch(rootDir);
|
|
309
374
|
}
|
|
310
|
-
|
|
311
375
|
/** @param {string} branchRef @param {Function} runner @param {string} mainRepo */
|
|
312
376
|
function findWorktreeForBranch(branchRef, runner, mainRepo) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
377
|
+
const result = runner(['-C', mainRepo, 'worktree', 'list', '--porcelain']);
|
|
378
|
+
const lines = ((result && result.stdout) || '').split('\n');
|
|
379
|
+
let current = null;
|
|
380
|
+
for (const line of lines) {
|
|
381
|
+
if (line.startsWith('worktree ')) {
|
|
382
|
+
current = { path: line.slice('worktree '.length).trim(), branch: null };
|
|
383
|
+
}
|
|
384
|
+
else if (!current) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
else if (line.startsWith('branch ')) {
|
|
388
|
+
current.branch = line.slice('branch '.length).trim();
|
|
389
|
+
}
|
|
390
|
+
else if (line === '') {
|
|
391
|
+
if (current.branch === branchRef) {
|
|
392
|
+
return current.path;
|
|
393
|
+
}
|
|
394
|
+
current = null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (current && current.branch === branchRef) {
|
|
398
|
+
return current.path;
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
330
401
|
}
|
|
331
|
-
|
|
332
402
|
/**
|
|
333
403
|
* Resolve the worktree the mission integrates back into.
|
|
334
404
|
*
|
|
@@ -340,376 +410,336 @@ function findWorktreeForBranch(branchRef, runner, mainRepo) {
|
|
|
340
410
|
*/
|
|
341
411
|
/** @param {string} slug @param {{rootDir?: string, gitFn?: Function}} [options] */
|
|
342
412
|
function resolveBaseWorktree(slug, options = {}) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const addResult = runner(['-C', mainRepo, 'worktree', 'add', worktreePath, base]);
|
|
371
|
-
if (!addResult || addResult.status !== 0) {
|
|
372
|
-
const detail = addResult ? [addResult.stdout, addResult.stderr].filter(Boolean).join('\n').trim() : '';
|
|
373
|
-
throw new Error(
|
|
374
|
-
`Could not create base worktree for base branch '${base}' at ${worktreePath}${detail ? `: ${detail}` : '.'}`
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
return worktreePath;
|
|
413
|
+
const rootDir = options.rootDir || process.cwd();
|
|
414
|
+
/** @type {Function | null} */
|
|
415
|
+
const gitFn = options.gitFn ?? null;
|
|
416
|
+
const runner = gitFn || gitModule.git;
|
|
417
|
+
const base = resolveMissionBaseBranch(slug, rootDir, { gitFn: gitFn });
|
|
418
|
+
const primary = gitFn ? getPrimaryBranch(rootDir, gitFn) : getPrimaryBranch(rootDir);
|
|
419
|
+
if (base === primary) {
|
|
420
|
+
return getPrimaryWorktree();
|
|
421
|
+
}
|
|
422
|
+
const mainRepo = getPrimaryWorktree();
|
|
423
|
+
const baseRef = `refs/heads/${base}`;
|
|
424
|
+
const existing = findWorktreeForBranch(baseRef, runner, mainRepo);
|
|
425
|
+
if (existing) {
|
|
426
|
+
return existing;
|
|
427
|
+
}
|
|
428
|
+
const branchExists = runner(['-C', mainRepo, 'show-ref', '--verify', '--quiet', baseRef]);
|
|
429
|
+
if (!branchExists || branchExists.status !== 0) {
|
|
430
|
+
throw new Error(`Mission ${slug} records base branch '${base}' but it does not exist locally. ` +
|
|
431
|
+
`Create or fetch the '${base}' base branch before integrating.`);
|
|
432
|
+
}
|
|
433
|
+
const worktreePath = conventionalBaseWorktreePath(base, mainRepo);
|
|
434
|
+
const addResult = runner(['-C', mainRepo, 'worktree', 'add', worktreePath, base]);
|
|
435
|
+
if (!addResult || addResult.status !== 0) {
|
|
436
|
+
const detail = addResult ? [addResult.stdout, addResult.stderr].filter(Boolean).join('\n').trim() : '';
|
|
437
|
+
throw new Error(`Could not create base worktree for base branch '${base}' at ${worktreePath}${detail ? ': ' + detail : '.'}`);
|
|
438
|
+
}
|
|
439
|
+
return worktreePath;
|
|
378
440
|
}
|
|
379
|
-
|
|
380
441
|
/** @param {string|undefined} [slug] @param {string} [rootDir] */
|
|
381
442
|
function getMissionYear(slug = undefined, rootDir = process.cwd()) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (fs.existsSync(missionDir)) {
|
|
418
|
-
return year;
|
|
419
|
-
}
|
|
443
|
+
if (process.env.MISSION_YEAR_OVERRIDE) {
|
|
444
|
+
return process.env.MISSION_YEAR_OVERRIDE;
|
|
445
|
+
}
|
|
446
|
+
const hasExplicitConfig = fs.existsSync(node_path_1.default.join(rootDir, 'workflow.config.json'));
|
|
447
|
+
if (slug && (missionUsesYearTier(rootDir) || (!hasExplicitConfig && fs.existsSync(node_path_1.default.join(rootDir, 'docs', 'missions'))))) {
|
|
448
|
+
const baseDir = missionUsesYearTier(rootDir)
|
|
449
|
+
? missionBaseDir(rootDir)
|
|
450
|
+
: node_path_1.default.join(rootDir, 'docs', 'missions');
|
|
451
|
+
if (fs.existsSync(baseDir)) {
|
|
452
|
+
try {
|
|
453
|
+
const stat = fs.statSync(baseDir);
|
|
454
|
+
if (!stat.isDirectory()) {
|
|
455
|
+
return new Date().getFullYear().toString();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
catch (_) {
|
|
459
|
+
return new Date().getFullYear().toString();
|
|
460
|
+
}
|
|
461
|
+
const years = fs.readdirSync(baseDir)
|
|
462
|
+
.filter(d => /^\d{4}$/.test(d))
|
|
463
|
+
.sort((a, b) => b.localeCompare(a));
|
|
464
|
+
const slugStr = slug;
|
|
465
|
+
const candidateSlugs = [slugStr.toLowerCase()];
|
|
466
|
+
const baseTaskMatch = slugStr.match(/^(task-\d+)/i);
|
|
467
|
+
if (baseTaskMatch) {
|
|
468
|
+
candidateSlugs.push(baseTaskMatch[1].toLowerCase());
|
|
469
|
+
}
|
|
470
|
+
for (const year of years) {
|
|
471
|
+
for (const s of candidateSlugs) {
|
|
472
|
+
const missionDir = node_path_1.default.join(baseDir, year, s);
|
|
473
|
+
if (fs.existsSync(missionDir)) {
|
|
474
|
+
return year;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
420
478
|
}
|
|
421
|
-
}
|
|
422
479
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
return new Date().getFullYear().toString();
|
|
480
|
+
return new Date().getFullYear().toString();
|
|
426
481
|
}
|
|
427
|
-
|
|
428
482
|
/** @param {string} slug @param {string} [rootDir] @param {{missionPath?: string}} options */
|
|
429
483
|
function findMissionDir(slug, rootDir = process.cwd(), options = {}) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const baseSlug = baseTaskMatch[1].toLowerCase();
|
|
443
|
-
const baseMissionDir = missionDirForSlug(rootDir, baseSlug);
|
|
444
|
-
if (fs.existsSync(baseMissionDir)) {return baseMissionDir;}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Read compatibility during the TASK-1243 migration window. New no-config
|
|
448
|
-
// missions are created flat, but existing year-tier history remains readable.
|
|
449
|
-
const legacyBaseDir = path.join(rootDir, 'docs', 'missions');
|
|
450
|
-
if (!missionUsesYearTier(rootDir) && !fs.existsSync(path.join(rootDir, 'workflow.config.json')) && fs.existsSync(legacyBaseDir)) {
|
|
451
|
-
const year = getMissionYear(slug, rootDir);
|
|
452
|
-
const legacyMissionDir = path.join(legacyBaseDir, year, slug);
|
|
453
|
-
if (fs.existsSync(legacyMissionDir)) {return legacyMissionDir;}
|
|
484
|
+
const opts = options;
|
|
485
|
+
if (opts.missionPath && fs.existsSync(opts.missionPath)) {
|
|
486
|
+
return fs.statSync(opts.missionPath).isDirectory() ? opts.missionPath : node_path_1.default.dirname(opts.missionPath);
|
|
487
|
+
}
|
|
488
|
+
if (!slug) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const missionDir = missionDirForSlug(rootDir, slug);
|
|
492
|
+
if (fs.existsSync(missionDir)) {
|
|
493
|
+
return missionDir;
|
|
494
|
+
}
|
|
495
|
+
const baseTaskMatch = slug.match(/^(task-\d+)/i);
|
|
454
496
|
if (baseTaskMatch) {
|
|
455
|
-
|
|
456
|
-
|
|
497
|
+
const baseSlug = baseTaskMatch[1].toLowerCase();
|
|
498
|
+
const baseMissionDir = missionDirForSlug(rootDir, baseSlug);
|
|
499
|
+
if (fs.existsSync(baseMissionDir)) {
|
|
500
|
+
return baseMissionDir;
|
|
501
|
+
}
|
|
457
502
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
503
|
+
const legacyBaseDir = node_path_1.default.join(rootDir, 'docs', 'missions');
|
|
504
|
+
if (!missionUsesYearTier(rootDir) && !fs.existsSync(node_path_1.default.join(rootDir, 'workflow.config.json')) && fs.existsSync(legacyBaseDir)) {
|
|
505
|
+
const year = getMissionYear(slug, rootDir);
|
|
506
|
+
const legacyMissionDir = node_path_1.default.join(legacyBaseDir, year, slug);
|
|
507
|
+
if (fs.existsSync(legacyMissionDir)) {
|
|
508
|
+
return legacyMissionDir;
|
|
509
|
+
}
|
|
510
|
+
if (baseTaskMatch) {
|
|
511
|
+
const legacyBaseMissionDir = node_path_1.default.join(legacyBaseDir, year, baseTaskMatch[1].toLowerCase());
|
|
512
|
+
if (fs.existsSync(legacyBaseMissionDir)) {
|
|
513
|
+
return legacyBaseMissionDir;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return null;
|
|
461
518
|
}
|
|
462
|
-
|
|
463
519
|
/** @param {string} slug @param {{cwd?: string, gitFn?: Function}} [options] */
|
|
464
520
|
function resolveWorktree(slug, options = {}) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
521
|
+
const cwd = options.cwd || process.cwd();
|
|
522
|
+
/** @type {Function | null} */
|
|
523
|
+
const gitFn = options.gitFn ?? null;
|
|
524
|
+
const runGit = gitFn || gitModule.git;
|
|
525
|
+
const branchRef = missionBranchRef(slug, cwd);
|
|
526
|
+
try {
|
|
527
|
+
const lines = runGit(['worktree', 'list', '--porcelain']).stdout.split('\n');
|
|
528
|
+
const matches = [];
|
|
529
|
+
let current = null;
|
|
530
|
+
for (const line of lines) {
|
|
531
|
+
if (line.startsWith('worktree ')) {
|
|
532
|
+
current = { path: line.slice('worktree '.length).trim(), branch: null, prunable: false };
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (!current) {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (line.startsWith('branch ')) {
|
|
539
|
+
current.branch = line.slice('branch '.length).trim();
|
|
540
|
+
}
|
|
541
|
+
else if (line.startsWith('prunable ')) {
|
|
542
|
+
current.prunable = true;
|
|
543
|
+
}
|
|
544
|
+
else if (line === '') {
|
|
545
|
+
if (current.branch === branchRef) {
|
|
546
|
+
matches.push(current);
|
|
547
|
+
}
|
|
548
|
+
current = null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (current && current.branch === branchRef) {
|
|
552
|
+
matches.push(current);
|
|
553
|
+
}
|
|
554
|
+
const liveMatches = matches.filter(m => !m.prunable);
|
|
555
|
+
if (liveMatches.length > 0) {
|
|
556
|
+
const cwdMatch = liveMatches.find(m => cwd === m.path || cwd.startsWith(m.path + '/'));
|
|
557
|
+
if (cwdMatch) {
|
|
558
|
+
return cwdMatch.path;
|
|
559
|
+
}
|
|
560
|
+
return liveMatches[0].path;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch (_) {
|
|
564
|
+
// fall through to branch-based cwd fallback
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
if (gitModule.getCurrentBranch(cwd) === missionBranchName(slug, cwd)) {
|
|
568
|
+
return cwd;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
catch (_) {
|
|
572
|
+
// fall through to null
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
515
575
|
}
|
|
516
|
-
|
|
517
576
|
/** @param {string} missionDir */
|
|
518
577
|
function findCheckpoints(missionDir) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
578
|
+
const files = fs.readdirSync(missionDir);
|
|
579
|
+
return files
|
|
580
|
+
.filter(f => /^(CHECKPOINT_|CP-\d+).*\.md$/i.test(f))
|
|
581
|
+
.sort(compareCheckpointFiles)
|
|
582
|
+
.map(f => node_path_1.default.join(missionDir, f));
|
|
524
583
|
}
|
|
525
|
-
|
|
526
584
|
/** @param {string} a @param {string} b */
|
|
527
585
|
function compareCheckpointFiles(a, b) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
return a.localeCompare(b);
|
|
586
|
+
const aOrder = checkpointOrder(a);
|
|
587
|
+
const bOrder = checkpointOrder(b);
|
|
588
|
+
if (aOrder !== bOrder) {
|
|
589
|
+
return aOrder - bOrder;
|
|
590
|
+
}
|
|
591
|
+
return a.localeCompare(b);
|
|
536
592
|
}
|
|
537
|
-
|
|
538
593
|
/** @param {string} filename */
|
|
539
594
|
function checkpointOrder(filename) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
return Number.MAX_SAFE_INTEGER;
|
|
595
|
+
const numericMatch = filename.match(/(?:CP-|CHECKPOINT_)(\d+)/i);
|
|
596
|
+
if (numericMatch) {
|
|
597
|
+
return Number(numericMatch[1]);
|
|
598
|
+
}
|
|
599
|
+
return Number.MAX_SAFE_INTEGER;
|
|
546
600
|
}
|
|
547
|
-
|
|
548
601
|
/** @param {string} filePath */
|
|
549
602
|
function getFirstLine(filePath) {
|
|
550
|
-
|
|
551
|
-
|
|
603
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
604
|
+
return content.split('\n')[0].replace(/^#+\s*/, '').trim();
|
|
552
605
|
}
|
|
553
|
-
|
|
554
606
|
/** @param {string} [slug] */
|
|
555
607
|
function missionTitle(slug) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
608
|
+
if (!slug) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
const missionDir = findMissionDir(slug);
|
|
612
|
+
if (!missionDir) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
const missionPath = node_path_1.default.join(missionDir, 'MISSION.md');
|
|
616
|
+
if (!fs.existsSync(missionPath)) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
const firstLine = fs.readFileSync(missionPath, 'utf8').split('\n')[0] || '';
|
|
620
|
+
return firstLine.replace(/^#\s*Mission:\s*/i, '').trim() || null;
|
|
565
621
|
}
|
|
566
|
-
|
|
567
|
-
const SUPPORTED_VERIFY_AREAS = new Set(['docs', 'workflow', 'web', 'server', 'auth', 'android', 'k8s', 'deps', 'all']);
|
|
568
|
-
|
|
622
|
+
exports.SUPPORTED_VERIFY_AREAS = new Set(['docs', 'workflow', 'web', 'server', 'auth', 'android', 'k8s', 'deps', 'all']);
|
|
569
623
|
/** @param {string} [area] */
|
|
570
624
|
function normalizeVerifyArea(area) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
625
|
+
if (!area) {
|
|
626
|
+
return 'docs';
|
|
627
|
+
}
|
|
628
|
+
if (area === 'auth-server') {
|
|
629
|
+
return 'auth';
|
|
630
|
+
}
|
|
631
|
+
return exports.SUPPORTED_VERIFY_AREAS.has(area) ? area : area;
|
|
574
632
|
}
|
|
575
|
-
|
|
576
633
|
/** @param {string} content */
|
|
577
634
|
function detectMissionAreaFromContent(content) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
// (no dots) referenced via `./` or `../`. The area argument must sit at end-of-line
|
|
581
|
-
// to prevent prose-matched paths (e.g. "run ./scripts/deploy.sh server before merge")
|
|
582
|
-
// from being mistaken for gate invocations. Falls back to `docs` when none found.
|
|
583
|
-
const gateMatch = content.match(
|
|
584
|
-
/(?:^|\s)(?:\.{1,2}\/[\w.\/-]+\.(?:sh|bash|py|rb)|\.{1,2}\/[a-z][\w-]*)\s+([a-zA-Z0-9_-]+)\s*(?:$|\n)/m
|
|
585
|
-
);
|
|
586
|
-
return normalizeVerifyArea(gateMatch ? gateMatch[1] : 'docs');
|
|
635
|
+
const gateMatch = content.match(/(?:^|\s)(?:\.{1,2}\/[\w.\/-]+\.(?:sh|bash|py|rb)|\.{1,2}\/[a-z][\w-]*)\s+([a-zA-Z0-9_-]+)\s*(?:$|\n)/m);
|
|
636
|
+
return normalizeVerifyArea(gateMatch ? gateMatch[1] : 'docs');
|
|
587
637
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
638
|
+
function findMissionArea(missionDir) {
|
|
639
|
+
const missionPath = node_path_1.default.join(missionDir, 'MISSION.md');
|
|
640
|
+
if (!fs.existsSync(missionPath)) {
|
|
641
|
+
return 'docs';
|
|
642
|
+
}
|
|
643
|
+
const content = fs.readFileSync(missionPath, 'utf8');
|
|
644
|
+
return detectMissionAreaFromContent(content);
|
|
595
645
|
}
|
|
596
|
-
|
|
597
646
|
/** @param {{commandRunner?: Function}} [options] */
|
|
598
647
|
function graphifyAvailable(options = {}) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
return probeGraphifyAvailability({ commandRunner: run }).available;
|
|
648
|
+
const commandRunner = options.commandRunner ?? null;
|
|
649
|
+
const cmdRunner = commandRunner || gitModule.run;
|
|
650
|
+
return probeGraphifyAvailability({ commandRunner: cmdRunner }).available;
|
|
603
651
|
}
|
|
604
|
-
|
|
605
652
|
/** @returns {string[]} */
|
|
606
653
|
function graphifyCommandCandidates() {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
return candidates;
|
|
654
|
+
const candidates = [];
|
|
655
|
+
const seen = new Set();
|
|
656
|
+
const pushCandidate = (candidate) => {
|
|
657
|
+
if (!candidate || seen.has(candidate)) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
seen.add(candidate);
|
|
661
|
+
candidates.push(candidate);
|
|
662
|
+
};
|
|
663
|
+
if (process.env.GRAPHIFY_BIN) {
|
|
664
|
+
pushCandidate(process.env.GRAPHIFY_BIN);
|
|
665
|
+
}
|
|
666
|
+
pushCandidate('graphify');
|
|
667
|
+
pushCandidate(node_path_1.default.join(node_os_1.default.homedir(), '.local', 'bin', 'graphify'));
|
|
668
|
+
return candidates;
|
|
623
669
|
}
|
|
624
|
-
|
|
625
670
|
/** @param {{commandRunner?: Function}} [options] */
|
|
626
671
|
function probeGraphifyAvailability(options = {}) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
return { available: false, reason: 'missing-command' };
|
|
672
|
+
const commandRunner = options.commandRunner ?? null;
|
|
673
|
+
const cmdRunner = commandRunner || gitModule.run;
|
|
674
|
+
for (const command of graphifyCommandCandidates()) {
|
|
675
|
+
try {
|
|
676
|
+
const result = cmdRunner(command, ['--help']);
|
|
677
|
+
return {
|
|
678
|
+
available: result.status !== null && result.status !== undefined,
|
|
679
|
+
command,
|
|
680
|
+
status: result.status ?? null
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
const e = error;
|
|
685
|
+
if (e && e.code === 'ENOENT') {
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
available: false,
|
|
690
|
+
reason: 'probe-failed',
|
|
691
|
+
command,
|
|
692
|
+
error
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return { available: false, reason: 'missing-command' };
|
|
654
697
|
}
|
|
655
|
-
|
|
656
698
|
/**
|
|
657
699
|
* @param {{rootDir?: string, commandRunner?: Function, log?: Function, startMessage?: string, failureHint?: string}} [options]
|
|
658
700
|
*/
|
|
701
|
+
/** @param {{rootDir?: string, commandRunner?: Function, log?: Function, startMessage?: string, failureHint?: string}} [options] */
|
|
659
702
|
function updateGraphifyKnowledgeGraph(options = {}) {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
cwd: rootDir,
|
|
687
|
-
stdio: 'inherit'
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
if (result.status !== 0) {
|
|
691
|
-
log(fmt.status('WARN', `graphify update failed with status ${result.status}. ${failureHint}`));
|
|
692
|
-
return { updated: false, skipped: true, reason: 'update-failed', status: result.status };
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
return { updated: true, skipped: false };
|
|
703
|
+
const rootDir = options.rootDir || process.cwd();
|
|
704
|
+
const commandRunner = options.commandRunner;
|
|
705
|
+
const logFn = options.log || fmt.log.plain;
|
|
706
|
+
const startMessage = options.startMessage || 'Updating graphify knowledge graph...';
|
|
707
|
+
const failureHint = options.failureHint || 'Continuing without blocking workflow.';
|
|
708
|
+
const cmdRunner = commandRunner || gitModule.run;
|
|
709
|
+
const probe = probeGraphifyAvailability({ commandRunner: cmdRunner });
|
|
710
|
+
if (!probe.available) {
|
|
711
|
+
if (probe.reason === 'missing-command') {
|
|
712
|
+
logFn(fmt.status('WARN', 'graphify not found in PATH. Skipping knowledge graph update.'));
|
|
713
|
+
return { updated: false, skipped: true, reason: 'missing-command' };
|
|
714
|
+
}
|
|
715
|
+
const probeError = probe.error && typeof probe.error === 'object' && 'message' in probe.error ? probe.error.message : 'unknown probe failure';
|
|
716
|
+
logFn(fmt.status('WARN', `graphify probe failed (${probeError}). Skipping knowledge graph update.`));
|
|
717
|
+
return { updated: false, skipped: true, reason: 'probe-failed' };
|
|
718
|
+
}
|
|
719
|
+
logFn(fmt.status('INFO', startMessage));
|
|
720
|
+
const result = cmdRunner(probe.command, ['update', '.'], {
|
|
721
|
+
cwd: rootDir,
|
|
722
|
+
stdio: 'inherit'
|
|
723
|
+
});
|
|
724
|
+
if (result.status !== 0) {
|
|
725
|
+
logFn(fmt.status('WARN', `graphify update failed with status ${result.status}. ${failureHint}`));
|
|
726
|
+
return { updated: false, skipped: true, reason: 'update-failed', status: result.status };
|
|
727
|
+
}
|
|
728
|
+
return { updated: true, skipped: false };
|
|
696
729
|
}
|
|
697
|
-
|
|
698
730
|
/** @param {string} rootDir @param {string} slug */
|
|
699
731
|
function missionPathForSlug(rootDir, slug) {
|
|
700
|
-
|
|
732
|
+
return node_path_1.default.join(missionDirForSlug(rootDir, slug), 'MISSION.md');
|
|
701
733
|
}
|
|
702
|
-
|
|
703
734
|
/** @param {string} rootDir @param {string} slug */
|
|
704
735
|
function missionDirForSlug(rootDir, slug) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
736
|
+
const parts = [missionBaseDir(rootDir)];
|
|
737
|
+
if (missionUsesYearTier(rootDir)) {
|
|
738
|
+
parts.push(getMissionYear(slug, rootDir));
|
|
739
|
+
}
|
|
740
|
+
parts.push(slug);
|
|
741
|
+
return node_path_1.default.join(...parts);
|
|
711
742
|
}
|
|
712
|
-
|
|
713
743
|
/**
|
|
714
744
|
* Infer mission slug from current context:
|
|
715
745
|
* 1. Explicit slugCandidate (task-NNN)
|
|
@@ -722,53 +752,53 @@ function missionDirForSlug(rootDir, slug) {
|
|
|
722
752
|
*/
|
|
723
753
|
/** @param {string} [slugCandidate] */
|
|
724
754
|
function inferSlug(slugCandidate) {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
755
|
+
if (slugCandidate && isMissionSlugCandidate(slugCandidate)) {
|
|
756
|
+
return slugCandidate.toLowerCase();
|
|
757
|
+
}
|
|
758
|
+
// 1. Check branch
|
|
759
|
+
try {
|
|
760
|
+
const branch = gitModule.getCurrentBranch();
|
|
761
|
+
const fromBranch = extractSlugFromBranch(branch);
|
|
762
|
+
if (fromBranch) {
|
|
763
|
+
return fromBranch;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
catch (_) {
|
|
767
|
+
// ignore
|
|
768
|
+
}
|
|
769
|
+
// 3. Check directory name
|
|
770
|
+
const cwd = process.cwd();
|
|
771
|
+
const dirName = node_path_1.default.basename(cwd);
|
|
772
|
+
const dirSlugMatch = dirName.match(/((?:task|adhoc)-[a-z0-9][a-z0-9-]*)$/i);
|
|
773
|
+
if (dirSlugMatch) {
|
|
774
|
+
return dirSlugMatch[1].toLowerCase();
|
|
775
|
+
}
|
|
776
|
+
// 3. Check worktree registry
|
|
777
|
+
try {
|
|
778
|
+
const lines = gitModule.git(['worktree', 'list', '--porcelain']).stdout.split('\n');
|
|
779
|
+
let currentPath = null;
|
|
780
|
+
for (const line of lines) {
|
|
781
|
+
if (line.startsWith('worktree ')) {
|
|
782
|
+
currentPath = line.slice('worktree '.length).trim();
|
|
783
|
+
}
|
|
784
|
+
else if (line.startsWith('branch ') && currentPath === cwd) {
|
|
785
|
+
const branch = line.slice('branch '.length).trim();
|
|
786
|
+
const shortBranch = branch.replace(/^refs\/heads\//, '');
|
|
787
|
+
const fromBranch = extractSlugFromBranch(shortBranch, cwd);
|
|
788
|
+
if (fromBranch) {
|
|
789
|
+
return fromBranch;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
else if (line === '') {
|
|
793
|
+
currentPath = null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
catch (_) {
|
|
798
|
+
// ignore
|
|
799
|
+
}
|
|
800
|
+
return null;
|
|
770
801
|
}
|
|
771
|
-
|
|
772
802
|
/**
|
|
773
803
|
* Parse files with merge conflicts from the output of `git merge --no-commit --no-ff`.
|
|
774
804
|
*
|
|
@@ -781,28 +811,33 @@ function inferSlug(slugCandidate) {
|
|
|
781
811
|
*/
|
|
782
812
|
/** @param {string} output */
|
|
783
813
|
function parseConflictFilesFromMergeOutput(output) {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
814
|
+
const seen = new Set();
|
|
815
|
+
const files = [];
|
|
816
|
+
for (const line of output.split('\n')) {
|
|
817
|
+
if (!line.startsWith('CONFLICT')) {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
const inMatch = line.match(/Merge conflict in (.+)$/);
|
|
821
|
+
if (inMatch) {
|
|
822
|
+
const f = inMatch[1].trim();
|
|
823
|
+
if (f && !seen.has(f)) {
|
|
824
|
+
seen.add(f);
|
|
825
|
+
files.push(f);
|
|
826
|
+
}
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
const colonIdx = line.indexOf(':');
|
|
830
|
+
if (colonIdx !== -1) {
|
|
831
|
+
const rest = line.slice(colonIdx + 1).trim();
|
|
832
|
+
const f = rest.split(/\s+/)[0];
|
|
833
|
+
if (f && !seen.has(f)) {
|
|
834
|
+
seen.add(f);
|
|
835
|
+
files.push(f);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return files;
|
|
804
840
|
}
|
|
805
|
-
|
|
806
841
|
/**
|
|
807
842
|
* Identify files that would have merge conflicts when merging `branch` into `rootDir`.
|
|
808
843
|
*
|
|
@@ -814,202 +849,166 @@ function parseConflictFilesFromMergeOutput(output) {
|
|
|
814
849
|
* @param {{ gitRunner?: Function }} [options]
|
|
815
850
|
* @returns {string[]} Relative paths of conflicting files (empty if no conflicts).
|
|
816
851
|
*/
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
// unfinished rebase, locked index, etc.). Surface the raw failure rather than returning
|
|
833
|
-
// an empty list, which callers would interpret as "no conflicts / merge was clean".
|
|
834
|
-
const summary = output.slice(0, 500) || '(no output)';
|
|
835
|
-
throw new Error(`git merge exited ${merge.status} with no CONFLICT lines — raw output:\n${summary}`);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
return conflictFiles;
|
|
852
|
+
/** @param {string} rootDir @param {string} branch @param {{gitRunner?: Function}} [options] */
|
|
853
|
+
function getConflictFiles(rootDir, branch, options = {}) {
|
|
854
|
+
const runner = options.gitRunner || gitModule.git;
|
|
855
|
+
const merge = runner(['-C', rootDir, 'merge', '--no-commit', '--no-ff', branch]);
|
|
856
|
+
runner(['-C', rootDir, 'merge', '--abort']);
|
|
857
|
+
if (merge.status === 0) {
|
|
858
|
+
return [];
|
|
859
|
+
}
|
|
860
|
+
const output = [merge.stdout, merge.stderr].filter(Boolean).join('\n');
|
|
861
|
+
const conflictFiles = parseConflictFilesFromMergeOutput(output);
|
|
862
|
+
if (conflictFiles.length === 0) {
|
|
863
|
+
const summary = output.slice(0, 500) || '(no output)';
|
|
864
|
+
throw new Error(`git merge exited ${merge.status} with no CONFLICT lines — raw output:\n${summary}`);
|
|
865
|
+
}
|
|
866
|
+
return conflictFiles;
|
|
839
867
|
}
|
|
840
|
-
|
|
841
868
|
/** @param {string} rootDir @param {Function} [gitRunner] */
|
|
842
869
|
function findLastNonNoiseCommit(rootDir, gitRunner) {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
const otherLocalBranches = branchesContaining.filter((/** @type {string} */ b) => b !== currentFullRef && b.startsWith('refs/heads/'));
|
|
867
|
-
if (otherLocalBranches.length > 0) {
|
|
868
|
-
return null;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
const log = gitRunner(['-C', rootDir, 'log', '-1', '--format=%s', commit], { stdio: 'pipe' });
|
|
872
|
-
if (log.status !== 0) {return null;}
|
|
873
|
-
const msg = log.stdout.trim();
|
|
874
|
-
|
|
875
|
-
const diff = gitRunner(['-C', rootDir, 'diff-tree', '--no-commit-id', '--name-only', '-r', commit], { stdio: 'pipe' });
|
|
876
|
-
if (diff.status !== 0) {return null;}
|
|
877
|
-
|
|
878
|
-
const files = diff.stdout.trim().split('\n').filter(Boolean);
|
|
879
|
-
if (files.length > 0) {
|
|
880
|
-
let isNoiseFiles = true;
|
|
881
|
-
for (const file of files) {
|
|
882
|
-
if (!file.startsWith('backlog/') && !file.endsWith('agents.local.json')) {
|
|
883
|
-
isNoiseFiles = false;
|
|
884
|
-
break;
|
|
870
|
+
const runner = gitRunner || gitModule.git;
|
|
871
|
+
const currentFullRef = runner(['-C', rootDir, 'rev-parse', '--symbolic-full-name', 'HEAD'], { stdio: 'pipe' }).stdout.trim();
|
|
872
|
+
let commit = 'HEAD';
|
|
873
|
+
for (let i = 0; i < 100; i++) {
|
|
874
|
+
const commitSha = runner(['-C', rootDir, 'rev-parse', commit], { stdio: 'pipe' }).stdout.trim();
|
|
875
|
+
const branchesContaining = runner(['-C', rootDir, 'branch', '-a', '--contains', commitSha, '--format=%(refname)'], { stdio: 'pipe' })
|
|
876
|
+
.stdout.trim().split('\n').filter(Boolean);
|
|
877
|
+
const isShared = branchesContaining.some((b) => b.startsWith('refs/remotes/'));
|
|
878
|
+
if (isShared) {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
const otherLocalBranches = branchesContaining.filter((b) => b !== currentFullRef && b.startsWith('refs/heads/'));
|
|
882
|
+
if (otherLocalBranches.length > 0) {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
const logResult = runner(['-C', rootDir, 'log', '-1', '--format=%s', commit], { stdio: 'pipe' });
|
|
886
|
+
if (logResult.status !== 0) {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
const msg = logResult.stdout.trim();
|
|
890
|
+
const diffResult = runner(['-C', rootDir, 'diff-tree', '--no-commit-id', '--name-only', '-r', commit], { stdio: 'pipe' });
|
|
891
|
+
if (diffResult.status !== 0) {
|
|
892
|
+
return null;
|
|
885
893
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
894
|
+
const files = diffResult.stdout.trim().split('\n').filter(Boolean);
|
|
895
|
+
if (files.length > 0) {
|
|
896
|
+
let isNoiseFiles = true;
|
|
897
|
+
for (const file of files) {
|
|
898
|
+
if (!file.startsWith('backlog/') && !file.endsWith('agents.local.json')) {
|
|
899
|
+
isNoiseFiles = false;
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
const isNoiseMsg = /^(Create task|Update task|backlog|assign|fixes|backlig|housekeeping|Archive task|fixing tasks|new mission|added new backlog task|docs: move|mission changes|random changes|new\/updated mission|task updates)/i.test(msg);
|
|
904
|
+
if (!isNoiseFiles || !isNoiseMsg) {
|
|
905
|
+
return commit;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
return commit;
|
|
910
|
+
}
|
|
911
|
+
commit = `${commit}^`;
|
|
912
|
+
}
|
|
913
|
+
return null;
|
|
901
914
|
}
|
|
902
|
-
|
|
903
915
|
/** @param {string} rootDir @param {Function} [gitRunner] */
|
|
904
916
|
function squashTrailingBacklogNoiseIntoPreviousMission(rootDir, gitRunner) {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
917
|
+
const runner = gitRunner || gitModule.git;
|
|
918
|
+
const status = runner(['-C', rootDir, 'status', '--porcelain']).stdout.trim();
|
|
919
|
+
if (status) {
|
|
920
|
+
fmt.log.warn(`Skipping noise squash in ${rootDir}: worktree is not clean.`);
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
const nonNoiseCommit = findLastNonNoiseCommit(rootDir, runner);
|
|
924
|
+
if (!nonNoiseCommit) {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
const headSha = runner(['-C', rootDir, 'rev-parse', 'HEAD']).stdout.trim();
|
|
928
|
+
const baseSha = runner(['-C', rootDir, 'rev-parse', nonNoiseCommit]).stdout.trim();
|
|
929
|
+
if (headSha !== baseSha) {
|
|
930
|
+
fmt.log.info(`Squashing trailing backlog noise into ${baseSha.substring(0, 7)}...`);
|
|
931
|
+
const date = runner(['-C', rootDir, 'log', '-1', '--format=%aD', baseSha]).stdout.trim();
|
|
932
|
+
const resetResult = runner(['-C', rootDir, 'reset', '--soft', baseSha]);
|
|
933
|
+
if (resetResult.status !== 0) {
|
|
934
|
+
fmt.log.fail(`Failed to reset to ${baseSha}: ${resetResult.stderr}`);
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
const commitResult = runner(['-C', rootDir, 'commit', '--amend', '--no-edit', '--date', date]);
|
|
938
|
+
if (commitResult.status !== 0) {
|
|
939
|
+
fmt.log.fail(`Failed to amend commit: ${commitResult.stderr}`);
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
912
944
|
return false;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
const nonNoiseCommit = findLastNonNoiseCommit(rootDir, gitRunner);
|
|
916
|
-
if (!nonNoiseCommit) {return false;}
|
|
917
|
-
|
|
918
|
-
const headSha = gitRunner(['-C', rootDir, 'rev-parse', 'HEAD']).stdout.trim();
|
|
919
|
-
const baseSha = gitRunner(['-C', rootDir, 'rev-parse', nonNoiseCommit]).stdout.trim();
|
|
920
|
-
|
|
921
|
-
if (headSha !== baseSha) {
|
|
922
|
-
fmt.log.info(`Squashing trailing backlog noise into ${baseSha.substring(0, 7)}...`);
|
|
923
|
-
const date = gitRunner(['-C', rootDir, 'log', '-1', '--format=%aD', baseSha]).stdout.trim();
|
|
924
|
-
const resetResult = gitRunner(['-C', rootDir, 'reset', '--soft', baseSha]);
|
|
925
|
-
if (resetResult.status !== 0) {
|
|
926
|
-
fmt.log.fail(`Failed to reset to ${baseSha}: ${resetResult.stderr}`);
|
|
927
|
-
return false;
|
|
928
|
-
}
|
|
929
|
-
const commitResult = gitRunner(['-C', rootDir, 'commit', '--amend', '--no-edit', '--date', date]);
|
|
930
|
-
if (commitResult.status !== 0) {
|
|
931
|
-
fmt.log.fail(`Failed to amend commit: ${commitResult.stderr}`);
|
|
932
|
-
return false;
|
|
933
|
-
}
|
|
934
|
-
return true;
|
|
935
|
-
}
|
|
936
|
-
return false;
|
|
937
945
|
}
|
|
938
|
-
|
|
939
946
|
/** @param {string} rootDir @param {Function} [gitRunner] */
|
|
940
947
|
function softResetTrailingBacklogNoise(rootDir, gitRunner) {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
948
|
+
const runner = gitRunner || gitModule.git;
|
|
949
|
+
const status = runner(['-C', rootDir, 'status', '--porcelain']).stdout.trim();
|
|
950
|
+
if (status) {
|
|
951
|
+
fmt.log.warn(`Skipping noise reset in ${rootDir}: worktree is not clean.`);
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
const nonNoiseCommit = findLastNonNoiseCommit(rootDir, runner);
|
|
955
|
+
if (!nonNoiseCommit) {
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
const headSha = runner(['-C', rootDir, 'rev-parse', 'HEAD']).stdout.trim();
|
|
959
|
+
const baseSha = runner(['-C', rootDir, 'rev-parse', nonNoiseCommit]).stdout.trim();
|
|
960
|
+
if (headSha !== baseSha) {
|
|
961
|
+
fmt.log.info(`Resetting trailing backlog noise back to ${baseSha.substring(0, 7)} to include in the integration...`);
|
|
962
|
+
const resetResult = runner(['-C', rootDir, 'reset', '--soft', baseSha]);
|
|
963
|
+
if (resetResult.status !== 0) {
|
|
964
|
+
fmt.log.fail(`Failed to reset to ${baseSha}: ${resetResult.stderr}`);
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
947
969
|
return false;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
const nonNoiseCommit = findLastNonNoiseCommit(rootDir, gitRunner);
|
|
951
|
-
if (!nonNoiseCommit) {return false;}
|
|
952
|
-
|
|
953
|
-
const headSha = gitRunner(['-C', rootDir, 'rev-parse', 'HEAD']).stdout.trim();
|
|
954
|
-
const baseSha = gitRunner(['-C', rootDir, 'rev-parse', nonNoiseCommit]).stdout.trim();
|
|
955
|
-
|
|
956
|
-
if (headSha !== baseSha) {
|
|
957
|
-
fmt.log.info(`Resetting trailing backlog noise back to ${baseSha.substring(0, 7)} to include in the integration...`);
|
|
958
|
-
const resetResult = gitRunner(['-C', rootDir, 'reset', '--soft', baseSha]);
|
|
959
|
-
if (resetResult.status !== 0) {
|
|
960
|
-
fmt.log.fail(`Failed to reset to ${baseSha}: ${resetResult.stderr}`);
|
|
961
|
-
return false;
|
|
962
|
-
}
|
|
963
|
-
return true;
|
|
964
|
-
}
|
|
965
|
-
return false;
|
|
966
970
|
}
|
|
967
|
-
|
|
968
971
|
/** @param {string} slug @param {string} [rootDir] @param {Function|null} [gitRunner] */
|
|
969
|
-
function findMissionDocInBranches(
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
972
|
+
function findMissionDocInBranches(slug, rootDir = process.cwd(), gitRunner) {
|
|
973
|
+
const runner = gitRunner || gitModule.git;
|
|
974
|
+
const candidates = [];
|
|
975
|
+
const baseSlugMatch = slug.match(/^(task-\d+)/i);
|
|
976
|
+
const baseSlug = baseSlugMatch ? baseSlugMatch[1].toLowerCase() : slug;
|
|
977
|
+
const files = [
|
|
978
|
+
node_path_1.default.relative(rootDir, missionPathForSlug(rootDir, slug)).split(node_path_1.default.sep).join('/'),
|
|
979
|
+
node_path_1.default.relative(rootDir, missionPathForSlug(rootDir, baseSlug)).split(node_path_1.default.sep).join('/')
|
|
980
|
+
];
|
|
981
|
+
const uniquePaths = Array.from(new Set(files));
|
|
982
|
+
let branchResult;
|
|
983
|
+
try {
|
|
984
|
+
branchResult = runner(['-C', rootDir, 'branch', '-a', '--format=%(refname:short)']);
|
|
985
|
+
if (branchResult.status !== 0) {
|
|
986
|
+
return candidates;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
catch (e) {
|
|
990
|
+
return candidates;
|
|
991
|
+
}
|
|
992
|
+
const branches = branchResult.stdout.trim().split('\n')
|
|
993
|
+
.filter(Boolean)
|
|
994
|
+
.filter((b) => !b.includes('HEAD'))
|
|
995
|
+
.filter((b) => b.endsWith(baseSlug) || b.includes(`/${baseSlug}-`) || b.includes(`/${baseSlug}/`));
|
|
996
|
+
for (const branch of branches) {
|
|
997
|
+
for (const f of uniquePaths) {
|
|
998
|
+
try {
|
|
999
|
+
const lsResult = runner(['-C', rootDir, 'ls-tree', '--name-only', branch, f]);
|
|
1000
|
+
if (lsResult.status === 0 && lsResult.stdout.trim() === f) {
|
|
1001
|
+
candidates.push({ branch, path: f });
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
catch (err) {
|
|
1006
|
+
// ignore
|
|
1007
|
+
}
|
|
1003
1008
|
}
|
|
1004
|
-
} catch (err) {
|
|
1005
|
-
// ignore
|
|
1006
|
-
}
|
|
1007
1009
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
return candidates;
|
|
1010
|
+
return candidates;
|
|
1011
1011
|
}
|
|
1012
|
-
|
|
1013
1012
|
/**
|
|
1014
1013
|
* Check if a file path is a mission artifact for a specific slug.
|
|
1015
1014
|
* Mission artifacts include:
|
|
@@ -1024,77 +1023,36 @@ function findMissionDocInBranches(/** @type {string} */ slug, /** @type {string}
|
|
|
1024
1023
|
*/
|
|
1025
1024
|
/** @param {string} file @param {string} slug @param {string} [rootDir] */
|
|
1026
1025
|
function isMissionArtifact(file, slug, rootDir = process.cwd()) {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1026
|
+
if (!file || !slug) {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
const missionDir = `${node_path_1.default.relative(rootDir, missionDirForSlug(rootDir, slug)).split(node_path_1.default.sep).join('/')}/`;
|
|
1030
|
+
if (file.startsWith(missionDir)) {
|
|
1031
|
+
return true;
|
|
1032
|
+
}
|
|
1033
|
+
const legacyMissionDir = `docs/missions/${getMissionYear(slug, rootDir)}/${slug}/`;
|
|
1034
|
+
if (file.startsWith(legacyMissionDir)) {
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
const escapedSlug = slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1038
|
+
const taskStorage = (0, product_config_js_1.resolveTaskStorage)(rootDir);
|
|
1039
|
+
const taskDirs = [taskStorage.tasksDir, taskStorage.completedDir]
|
|
1040
|
+
.map(dir => node_path_1.default.relative(rootDir, dir).split(node_path_1.default.sep).join('/'))
|
|
1041
|
+
.map(dir => dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
1042
|
+
const taskPattern = new RegExp(`^(?:${taskDirs.join('|')})/${escapedSlug}(?:\\s+-\\s+[^/]+\\.md|\\.md)$`, 'i');
|
|
1043
|
+
if (taskPattern.test(file)) {
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
return false;
|
|
1044
1047
|
}
|
|
1045
|
-
|
|
1046
1048
|
/** @param {string} [file] */
|
|
1047
1049
|
function isWorkflowGeneratedArtifact(file) {
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1050
|
+
if (!file) {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
return file.startsWith('.workflow/')
|
|
1054
|
+
|| file.startsWith('.sessions/')
|
|
1055
|
+
|| file.startsWith('.forgejo-local/')
|
|
1056
|
+
|| file === 'graphify-out'
|
|
1057
|
+
|| file.startsWith('graphify-out/');
|
|
1054
1058
|
}
|
|
1055
|
-
|
|
1056
|
-
module.exports = {
|
|
1057
|
-
resolveMissionAdapter,
|
|
1058
|
-
missionBaseDir,
|
|
1059
|
-
missionUsesYearTier,
|
|
1060
|
-
missionBranchPrefix,
|
|
1061
|
-
missionBranchName,
|
|
1062
|
-
missionBranchRef,
|
|
1063
|
-
extractSlugFromBranch,
|
|
1064
|
-
isMissionSlugCandidate,
|
|
1065
|
-
getPrimaryBranch,
|
|
1066
|
-
getPrimaryWorktree,
|
|
1067
|
-
resolveMainRepo,
|
|
1068
|
-
conventionalWorktreePath,
|
|
1069
|
-
conventionalBaseWorktreePath,
|
|
1070
|
-
detectLaunchBaseBranch,
|
|
1071
|
-
parseBaseBranchLine,
|
|
1072
|
-
readRecordedBaseBranch,
|
|
1073
|
-
resolveMissionBaseBranch,
|
|
1074
|
-
resolveBaseWorktree,
|
|
1075
|
-
getMissionYear,
|
|
1076
|
-
findMissionDir,
|
|
1077
|
-
resolveWorktree,
|
|
1078
|
-
findCheckpoints,
|
|
1079
|
-
compareCheckpointFiles,
|
|
1080
|
-
checkpointOrder,
|
|
1081
|
-
getFirstLine,
|
|
1082
|
-
findMissionArea,
|
|
1083
|
-
graphifyAvailable,
|
|
1084
|
-
probeGraphifyAvailability,
|
|
1085
|
-
updateGraphifyKnowledgeGraph,
|
|
1086
|
-
missionTitle,
|
|
1087
|
-
normalizeVerifyArea,
|
|
1088
|
-
detectMissionAreaFromContent,
|
|
1089
|
-
missionPathForSlug,
|
|
1090
|
-
missionDirForSlug,
|
|
1091
|
-
inferSlug,
|
|
1092
|
-
parseConflictFilesFromMergeOutput,
|
|
1093
|
-
getConflictFiles,
|
|
1094
|
-
findLastNonNoiseCommit,
|
|
1095
|
-
squashTrailingBacklogNoiseIntoPreviousMission,
|
|
1096
|
-
softResetTrailingBacklogNoise,
|
|
1097
|
-
findMissionDocInBranches,
|
|
1098
|
-
isMissionArtifact,
|
|
1099
|
-
isWorkflowGeneratedArtifact
|
|
1100
|
-
};
|