Skip to content

feat: Add Integration System for Artifacts and Tasks - #300

Closed
HuyNguyenDinh wants to merge 13 commits into
obra:mainfrom
HuyNguyenDinh:main
Closed

feat: Add Integration System for Artifacts and Tasks#300
HuyNguyenDinh wants to merge 13 commits into
obra:mainfrom
HuyNguyenDinh:main

Conversation

@HuyNguyenDinh

@HuyNguyenDinh HuyNguyenDinh commented Jan 19, 2026

Copy link
Copy Markdown

Summary

  • Implemented lib/config-core.js for loading project/user configuration.
  • Created using-integrations skill as a router for storage and task management.
  • Created using-notion and using-jira skills for external integrations.
  • Updated writing-plans and brainstorming skills to use the new integration system.

Test Plan

  • Unit tests for config-core.js pass.
  • Verified routing logic with simulation script.
  • Verified skill file creation.

Summary by CodeRabbit

  • New Features

    • Centralized configuration loader (user + project merge); unified task manager with Jira/Notion/Local providers; Notion sync CLI to mirror docs.
  • Documentation

    • New/updated skill guides for integrations, Notion, and task tracker; Jira skill marked deprecated; design docs for Notion sync and unified task manager.
  • Tests

    • Added test suites covering config loading, adapters, Notion sync/map logic, and CLI task manager flows.
  • Chores

    • Added local map placeholder and gitignore entry for local cache.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 19, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a configuration loader, a provider-agnostic task manager with Jira/Notion/Local adapters, a Notion sync utility and local map cache with CLI, numerous skill/docs updates to route saves through integrations, and tests covering config, adapters, and Notion sync behavior.

Changes

