specdacular 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/install.js +3 -1
- package/bin/specd.js +135 -0
- package/commands/specd.best-practices.md +75 -0
- package/commands/specd.docs.md +81 -0
- package/commands/specd.docs.review.md +80 -0
- package/commands/specd.generate-skills.learn.md +65 -0
- package/commands/specd.new-project.md +58 -0
- package/commands/specd.new-runner-task.md +52 -0
- package/commands/specd.new.md +6 -6
- package/commands/specd.runner-status.md +27 -0
- package/package.json +6 -2
- package/runner/main/agent/parser.js +39 -0
- package/runner/main/agent/runner.js +137 -0
- package/runner/main/agent/template.js +16 -0
- package/runner/main/bootstrap.js +69 -0
- package/runner/main/db.js +45 -0
- package/runner/main/index.js +103 -0
- package/runner/main/ipc.js +72 -0
- package/runner/main/notifications/telegram.js +45 -0
- package/runner/main/orchestrator.js +193 -0
- package/runner/main/paths.js +36 -0
- package/runner/main/pipeline/resolver.js +20 -0
- package/runner/main/pipeline/sequencer.js +42 -0
- package/runner/main/server/api.js +125 -0
- package/runner/main/server/index.js +33 -0
- package/runner/main/server/websocket.js +24 -0
- package/runner/main/state/manager.js +83 -0
- package/runner/main/template-manager.js +41 -0
- package/runner/main/test/agent-parser.test.js +44 -0
- package/runner/main/test/bootstrap.test.js +58 -0
- package/runner/main/test/db.test.js +72 -0
- package/runner/main/test/paths.test.js +29 -0
- package/runner/main/test/state-manager.test.js +72 -0
- package/runner/main/test/template-manager.test.js +66 -0
- package/runner/main/worktree/manager.js +95 -0
- package/runner/package.json +22 -0
- package/runner/preload.js +19 -0
- package/specdacular/HELP.md +20 -11
- package/specdacular/agents/best-practices-researcher.md +271 -0
- package/specdacular/agents/project-researcher.md +409 -0
- package/specdacular/references/load-context.md +4 -7
- package/specdacular/templates/orchestrator/CONCERNS.md +1 -1
- package/specdacular/templates/orchestrator/PROJECTS.md +3 -4
- package/specdacular/templates/tasks/PLAN.md +2 -2
- package/specdacular/templates/tasks/PROJECT.md +52 -0
- package/specdacular/templates/tasks/REQUIREMENTS.md +75 -0
- package/specdacular/workflows/best-practices.md +472 -0
- package/specdacular/workflows/context-add.md +16 -30
- package/specdacular/workflows/context-manual-review.md +7 -7
- package/specdacular/workflows/docs-review.md +273 -0
- package/specdacular/workflows/docs.md +420 -0
- package/specdacular/workflows/generate-learn-skill.md +214 -0
- package/specdacular/workflows/new-project.md +799 -0
- package/specdacular/workflows/new.md +5 -4
- package/specdacular/workflows/orchestrator/new.md +4 -4
- package/specdacular/workflows/orchestrator/plan.md +6 -6
- package/commands/specd.codebase.map.md +0 -72
- package/commands/specd.codebase.review.md +0 -39
- package/specdacular/workflows/map-codebase.md +0 -715
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test';
|
|
2
|
+
import { strict as a } from 'node:assert';
|
|
3
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { bootstrap } from '../bootstrap.js';
|
|
7
|
+
import { Paths } from '../paths.js';
|
|
8
|
+
|
|
9
|
+
describe('bootstrap', () => {
|
|
10
|
+
let tempDir;
|
|
11
|
+
let paths;
|
|
12
|
+
|
|
13
|
+
before(() => {
|
|
14
|
+
tempDir = mkdtempSync(join(tmpdir(), 'specd-test-'));
|
|
15
|
+
paths = new Paths(tempDir);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
after(() => {
|
|
19
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('creates all required directories and default files', async () => {
|
|
23
|
+
await bootstrap(paths);
|
|
24
|
+
|
|
25
|
+
a.ok(existsSync(paths.templatesDir));
|
|
26
|
+
a.ok(existsSync(paths.agentTemplatesDir));
|
|
27
|
+
a.ok(existsSync(paths.pipelineTemplatesDir));
|
|
28
|
+
a.ok(existsSync(paths.projectsDir));
|
|
29
|
+
|
|
30
|
+
a.ok(existsSync(paths.db));
|
|
31
|
+
const db = JSON.parse(readFileSync(paths.db, 'utf-8'));
|
|
32
|
+
a.deepEqual(db, { projects: [] });
|
|
33
|
+
|
|
34
|
+
a.ok(existsSync(paths.config));
|
|
35
|
+
const config = JSON.parse(readFileSync(paths.config, 'utf-8'));
|
|
36
|
+
a.equal(config.server.port, 3700);
|
|
37
|
+
|
|
38
|
+
// Default agent templates exist
|
|
39
|
+
a.ok(existsSync(join(paths.agentTemplatesDir, 'claude-planner.json')));
|
|
40
|
+
a.ok(existsSync(join(paths.agentTemplatesDir, 'claude-implementer.json')));
|
|
41
|
+
a.ok(existsSync(join(paths.agentTemplatesDir, 'claude-reviewer.json')));
|
|
42
|
+
|
|
43
|
+
// Default pipeline template exists
|
|
44
|
+
a.ok(existsSync(join(paths.pipelineTemplatesDir, 'default.json')));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('does not overwrite existing files on second run', async () => {
|
|
48
|
+
const config = JSON.parse(readFileSync(paths.config, 'utf-8'));
|
|
49
|
+
config.server.port = 9999;
|
|
50
|
+
const { writeFileSync } = await import('fs');
|
|
51
|
+
writeFileSync(paths.config, JSON.stringify(config));
|
|
52
|
+
|
|
53
|
+
await bootstrap(paths);
|
|
54
|
+
|
|
55
|
+
const reloaded = JSON.parse(readFileSync(paths.config, 'utf-8'));
|
|
56
|
+
a.equal(reloaded.server.port, 9999);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
2
|
+
import { strict as a } from 'node:assert';
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { ProjectDB } from '../db.js';
|
|
7
|
+
|
|
8
|
+
describe('ProjectDB', () => {
|
|
9
|
+
let tempDir;
|
|
10
|
+
let dbPath;
|
|
11
|
+
let db;
|
|
12
|
+
|
|
13
|
+
before(() => {
|
|
14
|
+
tempDir = mkdtempSync(join(tmpdir(), 'specd-db-test-'));
|
|
15
|
+
dbPath = join(tempDir, 'db.json');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
writeFileSync(dbPath, JSON.stringify({ projects: [] }));
|
|
20
|
+
db = new ProjectDB(dbPath);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
after(() => {
|
|
24
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('registers a project', () => {
|
|
28
|
+
const project = db.register('my-project', '/Users/victor/work/my-project');
|
|
29
|
+
a.equal(project.name, 'my-project');
|
|
30
|
+
a.equal(project.path, '/Users/victor/work/my-project');
|
|
31
|
+
a.equal(project.active, true);
|
|
32
|
+
a.ok(project.id);
|
|
33
|
+
a.ok(project.registeredAt);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('lists projects', () => {
|
|
37
|
+
db.register('proj-a', '/a');
|
|
38
|
+
db.register('proj-b', '/b');
|
|
39
|
+
const list = db.list();
|
|
40
|
+
a.equal(list.length, 2);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('gets a project by id', () => {
|
|
44
|
+
const project = db.register('my-project', '/path');
|
|
45
|
+
const found = db.get(project.id);
|
|
46
|
+
a.equal(found.name, 'my-project');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('finds a project by path', () => {
|
|
50
|
+
db.register('my-project', '/Users/victor/work/my-project');
|
|
51
|
+
const found = db.findByPath('/Users/victor/work/my-project');
|
|
52
|
+
a.equal(found.name, 'my-project');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('finds a project by subdirectory path', () => {
|
|
56
|
+
db.register('my-project', '/Users/victor/work');
|
|
57
|
+
const found = db.findByPath('/Users/victor/work/repo-a');
|
|
58
|
+
a.equal(found.name, 'my-project');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('unregisters a project', () => {
|
|
62
|
+
const project = db.register('my-project', '/path');
|
|
63
|
+
db.unregister(project.id);
|
|
64
|
+
a.equal(db.list().length, 0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('persists to disk', () => {
|
|
68
|
+
db.register('my-project', '/path');
|
|
69
|
+
const db2 = new ProjectDB(dbPath);
|
|
70
|
+
a.equal(db2.list().length, 1);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import { strict as a } from 'node:assert';
|
|
3
|
+
import { Paths } from '../paths.js';
|
|
4
|
+
|
|
5
|
+
describe('Paths', () => {
|
|
6
|
+
it('returns app data dir based on platform', () => {
|
|
7
|
+
const paths = new Paths('/tmp/test-specd');
|
|
8
|
+
a.equal(paths.root, '/tmp/test-specd');
|
|
9
|
+
a.equal(paths.db, '/tmp/test-specd/db.json');
|
|
10
|
+
a.equal(paths.config, '/tmp/test-specd/config.json');
|
|
11
|
+
a.equal(paths.templatesDir, '/tmp/test-specd/templates');
|
|
12
|
+
a.equal(paths.agentTemplatesDir, '/tmp/test-specd/templates/agents');
|
|
13
|
+
a.equal(paths.pipelineTemplatesDir, '/tmp/test-specd/templates/pipelines');
|
|
14
|
+
a.equal(paths.projectsDir, '/tmp/test-specd/projects');
|
|
15
|
+
a.equal(paths.electronDir, '/tmp/test-specd/electron');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns project-specific paths', () => {
|
|
19
|
+
const paths = new Paths('/tmp/test-specd');
|
|
20
|
+
const pp = paths.forProject('abc123');
|
|
21
|
+
a.equal(pp.dir, '/tmp/test-specd/projects/abc123');
|
|
22
|
+
a.equal(pp.projectJson, '/tmp/test-specd/projects/abc123/project.json');
|
|
23
|
+
a.equal(pp.statusJson, '/tmp/test-specd/projects/abc123/status.json');
|
|
24
|
+
a.equal(pp.tasksDir, '/tmp/test-specd/projects/abc123/tasks');
|
|
25
|
+
a.equal(pp.logsDir, '/tmp/test-specd/projects/abc123/logs');
|
|
26
|
+
a.equal(pp.agentsDir, '/tmp/test-specd/projects/abc123/agents');
|
|
27
|
+
a.equal(pp.pipelinesDir, '/tmp/test-specd/projects/abc123/pipelines');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
2
|
+
import { strict as a } from 'node:assert';
|
|
3
|
+
import { mkdtempSync, rmSync, readFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { StateManager } from '../state/manager.js';
|
|
7
|
+
|
|
8
|
+
describe('StateManager', () => {
|
|
9
|
+
let tempDir;
|
|
10
|
+
let statusPath;
|
|
11
|
+
let sm;
|
|
12
|
+
|
|
13
|
+
before(() => {
|
|
14
|
+
tempDir = mkdtempSync(join(tmpdir(), 'specd-state-test-'));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
statusPath = join(tempDir, `status-${Date.now()}.json`);
|
|
19
|
+
sm = new StateManager(statusPath);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
after(() => {
|
|
23
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('registers a task', () => {
|
|
27
|
+
sm.registerTask('task-001', { name: 'Add dark mode', pipeline: 'default' });
|
|
28
|
+
const state = sm.getState();
|
|
29
|
+
a.equal(state.tasks['task-001'].name, 'Add dark mode');
|
|
30
|
+
a.equal(state.tasks['task-001'].status, 'queued');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('updates task status', () => {
|
|
34
|
+
sm.registerTask('task-001', { name: 'Test', pipeline: 'default' });
|
|
35
|
+
sm.updateTaskStatus('task-001', 'in_progress');
|
|
36
|
+
a.equal(sm.getState().tasks['task-001'].status, 'in_progress');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('tracks stages', () => {
|
|
40
|
+
sm.registerTask('task-001', { name: 'Test', pipeline: 'default' });
|
|
41
|
+
sm.startStage('task-001', { stage: 'plan', agent: 'claude-planner' });
|
|
42
|
+
const task = sm.getState().tasks['task-001'];
|
|
43
|
+
a.equal(task.current_stage, 'plan');
|
|
44
|
+
a.equal(task.stages.length, 1);
|
|
45
|
+
a.equal(task.stages[0].status, 'running');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('completes stages with duration', () => {
|
|
49
|
+
sm.registerTask('task-001', { name: 'Test', pipeline: 'default' });
|
|
50
|
+
sm.startStage('task-001', { stage: 'plan', agent: 'claude-planner' });
|
|
51
|
+
sm.completeStage('task-001', 'success', 'Planned it');
|
|
52
|
+
const stage = sm.getState().tasks['task-001'].stages[0];
|
|
53
|
+
a.equal(stage.status, 'success');
|
|
54
|
+
a.equal(stage.summary, 'Planned it');
|
|
55
|
+
a.ok(stage.duration >= 0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('persists to disk', () => {
|
|
59
|
+
sm.registerTask('task-001', { name: 'Test', pipeline: 'default' });
|
|
60
|
+
sm.persist();
|
|
61
|
+
const data = JSON.parse(readFileSync(statusPath, 'utf-8'));
|
|
62
|
+
a.ok(data.tasks['task-001']);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('emits change events', () => {
|
|
66
|
+
const events = [];
|
|
67
|
+
sm.on('change', (e) => events.push(e));
|
|
68
|
+
sm.registerTask('task-001', { name: 'Test', pipeline: 'default' });
|
|
69
|
+
a.equal(events.length, 1);
|
|
70
|
+
a.equal(events[0].type, 'task_registered');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test';
|
|
2
|
+
import { strict as a } from 'node:assert';
|
|
3
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { TemplateManager } from '../template-manager.js';
|
|
7
|
+
import { Paths } from '../paths.js';
|
|
8
|
+
|
|
9
|
+
describe('TemplateManager', () => {
|
|
10
|
+
let tempDir;
|
|
11
|
+
let paths;
|
|
12
|
+
let tm;
|
|
13
|
+
|
|
14
|
+
before(() => {
|
|
15
|
+
tempDir = mkdtempSync(join(tmpdir(), 'specd-tm-test-'));
|
|
16
|
+
paths = new Paths(tempDir);
|
|
17
|
+
|
|
18
|
+
// Create global templates
|
|
19
|
+
mkdirSync(paths.agentTemplatesDir, { recursive: true });
|
|
20
|
+
mkdirSync(paths.pipelineTemplatesDir, { recursive: true });
|
|
21
|
+
|
|
22
|
+
writeFileSync(
|
|
23
|
+
join(paths.agentTemplatesDir, 'claude-planner.json'),
|
|
24
|
+
JSON.stringify({ cmd: 'claude -p', system_prompt: 'global planner' })
|
|
25
|
+
);
|
|
26
|
+
writeFileSync(
|
|
27
|
+
join(paths.pipelineTemplatesDir, 'default.json'),
|
|
28
|
+
JSON.stringify({ name: 'default', stages: [{ stage: 'plan', agent: 'claude-planner' }] })
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Create per-project override
|
|
32
|
+
const projectAgentsDir = join(paths.projectsDir, 'proj1', 'agents');
|
|
33
|
+
mkdirSync(projectAgentsDir, { recursive: true });
|
|
34
|
+
writeFileSync(
|
|
35
|
+
join(projectAgentsDir, 'claude-planner.json'),
|
|
36
|
+
JSON.stringify({ cmd: 'claude -p --model opus', system_prompt: 'custom planner' })
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
tm = new TemplateManager(paths);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
after(() => {
|
|
43
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('loads global agents', () => {
|
|
47
|
+
const agents = tm.getAgents();
|
|
48
|
+
a.equal(agents['claude-planner'].system_prompt, 'global planner');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('loads global pipelines', () => {
|
|
52
|
+
const pipelines = tm.getPipelines();
|
|
53
|
+
a.equal(pipelines['default'].stages.length, 1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns per-project agent override when present', () => {
|
|
57
|
+
const agents = tm.getAgents('proj1');
|
|
58
|
+
a.equal(agents['claude-planner'].system_prompt, 'custom planner');
|
|
59
|
+
a.equal(agents['claude-planner'].cmd, 'claude -p --model opus');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('falls back to global when no per-project override', () => {
|
|
63
|
+
const agents = tm.getAgents('proj-no-overrides');
|
|
64
|
+
a.equal(agents['claude-planner'].system_prompt, 'global planner');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// runner/main/worktree/manager.js
|
|
2
|
+
import { execSync, execFileSync } from 'child_process';
|
|
3
|
+
import { existsSync, mkdirSync, rmSync } from 'fs';
|
|
4
|
+
import { join, basename } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
|
|
7
|
+
export class WorktreeManager {
|
|
8
|
+
constructor(repoDir) {
|
|
9
|
+
this.repoDir = repoDir;
|
|
10
|
+
this.worktreesDir = join(tmpdir(), 'specd-worktrees', basename(repoDir));
|
|
11
|
+
this.active = new Map();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
create(taskId) {
|
|
15
|
+
const branch = `specd/${taskId}`;
|
|
16
|
+
const worktreePath = join(this.worktreesDir, taskId);
|
|
17
|
+
|
|
18
|
+
mkdirSync(this.worktreesDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
// Create branch from current HEAD
|
|
21
|
+
try {
|
|
22
|
+
execSync(`git branch ${branch}`, { cwd: this.repoDir, stdio: 'pipe' });
|
|
23
|
+
} catch {
|
|
24
|
+
// Branch may already exist
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
execSync(`git worktree add "${worktreePath}" ${branch}`, {
|
|
28
|
+
cwd: this.repoDir,
|
|
29
|
+
stdio: 'pipe',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this.active.set(taskId, worktreePath);
|
|
33
|
+
return worktreePath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
remove(taskId, deleteBranch = false) {
|
|
37
|
+
const worktreePath = this.active.get(taskId);
|
|
38
|
+
if (!worktreePath) return;
|
|
39
|
+
|
|
40
|
+
execSync(`git worktree remove "${worktreePath}" --force`, {
|
|
41
|
+
cwd: this.repoDir,
|
|
42
|
+
stdio: 'pipe',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (deleteBranch) {
|
|
46
|
+
try {
|
|
47
|
+
execSync(`git branch -D specd/${taskId}`, { cwd: this.repoDir, stdio: 'pipe' });
|
|
48
|
+
} catch {
|
|
49
|
+
// Branch may not exist
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.active.delete(taskId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
hasChanges(taskId) {
|
|
57
|
+
const worktreePath = this.active.get(taskId);
|
|
58
|
+
if (!worktreePath) return false;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const base = execSync('git merge-base HEAD main', { cwd: worktreePath, encoding: 'utf-8' }).trim();
|
|
62
|
+
const head = execSync('git rev-parse HEAD', { cwd: worktreePath, encoding: 'utf-8' }).trim();
|
|
63
|
+
return base !== head;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
createPR(taskId, taskName, summary) {
|
|
70
|
+
const worktreePath = this.active.get(taskId);
|
|
71
|
+
if (!worktreePath) return null;
|
|
72
|
+
|
|
73
|
+
const branch = `specd/${taskId}`;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
execFileSync('git', ['push', '-u', 'origin', branch], { cwd: worktreePath, stdio: 'pipe' });
|
|
77
|
+
const prUrl = execFileSync(
|
|
78
|
+
'gh', ['pr', 'create', '--title', taskName, '--body', summary, '--head', branch],
|
|
79
|
+
{ cwd: worktreePath, encoding: 'utf-8' }
|
|
80
|
+
).trim();
|
|
81
|
+
return prUrl;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(`PR creation failed for ${taskId}:`, err.message);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getPath(taskId) {
|
|
89
|
+
return this.active.get(taskId) || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
listActive() {
|
|
93
|
+
return [...this.active.keys()];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@specd/runner",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Specd Runner — Electron desktop app for autonomous agent orchestration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "main/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "electron .",
|
|
9
|
+
"dev": "NODE_ENV=development electron .",
|
|
10
|
+
"build:renderer": "cd renderer && npm run build"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"express": "^4.21.0",
|
|
14
|
+
"ws": "^8.18.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"electron": "^34.0.0"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
const ALLOWED_CHANNELS = new Set([
|
|
4
|
+
'get-projects', 'get-project-status', 'get-tasks',
|
|
5
|
+
'get-task', 'create-task', 'retry-task', 'get-task-logs', 'get-config',
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
contextBridge.exposeInMainWorld('specd', {
|
|
9
|
+
invoke: (channel, ...args) => {
|
|
10
|
+
if (!ALLOWED_CHANNELS.has(channel)) throw new Error(`Blocked IPC channel: ${channel}`);
|
|
11
|
+
return ipcRenderer.invoke(channel, ...args);
|
|
12
|
+
},
|
|
13
|
+
on: (channel, callback) => {
|
|
14
|
+
if (!ALLOWED_CHANNELS.has(channel)) throw new Error(`Blocked IPC channel: ${channel}`);
|
|
15
|
+
const subscription = (_event, ...args) => callback(...args);
|
|
16
|
+
ipcRenderer.on(channel, subscription);
|
|
17
|
+
return () => ipcRenderer.removeListener(channel, subscription);
|
|
18
|
+
},
|
|
19
|
+
});
|
package/specdacular/HELP.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| Command | Description |
|
|
10
10
|
|---------|-------------|
|
|
11
11
|
| `/specd.new [name]` | Initialize a task, start first discussion |
|
|
12
|
+
| `/specd.new-project [name]` | Bootstrap a new project from idea to structured plan |
|
|
12
13
|
| `/specd.continue [name] [--interactive\|--auto]` | Continue task lifecycle — picks up where you left off |
|
|
13
14
|
|
|
14
15
|
| `/specd.toolbox [<name>]` | Advanced task operations |
|
|
@@ -17,8 +18,10 @@
|
|
|
17
18
|
|
|
18
19
|
| Command | Description |
|
|
19
20
|
|---------|-------------|
|
|
20
|
-
| `/specd.
|
|
21
|
-
| `/specd.
|
|
21
|
+
| `/specd.best-practices` | Detect tech stack and generate best practices reference doc |
|
|
22
|
+
| `/specd.docs` | Generate topic-based docs and CLAUDE.md routing table |
|
|
23
|
+
| `/specd.docs.review` | Review and audit docs for accuracy, staleness, and coverage gaps |
|
|
24
|
+
| `/specd.generate-skills.learn` | Generate a /learn skill that captures lessons into project docs |
|
|
22
25
|
| `/specd.config` | Configure auto-commit settings for docs and code |
|
|
23
26
|
| `/specd.status [--all]` | Show task status dashboard |
|
|
24
27
|
| `/specd.help` | Show this help |
|
|
@@ -32,6 +35,11 @@
|
|
|
32
35
|
/specd.new → /specd.continue → continue → continue → done
|
|
33
36
|
```
|
|
34
37
|
|
|
38
|
+
**For new projects (greenfield):**
|
|
39
|
+
```
|
|
40
|
+
/specd.new-project → (questioning → research → requirements → roadmap → scaffold)
|
|
41
|
+
```
|
|
42
|
+
|
|
35
43
|
**You only need three commands:**
|
|
36
44
|
|
|
37
45
|
1. **`/specd.new [name]`** — Start here. Creates task folder, asks initial questions.
|
|
@@ -41,7 +49,7 @@
|
|
|
41
49
|
- Works across context windows — reads state fresh each time
|
|
42
50
|
- Modes: default (auto-runs, pauses at phase steps), `--interactive` (prompt at each step), `--auto` (run everything)
|
|
43
51
|
3. **`/specd.toolbox [name]`** — Direct access to task operations: Discuss, Research, Plan, Execute, Review
|
|
44
|
-
4. **`/specd.
|
|
52
|
+
4. **`/specd.docs.review`** — Review and audit docs for accuracy, staleness, and coverage gaps
|
|
45
53
|
|
|
46
54
|
### Quick Start
|
|
47
55
|
|
|
@@ -119,17 +127,18 @@ Hooks are markdown workflow files that run before and after pipeline steps. They
|
|
|
119
127
|
## Codebase Documentation
|
|
120
128
|
|
|
121
129
|
```
|
|
122
|
-
/specd.
|
|
130
|
+
/specd.docs
|
|
123
131
|
```
|
|
124
132
|
|
|
125
|
-
Spawns 4 parallel agents to analyze your codebase and
|
|
133
|
+
Spawns 4 parallel agents to analyze your codebase, then merges their outputs into topic-specific docs in `docs/` and writes a `CLAUDE.md` routing table:
|
|
134
|
+
|
|
135
|
+
| File | What it contains |
|
|
136
|
+
|------|------------------|
|
|
137
|
+
| **CLAUDE.md** | Routing table — "Working on X? Read docs/Y.md" |
|
|
138
|
+
| **docs/rules.md** | Always-true project rules (imports, naming, conventions) |
|
|
139
|
+
| **docs/{topic}.md** | Topic-specific patterns and guidance (dynamic) |
|
|
126
140
|
|
|
127
|
-
|
|
128
|
-
|----------|------------------|
|
|
129
|
-
| **MAP.md** | Navigation: modules, functions, entry points |
|
|
130
|
-
| **PATTERNS.md** | Code examples: services, errors, testing |
|
|
131
|
-
| **STRUCTURE.md** | Organization: where to put new code |
|
|
132
|
-
| **CONCERNS.md** | Warnings: gotchas, anti-patterns, debt |
|
|
141
|
+
Review existing docs with `/specd.docs.review` — checks accuracy, staleness, and coverage gaps.
|
|
133
142
|
|
|
134
143
|
---
|
|
135
144
|
|