@web42/stask 0.1.7 → 0.2.1
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/.agents/skills/stask-lead/SKILL.md +115 -0
- package/README.md +1 -1
- package/bin/stask.mjs +34 -2
- package/commands/heartbeat-all.mjs +87 -0
- package/commands/init.mjs +148 -0
- package/commands/projects.mjs +103 -0
- package/config.example.json +4 -3
- package/lib/env.mjs +24 -29
- package/lib/resolve-home.mjs +185 -0
- package/lib/tracker-db.mjs +4 -4
- package/package.json +2 -1
- package/skills/stask-lead.md +0 -88
- /package/{skills/stask-general.md → .agents/skills/stask-general/SKILL.md} +0 -0
- /package/{skills/stask-qa.md → .agents/skills/stask-qa/SKILL.md} +0 -0
- /package/{skills/stask-worker.md → .agents/skills/stask-worker/SKILL.md} +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stask-lead
|
|
3
|
+
description: Lead agent workflow — orchestrates the 6-phase spec process, creates subtasks, delegates work, triages PR feedback, coordinates the full task lifecycle from spec approval to PR merge.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lead Agent Workflow
|
|
7
|
+
|
|
8
|
+
You are the **Lead**. You own each task from spec approval through PR merge. You never write code directly — you delegate to Workers and coordinate the lifecycle.
|
|
9
|
+
|
|
10
|
+
## Multi-Project Awareness
|
|
11
|
+
|
|
12
|
+
stask supports multiple projects. Each project lives in a repo with a `.stask/` folder.
|
|
13
|
+
|
|
14
|
+
- **Auto-detection:** If you're inside a project repo (or its worktree), stask auto-detects the project.
|
|
15
|
+
- **Explicit selection:** Use `--project <name>` when working outside the repo or across projects.
|
|
16
|
+
- **Discover projects:** Run `stask projects` to list all registered projects.
|
|
17
|
+
- **Cross-project heartbeat:** Run `stask heartbeat-all <your-name>` to see pending work across all projects.
|
|
18
|
+
|
|
19
|
+
When heartbeat returns tasks with a `project` field, include `--project <name>` in all subsequent stask commands for that task.
|
|
20
|
+
|
|
21
|
+
## The 6-Phase Process
|
|
22
|
+
|
|
23
|
+
You follow a strict 6-phase process. Never skip a phase.
|
|
24
|
+
|
|
25
|
+
### Phase 1: Requirements & Analysis (With Yan Only)
|
|
26
|
+
- Receive Yan's request, identify ambiguities, resolve all unknowns before technical work
|
|
27
|
+
- Run Requirements Clarification + Analysis modes from `technical-spec-design` skill
|
|
28
|
+
|
|
29
|
+
### Phase 2: Technical Exploration (With Team)
|
|
30
|
+
- Spawn Gilfoyle, Dinesh, and Jared as subagents to produce technical deliverables
|
|
31
|
+
- Use structured prompts with Context, What To Do, and Required Deliverables sections
|
|
32
|
+
- Wait for all to return before consolidating
|
|
33
|
+
|
|
34
|
+
### Phase 3: Design & Architecture (Consolidation)
|
|
35
|
+
- Run Design + Architecture modes from `technical-spec-design` skill
|
|
36
|
+
- Consolidate team findings into the final spec with all required sections
|
|
37
|
+
- Save to `../shared/specs/<task-name>.md`
|
|
38
|
+
|
|
39
|
+
### Phase 4: Approval & Delegation
|
|
40
|
+
- Create task via `stask create` (uploads spec to Slack, creates in tracker.db)
|
|
41
|
+
- Wait for Yan to check `spec_approved` or run `stask approve`
|
|
42
|
+
- Create subtasks using the breakdowns from Phase 2
|
|
43
|
+
- Transition to In-Progress (auto-creates worktree + branch)
|
|
44
|
+
|
|
45
|
+
### Phase 5: Implementation (Spawn Workers)
|
|
46
|
+
- Spawn workers with Implementation Prompts referencing their spec section
|
|
47
|
+
- Monitor via `stask heartbeat richard`
|
|
48
|
+
- When all subtasks Done → auto-transitions to Testing
|
|
49
|
+
|
|
50
|
+
### Phase 6: QA → Review → Done
|
|
51
|
+
- Jared tests against ACs
|
|
52
|
+
- If QA FAIL: transition back to In-Progress, create fix subtasks, re-delegate
|
|
53
|
+
- If QA PASS: create PR, transition to "Ready for Human Review"
|
|
54
|
+
- If Yan merges: transition to Done
|
|
55
|
+
|
|
56
|
+
## Commands You Use
|
|
57
|
+
|
|
58
|
+
| Command | When |
|
|
59
|
+
|---------|------|
|
|
60
|
+
| `stask heartbeat <your-name>` | Check what work you have pending |
|
|
61
|
+
| `stask heartbeat-all <your-name>` | Check work across ALL projects |
|
|
62
|
+
| `stask show <task-id>` | View task details, subtasks, and status |
|
|
63
|
+
| `stask subtask create --parent <id> --name "..." --assign <worker>` | Break work into subtasks |
|
|
64
|
+
| `stask transition <task-id> In-Progress` | Start work (auto-creates worktree) |
|
|
65
|
+
| `stask transition <task-id> "Ready for Human Review"` | After QA pass + PR created |
|
|
66
|
+
| `stask transition <task-id> Done` | After Human merges the PR |
|
|
67
|
+
| `stask pr-status <task-id>` | Check PR comments and merge status |
|
|
68
|
+
| `stask assign <task-id> <name>` | Reassign a task |
|
|
69
|
+
| `stask create --spec <path> --name "..." --type Feature` | Create new task with spec |
|
|
70
|
+
| `stask spec-update <task-id> --spec <path>` | Update spec after Yan feedback |
|
|
71
|
+
| `stask projects` | List all registered projects |
|
|
72
|
+
|
|
73
|
+
> **Tip:** Add `--project <name>` to any command when working outside the project repo or across multiple projects.
|
|
74
|
+
|
|
75
|
+
## When You Receive Work
|
|
76
|
+
|
|
77
|
+
### Spec Approved (To-Do, assigned to you)
|
|
78
|
+
1. Read the spec (use the Slack file ID from `stask show`)
|
|
79
|
+
2. Create subtasks: `stask subtask create --parent T-XXX --name "..." --assign <worker>`
|
|
80
|
+
3. Transition: `stask transition T-XXX In-Progress`
|
|
81
|
+
|
|
82
|
+
### QA Passed (Testing, reassigned to you)
|
|
83
|
+
1. Read the spec, QA report, git log, and diff
|
|
84
|
+
2. Create a draft PR: `gh pr create --draft` in the worktree
|
|
85
|
+
3. Write a rich PR description (summary, changes, QA results, screenshots)
|
|
86
|
+
4. Transition: `stask transition T-XXX "Ready for Human Review"`
|
|
87
|
+
|
|
88
|
+
### QA Failed (In-Progress, reassigned to you)
|
|
89
|
+
1. Review the QA report — identify what failed
|
|
90
|
+
2. Create NEW fix subtasks: `stask subtask create --parent T-XXX --name "Fix: ..." --assign <worker>`
|
|
91
|
+
3. Workers fix in the same worktree (same branch, same PR)
|
|
92
|
+
4. When fix subtasks are Done, auto-transitions back to Testing
|
|
93
|
+
|
|
94
|
+
### PR Feedback (Ready for Human Review, detected by heartbeat)
|
|
95
|
+
1. Read the feedback and judge:
|
|
96
|
+
- **Code change needed** (bug, wrong behavior, missing feature):
|
|
97
|
+
- `stask transition T-XXX In-Progress`
|
|
98
|
+
- Create fix subtasks, delegate to Workers
|
|
99
|
+
- After fixes: QA re-tests, you update PR, transition back to RHR
|
|
100
|
+
- **Cosmetic fix** (PR description, naming):
|
|
101
|
+
- Fix directly on GitHub. No state change needed.
|
|
102
|
+
2. The PR stays open. The branch stays the same. All prior data is preserved.
|
|
103
|
+
|
|
104
|
+
### PR Merged (detected by heartbeat)
|
|
105
|
+
- Run `stask transition T-XXX Done`
|
|
106
|
+
|
|
107
|
+
## Key Rules
|
|
108
|
+
|
|
109
|
+
- **Never write code yourself.** Delegate to Workers via subtasks.
|
|
110
|
+
- **Never skip QA.** Even for small fixes, the QA cycle must complete.
|
|
111
|
+
- **The PR is your responsibility.** Write a description that helps the Human review quickly.
|
|
112
|
+
- **External PR comments** (not from Human): Send Human a Slack DM. Do NOT act on them.
|
|
113
|
+
- **Old Done subtasks stay Done** when cycling back to In-Progress. Only create NEW fix subtasks.
|
|
114
|
+
- **Spec before code.** No implementation starts without an approved spec.
|
|
115
|
+
- **Ambiguity first.** Resolve unknowns with Yan before delegating to the team.
|
package/README.md
CHANGED
package/bin/stask.mjs
CHANGED
|
@@ -23,7 +23,30 @@
|
|
|
23
23
|
import fs from 'fs';
|
|
24
24
|
import path from 'path';
|
|
25
25
|
import { fileURLToPath } from 'url';
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
// ─── Extract --project flag before module loading ─────────────────
|
|
28
|
+
// Must happen before env.mjs is imported since it resolves project root at import time.
|
|
29
|
+
{
|
|
30
|
+
const idx = process.argv.indexOf('--project');
|
|
31
|
+
if (idx !== -1 && idx + 1 < process.argv.length) {
|
|
32
|
+
process.env.STASK_PROJECT = process.argv[idx + 1];
|
|
33
|
+
// Remove --project <name> from argv so commands don't see it
|
|
34
|
+
process.argv.splice(idx, 2);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Commands that don't need a project context (work with global registry only)
|
|
39
|
+
const NO_PROJECT_COMMANDS = new Set(['init', 'projects', 'heartbeat-all']);
|
|
40
|
+
|
|
41
|
+
const _cmd = process.argv[2];
|
|
42
|
+
if (NO_PROJECT_COMMANDS.has(_cmd)) {
|
|
43
|
+
// Skip project resolution — these commands handle it themselves
|
|
44
|
+
const mod = await import(`../commands/${_cmd}.mjs`);
|
|
45
|
+
await mod.run(process.argv.slice(3));
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { loadEnv, CONFIG } = await import('../lib/env.mjs');
|
|
27
50
|
|
|
28
51
|
// Auto-load env before anything else
|
|
29
52
|
loadEnv();
|
|
@@ -44,7 +67,6 @@ function ensureSyncDaemon() {
|
|
|
44
67
|
}
|
|
45
68
|
|
|
46
69
|
// Don't auto-start for sync-daemon commands (avoid recursion) or read-only queries
|
|
47
|
-
const _cmd = process.argv[2];
|
|
48
70
|
if (_cmd && _cmd !== 'sync-daemon' && _cmd !== '--help' && _cmd !== '-h') {
|
|
49
71
|
ensureSyncDaemon();
|
|
50
72
|
}
|
|
@@ -68,6 +90,8 @@ const COMMANDS = {
|
|
|
68
90
|
'assign': () => import('../commands/assign.mjs'),
|
|
69
91
|
'sync': () => import('../commands/sync.mjs'),
|
|
70
92
|
'sync-daemon': () => import('../commands/sync-daemon.mjs'),
|
|
93
|
+
// Multi-project commands (also handled as NO_PROJECT_COMMANDS above for init/projects/heartbeat-all)
|
|
94
|
+
'heartbeat-all': () => import('../commands/heartbeat-all.mjs'),
|
|
71
95
|
};
|
|
72
96
|
|
|
73
97
|
const SUBTASK_COMMANDS = {
|
|
@@ -148,6 +172,14 @@ Read-only commands:
|
|
|
148
172
|
Sync commands:
|
|
149
173
|
sync Run one bidirectional sync cycle
|
|
150
174
|
sync-daemon start|stop|status Manage background sync daemon
|
|
175
|
+
|
|
176
|
+
Multi-project commands:
|
|
177
|
+
init <name> --repo <path> Create a new stask project
|
|
178
|
+
projects [show <name>] List/show registered projects
|
|
179
|
+
heartbeat-all <agent-name> Get pending work across all projects
|
|
180
|
+
|
|
181
|
+
Global flags:
|
|
182
|
+
--project <name> Target a specific project (otherwise auto-detected from cwd)
|
|
151
183
|
`);
|
|
152
184
|
}
|
|
153
185
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stask heartbeat-all — Get pending work for an agent across ALL projects.
|
|
3
|
+
*
|
|
4
|
+
* Usage: stask heartbeat-all <agent-name>
|
|
5
|
+
*
|
|
6
|
+
* Iterates over all registered projects, checks if the agent is configured
|
|
7
|
+
* in each project, and runs heartbeat per project. Returns combined JSON.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { execFileSync } from 'child_process';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { loadProjectsRegistry } from '../lib/resolve-home.mjs';
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const STASK_BIN = path.resolve(__dirname, '../bin/stask.mjs');
|
|
18
|
+
|
|
19
|
+
export async function run(args) {
|
|
20
|
+
const agentName = args[0];
|
|
21
|
+
|
|
22
|
+
if (!agentName) {
|
|
23
|
+
console.error('Usage: stask heartbeat-all <agent-name>');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const registry = loadProjectsRegistry();
|
|
28
|
+
const projects = Object.entries(registry.projects || {});
|
|
29
|
+
|
|
30
|
+
if (projects.length === 0) {
|
|
31
|
+
console.log(JSON.stringify({ agent: agentName, pendingTasks: [], projects: [] }, null, 2));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const allPendingTasks = [];
|
|
36
|
+
const projectResults = [];
|
|
37
|
+
|
|
38
|
+
for (const [projectName, projectInfo] of projects) {
|
|
39
|
+
const configPath = path.join(projectInfo.repoPath, '.stask', 'config.json');
|
|
40
|
+
|
|
41
|
+
// Skip projects without config
|
|
42
|
+
if (!fs.existsSync(configPath)) continue;
|
|
43
|
+
|
|
44
|
+
// Check if agent is in this project's config
|
|
45
|
+
try {
|
|
46
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
47
|
+
const agentConfig = config.agents?.[agentName.toLowerCase()];
|
|
48
|
+
if (!agentConfig) continue;
|
|
49
|
+
|
|
50
|
+
// Run heartbeat for this project via subprocess
|
|
51
|
+
const result = execFileSync(
|
|
52
|
+
process.execPath,
|
|
53
|
+
[STASK_BIN, 'heartbeat', agentName, '--project', projectName],
|
|
54
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const heartbeat = JSON.parse(result);
|
|
58
|
+
|
|
59
|
+
// Tag each pending task with the project name
|
|
60
|
+
for (const task of heartbeat.pendingTasks || []) {
|
|
61
|
+
task.project = projectName;
|
|
62
|
+
// Inject --project into prompt so agent uses it in subsequent commands
|
|
63
|
+
if (task.prompt) {
|
|
64
|
+
task.prompt = task.prompt.replace(
|
|
65
|
+
/\bstask\b(?!\s+--project)/g,
|
|
66
|
+
`stask --project ${projectName}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
allPendingTasks.push(task);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
projectResults.push({ project: projectName, role: agentConfig.role, taskCount: (heartbeat.pendingTasks || []).length });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Log to stderr but don't fail — other projects may still work
|
|
75
|
+
console.error(`WARNING: heartbeat failed for project "${projectName}": ${err.message}`);
|
|
76
|
+
projectResults.push({ project: projectName, error: err.message });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = {
|
|
81
|
+
agent: agentName,
|
|
82
|
+
pendingTasks: allPendingTasks,
|
|
83
|
+
projects: projectResults,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
console.log(JSON.stringify(result, null, 2));
|
|
87
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stask init — Create a new stask project.
|
|
3
|
+
*
|
|
4
|
+
* Usage: stask init <project-name> --repo <path> [--worktrees <path>] [--specs <path>]
|
|
5
|
+
*
|
|
6
|
+
* Creates .stask/ in the target repo with scaffolded config.json and .gitignore.
|
|
7
|
+
* Registers the project in ~/.stask/projects.json.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { loadProjectsRegistry, saveProjectsRegistry, GLOBAL_STASK_DIR } from '../lib/resolve-home.mjs';
|
|
13
|
+
|
|
14
|
+
function parseArgs(args) {
|
|
15
|
+
const name = args[0];
|
|
16
|
+
if (!name || name.startsWith('--')) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const opts = { name };
|
|
20
|
+
for (let i = 1; i < args.length; i += 2) {
|
|
21
|
+
const flag = args[i];
|
|
22
|
+
const val = args[i + 1];
|
|
23
|
+
if (!val) break;
|
|
24
|
+
if (flag === '--repo') opts.repo = val;
|
|
25
|
+
else if (flag === '--worktrees') opts.worktrees = val;
|
|
26
|
+
else if (flag === '--specs') opts.specs = val;
|
|
27
|
+
}
|
|
28
|
+
return opts;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const CONFIG_TEMPLATE = (name, specsDir, worktreeBaseDir) => ({
|
|
32
|
+
project: name,
|
|
33
|
+
specsDir,
|
|
34
|
+
projectRepoPath: '.',
|
|
35
|
+
worktreeBaseDir,
|
|
36
|
+
staleSessionMinutes: 30,
|
|
37
|
+
syncIntervalSeconds: 60,
|
|
38
|
+
maxQaRetries: 3,
|
|
39
|
+
|
|
40
|
+
human: {
|
|
41
|
+
name: 'YourName',
|
|
42
|
+
slackUserId: 'UXXXXXXXXXX',
|
|
43
|
+
githubUsername: 'your-github-username',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
agents: {
|
|
47
|
+
'lead-agent': { role: 'lead', slackUserId: 'UXXXXXXXXXX' },
|
|
48
|
+
'worker-1': { role: 'worker', slackUserId: 'UXXXXXXXXXX' },
|
|
49
|
+
'worker-2': { role: 'worker', slackUserId: 'UXXXXXXXXXX' },
|
|
50
|
+
'qa-agent': { role: 'qa', slackUserId: 'UXXXXXXXXXX' },
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
slack: {
|
|
54
|
+
listId: 'YOUR_SLACK_LIST_ID',
|
|
55
|
+
columns: {
|
|
56
|
+
name: 'ColXXXXXXXXX',
|
|
57
|
+
task_id: 'ColXXXXXXXXX',
|
|
58
|
+
status: 'ColXXXXXXXXX',
|
|
59
|
+
assignee: 'ColXX',
|
|
60
|
+
spec: 'ColXXXXXXXXX',
|
|
61
|
+
type: 'ColXXXXXXXXX',
|
|
62
|
+
worktree: 'ColXXXXXXXXX',
|
|
63
|
+
pr: 'ColXXXXXXXXX',
|
|
64
|
+
qa_report_1: 'ColXXXXXXXXX',
|
|
65
|
+
qa_report_2: 'ColXXXXXXXXX',
|
|
66
|
+
qa_report_3: 'ColXXXXXXXXX',
|
|
67
|
+
completed: 'ColXX',
|
|
68
|
+
spec_approved: 'ColXXXXXXXXX',
|
|
69
|
+
pr_status: 'ColXXXXXXXXX',
|
|
70
|
+
},
|
|
71
|
+
statusOptions: {
|
|
72
|
+
'To-Do': 'OptXXXXXXXXX',
|
|
73
|
+
'In-Progress': 'OptXXXXXXXXX',
|
|
74
|
+
'Testing': 'OptXXXXXXXXX',
|
|
75
|
+
'Ready for Human Review': 'OptXXXXXXXXX',
|
|
76
|
+
'Blocked': 'OptXXXXXXXXX',
|
|
77
|
+
'Done': 'OptXXXXXXXXX',
|
|
78
|
+
},
|
|
79
|
+
typeOptions: {
|
|
80
|
+
Feature: 'OptXXXXXXXXX',
|
|
81
|
+
Bug: 'OptXXXXXXXXX',
|
|
82
|
+
Improvement: 'OptXXXXXXXXX',
|
|
83
|
+
Research: 'OptXXXXXXXXX',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const GITIGNORE_CONTENT = `# stask runtime data (do not commit)
|
|
89
|
+
tracker.db
|
|
90
|
+
tracker.db-wal
|
|
91
|
+
tracker.db-shm
|
|
92
|
+
FILE_REGISTRY.json
|
|
93
|
+
logs/
|
|
94
|
+
pr-status/
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
export async function run(args) {
|
|
98
|
+
const opts = parseArgs(args);
|
|
99
|
+
if (!opts || !opts.repo) {
|
|
100
|
+
console.error('Usage: stask init <project-name> --repo <path> [--worktrees <path>] [--specs <path>]');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const repoPath = path.resolve(opts.repo);
|
|
105
|
+
if (!fs.existsSync(repoPath)) {
|
|
106
|
+
console.error(`ERROR: Repo path does not exist: ${repoPath}`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const staskDir = path.join(repoPath, '.stask');
|
|
111
|
+
if (fs.existsSync(path.join(staskDir, 'config.json'))) {
|
|
112
|
+
console.error(`ERROR: Project already initialized at ${staskDir}`);
|
|
113
|
+
console.error('Delete .stask/config.json first if you want to re-initialize.');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const specsDir = opts.specs || './specs';
|
|
118
|
+
const worktreeBaseDir = opts.worktrees || path.join(GLOBAL_STASK_DIR, 'worktrees', opts.name);
|
|
119
|
+
|
|
120
|
+
// Create .stask/ directory
|
|
121
|
+
fs.mkdirSync(staskDir, { recursive: true });
|
|
122
|
+
|
|
123
|
+
// Write config.json
|
|
124
|
+
const config = CONFIG_TEMPLATE(opts.name, specsDir, worktreeBaseDir);
|
|
125
|
+
fs.writeFileSync(
|
|
126
|
+
path.join(staskDir, 'config.json'),
|
|
127
|
+
JSON.stringify(config, null, 2) + '\n'
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Write .gitignore
|
|
131
|
+
fs.writeFileSync(path.join(staskDir, '.gitignore'), GITIGNORE_CONTENT);
|
|
132
|
+
|
|
133
|
+
// Register in global projects.json
|
|
134
|
+
const registry = loadProjectsRegistry();
|
|
135
|
+
registry.projects = registry.projects || {};
|
|
136
|
+
registry.projects[opts.name] = { repoPath };
|
|
137
|
+
saveProjectsRegistry(registry);
|
|
138
|
+
|
|
139
|
+
console.log(`Project "${opts.name}" initialized.`);
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log(` Config: ${path.join(staskDir, 'config.json')}`);
|
|
142
|
+
console.log(` Registry: ${path.join(GLOBAL_STASK_DIR, 'projects.json')}`);
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log('Next steps:');
|
|
145
|
+
console.log(' 1. Edit .stask/config.json — fill in agents, Slack list/column IDs, human info');
|
|
146
|
+
console.log(' 2. Set SLACK_TOKEN env var or add to ~/.stask/config.json');
|
|
147
|
+
console.log(` 3. Run: cd ${repoPath} && stask list`);
|
|
148
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stask projects — List and show registered stask projects.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* stask projects List all registered projects
|
|
6
|
+
* stask projects show <name> Show project details + task count
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import Database from 'better-sqlite3';
|
|
12
|
+
import { loadProjectsRegistry } from '../lib/resolve-home.mjs';
|
|
13
|
+
|
|
14
|
+
export async function run(args) {
|
|
15
|
+
const subCmd = args[0];
|
|
16
|
+
|
|
17
|
+
if (subCmd === 'show') {
|
|
18
|
+
return showProject(args[1]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return listProjects();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function listProjects() {
|
|
25
|
+
const registry = loadProjectsRegistry();
|
|
26
|
+
const entries = Object.entries(registry.projects || {});
|
|
27
|
+
|
|
28
|
+
if (entries.length === 0) {
|
|
29
|
+
console.log('No projects registered.');
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log('Create one with: stask init <name> --repo <path>');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const maxName = Math.max(...entries.map(([n]) => n.length));
|
|
36
|
+
console.log('Registered projects:');
|
|
37
|
+
console.log('');
|
|
38
|
+
for (const [name, info] of entries) {
|
|
39
|
+
const staskDir = path.join(info.repoPath, '.stask');
|
|
40
|
+
const hasConfig = fs.existsSync(path.join(staskDir, 'config.json'));
|
|
41
|
+
const status = hasConfig ? '' : ' (missing .stask/config.json)';
|
|
42
|
+
console.log(` ${name.padEnd(maxName + 2)}${info.repoPath}${status}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function showProject(name) {
|
|
47
|
+
if (!name) {
|
|
48
|
+
console.error('Usage: stask projects show <name>');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const registry = loadProjectsRegistry();
|
|
53
|
+
const project = registry.projects?.[name];
|
|
54
|
+
if (!project) {
|
|
55
|
+
console.error(`ERROR: Unknown project "${name}".`);
|
|
56
|
+
console.error('');
|
|
57
|
+
console.error('Run `stask projects` to see all registered projects.');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const staskDir = path.join(project.repoPath, '.stask');
|
|
62
|
+
const configPath = path.join(staskDir, 'config.json');
|
|
63
|
+
|
|
64
|
+
console.log(`Project: ${name}`);
|
|
65
|
+
console.log(`Repo: ${project.repoPath}`);
|
|
66
|
+
console.log(`Data: ${staskDir}`);
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(configPath)) {
|
|
69
|
+
console.log('Status: NOT CONFIGURED (missing .stask/config.json)');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
75
|
+
|
|
76
|
+
// Show agents
|
|
77
|
+
const agents = Object.entries(config.agents || {});
|
|
78
|
+
if (agents.length > 0) {
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log('Agents:');
|
|
81
|
+
for (const [agentName, info] of agents) {
|
|
82
|
+
console.log(` ${agentName.padEnd(16)}${info.role}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Show task count if DB exists
|
|
87
|
+
const dbPath = path.join(staskDir, 'tracker.db');
|
|
88
|
+
if (fs.existsSync(dbPath)) {
|
|
89
|
+
try {
|
|
90
|
+
const db = new Database(dbPath, { readonly: true });
|
|
91
|
+
const total = db.prepare('SELECT COUNT(*) as c FROM tasks WHERE parent_id IS NULL').get().c;
|
|
92
|
+
const active = db.prepare("SELECT COUNT(*) as c FROM tasks WHERE parent_id IS NULL AND status != 'Done'").get().c;
|
|
93
|
+
db.close();
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log(`Tasks: ${active} active, ${total} total`);
|
|
96
|
+
} catch {
|
|
97
|
+
// DB might not have tables yet
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
console.log('Status: CONFIG ERROR (could not parse config.json)');
|
|
102
|
+
}
|
|
103
|
+
}
|
package/config.example.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"
|
|
2
|
+
"project": "your-project-name",
|
|
3
|
+
"specsDir": "./specs",
|
|
4
|
+
"projectRepoPath": ".",
|
|
5
|
+
"worktreeBaseDir": "~/.stask/worktrees/your-project-name",
|
|
5
6
|
"staleSessionMinutes": 30,
|
|
6
7
|
"syncIntervalSeconds": 60,
|
|
7
8
|
"maxQaRetries": 3,
|
package/lib/env.mjs
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* env.mjs — Loads config.json
|
|
3
|
-
*
|
|
2
|
+
* env.mjs — Loads project config.json, resolves secrets via layered
|
|
3
|
+
* token resolution, and imports bundled libs.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Project root resolution:
|
|
6
|
+
* 1. --project <name> → ~/.stask/projects.json registry lookup
|
|
7
|
+
* 2. Walk up from cwd looking for .stask/config.json
|
|
8
|
+
* 3. Helpful error if no project found
|
|
9
|
+
*
|
|
10
|
+
* Token resolution (SLACK_TOKEN):
|
|
11
|
+
* 1. SLACK_TOKEN env var
|
|
12
|
+
* 2. ~/.stask/config.json → projects.<name>.slackToken
|
|
13
|
+
* 3. ~/.stask/config.json → slackToken (global default)
|
|
7
14
|
*/
|
|
8
15
|
|
|
9
16
|
import fs from 'fs';
|
|
10
17
|
import path from 'path';
|
|
11
|
-
import os from 'os';
|
|
12
18
|
import { fileURLToPath } from 'url';
|
|
19
|
+
import { resolveProjectRoot, resolveProjectName, resolveSlackToken } from './resolve-home.mjs';
|
|
13
20
|
|
|
14
21
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
22
|
|
|
16
|
-
// ─── STASK_HOME —
|
|
23
|
+
// ─── STASK_HOME — resolved project .stask/ directory ──────────────
|
|
17
24
|
|
|
18
|
-
export const STASK_HOME =
|
|
25
|
+
export const STASK_HOME = resolveProjectRoot();
|
|
19
26
|
|
|
20
|
-
// ─── Load config.json from
|
|
27
|
+
// ─── Load config.json from project .stask/ ────────────────────────
|
|
21
28
|
|
|
22
29
|
let _config = null;
|
|
23
30
|
|
|
@@ -27,7 +34,7 @@ export function loadConfig() {
|
|
|
27
34
|
const configPath = path.join(STASK_HOME, 'config.json');
|
|
28
35
|
if (!fs.existsSync(configPath)) {
|
|
29
36
|
console.error(`ERROR: Config not found at ${configPath}`);
|
|
30
|
-
console.error(`Run:
|
|
37
|
+
console.error(`Run: stask init <project-name> --repo <path>`);
|
|
31
38
|
process.exit(1);
|
|
32
39
|
}
|
|
33
40
|
|
|
@@ -38,11 +45,10 @@ export function loadConfig() {
|
|
|
38
45
|
|
|
39
46
|
_config = {
|
|
40
47
|
...raw,
|
|
41
|
-
// Resolved absolute paths —
|
|
48
|
+
// Resolved absolute paths — runtime data lives in .stask/
|
|
42
49
|
staskRoot,
|
|
43
50
|
staskHome: STASK_HOME,
|
|
44
51
|
dbPath: path.join(STASK_HOME, 'tracker.db'),
|
|
45
|
-
envFile: path.join(STASK_HOME, '.env'),
|
|
46
52
|
registryPath: path.join(STASK_HOME, 'FILE_REGISTRY.json'),
|
|
47
53
|
// Backward compat: expose specsDir as workspace too
|
|
48
54
|
workspace: raw.specsDir,
|
|
@@ -50,33 +56,22 @@ export function loadConfig() {
|
|
|
50
56
|
return _config;
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
// ───
|
|
59
|
+
// ─── Load secrets (Slack token) via layered resolution ─────────────
|
|
54
60
|
|
|
55
61
|
let _envLoaded = false;
|
|
56
62
|
|
|
57
63
|
export function loadEnv() {
|
|
58
64
|
if (_envLoaded) return;
|
|
59
65
|
const config = loadConfig();
|
|
60
|
-
|
|
61
|
-
const lines = fs.readFileSync(config.envFile, 'utf-8').split('\n');
|
|
62
|
-
for (const line of lines) {
|
|
63
|
-
const match = line.match(/^([^#=]+)=(.*)$/);
|
|
64
|
-
if (match) {
|
|
65
|
-
const key = match[1].trim();
|
|
66
|
-
const val = match[2].trim();
|
|
67
|
-
if (!process.env[key]) process.env[key] = val;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
} catch (err) {
|
|
71
|
-
console.error(`WARNING: Could not load env file ${config.envFile}: ${err.message}`);
|
|
72
|
-
}
|
|
73
|
-
_envLoaded = true;
|
|
66
|
+
const projectName = resolveProjectName();
|
|
74
67
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
// Resolve Slack token via layered precedence
|
|
69
|
+
const slackToken = resolveSlackToken(projectName);
|
|
70
|
+
if (slackToken && !process.env.SLACK_TOKEN) {
|
|
71
|
+
process.env.SLACK_TOKEN = slackToken;
|
|
78
72
|
}
|
|
79
73
|
|
|
74
|
+
_envLoaded = true;
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
// ─── Local lib imports (all bundled in stask/lib/) ─────────────────
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolve-home.mjs — Resolves the stask project root directory.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. --project <name> or STASK_PROJECT env var → look up ~/.stask/projects.json
|
|
6
|
+
* 2. Walk up from cwd looking for .stask/config.json (like git finds .git/)
|
|
7
|
+
* 3. No project found → helpful error listing registered projects, exit(1)
|
|
8
|
+
*
|
|
9
|
+
* This module has ZERO dependencies on env.mjs or tracker-db.mjs to avoid circular imports.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import os from 'os';
|
|
15
|
+
|
|
16
|
+
const GLOBAL_STASK_DIR = path.join(os.homedir(), '.stask');
|
|
17
|
+
const REGISTRY_PATH = path.join(GLOBAL_STASK_DIR, 'projects.json');
|
|
18
|
+
const CENTRAL_CONFIG_PATH = path.join(GLOBAL_STASK_DIR, 'config.json');
|
|
19
|
+
|
|
20
|
+
// ─── Projects registry ───────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export function loadProjectsRegistry() {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf-8'));
|
|
25
|
+
} catch {
|
|
26
|
+
return { projects: {} };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function saveProjectsRegistry(registry) {
|
|
31
|
+
fs.mkdirSync(GLOBAL_STASK_DIR, { recursive: true });
|
|
32
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Central config (secrets) ─────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export function loadCentralConfig() {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(fs.readFileSync(CENTRAL_CONFIG_PATH, 'utf-8'));
|
|
40
|
+
} catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Resolve SLACK_TOKEN ──────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve Slack token with layered precedence:
|
|
49
|
+
* 1. SLACK_TOKEN env var
|
|
50
|
+
* 2. Central config → projects.<name>.slackToken
|
|
51
|
+
* 3. Central config → slackToken (global default)
|
|
52
|
+
*/
|
|
53
|
+
export function resolveSlackToken(projectName) {
|
|
54
|
+
if (process.env.SLACK_TOKEN) return process.env.SLACK_TOKEN;
|
|
55
|
+
|
|
56
|
+
const central = loadCentralConfig();
|
|
57
|
+
if (projectName && central.projects?.[projectName]?.slackToken) {
|
|
58
|
+
return central.projects[projectName].slackToken;
|
|
59
|
+
}
|
|
60
|
+
if (central.slackToken) return central.slackToken;
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Project name from argv ───────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export function extractProjectFlag(argv) {
|
|
68
|
+
const idx = argv.indexOf('--project');
|
|
69
|
+
if (idx !== -1 && idx + 1 < argv.length) {
|
|
70
|
+
return argv[idx + 1];
|
|
71
|
+
}
|
|
72
|
+
return process.env.STASK_PROJECT || null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Walk up to find .stask/ ──────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function findStaskDir(startDir) {
|
|
78
|
+
let dir = path.resolve(startDir);
|
|
79
|
+
const root = path.parse(dir).root;
|
|
80
|
+
while (dir !== root) {
|
|
81
|
+
const candidate = path.join(dir, '.stask', 'config.json');
|
|
82
|
+
if (fs.existsSync(candidate)) {
|
|
83
|
+
return path.join(dir, '.stask');
|
|
84
|
+
}
|
|
85
|
+
dir = path.dirname(dir);
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Format registered projects for error messages ────────────────
|
|
91
|
+
|
|
92
|
+
function formatProjectList(registry) {
|
|
93
|
+
const entries = Object.entries(registry.projects || {});
|
|
94
|
+
if (entries.length === 0) return ' (none registered)';
|
|
95
|
+
const maxName = Math.max(...entries.map(([n]) => n.length));
|
|
96
|
+
return entries
|
|
97
|
+
.map(([name, info]) => ` ${name.padEnd(maxName + 2)}${info.repoPath}`)
|
|
98
|
+
.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Main resolver ────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
let _resolved = null;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the stask project root (.stask/ directory).
|
|
107
|
+
* Returns an absolute path to the .stask/ folder.
|
|
108
|
+
* Exits with helpful error if no project is found.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveProjectRoot() {
|
|
111
|
+
if (_resolved) return _resolved;
|
|
112
|
+
|
|
113
|
+
const projectName = extractProjectFlag(process.argv);
|
|
114
|
+
|
|
115
|
+
// 1. Explicit --project flag or STASK_PROJECT env var
|
|
116
|
+
if (projectName) {
|
|
117
|
+
const registry = loadProjectsRegistry();
|
|
118
|
+
const project = registry.projects?.[projectName];
|
|
119
|
+
if (!project) {
|
|
120
|
+
console.error(`ERROR: Unknown project "${projectName}".`);
|
|
121
|
+
console.error('');
|
|
122
|
+
console.error('Registered projects:');
|
|
123
|
+
console.error(formatProjectList(registry));
|
|
124
|
+
console.error('');
|
|
125
|
+
console.error('Run `stask projects` to see all projects.');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const staskDir = path.join(project.repoPath, '.stask');
|
|
129
|
+
if (!fs.existsSync(path.join(staskDir, 'config.json'))) {
|
|
130
|
+
console.error(`ERROR: Project "${projectName}" is registered but .stask/config.json is missing at ${staskDir}.`);
|
|
131
|
+
console.error('');
|
|
132
|
+
console.error(`Run \`stask init ${projectName} --repo ${project.repoPath}\` to re-scaffold.`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
_resolved = staskDir;
|
|
136
|
+
return _resolved;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. Walk up from cwd
|
|
140
|
+
const found = findStaskDir(process.cwd());
|
|
141
|
+
if (found) {
|
|
142
|
+
_resolved = found;
|
|
143
|
+
return _resolved;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 3. No project found — helpful error
|
|
147
|
+
const registry = loadProjectsRegistry();
|
|
148
|
+
console.error('ERROR: No stask project found.');
|
|
149
|
+
console.error('');
|
|
150
|
+
console.error('No .stask/ folder found in the current directory or any parent.');
|
|
151
|
+
console.error('');
|
|
152
|
+
|
|
153
|
+
const entries = Object.entries(registry.projects || {});
|
|
154
|
+
if (entries.length > 0) {
|
|
155
|
+
console.error('Your registered projects:');
|
|
156
|
+
console.error(formatProjectList(registry));
|
|
157
|
+
console.error('');
|
|
158
|
+
console.error('To work on a project:');
|
|
159
|
+
console.error(` cd ${entries[0][1].repoPath}${' '.repeat(6)}# auto-detects .stask/`);
|
|
160
|
+
console.error(` stask --project ${entries[0][0]} <command> # explicit project selection`);
|
|
161
|
+
} else {
|
|
162
|
+
console.error('No projects registered yet.');
|
|
163
|
+
}
|
|
164
|
+
console.error('');
|
|
165
|
+
console.error('To create a new project:');
|
|
166
|
+
console.error(' stask init <name> --repo <path>');
|
|
167
|
+
console.error('');
|
|
168
|
+
console.error('Run `stask projects` to see all projects.');
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the project name from the resolved .stask/config.json.
|
|
174
|
+
*/
|
|
175
|
+
export function resolveProjectName() {
|
|
176
|
+
const staskDir = resolveProjectRoot();
|
|
177
|
+
try {
|
|
178
|
+
const config = JSON.parse(fs.readFileSync(path.join(staskDir, 'config.json'), 'utf-8'));
|
|
179
|
+
return config.project || path.basename(path.dirname(staskDir));
|
|
180
|
+
} catch {
|
|
181
|
+
return path.basename(path.dirname(staskDir));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export { GLOBAL_STASK_DIR };
|
package/lib/tracker-db.mjs
CHANGED
|
@@ -11,14 +11,14 @@ import fs from 'fs';
|
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
|
|
14
|
-
// Resolve paths
|
|
15
|
-
import
|
|
14
|
+
// Resolve paths via resolve-home.mjs (no circular dep with env.mjs)
|
|
15
|
+
import { resolveProjectRoot } from './resolve-home.mjs';
|
|
16
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const STASK_HOME =
|
|
17
|
+
const STASK_HOME = resolveProjectRoot();
|
|
18
18
|
const STASK_ROOT = path.resolve(__dirname, '..'); // Package install dir
|
|
19
19
|
const _configRaw = JSON.parse(fs.readFileSync(path.join(STASK_HOME, 'config.json'), 'utf-8'));
|
|
20
20
|
const WORKSPACE_DIR = _configRaw.specsDir;
|
|
21
|
-
const TASKS_DIR = STASK_HOME; // Runtime data lives in
|
|
21
|
+
const TASKS_DIR = STASK_HOME; // Runtime data lives in .stask/
|
|
22
22
|
const DB_PATH = path.join(STASK_HOME, 'tracker.db');
|
|
23
23
|
|
|
24
24
|
// ─── Schema ─────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@web42/stask",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "SQLite-backed task lifecycle CLI with Slack sync for AI agent teams",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"lib/",
|
|
12
12
|
"commands/",
|
|
13
13
|
"skills/",
|
|
14
|
+
".agents/",
|
|
14
15
|
"config.example.json"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
package/skills/stask-lead.md
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: stask-lead
|
|
3
|
-
description: Lead agent workflow — creates subtasks, delegates work, triages PR feedback, coordinates the full task lifecycle from spec approval to PR merge.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Lead Agent Workflow
|
|
7
|
-
|
|
8
|
-
You are the **Lead**. You own each task from spec approval through PR merge. You never write code directly — you delegate to Workers and coordinate the lifecycle.
|
|
9
|
-
|
|
10
|
-
## Your Responsibilities
|
|
11
|
-
|
|
12
|
-
1. **Read the spec** when a task is approved and assigned to you
|
|
13
|
-
2. **Create subtasks** breaking the spec into implementable units
|
|
14
|
-
3. **Delegate** each subtask to a Worker agent
|
|
15
|
-
4. **Transition to In-Progress** (auto-creates worktree)
|
|
16
|
-
5. **Triage QA failures** — review reports, create fix subtasks, re-delegate
|
|
17
|
-
6. **Create the PR** after QA passes — write a rich description with summary, changes, QA results
|
|
18
|
-
7. **Triage PR feedback** — decide if code change or cosmetic fix
|
|
19
|
-
8. **Transition to Done** when the Human merges the PR
|
|
20
|
-
|
|
21
|
-
## Commands You Use
|
|
22
|
-
|
|
23
|
-
| Command | When |
|
|
24
|
-
|---------|------|
|
|
25
|
-
| `npx @web42/stask heartbeat <your-name>` | Check what work you have pending |
|
|
26
|
-
| `npx @web42/stask show <task-id>` | View task details, subtasks, and status |
|
|
27
|
-
| `npx @web42/stask subtask create --parent <id> --name "..." --assign <worker>` | Break work into subtasks |
|
|
28
|
-
| `npx @web42/stask transition <task-id> In-Progress` | Start work (auto-creates worktree) |
|
|
29
|
-
| `npx @web42/stask transition <task-id> "Ready for Human Review"` | After QA pass + PR created |
|
|
30
|
-
| `npx @web42/stask transition <task-id> Done` | After Human merges the PR |
|
|
31
|
-
| `npx @web42/stask pr-status <task-id>` | Check PR comments and merge status |
|
|
32
|
-
| `npx @web42/stask assign <task-id> <name>` | Reassign a task |
|
|
33
|
-
|
|
34
|
-
## When You Receive Work
|
|
35
|
-
|
|
36
|
-
### Spec Approved (To-Do, assigned to you)
|
|
37
|
-
1. Read the spec (use the Slack file ID from `npx @web42/stask show`)
|
|
38
|
-
2. Create subtasks: `npx @web42/stask subtask create --parent T-XXX --name "..." --assign <worker>`
|
|
39
|
-
3. Transition: `npx @web42/stask transition T-XXX In-Progress`
|
|
40
|
-
|
|
41
|
-
### QA Passed (Testing, reassigned to you)
|
|
42
|
-
1. Read the spec, QA report, git log, and diff
|
|
43
|
-
2. Create a draft PR: `gh pr create --draft` in the worktree
|
|
44
|
-
3. Write a rich PR description (summary, changes, QA results, screenshots)
|
|
45
|
-
4. Transition: `npx @web42/stask transition T-XXX "Ready for Human Review"`
|
|
46
|
-
|
|
47
|
-
### QA Failed (In-Progress, reassigned to you)
|
|
48
|
-
1. Review the QA report — identify what failed
|
|
49
|
-
2. Create NEW fix subtasks: `npx @web42/stask subtask create --parent T-XXX --name "Fix: ..." --assign <worker>`
|
|
50
|
-
3. Workers fix in the same worktree (same branch, same PR)
|
|
51
|
-
4. When fix subtasks are Done, auto-transitions back to Testing
|
|
52
|
-
|
|
53
|
-
### PR Feedback (Ready for Human Review, detected by heartbeat)
|
|
54
|
-
1. Read the feedback and judge:
|
|
55
|
-
- **Code change needed** (bug, wrong behavior, missing feature):
|
|
56
|
-
- `npx @web42/stask transition T-XXX In-Progress`
|
|
57
|
-
- Create fix subtasks, delegate to Workers
|
|
58
|
-
- After fixes: QA re-tests, you update PR, transition back to RHR
|
|
59
|
-
- **Cosmetic fix** (PR description, naming):
|
|
60
|
-
- Fix directly on GitHub. No state change needed.
|
|
61
|
-
2. The PR stays open. The branch stays the same. All prior data is preserved.
|
|
62
|
-
|
|
63
|
-
### PR Merged (detected by heartbeat)
|
|
64
|
-
- Run `npx @web42/stask transition T-XXX Done`
|
|
65
|
-
|
|
66
|
-
## Thread Communication
|
|
67
|
-
|
|
68
|
-
**Post to the task thread at every step.** The thread reference is in your heartbeat output (`thread.channelId` + `thread.threadTs`). Use `chat.postMessage` with `thread_ts` to reply.
|
|
69
|
-
|
|
70
|
-
You must post when you:
|
|
71
|
-
- Receive a task and start planning subtasks
|
|
72
|
-
- Create each subtask (who it's assigned to, what it does)
|
|
73
|
-
- Transition the task to any new status
|
|
74
|
-
- Review a QA failure and plan fixes
|
|
75
|
-
- Create the PR (include the PR link)
|
|
76
|
-
- Address PR feedback
|
|
77
|
-
- Encounter any blocker or unexpected issue
|
|
78
|
-
|
|
79
|
-
Example: "Creating 3 subtasks for T-006: (1) Build invite API → Gilfoyle, (2) Build invite UI → Dinesh, (3) Add email notifications → Gilfoyle. Transitioning to In-Progress."
|
|
80
|
-
|
|
81
|
-
## Key Rules
|
|
82
|
-
|
|
83
|
-
- **Never write code yourself.** Delegate to Workers via subtasks.
|
|
84
|
-
- **Never skip QA.** Even for small fixes, the QA cycle must complete.
|
|
85
|
-
- **The PR is your responsibility.** Write a description that helps the Human review quickly.
|
|
86
|
-
- **External PR comments** (not from Human): Send Human a Slack DM. Do NOT act on them.
|
|
87
|
-
- **Old Done subtasks stay Done** when cycling back to In-Progress. Only create NEW fix subtasks.
|
|
88
|
-
- **Post every action to the task thread.** The thread is how the team stays informed.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|