Cohort / File(s) Summary
Configuration core & tests
lib/config-core.js, lib/skills-core.js, tests/lib/config-core.test.js
New loadConfig(projectDir) loads and merges default → user (~/.superpowers/config.json) → project (/.superpowers/config.json), validates JSON, protects against prototype pollution, returns cloned result; lib/skills-core.js re-exports it; tests cover defaults, overrides, extra keys, and malformed JSON.
Notion sync, map & tests
lib/notion-sync.js, .superpowers/notion-map.json, tests/lib/notion-sync.test.js
New Notion sync library + CLI: notionRequest wrapper, ensurePage/archivePage, syncFile/syncDirectory, markdown→Notion blocks, conservative update (append-only), local map persistence (.superpowers/notion-map.json), and tests for map load/save and ID normalization.
Task manager & adapters
lib/task-manager.js, lib/adapters/notion-adapter.js, lib/adapters/jira-adapter.js, lib/adapters/local-adapter.js
New task-manager selects adapter by config (jira
Adapter tests
tests/lib/notion-adapter.test.js, tests/lib/jira-adapter.test.js, tests/lib/local-adapter.test.js
New tests: NotionAdapter subtask fallback behavior; JiraAdapter request payloads and constructor validation; LocalAdapter file operations (ensure/create/log/subtask).
Task-manager CLI integration tests
tests/lib/task-manager.test.js
Integration tests exercising lib/task-manager.js CLI against LocalAdapter: create, log-work, subtask flows and TODO.md assertions.
Skills: integrations, task-tracker, Notion, Jira docs
skills/using-integrations/SKILL.md, skills/using-task-tracker/SKILL.md, skills/using-notion/SKILL.md, skills/using-jira/SKILL.md
New/updated skill docs describing routing of storage and project-management operations to configured providers, examples, Notion preconditions, and a Jira deprecation redirect.
Skills updated to invoke integrations
skills/brainstorming/SKILL.md, skills/writing-plans/SKILL.md
Replaced direct local-save instructions with steps to invoke the using-integrations action to persist artifacts (local or remote fallback).
Guidance & review docs
skills/requesting-code-review/code-reviewer.md, skills/test-driven-development/testing-anti-patterns.md, skills/verification-before-completion/SKILL.md
Added reviewer procedural steps, a new testing anti-pattern entry (duplicate in diff), swapped some fenced blocks to language-tagged text, and added configuration-change verification guidance.
Design documents
docs/plans/2026-01-20-notion-integration-design.md, docs/plans/2026-01-20-unified-task-manager-design.md
Added Notion recursive mirror design, notion-map DB/cache, and unified task manager architecture with adapter plan and migration path.
Misc files
.gitignore, .superpowers/notion-map.json
Added .superpowers/ to .gitignore and an empty .superpowers/notion-map.json placeholder.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as "CLI / sync command"
  participant Config as "loadConfig()"
  participant FS as "Local FS (.superpowers map)"
  participant Notion as "Notion API"

  CLI->>Config: loadConfig()
  Config-->>CLI: config (NOTION key, roots)
  CLI->>FS: getMap() / read map file
  FS-->>CLI: map (may be {})
  CLI->>FS: read local file(s) / derive title
  CLI->>Notion: notionRequest(search/create page) with parentId
  Notion-->>CLI: page info (id) or created page
  CLI->>FS: saveMap(updated mapping)
  FS-->>CLI: saved
  CLI->>CLI: update/skip page content (append-only strategy)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through configs, merged defaults with care,
I nudged pages into Notion and left a map there,
I made tasks and subtasks, fell back when clouds were gray,
I wrote tests that prove it, then bounced along my way,
A tiny rabbit coder — synced, documented, aware.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Add Integration System for Artifacts and Tasks' clearly and concisely summarizes the main change: introducing a new integration system that routes artifact storage and task management to external providers (Notion, Jira) or local fallbacks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@skills/test-driven-development/testing-anti-patterns.md`:
- Around line 292-314: The fenced code block under the "Gate Function" heading
is missing a language identifier (markdownlint MD040); update the opening fence
for that block (the triple backticks directly following the "Gate Function"
section) to include a language tag such as "text" (e.g., change ``` to ```text)
so the block is properly annotated.

In `@skills/verification-before-completion/SKILL.md`:
- Around line 78-86: Add a language tag to the fenced code block that begins
with "BEFORE claiming works:" in SKILL.md to satisfy markdownlint MD040; update
the opening ``` to ```text so the block reads ```text ... ``` and leave the
contents unchanged, ensuring the fenced block is properly closed with ``` at the
end.
🧹 Nitpick comments (7)
lib/config-core.js (2)

12-36: Consider potential issues with synchronous I/O and error details.

A few observations:

  1. TOCTOU race: existsSync followed by readFileSync has a small window where the file could be deleted. Consider wrapping the read in a try-catch and handling ENOENT specifically.

  2. Swallowed error context: The warning message doesn't include the actual error, making debugging harder when JSON parsing fails.

  3. Synchronous I/O: This is acceptable for CLI/startup scenarios but could block if called on a request path.

♻️ Optional: Include error message for easier debugging
         try {
             userConfig = JSON.parse(fs.readFileSync(userConfigPath, 'utf8'));
         } catch (error) {
-            console.warn(`Warning: Failed to parse user config at ${userConfigPath}`);
+            console.warn(`Warning: Failed to parse user config at ${userConfigPath}: ${error.message}`);
         }
         try {
             projectConfig = JSON.parse(fs.readFileSync(projectConfigPath, 'utf8'));
         } catch (error) {
-            console.warn(`Warning: Failed to parse project config at ${projectConfigPath}`);
+            console.warn(`Warning: Failed to parse project config at ${projectConfigPath}: ${error.message}`);
         }

44-67: Verify prototype pollution safety and array merge behavior.

Two considerations for mergeDeep:

  1. Prototype pollution: If a malicious config contains __proto__ or constructor keys, they could be assigned to the output object. Consider adding a check to skip dangerous keys.

  2. Array handling: Arrays are replaced entirely (source wins) rather than merged. This is a valid design choice but should be documented or tested to confirm intent.

🔒 Recommended: Guard against prototype pollution
 function mergeDeep(target, source) {
     if (isObject(target) && isObject(source)) {
         const output = Object.assign({}, target);
         Object.keys(source).forEach(key => {
+            // Skip prototype pollution vectors
+            if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
+                return;
+            }
             if (isObject(source[key])) {
skills/using-jira/SKILL.md (2)

27-46: Consider security note about credential handling in curl example.

The curl example correctly uses environment variables for credentials. A few suggestions:

  1. The -D- flag dumps response headers to stdout, which may clutter output. Consider using -s (silent) or -w for specific output formatting if headers aren't needed.

  2. Consider adding a note that JIRA_API_TOKEN should be an API token (not password) for Atlassian Cloud, as basic auth with passwords is deprecated.

📝 Optional: Simplify curl flags
 ```bash
-curl -D- \
+curl -s \
   -u $JIRA_EMAIL:$JIRA_API_TOKEN \

48-55: Documentation is incomplete for Update Ticket action.

The Update Ticket section references "Jira REST API documentation" but doesn't provide an example like the Create Ticket section does. Consider adding a basic curl example for consistency, or at minimum a direct link to the relevant API docs.

skills/using-integrations/SKILL.md (1)

19-26: Relative import path may cause confusion.

The import './lib/config-core.js' assumes the script is run from the repository root. This could fail or confuse users if they execute from a different directory.

Consider either:

  1. Using an absolute path or package import (if published)
  2. Adding a note about the required working directory
  3. Showing how to resolve the path dynamically
📝 Optional: Add working directory note
 ### 1. Determine Configuration
-First, run this Node.js script to check configuration:
+First, run this Node.js script from the repository root to check configuration:
 ```javascript
tests/lib/config-core.test.js (2)

27-73: Add test for malformed JSON handling.

The test suite covers the happy paths well but doesn't test the error handling path where config files contain invalid JSON. This is important since loadConfig has specific behavior (warn and continue with empty config) for parse failures.

🧪 Suggested: Add test for malformed JSON
// Test 5: Malformed JSON should warn and return empty/partial config
console.log('Test 5: Malformed JSON in user config');
fs.writeFileSync(path.join(USER_CONFIG_DIR, 'config.json'), '{ invalid json }');
fs.unlinkSync(path.join(PROJECT_CONFIG_DIR, 'config.json')); // Remove project config
config = loadConfig(PROJECT_DIR);
assert.deepStrictEqual(config, {}); // Should fall back to empty

1-1: Remove empty leading line.

The file starts with an empty line before the imports. This is a minor style inconsistency.

✨ Suggested fix
-
 import { loadConfig } from '../../lib/config-core.js';

Comment thread skills/test-driven-development/testing-anti-patterns.md
Comment thread skills/verification-before-completion/SKILL.md
@obra

obra commented Jan 19, 2026

Copy link
Copy Markdown
Owner

Can you give me a human-written description of what you were trying to do with this PR?

@HuyNguyenDinh

Copy link
Copy Markdown
Author

Can you give me a human-written description of what you were trying to do with this PR?

I'm trying to integrate Notion for storing Documents and Implementation Plans which storing in Local or Git (if you commit), beside that I've realized that we didn't applied the project management concept to improve QA/QC stage so I'm trying to to add the jira or other management tools for work logs which make us visible what the agents did, our performances with agentic workflow and where we stuck at most.

Dinh Huy and others added 5 commits January 20, 2026 13:46
- Add `lib/notion-sync.js` for recursive documentation mirroring
- Add `lib/task-manager.js` with adapters for Notion, Jira, and Local
- Update `lib/config-core.js` to support new configuration schemas
- Update `skills/using-notion` to use new sync script
- Add `skills/using-task-tracker` and deprecate `skills/using-jira`
- Fix Notion adapter fallback to return actual block ID instead of hardcoded 'fallback'
- Add error handling for malformed JSON in notion-sync getMap()
- Add directory creation in notion-sync saveMap() to prevent ENOENT errors
- Fix parent ID normalization to handle both hyphenated and non-hyphenated formats
- Add project_key to Jira config defaults for completeness
- Add comprehensive tests for all fixes using TDD

All fixes implemented following Test-Driven Development:
- tests/lib/notion-adapter.test.js: Test fallback block ID capture
- tests/lib/notion-sync.test.js: Test file system safety and ID normalization

Resolves CodeRabbit review comments from PR #3

Co-Authored-By: Claude <noreply@anthropic.com>
- Update documentation to clarify mapping store source of truth
- Add proper validation and error handling to Jira adapter
- Fix local adapter ID collision issue with better random generation
- Add .superpowers/ to .gitignore
- Fix trailing whitespace in notion-sync.js

These changes address remaining review feedback and code quality improvements.

Co-Authored-By: Claude <noreply@anthropic.com>
The test was expecting an empty object when JSON is malformed, but
with the new default config system, it should return defaults.

Co-Authored-By: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@docs/plans/2026-01-20-notion-integration-design.md`:
- Around line 30-32: The nested unordered list under the "Skill Instructions"
heading uses a 3-space indent instead of the 2-space indent markdownlint
expects; fix by adjusting the nested list lines (the bullets referencing "Update
`skills/using-notion/SKILL.md`..." and "Since skills are text instructions, we
might need a helper script...") to use exactly two spaces before the "-" so the
nested bullets are indented by 2 spaces relative to the parent list item; keep
the content unchanged aside from whitespace.

In `@lib/adapters/jira-adapter.js`:
- Around line 89-100: The createSubtask method builds a URL from
response.id/response.key without validating the response like createTask does;
update createSubtask to validate the response returned from
this.request('issue', 'POST', body) (e.g., ensure response && response.id &&
response.key), and if validation fails throw or return an explicit
error/nullable value instead of constructing a URL with undefined values;
reference the createSubtask function and mirror the validation logic/pattern
used in createTask to locate and implement the fix.

In `@lib/adapters/local-adapter.js`:
- Around line 27-35: In logWork, you build a RegExp directly from the
user-supplied id which can contain regex metacharacters; fix by escaping the id
before constructing the RegExp (e.g., add an escapeRegExp utility that replaces
[.*+?^${}()|[\]\\] with backslash-escaped versions) and use const taskRegex =
new RegExp(`## \\[${escapeRegExp(id)}\\].*`, 'g') (or import a safe escaping
helper) so the regex matches the literal id.

In `@lib/notion-sync.js`:
- Around line 165-181: updatePageContent currently returns early when
children.results.length > 0, silently skipping updates to non-empty pages;
change it to support an explicit opt-in overwrite mode and make the behavior
visible: add an overwrite boolean parameter to updatePageContent (and propagate
it from the CLI option), and when overwrite is true iterate over
children.results and remove/archive each block via notionRequest (use the same
"blocks/{id}" endpoint and appropriate delete/archive request), then append new
blocks; when overwrite is false keep the current safe behavior but replace the
console.log with a clear warning that includes the pageId and guidance to use
--overwrite or README for full sync; update the CLI help/README to document the
--overwrite flag and its consequences.
- Around line 99-113: The page creation payload uses an incorrect shape for the
title property when creating a page with parent: { page_id: ... } so the Notion
API rejects it; update the payload built in the code that calls notionRequest
(the newPage creation block) so that properties.title is an object with a title
array (i.e., properties: { title: { title: [ ... ] } } ) and each entry uses the
Notion rich text shape (type: 'text', text: { content: title }) instead of the
current direct array; keep children as markdownToBlocks(content) and keep
parent: { page_id: parentId } unchanged.

In `@lib/task-manager.js`:
- Around line 1-4: Replace any use of import.meta.filename with a cross-version
approach: import fileURLToPath from 'url' and path from 'path', compute const
__filename = fileURLToPath(import.meta.url) and const __dirname =
path.dirname(path.resolve(__filename)), then use path.resolve(__dirname, ...)
for comparisons; update this in lib/task-manager.js (where import.meta.filename
is used) and lib/notion-sync.js (around line 26) and ensure all path comparisons
use path.resolve for reliable behavior across Node versions.

In `@tests/lib/config-core.test.js`:
- Around line 77-82: In the catch block that currently calls process.exit(1),
replace that immediate exit with setting process.exitCode = 1 so the process
will signal failure without aborting execution; leave the console.error call
intact and allow the finally block to call teardown() to restore os.homedir and
remove TEST_DIR (i.e., change the usage of process.exit(1) to process.exitCode =
1 in the catch surrounding teardown()).
🧹 Nitpick comments (3)
tests/lib/notion-sync.test.js (1)

9-90: Avoid duplicating production helpers in tests.

These tests reimplement getMap, saveMap, and searchChildPage. If the production logic changes, tests may still pass. Consider exporting these helpers from lib/notion-sync.js (or adding a MAP_FILE override) so tests exercise the real implementation.

lib/notion-sync.js (1)

28-53: Add a request timeout to Notion API calls.

Without a timeout, a hung network call can stall the CLI indefinitely. The Jira adapter implements this pattern with a 30-second timeout using AbortController; apply the same approach to notionRequest.

🕒 Suggested timeout addition
 export async function notionRequest(endpoint, method = 'GET', body = null) {
     if (!NOTION_API_KEY) {
         throw new Error("Missing Notion API Key in config");
     }

+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 30000);
+
     const headers = {
         'Authorization': `Bearer ${NOTION_API_KEY}`,
         'Notion-Version': NOTION_VERSION,
         'Content-Type': 'application/json'
     };

     const options = {
         method,
-        headers
+        headers,
+        signal: controller.signal
     };

     if (body) {
         options.body = JSON.stringify(body);
     }

-    const response = await fetch(`https://api.notion.com/v1/${endpoint}`, options);
-    if (!response.ok) {
-        const text = await response.text();
-        throw new Error(`Notion API Error ${response.status}: ${text}`);
-    }
-    return response.json();
+    try {
+        const response = await fetch(`https://api.notion.com/v1/${endpoint}`, options);
+        if (!response.ok) {
+            const text = await response.text();
+            throw new Error(`Notion API Error ${response.status}: ${text}`);
+        }
+        return response.json();
+    } finally {
+        clearTimeout(timeoutId);
+    }
 }
lib/adapters/notion-adapter.js (1)

11-90: Validate database schema on initialization to fail fast on property mismatches.

The createTask and createSubtask methods hardcode property names (Name, Status, Type, Parent Item) without verifying they exist in the Notion database. If the schema differs, calls fail with opaque API errors. Use the Notion API's GET /v1/databases/{database_id} endpoint in the constructor to validate that required properties exist and have correct types, so configuration errors surface immediately rather than at runtime.

Comment on lines +30 to +32
- **Skill Instructions**:
- Update `skills/using-notion/SKILL.md` to include a recursive strategy or a script reference.
- Since skills are text instructions, we might need a helper script in `lib/notion-sync.js` to handle the recursion complexity, which the skill then invokes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix unordered list indentation (MD007).

markdownlint expects a 2‑space indent for nested list items here.

🧹 Proposed fix
-   - Update `skills/using-notion/SKILL.md` to include a recursive strategy or a script reference.
-   - Since skills are text instructions, we might need a helper script in `lib/notion-sync.js` to handle the recursion complexity, which the skill then invokes.
+  - Update `skills/using-notion/SKILL.md` to include a recursive strategy or a script reference.
+  - Since skills are text instructions, we might need a helper script in `lib/notion-sync.js` to handle the recursion complexity, which the skill then invokes.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **Skill Instructions**:
- Update `skills/using-notion/SKILL.md` to include a recursive strategy or a script reference.
- Since skills are text instructions, we might need a helper script in `lib/notion-sync.js` to handle the recursion complexity, which the skill then invokes.
- **Skill Instructions**:
- Update `skills/using-notion/SKILL.md` to include a recursive strategy or a script reference.
- Since skills are text instructions, we might need a helper script in `lib/notion-sync.js` to handle the recursion complexity, which the skill then invokes.
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

31-31: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)


