Skip to content

Update from original project#41

Merged
shoootyou merged 7 commits intodevfrom
feat/update
Feb 26, 2026
Merged

Update from original project#41
shoootyou merged 7 commits intodevfrom
feat/update

Conversation

@shoootyou
Copy link
Owner

@github-actions
Copy link

github-actions bot commented Feb 15, 2026

✅ Validation Passed

All checks completed successfully:

  • ✓ Tests passed
  • ✓ Tarball created and validated
  • ✓ Installation verified

This PR is ready for review.

View details

Workflow run details


Last updated: 2026-02-26T09:41:03.596Z

const baseExists = dirs.some(d => d.startsWith(normalized + '-') || d === normalized);

// Find existing decimal phases for this base
const decimalPattern = new RegExp(`^${normalized}\\.(\\d+)`);

Check failure

Code scanning / CodeQL

Regular expression injection High

This regular expression is constructed from a
command-line argument
.

Copilot Autofix

AI 7 days ago

In general, to fix regex injection when embedding user input in a RegExp constructor, the untrusted value must be passed through a function that escapes all regex metacharacters (for example, the existing escapeRegex helper) before interpolation. That way the input is treated purely as literal text within the pattern.

For this specific case, the best fix with no functional change is:

  • Keep using normalizePhaseName(basePhase) for phase normalization.
  • Before using normalized inside the regex, create an escaped version with escapeRegex(normalized).
  • Use the escaped version in the template string passed to new RegExp, i.e., change new RegExp(`^${normalized}\\.(\\d+)`) to use escapedNormalized instead.

escapeRegex is already imported from ./core.cjs at the top of templates/get-shit-done/bin/lib/phase.cjs, so no new imports or dependencies are required. The change is localized to cmdPhaseNextDecimal in phase.cjs, around lines 113–115.

Suggested changeset 1
templates/get-shit-done/bin/lib/phase.cjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/templates/get-shit-done/bin/lib/phase.cjs b/templates/get-shit-done/bin/lib/phase.cjs
--- a/templates/get-shit-done/bin/lib/phase.cjs
+++ b/templates/get-shit-done/bin/lib/phase.cjs
@@ -111,7 +111,8 @@
     const baseExists = dirs.some(d => d.startsWith(normalized + '-') || d === normalized);
 
     // Find existing decimal phases for this base
