spec-and-loop 3.3.2 → 3.3.4
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/OPENSPEC-RALPH-BP.md +20 -0
- package/QUICKSTART.md +2 -0
- package/README.md +20 -2
- package/lib/mini-ralph/history.js +77 -2
- package/lib/mini-ralph/index.js +8 -0
- package/lib/mini-ralph/invoker.js +29 -3
- package/lib/mini-ralph/prompt.js +40 -3
- package/lib/mini-ralph/runner-autocommit.js +440 -0
- package/lib/mini-ralph/runner-baseline-gate.js +431 -0
- package/lib/mini-ralph/runner-handoff.js +338 -0
- package/lib/mini-ralph/runner-pending-dirty.js +168 -0
- package/lib/mini-ralph/runner.js +518 -1202
- package/lib/mini-ralph/state.js +35 -3
- package/lib/mini-ralph/status.js +37 -1
- package/lib/mini-ralph/supervisor-rules.js +379 -0
- package/lib/mini-ralph/supervisor-state.js +218 -0
- package/lib/mini-ralph/supervisor.js +1319 -0
- package/package.json +1 -1
- package/scripts/mini-ralph-cli.js +75 -2
- package/scripts/ralph-run.sh +121 -2
- package/scripts/supervisor-prompt.md +134 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* runner-autocommit.js — Auto-commit pipeline split out of runner.js.
|
|
5
|
+
*
|
|
6
|
+
* Responsible for assembling the per-iteration staging allowlist, filtering
|
|
7
|
+
* gitignored paths, building the task-aware commit message, detecting
|
|
8
|
+
* protected OpenSpec artifacts (proposal.md / design.md / specs/**), and
|
|
9
|
+
* shelling out to `git add` / `git commit`. Every helper here was moved
|
|
10
|
+
* verbatim from runner.js — no behavior change.
|
|
11
|
+
*
|
|
12
|
+
* Conventional git tooling renders the first commit-message line in ~50–72
|
|
13
|
+
* columns, so we cap the subject line accordingly and preserve the full
|
|
14
|
+
* task descriptions in the commit body.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const childProcess = require('child_process');
|
|
19
|
+
|
|
20
|
+
const SUBJECT_MAX_LENGTH = 72;
|
|
21
|
+
|
|
22
|
+
function _formatAutoCommitIgnoreBlock(iteration, anomaly) {
|
|
23
|
+
const SEP = '================================================================================\n';
|
|
24
|
+
const pathLines = (anomaly.ignoredPaths || []).map(p => ` - ${p}`).join('\n');
|
|
25
|
+
return (
|
|
26
|
+
SEP +
|
|
27
|
+
`⚠ AUTO-COMMIT IGNORE FILTER FIRED (iteration ${iteration}, type: ${anomaly.type})\n` +
|
|
28
|
+
`Paths filtered because .gitignore matches:\n` +
|
|
29
|
+
pathLines + '\n' +
|
|
30
|
+
`Consequence: these paths are NOT in the latest commit.\n` +
|
|
31
|
+
`Remediation (pick one):\n` +
|
|
32
|
+
` 1. git add -f <path> # one-time unblock, if you want it tracked\n` +
|
|
33
|
+
` 2. edit .gitignore # narrow or remove the matching rule\n` +
|
|
34
|
+
` 3. pass --no-auto-commit on the ralph-run invocation\n` +
|
|
35
|
+
SEP
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Auto-commit changed files after a successful iteration.
|
|
41
|
+
* Silently skips if git is unavailable, there is nothing to commit, or the
|
|
42
|
+
* iteration did not complete any tasks.
|
|
43
|
+
*
|
|
44
|
+
* @param {number} iteration
|
|
45
|
+
* @param {object} opts
|
|
46
|
+
* @param {Array<object>} [opts.completedTasks]
|
|
47
|
+
* @param {Array<string>} [opts.filesToStage]
|
|
48
|
+
* @param {boolean} [opts.verbose]
|
|
49
|
+
*/
|
|
50
|
+
function _autoCommit(iteration, opts = {}) {
|
|
51
|
+
const { completedTasks = [], filesToStage = [], tasksFile = null, verbose = false, reporter = null } = opts;
|
|
52
|
+
const message = _formatAutoCommitMessage(iteration, completedTasks);
|
|
53
|
+
|
|
54
|
+
if (!message) {
|
|
55
|
+
if (verbose) {
|
|
56
|
+
process.stderr.write('[mini-ralph] auto-commit skipped: no completed tasks detected\n');
|
|
57
|
+
}
|
|
58
|
+
return { attempted: false, committed: false, anomaly: null };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!Array.isArray(filesToStage) || filesToStage.length === 0) {
|
|
62
|
+
if (verbose) {
|
|
63
|
+
process.stderr.write('[mini-ralph] auto-commit skipped: no iteration files to stage\n');
|
|
64
|
+
}
|
|
65
|
+
return { attempted: false, committed: false, anomaly: null };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const protectedArtifacts = _detectProtectedCommitArtifacts(filesToStage, tasksFile);
|
|
69
|
+
if (protectedArtifacts.length > 0) {
|
|
70
|
+
const anomaly = {
|
|
71
|
+
type: 'protected_artifacts',
|
|
72
|
+
message:
|
|
73
|
+
'Auto-commit blocked: loop-managed commits cannot include protected OpenSpec artifacts: ' +
|
|
74
|
+
protectedArtifacts.join(', '),
|
|
75
|
+
protectedArtifacts,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
process.stderr.write(`[mini-ralph] warning: ${anomaly.message}\n`);
|
|
79
|
+
return { attempted: true, committed: false, anomaly };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { kept: keptPaths, dropped: droppedPaths } = _filterGitignored(filesToStage, process.cwd());
|
|
83
|
+
|
|
84
|
+
if (droppedPaths.length > 0) {
|
|
85
|
+
const pathWord = droppedPaths.length === 1 ? 'path' : 'paths';
|
|
86
|
+
const allIgnored = keptPaths.length === 0;
|
|
87
|
+
const warnLines = allIgnored
|
|
88
|
+
? [
|
|
89
|
+
`auto-commit iter ${iteration} skipped: all ${droppedPaths.length} ${pathWord} are gitignored`,
|
|
90
|
+
...droppedPaths.map(p => ` - ${p}`),
|
|
91
|
+
' hint: `git add -f <path>` once, or adjust .gitignore',
|
|
92
|
+
].join('\n')
|
|
93
|
+
: [
|
|
94
|
+
`auto-commit iter ${iteration}: filtered ${droppedPaths.length} gitignored ${pathWord}, committing ${keptPaths.length} ${keptPaths.length === 1 ? 'other' : 'others'}`,
|
|
95
|
+
...droppedPaths.map(p => ` - ${p}`),
|
|
96
|
+
].join('\n');
|
|
97
|
+
if (reporter) {
|
|
98
|
+
reporter.note(warnLines, 'error');
|
|
99
|
+
} else {
|
|
100
|
+
const fallbackMsg = allIgnored
|
|
101
|
+
? `Auto-commit skipped: all paths are gitignored: ${droppedPaths.join(', ')}`
|
|
102
|
+
: `Auto-commit filtered gitignored paths: ${droppedPaths.join(', ')}`;
|
|
103
|
+
process.stderr.write(`[mini-ralph] warning: ${fallbackMsg}\n`);
|
|
104
|
+
}
|
|
105
|
+
if (allIgnored) {
|
|
106
|
+
const anomaly = {
|
|
107
|
+
type: 'all_paths_ignored',
|
|
108
|
+
message: `Auto-commit skipped: all paths are gitignored: ${droppedPaths.join(', ')}`,
|
|
109
|
+
ignoredPaths: droppedPaths,
|
|
110
|
+
};
|
|
111
|
+
// task 5.1: emit loud direct stderr block, bypassing reporter dedup/buffering
|
|
112
|
+
process.stderr.write(_formatAutoCommitIgnoreBlock(iteration, anomaly));
|
|
113
|
+
return {
|
|
114
|
+
attempted: true,
|
|
115
|
+
committed: false,
|
|
116
|
+
anomaly,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const stagePaths = droppedPaths.length > 0 ? keptPaths : filesToStage;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// Use `git add -A -- <paths>` (not plain `git add -- <paths>`) so deletions
|
|
125
|
+
// and renames are staged alongside modifications/additions. Tasks that call
|
|
126
|
+
// `git rm` via a shell tool leave the path absent from the working tree but
|
|
127
|
+
// still present in `git status --porcelain`, which means the plain form
|
|
128
|
+
// would error with `fatal: pathspec did not match`. Scoping to the per-path
|
|
129
|
+
// allowlist preserves the protected-artifact guarantee.
|
|
130
|
+
childProcess.execFileSync('git', ['add', '-A', '--', ...stagePaths], {
|
|
131
|
+
stdio: verbose ? 'inherit' : ['pipe', 'pipe', 'pipe'],
|
|
132
|
+
encoding: 'utf8',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const stagedFiles = childProcess.execFileSync('git', ['diff', '--cached', '--name-only'], {
|
|
136
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
137
|
+
encoding: 'utf8',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!stagedFiles.trim()) {
|
|
141
|
+
const anomaly = {
|
|
142
|
+
type: 'nothing_staged',
|
|
143
|
+
message: 'Auto-commit failed: nothing was staged after git add',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
process.stderr.write(`[mini-ralph] warning: ${anomaly.message}\n`);
|
|
147
|
+
if (verbose) {
|
|
148
|
+
process.stderr.write('[mini-ralph] auto-commit skipped: nothing staged\n');
|
|
149
|
+
}
|
|
150
|
+
return { attempted: true, committed: false, anomaly };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
childProcess.execFileSync('git', ['commit', '-m', message], {
|
|
154
|
+
stdio: verbose ? 'inherit' : ['pipe', 'pipe', 'pipe'],
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (verbose) {
|
|
159
|
+
process.stderr.write(`[mini-ralph] auto-committed: ${message}\n`);
|
|
160
|
+
}
|
|
161
|
+
if (droppedPaths.length > 0) {
|
|
162
|
+
const anomaly = {
|
|
163
|
+
type: 'paths_ignored_filtered',
|
|
164
|
+
message: 'Auto-commit succeeded but filtered gitignored paths: ' + droppedPaths.join(', '),
|
|
165
|
+
ignoredPaths: droppedPaths,
|
|
166
|
+
};
|
|
167
|
+
// task 5.1: emit loud direct stderr block, bypassing reporter dedup/buffering
|
|
168
|
+
process.stderr.write(_formatAutoCommitIgnoreBlock(iteration, anomaly));
|
|
169
|
+
return {
|
|
170
|
+
attempted: true,
|
|
171
|
+
committed: true,
|
|
172
|
+
anomaly,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return { attempted: true, committed: true, anomaly: null };
|
|
176
|
+
} catch (err) {
|
|
177
|
+
const anomaly = {
|
|
178
|
+
type: 'commit_failed',
|
|
179
|
+
message: `Auto-commit failed: ${_gitErrorMessage(err)}`,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
process.stderr.write(`[mini-ralph] warning: ${anomaly.message}\n`);
|
|
183
|
+
return { attempted: true, committed: false, anomaly };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Filter gitignored paths out of a list using `git check-ignore --stdin`.
|
|
189
|
+
*
|
|
190
|
+
* Exit-code semantics of `git check-ignore`:
|
|
191
|
+
* 0 – at least one path is ignored; stdout lists the ignored paths.
|
|
192
|
+
* 1 – no paths are ignored (Node's execFileSync throws; we catch status===1).
|
|
193
|
+
* other / ENOENT / any thrown error – fallback: treat all paths as kept.
|
|
194
|
+
*
|
|
195
|
+
* @param {string[]} paths - Repo-relative paths to test.
|
|
196
|
+
* @param {string} cwd - Working directory for the git command.
|
|
197
|
+
* @returns {{ kept: string[], dropped: string[] }}
|
|
198
|
+
*/
|
|
199
|
+
function _filterGitignored(paths, cwd) {
|
|
200
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
201
|
+
return { kept: [], dropped: [] };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const stdout = childProcess.execFileSync(
|
|
206
|
+
'git',
|
|
207
|
+
['check-ignore', '--stdin'],
|
|
208
|
+
{
|
|
209
|
+
input: paths.join('\n'),
|
|
210
|
+
cwd: cwd || process.cwd(),
|
|
211
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
212
|
+
encoding: 'utf8',
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const dropped = stdout
|
|
217
|
+
.split('\n')
|
|
218
|
+
.map((l) => l.trim())
|
|
219
|
+
.filter(Boolean);
|
|
220
|
+
const droppedSet = new Set(dropped);
|
|
221
|
+
const kept = paths.filter((p) => !droppedSet.has(p));
|
|
222
|
+
return { kept, dropped };
|
|
223
|
+
} catch (err) {
|
|
224
|
+
if (err && err.status === 1) {
|
|
225
|
+
return { kept: paths.slice(), dropped: [] };
|
|
226
|
+
}
|
|
227
|
+
return { kept: paths.slice(), dropped: [] };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function _mergePathLists(...lists) {
|
|
232
|
+
const merged = new Set();
|
|
233
|
+
for (const list of lists) {
|
|
234
|
+
for (const file of list || []) {
|
|
235
|
+
const relativeFile = _repoRelativePath(file);
|
|
236
|
+
if (relativeFile) {
|
|
237
|
+
merged.add(relativeFile);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return Array.from(merged);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Build the explicit per-iteration git staging allowlist.
|
|
246
|
+
*
|
|
247
|
+
* @param {Array<string>} filesChanged
|
|
248
|
+
* @param {Array<object>} completedTasks
|
|
249
|
+
* @param {string|null|undefined} tasksFile
|
|
250
|
+
* @returns {Array<string>}
|
|
251
|
+
*/
|
|
252
|
+
function _buildAutoCommitAllowlist(filesChanged, completedTasks, tasksFile) {
|
|
253
|
+
const allowlist = new Set();
|
|
254
|
+
|
|
255
|
+
for (const file of filesChanged || []) {
|
|
256
|
+
const relativeFile = _repoRelativePath(file);
|
|
257
|
+
if (relativeFile) {
|
|
258
|
+
allowlist.add(relativeFile);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (Array.isArray(completedTasks) && completedTasks.length > 0 && tasksFile) {
|
|
263
|
+
const relativeTasksFile = _repoRelativePath(tasksFile);
|
|
264
|
+
if (relativeTasksFile) {
|
|
265
|
+
allowlist.add(relativeTasksFile);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return Array.from(allowlist);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Return tasks that became completed during the current iteration.
|
|
274
|
+
*
|
|
275
|
+
* @param {Array<object>} beforeTasks
|
|
276
|
+
* @param {Array<object>} afterTasks
|
|
277
|
+
* @returns {Array<object>}
|
|
278
|
+
*/
|
|
279
|
+
function _completedTaskDelta(beforeTasks, afterTasks) {
|
|
280
|
+
const beforeCompleted = new Set(
|
|
281
|
+
(beforeTasks || [])
|
|
282
|
+
.filter((task) => task.status === 'completed')
|
|
283
|
+
.map(_taskIdentity)
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
return (afterTasks || []).filter(
|
|
287
|
+
(task) => task.status === 'completed' && !beforeCompleted.has(_taskIdentity(task))
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Build a task-aware commit message for an iteration.
|
|
293
|
+
*
|
|
294
|
+
* The subject line (first line) is kept short — conventional git tooling
|
|
295
|
+
* assumes ~50–72 characters — so `git log --oneline` stays readable even when
|
|
296
|
+
* the underlying task description is a multi-sentence normative blob. The
|
|
297
|
+
* full, untruncated task descriptions are preserved in the commit body.
|
|
298
|
+
*
|
|
299
|
+
* @param {number} iteration
|
|
300
|
+
* @param {Array<object>} completedTasks
|
|
301
|
+
* @returns {string}
|
|
302
|
+
*/
|
|
303
|
+
function _formatAutoCommitMessage(iteration, completedTasks) {
|
|
304
|
+
if (!Array.isArray(completedTasks) || completedTasks.length === 0) {
|
|
305
|
+
return '';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const rawSummary = completedTasks.length === 1
|
|
309
|
+
? completedTasks[0].description
|
|
310
|
+
: `complete ${completedTasks.length} tasks`;
|
|
311
|
+
|
|
312
|
+
const prefix = `Ralph iteration ${iteration}: `;
|
|
313
|
+
const subjectBudget = Math.max(20, SUBJECT_MAX_LENGTH - prefix.length);
|
|
314
|
+
const summary = _truncateSubjectSummary(rawSummary, subjectBudget);
|
|
315
|
+
|
|
316
|
+
const taskLines = completedTasks.map(
|
|
317
|
+
(task) => `- [x] ${task.fullDescription || task.description}`
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
return `${prefix}${summary}\n\nTasks completed:\n${taskLines.join('\n')}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Reduce a task description to a short, single-line commit subject.
|
|
325
|
+
*
|
|
326
|
+
* Strategy:
|
|
327
|
+
* 1. Collapse whitespace onto a single line.
|
|
328
|
+
* 2. Prefer the first sentence (up to `.`, `!`, `?`) when it is not itself
|
|
329
|
+
* longer than the allowed budget.
|
|
330
|
+
* 3. Otherwise hard-truncate at a word boundary and append an ellipsis.
|
|
331
|
+
*
|
|
332
|
+
* @param {string} text
|
|
333
|
+
* @param {number} budget
|
|
334
|
+
* @returns {string}
|
|
335
|
+
*/
|
|
336
|
+
function _truncateSubjectSummary(text, budget) {
|
|
337
|
+
const oneLine = String(text == null ? '' : text).replace(/\s+/g, ' ').trim();
|
|
338
|
+
if (oneLine.length === 0) return '';
|
|
339
|
+
if (oneLine.length <= budget) return oneLine;
|
|
340
|
+
|
|
341
|
+
const sentenceMatch = oneLine.match(/^(.+?[.!?])(\s|$)/);
|
|
342
|
+
if (sentenceMatch) {
|
|
343
|
+
const candidate = sentenceMatch[1].trim();
|
|
344
|
+
if (candidate.length > 0 && candidate.length <= budget) {
|
|
345
|
+
return candidate;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const ellipsis = '…';
|
|
350
|
+
const hardBudget = Math.max(1, budget - ellipsis.length);
|
|
351
|
+
const sliced = oneLine.slice(0, hardBudget);
|
|
352
|
+
const lastSpace = sliced.lastIndexOf(' ');
|
|
353
|
+
const cut = lastSpace > Math.floor(hardBudget / 2) ? sliced.slice(0, lastSpace) : sliced;
|
|
354
|
+
return `${cut.replace(/[\s,;:.!?-]+$/, '')}${ellipsis}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function _taskIdentity(task) {
|
|
358
|
+
return task.number
|
|
359
|
+
? `${task.number}|${task.fullDescription || task.description}`
|
|
360
|
+
: (task.fullDescription || task.description);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function _repoRelativePath(filePath) {
|
|
364
|
+
if (!filePath || typeof filePath !== 'string') return '';
|
|
365
|
+
const normalized = path.normalize(filePath);
|
|
366
|
+
if (!normalized || normalized === '.') return '';
|
|
367
|
+
const relative = path.isAbsolute(normalized)
|
|
368
|
+
? path.relative(process.cwd(), normalized)
|
|
369
|
+
: normalized;
|
|
370
|
+
|
|
371
|
+
if (!relative || relative.startsWith('..')) {
|
|
372
|
+
return '';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return relative.split(path.sep).join('/');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function _detectProtectedCommitArtifacts(filesToStage, tasksFile) {
|
|
379
|
+
if (!Array.isArray(filesToStage) || filesToStage.length === 0 || !tasksFile) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const relativeTasksFile = _repoRelativePath(tasksFile);
|
|
384
|
+
if (!relativeTasksFile) {
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const changeRoot = path.posix.dirname(relativeTasksFile);
|
|
389
|
+
const protectedArtifacts = [];
|
|
390
|
+
|
|
391
|
+
for (const file of filesToStage) {
|
|
392
|
+
const normalized = _repoRelativePath(file);
|
|
393
|
+
if (!normalized) continue;
|
|
394
|
+
|
|
395
|
+
const isProposal = normalized === `${changeRoot}/proposal.md`;
|
|
396
|
+
const isDesign = normalized === `${changeRoot}/design.md`;
|
|
397
|
+
const isSpec = normalized.startsWith(`${changeRoot}/specs/`) && normalized.endsWith('/spec.md');
|
|
398
|
+
|
|
399
|
+
if (isProposal || isDesign || isSpec) {
|
|
400
|
+
protectedArtifacts.push(normalized);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return protectedArtifacts;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function _gitErrorMessage(err) {
|
|
408
|
+
if (!err) return 'unknown git error';
|
|
409
|
+
|
|
410
|
+
const stderr = _coerceGitErrorStream(err.stderr);
|
|
411
|
+
const stdout = _coerceGitErrorStream(err.stdout);
|
|
412
|
+
|
|
413
|
+
if (stderr) return stderr;
|
|
414
|
+
if (stdout) return stdout;
|
|
415
|
+
if (err.message) return err.message;
|
|
416
|
+
return 'unknown git error';
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function _coerceGitErrorStream(stream) {
|
|
420
|
+
if (!stream) return '';
|
|
421
|
+
if (Buffer.isBuffer(stream)) return stream.toString('utf8').trim();
|
|
422
|
+
if (typeof stream === 'string') return stream.trim();
|
|
423
|
+
return '';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
module.exports = {
|
|
427
|
+
_autoCommit,
|
|
428
|
+
_formatAutoCommitIgnoreBlock,
|
|
429
|
+
_filterGitignored,
|
|
430
|
+
_mergePathLists,
|
|
431
|
+
_buildAutoCommitAllowlist,
|
|
432
|
+
_completedTaskDelta,
|
|
433
|
+
_formatAutoCommitMessage,
|
|
434
|
+
_truncateSubjectSummary,
|
|
435
|
+
_taskIdentity,
|
|
436
|
+
_repoRelativePath,
|
|
437
|
+
_detectProtectedCommitArtifacts,
|
|
438
|
+
_gitErrorMessage,
|
|
439
|
+
_coerceGitErrorStream,
|
|
440
|
+
};
|