32-32: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)

🤖 Prompt for AI Agents
In `@docs/plans/2026-01-20-notion-integration-design.md` around lines 30 - 32, The
nested unordered list under the "Skill Instructions" heading uses a 3-space
indent instead of the 2-space indent markdownlint expects; fix by adjusting the
nested list lines (the bullets referencing "Update
`skills/using-notion/SKILL.md`..." and "Since skills are text instructions, we
might need a helper script...") to use exactly two spaces before the "-" so the
nested bullets are indented by 2 spaces relative to the parent list item; keep
the content unchanged aside from whitespace.

Comment on lines +89 to +100
async createSubtask(parentId, title) {
const body = {
fields: {
project: { key: this.projectKey },
parent: { key: parentId },
summary: title,
issuetype: { name: 'Sub-task' }
}
};
const response = await this.request('issue', 'POST', body);
return { id: response.id, key: response.key, url: `${this.host}/browse/${response.key}` };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check the file size and read the jira-adapter.js file
wc -l lib/adapters/jira-adapter.js

Repository: obra/superpowers

Length of output: 176


🏁 Script executed:

# Read the full file to understand the context and find createTask method
cat -n lib/adapters/jira-adapter.js

Repository: obra/superpowers

Length of output: 4242


🌐 Web query:

Jira REST API 3 create issue response format id key

💡 Result:

When you successfully create an issue with Jira REST API v3 (POST /rest/api/3/issue) the response body is JSON containing at least:

  • id (string) — the internal numeric ID as a string
  • key (string) — the issue key (e.g. PROJ-123)
  • self (string) — URL to the created issue

Example:
{"id":"15735","key":"SSP-28","self":"https://your-site.atlassian.net/rest/api/3/issue/15735"}. Bulk-create returns an object with an "issues" array of those entries. [1][2]

Sources:
[1] Jira Cloud REST API — Create issue (response schema).
[2] Jira REST API example create issue (example response).


Validate sub-task creation response before building the URL.

createSubtask lacks response validation that createTask already has (line 52). If Jira returns an unexpected payload, this returns undefined id/key and a broken URL. Add the validation check:

     async createSubtask(parentId, title) {
         const body = {
             fields: {
                 project: { key: this.projectKey },
                 parent: { key: parentId },
                 summary: title,
                 issuetype: { name: 'Sub-task' }
             }
         };
         const response = await this.request('issue', 'POST', body);
+        if (!response?.id || !response?.key) {
+            throw new Error('Unexpected Jira API response: missing id or key');
+        }
         return { id: response.id, key: response.key, url: `${this.host}/browse/${response.key}` };
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async createSubtask(parentId, title) {
const body = {
fields: {
project: { key: this.projectKey },
parent: { key: parentId },
summary: title,
issuetype: { name: 'Sub-task' }
}
};
const response = await this.request('issue', 'POST', body);
return { id: response.id, key: response.key, url: `${this.host}/browse/${response.key}` };
}
async createSubtask(parentId, title) {
const body = {
fields: {
project: { key: this.projectKey },
parent: { key: parentId },
summary: title,
issuetype: { name: 'Sub-task' }
}
};
const response = await this.request('issue', 'POST', body);
if (!response?.id || !response?.key) {
throw new Error('Unexpected Jira API response: missing id or key');
}
return { id: response.id, key: response.key, url: `${this.host}/browse/${response.key}` };
}
🤖 Prompt for AI Agents
In `@lib/adapters/jira-adapter.js` around lines 89 - 100, The createSubtask method
builds a URL from response.id/response.key without validating the response like
createTask does; update createSubtask to validate the response returned from
this.request('issue', 'POST', body) (e.g., ensure response && response.id &&
response.key), and if validation fails throw or return an explicit
error/nullable value instead of constructing a URL with undefined values;
reference the createSubtask function and mirror the validation logic/pattern
used in createTask to locate and implement the fix.

Comment on lines +27 to +35
async logWork(id, timeSpent, comment = '') {
this.ensureFile();
let content = fs.readFileSync(this.filePath, 'utf8');
const taskRegex = new RegExp(`## \\[${id}\\].*`, 'g');
const match = taskRegex.exec(content);

if (!match) {
throw new Error(`Task ${id} not found locally.`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and examine the relevant lines
cat -n lib/adapters/local-adapter.js | sed -n '20,45p'

Repository: obra/superpowers

Length of output: 1461


🏁 Script executed:

# Check if there are any escaping utilities already defined in the file
rg -A 5 -B 5 "escape|regex" lib/adapters/local-adapter.js

Repository: obra/superpowers

Length of output: 699


🏁 Script executed:

# Search for where logWork is called to understand how id is provided
rg -n "logWork\(" --type js

Repository: obra/superpowers

Length of output: 386


🏁 Script executed:

# Look for any validation or sanitization of the id parameter
rg -B 10 "logWork.*id" lib/adapters/local-adapter.js

Repository: obra/superpowers

Length of output: 540


🏁 Script executed:

# Check the context around line 50 in task-manager.js where logWork is called
cat -n lib/task-manager.js | sed -n '40,65p'

Repository: obra/superpowers

Length of output: 1378


🏁 Script executed:

# Look for where the id in task-manager.js comes from (user input, parsing, etc.)
cat -n lib/task-manager.js | head -70

Repository: obra/superpowers

Length of output: 2739


Escape task IDs before building the regex.

The id parameter is user-supplied via CLI arguments and passed directly to the regex constructor without sanitization. If a user provides an id containing regex metacharacters (e.g., test[123] or task.*), the regex will behave unexpectedly or fail to match.

🔧 Proposed fix
-        const taskRegex = new RegExp(`## \\[${id}\\].*`, 'g');
+        const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+        const taskRegex = new RegExp(`## \\[${escapedId}\\].*`);
🤖 Prompt for AI Agents
In `@lib/adapters/local-adapter.js` around lines 27 - 35, In logWork, you build a
RegExp directly from the user-supplied id which can contain regex
metacharacters; fix by escaping the id before constructing the RegExp (e.g., add
an escapeRegExp utility that replaces [.*+?^${}()|[\]\\] with backslash-escaped
versions) and use const taskRegex = new RegExp(`## \\[${escapeRegExp(id)}\\].*`,
'g') (or import a safe escaping helper) so the regex matches the literal id.

Comment thread lib/notion-sync.js
Comment on lines +99 to +113
// 3. Create Page
console.log(`Creating new page for "${title}"...`);
const newPage = await notionRequest('pages', 'POST', {
parent: { page_id: parentId },
properties: {
title: [
{
text: {
content: title
}
}
]
},
children: content ? markdownToBlocks(content) : []
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "notion-sync.js" -type f | head -5

Repository: obra/superpowers

Length of output: 176


🏁 Script executed:

git ls-files | grep -E "(notion|test)" | head -20

Repository: obra/superpowers

Length of output: 889


🌐 Web query:

Notion API create page with page_id parent properties title format

💡 Result:

Use the pages.create endpoint with parent.type "page_id" and set properties with a single "title" property whose value is a title-type array of rich-text objects.

Minimal JSON (curl or body for SDK):

{
"parent": { "type": "page_id", "page_id": "YOUR_PAGE_ID" },
"properties": {
"title": {
"type": "title",
"title": [
{
"type": "text",
"text": { "content": "My new page title", "link": null }
}
]
}
}
}

JS (notion-client) example:

await notion.pages.create({
parent: { page_id: "YOUR_PAGE_ID" },
properties: {
title: {
type: "title",
title: [{ type: "text", text: { content: "My new page title" } }]
}
}
});

Notes:

  • When parent.type is "page_id", the only valid key in properties is the title property. (developers.notion.com)

Sources: Notion API reference — Create a page; Working with page content; Page property values. (developers.notion.com)

Citations:


🏁 Script executed:

sed -n '90,120p' lib/notion-sync.js

Repository: obra/superpowers

Length of output: 809


🏁 Script executed:

grep -n "notionRequest" lib/notion-sync.js | head -5

Repository: obra/superpowers

Length of output: 406


🏁 Script executed:

rg "function notionRequest|const notionRequest|export.*notionRequest" -A 10

Repository: obra/superpowers

Length of output: 608


🏁 Script executed:

sed -n '1,40p' lib/notion-sync.js

Repository: obra/superpowers

Length of output: 1216


🏁 Script executed:

sed -n '28,55p' lib/notion-sync.js

Repository: obra/superpowers

Length of output: 791


🏁 Script executed:

rg "title.*\[" lib/ --type js -B 2 -A 2

Repository: obra/superpowers

Length of output: 1783


🏁 Script executed:

grep -r "title.*type.*title" . --include="*.js" --include="*.md"

Repository: obra/superpowers

Length of output: 42


🏁 Script executed:

cat lib/adapters/notion-adapter.js | head -80

Repository: obra/superpowers

Length of output: 2543


🏁 Script executed:

find . -name "*.md" -type f -exec grep -l "notion\|Notion" {} \; | head -5

Repository: obra/superpowers

Length of output: 272


🏁 Script executed:

cat ./docs/plans/2026-01-20-notion-integration-design.md | head -100

Repository: obra/superpowers

Length of output: 2811


🏁 Script executed:

rg "parent.*page_id|parent.*database_id" lib/ -B 2 -A 5

Repository: obra/superpowers

Length of output: 2592


Fix page creation payload for page-parented pages.

The Notion API rejects the current title property format when creating a page with parent: { page_id: ... }. The API requires title to be an object containing a title array, not a direct array.

Proposed fix
     const newPage = await notionRequest('pages', 'POST', {
         parent: { page_id: parentId },
         properties: {
-            title: [
-                {
-                    text: {
-                        content: title
-                    }
-                }
-            ]
+            title: {
+                title: [
+                    {
+                        text: {
+                            content: title
+                        }
+                    }
+                ]
+            }
         },
         children: content ? markdownToBlocks(content) : []
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 3. Create Page
console.log(`Creating new page for "${title}"...`);
const newPage = await notionRequest('pages', 'POST', {
parent: { page_id: parentId },
properties: {
title: [
{
text: {
content: title
}
}
]
},
children: content ? markdownToBlocks(content) : []
});
// 3. Create Page
console.log(`Creating new page for "${title}"...`);
const newPage = await notionRequest('pages', 'POST', {
parent: { page_id: parentId },
properties: {
title: {
title: [
{
text: {
content: title
}
}
]
}
},
children: content ? markdownToBlocks(content) : []
});
🤖 Prompt for AI Agents
In `@lib/notion-sync.js` around lines 99 - 113, The page creation payload uses an
incorrect shape for the title property when creating a page with parent: {
page_id: ... } so the Notion API rejects it; update the payload built in the
code that calls notionRequest (the newPage creation block) so that
properties.title is an object with a title array (i.e., properties: { title: {
title: [ ... ] } } ) and each entry uses the Notion rich text shape (type:
'text', text: { content: title }) instead of the current direct array; keep
children as markdownToBlocks(content) and keep parent: { page_id: parentId }
unchanged.

Comment thread lib/notion-sync.js
Comment on lines +165 to +181
async function updatePageContent(pageId, content) {
// Notion API replacement is complex (block management).
// For MVP, we might just append or replace if possible, but Notion API doesn't support "replace all content" easily.
// We would need to delete all blocks and append new ones.
console.log(`Updating content for page ${pageId}...`);

// 1. Get existing blocks
const children = await notionRequest(`blocks/${pageId}/children`, 'GET');

// 2. Delete existing blocks (batching helps, but doing one by one for safety/simplicity first)
// Notion limits this. For a robust sync, this part needs care.
// For this implementation plan, we will just Append if empty, or warn if not.
if (children.results.length > 0) {
console.log(`Page ${pageId} is not empty. Skipping content overwrite for safety in MVP.`);
// In full version: Archive all children blocks, then append new ones.
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check the file structure and size
wc -l lib/notion-sync.js
head -20 lib/notion-sync.js

Repository: obra/superpowers

Length of output: 706


🏁 Script executed:

# Look at the specific function around lines 165-181
sed -n '160,190p' lib/notion-sync.js

Repository: obra/superpowers

Length of output: 1376


🏁 Script executed:

# Search for other sync-related functions and patterns to understand intended behavior
rg -A 5 -B 5 'updatePageContent|syncStrategy|append|replace' lib/notion-sync.js

Repository: obra/superpowers

Length of output: 2344


🏁 Script executed:

# Look for README or documentation about MVP status
find . -maxdepth 2 -type f \( -name "README*" -o -name "DESIGN*" -o -name "*.md" \) | head -20

Repository: obra/superpowers

Length of output: 337


🏁 Script executed:

# Search for TODO/FIXME comments related to sync or update behavior
rg -i 'todo|fixme|full.version|mvp' lib/notion-sync.js

Repository: obra/superpowers

Length of output: 434


🏁 Script executed:

# Check if there are any configuration options for sync behavior
rg -B 3 -A 3 'sync|update|append|overwrite' lib/config-core.js | head -50

Repository: obra/superpowers

Length of output: 42


🏁 Script executed:

# Look at how syncFile is exported and used
rg -B 5 -A 10 'export.*syncFile|syncFile' lib/notion-sync.js | head -100

Repository: obra/superpowers

Length of output: 1784


Sync skips updates to non-empty pages by design, but this limitation should be more visible.

updatePageContent returns early for pages with existing blocks, preventing updates when syncing file changes. This is an intentional MVP limitation noted in the code ("safety"), but it creates a gap between what users expect from a sync tool and the actual behavior. File changes won't propagate to Notion without manually clearing the page first.

Consider one of:

  • Implement full block deletion and replacement in MVP (removing the safety guard)
  • Add a --overwrite flag to allow users to opt into updates for non-empty pages
  • Document this limitation clearly in the CLI help and README to set expectations
🤖 Prompt for AI Agents
In `@lib/notion-sync.js` around lines 165 - 181, updatePageContent currently
returns early when children.results.length > 0, silently skipping updates to
non-empty pages; change it to support an explicit opt-in overwrite mode and make
the behavior visible: add an overwrite boolean parameter to updatePageContent
(and propagate it from the CLI option), and when overwrite is true iterate over
children.results and remove/archive each block via notionRequest (use the same
"blocks/{id}" endpoint and appropriate delete/archive request), then append new
blocks; when overwrite is false keep the current safe behavior but replace the
console.log with a clear warning that includes the pageId and guidance to use
--overwrite or README for full sync; update the CLI help/README to document the
--overwrite flag and its consequences.

Comment thread lib/task-manager.js
Comment on lines +1 to +4
import { loadConfig } from './config-core.js';
import { LocalAdapter } from './adapters/local-adapter.js';
import { NotionAdapter } from './adapters/notion-adapter.js';
import { JiraAdapter } from './adapters/jira-adapter.js';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the file
find . -name "task-manager.js" -type f

Repository: obra/superpowers

Length of output: 81


🏁 Script executed:

# Read the file to see the imports and the CLI check code
cat -n ./lib/task-manager.js

Repository: obra/superpowers

Length of output: 2739


🏁 Script executed:

# Let me verify Node.js ESM import.meta properties by checking Node.js docs and testing behavior
# First, check if there are any other Node.js files that might use import.meta to understand patterns
rg "import\.meta\." lib/ --type js

Repository: obra/superpowers

Length of output: 194


🏁 Script executed:

# Check the package.json for Node version requirements
cat package.json | head -30

Repository: obra/superpowers

Length of output: 104


🌐 Web query:

Node.js ESM import.meta.filename vs import.meta.url fileURLToPath

💡 Result:

Short answer

  • import.meta.url — built-in ESM meta property: the module's absolute file: URL (works in Node and browsers). Use it with url/file APIs. [1][2]
  • fileURLToPath(import.meta.url) — Node utility to convert that file: URL to a filesystem path (same string format as CommonJS __filename). [1][2]
  • import.meta.filename (and import.meta.dirname) — Node added these convenience properties (introduced v20.11.0/v21.2.0 as experimental, stabilized in later releases): they equal fileURLToPath(import.meta.url) (filename with symlinks resolved) and path.dirname(...) respectively. Only available for file: modules. [1][3]

Minimal examples

  • canonical conversion:
    import { fileURLToPath } from 'url';
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = new URL('.', import.meta.url).pathname; // or path.dirname(__filename)

  • using import.meta.filename (Node >= v20.11 / stable later):
    // in an ESM file
    console.log(import.meta.filename); // absolute path (symlinks resolved)
    console.log(import.meta.dirname); // directory

Notes

  • import.meta.filename/dirname are Node-specific (not in browsers) and only for file: URLs. For maximum compatibility use fileURLToPath(import.meta.url). [1][2][3]

Sources
[1] Node.js ESM docs (import.meta.url, fileURLToPath examples).
[2] MDN — import.meta and URL-based resolution examples.
[3] Node.js docs (import.meta.filename / dirname added in v20.11.0/v21.2.0; stabilized in later releases).


import.meta.filename is Node-specific and only available in v20.11.0+

This won't work reliably across Node versions. Use fileURLToPath(import.meta.url) and path.resolve() for cross-version compatibility and proper path comparison.

🔧 Proposed fix
 import { loadConfig } from './config-core.js';
 import { LocalAdapter } from './adapters/local-adapter.js';
 import { NotionAdapter } from './adapters/notion-adapter.js';
 import { JiraAdapter } from './adapters/jira-adapter.js';
+import path from 'path';
+import { fileURLToPath } from 'url';
 
 const config = loadConfig();
 const pmConfig = config.project_management || {};
@@ -23,7 +25,8 @@ switch (provider) {
 }
 
 // CLI Logic
-if (process.argv[1] === import.meta.filename) {
+const entryFile = process.argv[1] ? path.resolve(process.argv[1]) : null;
+if (entryFile === fileURLToPath(import.meta.url)) {

Also applies to: lib/notion-sync.js:26

🤖 Prompt for AI Agents
In `@lib/task-manager.js` around lines 1 - 4, Replace any use of
import.meta.filename with a cross-version approach: import fileURLToPath from
'url' and path from 'path', compute const __filename =
fileURLToPath(import.meta.url) and const __dirname =
path.dirname(path.resolve(__filename)), then use path.resolve(__dirname, ...)
for comparisons; update this in lib/task-manager.js (where import.meta.filename
is used) and lib/notion-sync.js (around line 26) and ensure all path comparisons
use path.resolve for reliable behavior across Node versions.

Comment on lines +77 to +82
} catch (err) {
console.error('Test failed:', err);
process.exit(1);
} finally {
teardown();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find and read the test file
fd "config-core.test.js" tests/

Repository: obra/superpowers

Length of output: 89


🏁 Script executed:

# Read the specific lines with context
if [ -f "tests/lib/config-core.test.js" ]; then
  cat -n "tests/lib/config-core.test.js" | sed -n '60,95p'
fi

Repository: obra/superpowers

Length of output: 1275


🏁 Script executed:

# Search for teardown definition in the file
if [ -f "tests/lib/config-core.test.js" ]; then
  rg "teardown" "tests/lib/config-core.test.js" -B 3 -A 3
fi

Repository: obra/superpowers

Length of output: 355


🌐 Web query:

Node.js process.exit() finally block execution behavior

💡 Result:

Short answer: calling process.exit() can prevent a try...finally finally block from running. Node's process.exit() instructs the runtime to terminate synchronously "as quickly as possible" (it only guarantees that 'exit' listeners run synchronously), so if you call it before the finally is reached the finally code may never execute. Use process.exitCode or allow the event loop to drain for a graceful shutdown instead. [1][2]

Sources:

  • Node.js process.exit() docs — notes synchronous termination and that only 'exit' listeners are run. [1]
  • Discussion/answers noting process.exit() as a cause for finally not executing. [2]

Replace process.exit(1) with process.exitCode = 1 to ensure teardown runs.

The process.exit() call bypasses the finally block, preventing teardown() from restoring os.homedir and removing the temp TEST_DIR. This leaves the test environment in a dirty state when tests fail.

Suggested fix
    } catch (err) {
        console.error('Test failed:', err);
-       process.exit(1);
+       process.exitCode = 1;
    } finally {
        teardown();
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (err) {
console.error('Test failed:', err);
process.exit(1);
} finally {
teardown();
}
} catch (err) {
console.error('Test failed:', err);
process.exitCode = 1;
} finally {
teardown();
}
🤖 Prompt for AI Agents
In `@tests/lib/config-core.test.js` around lines 77 - 82, In the catch block that
currently calls process.exit(1), replace that immediate exit with setting
process.exitCode = 1 so the process will signal failure without aborting
execution; leave the console.error call intact and allow the finally block to
call teardown() to restore os.homedir and remove TEST_DIR (i.e., change the
usage of process.exit(1) to process.exitCode = 1 in the catch surrounding
teardown()).

Dinh Huy and others added 2 commits January 20, 2026 20:00
Adds missing unit and integration tests for:
- LocalAdapter (file creation, task appending)
- JiraAdapter (payload validation, mocking)
- TaskManager (CLI argument integration)
@obra

obra commented Jan 22, 2026

Copy link
Copy Markdown
Owner

Hi. These changes don't look like a great fit for Superpowers writ large. I'm not particularly interested in adding direct Notion and Jira integration.

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.

2 participants