-    const decimalPattern = new RegExp(`^${normalized}\\.(\\d+)`);
+    const escapedNormalized = escapeRegex(normalized);
+    const decimalPattern = new RegExp(`^${escapedNormalized}\\.(\\d+)`);
     const existingDecimals = [];
 
     for (const dir of dirs) {
EOF
@@ -111,7 +111,8 @@
const baseExists = dirs.some(d => d.startsWith(normalized + '-') || d === normalized);

// Find existing decimal phases for this base
const decimalPattern = new RegExp(`^${normalized}\\.(\\d+)`);
const escapedNormalized = escapeRegex(normalized);
const decimalPattern = new RegExp(`^${escapedNormalized}\\.(\\d+)`);
const existingDecimals = [];

for (const dir of dirs) {
Copilot is powered by AI and may make mistakes. Always verify output.
// Normalize input then strip leading zeros for flexible matching
const normalizedAfter = normalizePhaseName(afterPhase);
const unpadded = normalizedAfter.replace(/^0+/, '');
const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');

Check failure

Code scanning / CodeQL

Incomplete string escaping or encoding High

This does not escape backslash characters in the input.

Copilot Autofix

AI 7 days ago

In general, when building a regular expression from user input, that input must be passed through a function that escapes all regex metacharacters, not just some of them. Relying on a simple .replace('.', '\.') is error-prone because it does not handle other special characters such as \, *, +, ?, etc. The best fix here is to replace the manual escaping on line 376 with a call to the already-imported escapeRegex helper, which is presumably designed to escape arbitrary strings for safe use in regular expressions.

Concretely, in templates/get-shit-done/bin/lib/phase.cjs, at line 376 we should replace:

const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');

with:

const afterPhaseEscaped = escapeRegex(unpadded);

This uses the existing escapeRegex function imported from ./core.cjs on line 7, so no new imports or additional helper functions are needed. The rest of the logic can remain unchanged: afterPhaseEscaped is interpolated into the RegExp constructor as before, but now it will be fully escaped, including backslashes, ensuring that even unusual user inputs cannot break the pattern.

Suggested changeset 1
templates/get-shit-done/bin/lib/phase.cjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/templates/get-shit-done/bin/lib/phase.cjs b/templates/get-shit-done/bin/lib/phase.cjs
--- a/templates/get-shit-done/bin/lib/phase.cjs
+++ b/templates/get-shit-done/bin/lib/phase.cjs
@@ -373,7 +373,7 @@
   // Normalize input then strip leading zeros for flexible matching
   const normalizedAfter = normalizePhaseName(afterPhase);
   const unpadded = normalizedAfter.replace(/^0+/, '');
-  const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
+  const afterPhaseEscaped = escapeRegex(unpadded);
   const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
   if (!targetPattern.test(content)) {
     error(`Phase ${afterPhase} not found in ROADMAP.md`);
EOF
@@ -373,7 +373,7 @@
// Normalize input then strip leading zeros for flexible matching
const normalizedAfter = normalizePhaseName(afterPhase);
const unpadded = normalizedAfter.replace(/^0+/, '');
const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
const afterPhaseEscaped = escapeRegex(unpadded);
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
if (!targetPattern.test(content)) {
error(`Phase ${afterPhase} not found in ROADMAP.md`);
Copilot is powered by AI and may make mistakes. Always verify output.
const normalizedAfter = normalizePhaseName(afterPhase);
const unpadded = normalizedAfter.replace(/^0+/, '');
const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');

Check failure

Code scanning / CodeQL

Regular expression injection High

This regular expression is constructed from a
command-line argument
.

Copilot Autofix

AI 7 days ago

General fix: Whenever user input is interpolated into a RegExp constructor, escape all regex metacharacters first using a dedicated escaping function (like the existing escapeRegex in core.cjs), and then build the pattern using that escaped version.

Best concrete fix here: in cmdPhaseInsert in templates/get-shit-done/bin/lib/phase.cjs, change how afterPhaseEscaped is computed. Instead of only escaping dots via .replace(/\./g, '\\.'), we should pass unpadded through escapeRegex, which already correctly escapes all regex metacharacters. The rest of the logic (normalization, removing leading zeros, building the targetPattern with 0*${...}) can remain untouched, because escapeRegex will return a safe literal representation of the phase number, including any dots. This preserves intended matching semantics while preventing a user from injecting their own regex fragments.

Concretely:

  • In cmdPhaseInsert:
    • Replace line 376: const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
    • With: const afterPhaseEscaped = escapeRegex(unpadded);
  • No new imports are necessary because escapeRegex is already imported from ./core.cjs at the top of phase.cjs.

This change keeps functionality the same for legitimate inputs (numbers and dots) while robustly preventing regex injection.


Suggested changeset 1
templates/get-shit-done/bin/lib/phase.cjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/templates/get-shit-done/bin/lib/phase.cjs b/templates/get-shit-done/bin/lib/phase.cjs
--- a/templates/get-shit-done/bin/lib/phase.cjs
+++ b/templates/get-shit-done/bin/lib/phase.cjs
@@ -373,7 +373,7 @@
   // Normalize input then strip leading zeros for flexible matching
   const normalizedAfter = normalizePhaseName(afterPhase);
   const unpadded = normalizedAfter.replace(/^0+/, '');
-  const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
+  const afterPhaseEscaped = escapeRegex(unpadded);
   const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
   if (!targetPattern.test(content)) {
     error(`Phase ${afterPhase} not found in ROADMAP.md`);
EOF
@@ -373,7 +373,7 @@
// Normalize input then strip leading zeros for flexible matching
const normalizedAfter = normalizePhaseName(afterPhase);
const unpadded = normalizedAfter.replace(/^0+/, '');
const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
const afterPhaseEscaped = escapeRegex(unpadded);
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
if (!targetPattern.test(content)) {
error(`Phase ${afterPhase} not found in ROADMAP.md`);
Copilot is powered by AI and may make mistakes. Always verify output.
try {
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);

Check failure

Code scanning / CodeQL

Regular expression injection High

This regular expression is constructed from a
command-line argument
.

Copilot Autofix

AI 7 days ago

In general, to fix regular expression injection you must ensure that any user-controlled string interpolated into a regex is first sanitized by escaping all regex metacharacters. This converts the user input into a literal substring within the regex rather than allowing it to alter the regex structure.

For this specific case, the best fix with minimal behavioral change is to escape normalizedBase before interpolating it into the RegExp at line 390 in templates/get-shit-done/bin/lib/phase.cjs. The project already defines and exports an escapeRegex function in core.cjs and imports it at the top of phase.cjs, so we should use it. Concretely:

  • In cmdPhaseInsert, after computing normalizedBase = normalizePhaseName(afterPhase), add a new constant normalizedBaseEscaped = escapeRegex(normalizedBase).
  • Use normalizedBaseEscaped instead of normalizedBase when constructing decimalPattern:
    • Change const decimalPattern = new RegExp(\^${normalizedBase}\.(\d+)`);toconst decimalPattern = new RegExp(`^${normalizedBaseEscaped}\.\(\d+)`);`.
  • This preserves the existing semantics (matching directory names that start with that phase base followed by a dot and digits) while ensuring that if normalizedBase ever contains any unexpected regex metacharacters, they are treated as literals and cannot alter the regex.

No new imports or dependencies are needed; escapeRegex is already imported from ./core.cjs.


Suggested changeset 1
templates/get-shit-done/bin/lib/phase.cjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/templates/get-shit-done/bin/lib/phase.cjs b/templates/get-shit-done/bin/lib/phase.cjs
--- a/templates/get-shit-done/bin/lib/phase.cjs
+++ b/templates/get-shit-done/bin/lib/phase.cjs
@@ -382,12 +382,13 @@
   // Calculate next decimal using existing logic
   const phasesDir = path.join(cwd, '.planning', 'phases');
   const normalizedBase = normalizePhaseName(afterPhase);
+  const normalizedBaseEscaped = escapeRegex(normalizedBase);
   let existingDecimals = [];
 
   try {
     const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
     const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
-    const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
+    const decimalPattern = new RegExp(`^${normalizedBaseEscaped}\\.(\\d+)`);
     for (const dir of dirs) {
       const dm = dir.match(decimalPattern);
       if (dm) existingDecimals.push(parseInt(dm[1], 10));
EOF
@@ -382,12 +382,13 @@
// Calculate next decimal using existing logic
const phasesDir = path.join(cwd, '.planning', 'phases');
const normalizedBase = normalizePhaseName(afterPhase);
const normalizedBaseEscaped = escapeRegex(normalizedBase);
let existingDecimals = [];

try {
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
const decimalPattern = new RegExp(`^${normalizedBaseEscaped}\\.(\\d+)`);
for (const dir of dirs) {
const dm = dir.match(decimalPattern);
if (dm) existingDecimals.push(parseInt(dm[1], 10));
Copilot is powered by AI and may make mistakes. Always verify output.
const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run {{COMMAND_PREFIX}}plan-phase ${decimalPhase} to break down)\n`;

// Insert after the target phase section
const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');

Check failure

Code scanning / CodeQL

Regular expression injection High

This regular expression is constructed from a
command-line argument
.

Copilot Autofix

AI 7 days ago

In general, to fix regex injection you must ensure that any user-controlled portion interpolated into a regular expression is first passed through a function that escapes all regex metacharacters, or you must validate and reject any values that don’t match a strict allowed pattern.

In this codebase, core.cjs already defines escapeRegex, which correctly escapes regex metacharacters. The safest and least intrusive fix is therefore to apply escapeRegex to the phase portion before interpolating it into new RegExp(...). Currently, afterPhaseEscaped is derived via normalizedAfter.replace(/^0+/, '') followed by .replace(/\./g, '\\.'), which only escapes dots. We should instead build a pure data representation of the phase identifier, then run that through escapeRegex. Concretely:

  • Keep the numeric/letter normalization logic (normalizePhaseName and stripping leading zeros) as-is to preserve existing behavior.
  • Replace the manual .replace(/\./g, '\\.') with a call to escapeRegex, so that any character that has regex meaning is escaped, including . which was already being handled.
  • Use this new escaped string both where targetPattern is built (line 377) and where headerPattern is built (line 410), ensuring both regex constructions are safe even if afterPhase contains unexpected characters.
  • Optionally, to tighten semantics further, we can also escape normalizedBase when building decimalPattern for directory names, though those are created internally; still, it is cheap and safe to do.

No new methods or imports are required: escapeRegex is already exported by core.cjs and imported at the top of phase.cjs. All changes are confined to templates/get-shit-done/bin/lib/phase.cjs within the shown snippet.


Suggested changeset 1
templates/get-shit-done/bin/lib/phase.cjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/templates/get-shit-done/bin/lib/phase.cjs b/templates/get-shit-done/bin/lib/phase.cjs
--- a/templates/get-shit-done/bin/lib/phase.cjs
+++ b/templates/get-shit-done/bin/lib/phase.cjs
@@ -373,7 +373,7 @@
   // Normalize input then strip leading zeros for flexible matching
   const normalizedAfter = normalizePhaseName(afterPhase);
   const unpadded = normalizedAfter.replace(/^0+/, '');
-  const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
+  const afterPhaseEscaped = escapeRegex(unpadded);
   const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
   if (!targetPattern.test(content)) {
     error(`Phase ${afterPhase} not found in ROADMAP.md`);
@@ -382,12 +382,13 @@
   // Calculate next decimal using existing logic
   const phasesDir = path.join(cwd, '.planning', 'phases');
   const normalizedBase = normalizePhaseName(afterPhase);
+  const normalizedBaseEscaped = escapeRegex(normalizedBase);
   let existingDecimals = [];
 
   try {
     const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
     const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
-    const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
+    const decimalPattern = new RegExp(`^${normalizedBaseEscaped}\\.(\\d+)`);
     for (const dir of dirs) {
       const dm = dir.match(decimalPattern);
       if (dm) existingDecimals.push(parseInt(dm[1], 10));
EOF
@@ -373,7 +373,7 @@
// Normalize input then strip leading zeros for flexible matching
const normalizedAfter = normalizePhaseName(afterPhase);
const unpadded = normalizedAfter.replace(/^0+/, '');
const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
const afterPhaseEscaped = escapeRegex(unpadded);
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
if (!targetPattern.test(content)) {
error(`Phase ${afterPhase} not found in ROADMAP.md`);
@@ -382,12 +382,13 @@
// Calculate next decimal using existing logic
const phasesDir = path.join(cwd, '.planning', 'phases');
const normalizedBase = normalizePhaseName(afterPhase);
const normalizedBaseEscaped = escapeRegex(normalizedBase);
let existingDecimals = [];

try {
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
const decimalPattern = new RegExp(`^${normalizedBaseEscaped}\\.(\\d+)`);
for (const dir of dirs) {
const dm = dir.match(decimalPattern);
if (dm) existingDecimals.push(parseInt(dm[1], 10));
Copilot is powered by AI and may make mistakes. Always verify output.
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));

// Find sibling decimals with higher numbers
const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);

Check failure

Code scanning / CodeQL

Regular expression injection High

This regular expression is constructed from a
command-line argument
.

Copilot Autofix

AI 7 days ago

General approach: when building a RegExp from any value influenced by user input (here, CLI arguments), ensure the interpolated segment cannot introduce regex meta-characters. This is typically done either by (a) strictly validating the value against an allowed-character whitelist (e.g., digits only) and/or (b) escaping it with a function like escapeRegex.

Best fix here: we already have an escapeRegex helper exported from core.cjs. While baseInt is currently digits-only, we can make the regex construction explicitly safe by escaping baseInt before interpolation. This will not change existing behavior (since digits are unchanged by escaping) but will prevent issues if normalizePhaseName is ever relaxed or changed. Concretely, in templates/get-shit-done/bin/lib/phase.cjs, in cmdPhaseRemove’s renumbering logic, change:

const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);

to:

const safeBaseInt = escapeRegex(baseInt);
const decPattern = new RegExp(`^${safeBaseInt}\\.(\\d+)-(.+)$`);

This uses the existing escapeRegex import on line 7, so no new imports are needed. No other functional changes are required.

Suggested changeset 1
templates/get-shit-done/bin/lib/phase.cjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/templates/get-shit-done/bin/lib/phase.cjs b/templates/get-shit-done/bin/lib/phase.cjs
--- a/templates/get-shit-done/bin/lib/phase.cjs
+++ b/templates/get-shit-done/bin/lib/phase.cjs
@@ -493,7 +493,8 @@
       const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
 
       // Find sibling decimals with higher numbers
-      const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
+      const safeBaseInt = escapeRegex(baseInt);
+      const decPattern = new RegExp(`^${safeBaseInt}\\.(\\d+)-(.+)$`);
       const toRename = [];
       for (const dir of dirs) {
         const dm = dir.match(decPattern);
EOF
@@ -493,7 +493,8 @@
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));

// Find sibling decimals with higher numbers
const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
const safeBaseInt = escapeRegex(baseInt);
const decPattern = new RegExp(`^${safeBaseInt}\\.(\\d+)-(.+)$`);
const toRename = [];
for (const dir of dirs) {
const dm = dir.match(decPattern);
Copilot is powered by AI and may make mistakes. Always verify output.
}
current = current[key];
}
current[keys[keys.length - 1]] = parsedValue;

Check warning

Code scanning / CodeQL

Prototype-polluting function Medium

The property chain
here
is recursively assigned to
current
without guarding against prototype pollution.

Copilot Autofix

AI 7 days ago

In general, to fix this kind of issue you must prevent user-controlled property names from reaching special prototype-related keys such as __proto__, constructor, and prototype, or only recurse/assign when the destination already has that key as an own property. Here the simplest, least invasive fix is to reject or skip any key path segment that matches a dangerous name, and fail with a clear error rather than silently polluting prototypes.

Concretely, in cmdConfigSet in templates/get-shit-done/bin/lib/config.cjs, we should:

  • Before iterating over keys, validate that none of the segments equals "__proto__", "constructor", or "prototype". If such a segment is found, call error(...) and stop.
  • Optionally, we could also ensure during traversal that we never create or traverse those keys, but the pre-validation step is sufficient and simpler.

This change only touches the cmdConfigSet function around where keys is derived and before the for (let i = 0; i < keys.length - 1; i++) loop. No new imports or external libraries are required; we reuse the existing error function from ./core.cjs.

Suggested changeset 1
templates/get-shit-done/bin/lib/config.cjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/templates/get-shit-done/bin/lib/config.cjs b/templates/get-shit-done/bin/lib/config.cjs
--- a/templates/get-shit-done/bin/lib/config.cjs
+++ b/templates/get-shit-done/bin/lib/config.cjs
@@ -99,6 +99,14 @@
 
   // Set nested value using dot notation (e.g., "workflow.research")
   const keys = keyPath.split('.');
+
+  // Prevent prototype pollution via special property names in the key path
+  for (const segment of keys) {
+    if (segment === '__proto__' || segment === 'constructor' || segment === 'prototype') {
+      error('Invalid config key path segment: ' + segment);
+    }
+  }
+
   let current = config;
   for (let i = 0; i < keys.length - 1; i++) {
     const key = keys[i];
EOF
@@ -99,6 +99,14 @@

// Set nested value using dot notation (e.g., "workflow.research")
const keys = keyPath.split('.');

// Prevent prototype pollution via special property names in the key path
for (const segment of keys) {
if (segment === '__proto__' || segment === 'constructor' || segment === 'prototype') {
error('Invalid config key path segment: ' + segment);
}
}

let current = config;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
Copilot is powered by AI and may make mistakes. Always verify output.
@shoootyou shoootyou changed the base branch from main to dev February 26, 2026 09:48
@shoootyou shoootyou merged commit cc82b55 into dev Feb 26, 2026
2 of 3 checks passed
@shoootyou shoootyou deleted the feat/update branch February 26, 2026 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant