spets 0.1.3 → 0.1.5
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 +59 -15
- package/dist/{chunk-XYU22TND.js → chunk-YK5ZZE4P.js} +2 -0
- package/dist/index.js +1135 -171
- package/dist/{state-H2GQS43T.js → state-JLYPJWUT.js} +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
generateTaskId,
|
|
4
4
|
getGitHubConfig,
|
|
5
5
|
getOutputPath,
|
|
6
|
+
getOutputsDir,
|
|
6
7
|
getSpetsDir,
|
|
8
|
+
getStepsDir,
|
|
7
9
|
getWorkflowState,
|
|
8
10
|
listTasks,
|
|
9
11
|
loadConfig,
|
|
@@ -14,7 +16,7 @@ import {
|
|
|
14
16
|
saveTaskMetadata,
|
|
15
17
|
spetsExists,
|
|
16
18
|
updateDocumentStatus
|
|
17
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-YK5ZZE4P.js";
|
|
18
20
|
|
|
19
21
|
// src/index.ts
|
|
20
22
|
import { Command } from "commander";
|
|
@@ -23,7 +25,24 @@ import { Command } from "commander";
|
|
|
23
25
|
import { mkdirSync, writeFileSync } from "fs";
|
|
24
26
|
import { join, dirname } from "path";
|
|
25
27
|
import { fileURLToPath } from "url";
|
|
28
|
+
import { execSync } from "child_process";
|
|
26
29
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
function getGitHubInfoFromRemote() {
|
|
31
|
+
try {
|
|
32
|
+
const remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
|
|
33
|
+
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
|
|
34
|
+
if (sshMatch) {
|
|
35
|
+
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
36
|
+
}
|
|
37
|
+
const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/);
|
|
38
|
+
if (httpsMatch) {
|
|
39
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
27
46
|
async function initCommand(options) {
|
|
28
47
|
const cwd = process.cwd();
|
|
29
48
|
const spetsDir = getSpetsDir(cwd);
|
|
@@ -36,7 +55,8 @@ async function initCommand(options) {
|
|
|
36
55
|
mkdirSync(join(spetsDir, "outputs"), { recursive: true });
|
|
37
56
|
mkdirSync(join(spetsDir, "hooks"), { recursive: true });
|
|
38
57
|
const templatesDir = join(__dirname, "..", "templates");
|
|
39
|
-
|
|
58
|
+
const githubInfo = getGitHubInfoFromRemote();
|
|
59
|
+
writeFileSync(join(spetsDir, "config.yml"), getDefaultConfig(githubInfo));
|
|
40
60
|
createDefaultSteps(spetsDir);
|
|
41
61
|
createClaudeCommand(cwd);
|
|
42
62
|
console.log("Initialized spets in .spets/");
|
|
@@ -48,27 +68,39 @@ async function initCommand(options) {
|
|
|
48
68
|
console.log(" .claude/commands/spets.md - Claude Code command");
|
|
49
69
|
if (options.github) {
|
|
50
70
|
createGitHubWorkflow(cwd);
|
|
51
|
-
console.log(" .github/workflows/spets.yml
|
|
71
|
+
console.log(" .github/workflows/spets.yml - GitHub Actions workflow");
|
|
72
|
+
console.log(" .github/ISSUE_TEMPLATE/spets-task.yml - Issue template");
|
|
52
73
|
}
|
|
53
74
|
console.log("");
|
|
54
75
|
console.log("Next steps:");
|
|
55
76
|
console.log(" 1. Edit .spets/config.yml to customize your workflow");
|
|
56
77
|
console.log(" 2. Customize step instructions in .spets/steps/");
|
|
57
78
|
if (options.github) {
|
|
58
|
-
console.log(" 3. Add
|
|
59
|
-
console.log(' 4.
|
|
79
|
+
console.log(" 3. Add CLAUDE_CODE_OAUTH_TOKEN to your repo secrets");
|
|
80
|
+
console.log(' 4. Create a new Issue using the "Spets Task" template');
|
|
60
81
|
} else {
|
|
61
82
|
console.log(' 3. Run: spets start "your task description"');
|
|
62
83
|
}
|
|
63
84
|
}
|
|
64
|
-
function getDefaultConfig() {
|
|
85
|
+
function getDefaultConfig(githubInfo) {
|
|
86
|
+
const githubSection = githubInfo ? `
|
|
87
|
+
# GitHub integration (auto-detected from git remote)
|
|
88
|
+
github:
|
|
89
|
+
owner: ${githubInfo.owner}
|
|
90
|
+
repo: ${githubInfo.repo}
|
|
91
|
+
` : `
|
|
92
|
+
# GitHub integration (uncomment and fill in to enable)
|
|
93
|
+
# github:
|
|
94
|
+
# owner: your-org
|
|
95
|
+
# repo: your-repo
|
|
96
|
+
`;
|
|
65
97
|
return `# Spets Configuration
|
|
66
98
|
# Define your workflow steps here
|
|
67
99
|
|
|
68
100
|
steps:
|
|
69
101
|
- 01-plan
|
|
70
102
|
- 02-implement
|
|
71
|
-
|
|
103
|
+
${githubSection}
|
|
72
104
|
# Optional hooks (shell scripts)
|
|
73
105
|
# hooks:
|
|
74
106
|
# preStep: "./hooks/pre-step.sh"
|
|
@@ -247,82 +279,179 @@ function createClaudeCommand(cwd) {
|
|
|
247
279
|
function getClaudeCommand() {
|
|
248
280
|
return `# Spets - Spec Driven Development
|
|
249
281
|
|
|
250
|
-
|
|
282
|
+
Spec-Driven Development workflow execution skill for Claude Code.
|
|
283
|
+
|
|
284
|
+
---
|
|
251
285
|
|
|
252
|
-
##
|
|
286
|
+
## CRITICAL: Execution Model
|
|
253
287
|
|
|
288
|
+
This skill runs **deterministically** within the current Claude Code session.
|
|
289
|
+
- NO subprocess spawning for AI/LLM calls
|
|
290
|
+
- The orchestrator manages state via JSON protocol
|
|
291
|
+
- Each step follows a strict state machine flow
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Orchestrator Commands
|
|
296
|
+
|
|
297
|
+
All workflow state is managed by the orchestrator CLI:
|
|
298
|
+
|
|
299
|
+
\`\`\`bash
|
|
300
|
+
npx spets orchestrate init "<description>" # Start new workflow
|
|
301
|
+
npx spets orchestrate done <taskId> # Mark step complete
|
|
302
|
+
npx spets orchestrate clarified <taskId> '<json>' # Submit answers
|
|
303
|
+
npx spets orchestrate approve <taskId> # Approve and continue
|
|
304
|
+
npx spets orchestrate revise <taskId> "<feedback>" # Request revision
|
|
305
|
+
npx spets orchestrate reject <taskId> # Stop workflow
|
|
306
|
+
npx spets orchestrate stop <taskId> # Pause (can resume)
|
|
307
|
+
npx spets orchestrate status <taskId> # Get current status
|
|
254
308
|
\`\`\`
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Execution Flow
|
|
313
|
+
|
|
314
|
+
### 1. Workflow Start (FIRST ACTION - NO EXCEPTIONS)
|
|
315
|
+
|
|
316
|
+
When skill is loaded, **immediately** run:
|
|
317
|
+
|
|
318
|
+
\`\`\`bash
|
|
319
|
+
npx spets orchestrate init "{user_request}"
|
|
258
320
|
\`\`\`
|
|
259
321
|
|
|
260
|
-
|
|
322
|
+
### 2. Document Generation (type: step)
|
|
323
|
+
|
|
324
|
+
When you receive \`type: "step"\`:
|
|
325
|
+
|
|
326
|
+
1. **Read the instruction file** from context.instruction
|
|
327
|
+
2. **Read the template file** from context.template (if exists)
|
|
328
|
+
3. **Read previous output** from context.previousOutput (if exists)
|
|
329
|
+
4. **Generate the document** following the instruction and template
|
|
330
|
+
5. **Write the document** to context.output
|
|
331
|
+
6. **Mark step as done**: \`npx spets orchestrate done {taskId}\`
|
|
332
|
+
|
|
333
|
+
### 3. Question Resolution (type: checkpoint, checkpoint: clarify)
|
|
334
|
+
|
|
335
|
+
1. **Use AskUserQuestion** to ask the questions
|
|
336
|
+
2. **Collect answers** into JSON format
|
|
337
|
+
3. **Submit answers**: \`npx spets orchestrate clarified {taskId} '<answers_json>'\`
|
|
338
|
+
|
|
339
|
+
### 4. Approval Request (type: checkpoint, checkpoint: approve)
|
|
340
|
+
|
|
341
|
+
1. **Display document summary** from specPath
|
|
342
|
+
2. **Use AskUserQuestion** with options: Approve, Revise, Reject, Stop
|
|
343
|
+
3. **Execute the selected action**
|
|
344
|
+
|
|
345
|
+
### 5. Workflow Complete (type: complete)
|
|
261
346
|
|
|
262
|
-
|
|
347
|
+
Display completion message based on status (completed, stopped, rejected)
|
|
263
348
|
|
|
264
|
-
|
|
265
|
-
2. Execute the appropriate spets CLI command using Bash
|
|
266
|
-
3. For 'start', read the generated documents and help iterate
|
|
349
|
+
---
|
|
267
350
|
|
|
268
|
-
|
|
351
|
+
## Response Type Actions Summary
|
|
269
352
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
353
|
+
| Type | Checkpoint | Action |
|
|
354
|
+
|------|------------|--------|
|
|
355
|
+
| step | - | Generate document, write to output, call done |
|
|
356
|
+
| checkpoint | clarify | Ask questions, call clarified with answers |
|
|
357
|
+
| checkpoint | approve | Show doc, ask for decision, call appropriate command |
|
|
358
|
+
| complete | - | Show final message, end skill |
|
|
359
|
+
| error | - | Show error, end skill |
|
|
274
360
|
|
|
275
|
-
|
|
361
|
+
---
|
|
276
362
|
|
|
277
|
-
|
|
278
|
-
2. Present the current workflow state
|
|
363
|
+
## Critical Implementation Rules
|
|
279
364
|
|
|
280
|
-
|
|
365
|
+
1. **ORCHESTRATOR FIRST** - Always call orchestrator before any action
|
|
366
|
+
2. **ONE STEP AT A TIME** - Complete one step, get approval, then next
|
|
367
|
+
3. **DETERMINISTIC FLOW** - Follow the exact state machine transitions
|
|
368
|
+
4. **NO PROCESS SPAWN** - Generate documents in current session
|
|
369
|
+
5. **WAIT FOR APPROVAL** - Never proceed without user approval
|
|
281
370
|
|
|
282
|
-
|
|
283
|
-
2. Continue the workflow from where it paused
|
|
371
|
+
---
|
|
284
372
|
|
|
285
373
|
## Example Session
|
|
286
374
|
|
|
287
|
-
|
|
375
|
+
\`\`\`
|
|
376
|
+
User: /spets Add user authentication
|
|
377
|
+
|
|
378
|
+
[Skill calls: npx spets orchestrate init "Add user authentication"]
|
|
379
|
+
[Receives: type=step, step=01-plan]
|
|
380
|
+
|
|
381
|
+
[Skill reads instruction.md and template.md]
|
|
382
|
+
[Skill generates plan document]
|
|
383
|
+
[Skill writes to .spets/outputs/.../01-plan.md]
|
|
384
|
+
[Skill calls: npx spets orchestrate done mku3abc-xyz]
|
|
385
|
+
|
|
386
|
+
[Receives: type=checkpoint, checkpoint=approve]
|
|
387
|
+
[Skill shows document summary]
|
|
388
|
+
[Skill asks user: Approve/Revise/Reject/Stop?]
|
|
288
389
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
390
|
+
User: Approve
|
|
391
|
+
|
|
392
|
+
[Skill calls: npx spets orchestrate approve mku3abc-xyz]
|
|
393
|
+
[Receives: type=step, step=02-implement]
|
|
394
|
+
|
|
395
|
+
[... continues with implementation step ...]
|
|
396
|
+
\`\`\`
|
|
295
397
|
|
|
296
398
|
$ARGUMENTS
|
|
297
|
-
|
|
298
|
-
args: Additional arguments for the command
|
|
399
|
+
description: The task description for the workflow
|
|
299
400
|
`;
|
|
300
401
|
}
|
|
301
402
|
function createGitHubWorkflow(cwd) {
|
|
302
403
|
const workflowDir = join(cwd, ".github", "workflows");
|
|
404
|
+
const templateDir = join(cwd, ".github", "ISSUE_TEMPLATE");
|
|
303
405
|
mkdirSync(workflowDir, { recursive: true });
|
|
406
|
+
mkdirSync(templateDir, { recursive: true });
|
|
304
407
|
writeFileSync(join(workflowDir, "spets.yml"), getGitHubWorkflow());
|
|
408
|
+
writeFileSync(join(templateDir, "spets-task.yml"), getIssueTemplate());
|
|
409
|
+
}
|
|
410
|
+
function getIssueTemplate() {
|
|
411
|
+
return `name: Spets Task
|
|
412
|
+
description: Start a Spets workflow
|
|
413
|
+
labels: ["spets"]
|
|
414
|
+
body:
|
|
415
|
+
- type: input
|
|
416
|
+
id: task
|
|
417
|
+
attributes:
|
|
418
|
+
label: Task Description
|
|
419
|
+
description: What do you want to accomplish?
|
|
420
|
+
placeholder: "Add user authentication"
|
|
421
|
+
validations:
|
|
422
|
+
required: true
|
|
423
|
+
- type: input
|
|
424
|
+
id: branch
|
|
425
|
+
attributes:
|
|
426
|
+
label: Branch Name
|
|
427
|
+
description: Leave empty to auto-generate (spets/<issue-number>)
|
|
428
|
+
placeholder: "feature/my-feature"
|
|
429
|
+
validations:
|
|
430
|
+
required: false
|
|
431
|
+
`;
|
|
305
432
|
}
|
|
306
433
|
function getGitHubWorkflow() {
|
|
307
434
|
const gh = (expr) => `\${{ ${expr} }}`;
|
|
308
435
|
return `# Spets GitHub Action
|
|
309
|
-
#
|
|
436
|
+
# Handles workflow start from Issue creation and commands from comments
|
|
310
437
|
|
|
311
438
|
name: Spets Workflow
|
|
312
439
|
|
|
313
440
|
on:
|
|
441
|
+
issues:
|
|
442
|
+
types: [opened]
|
|
314
443
|
issue_comment:
|
|
315
444
|
types: [created]
|
|
316
445
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
contains(github.event.comment.body, '/approve') ||
|
|
322
|
-
contains(github.event.comment.body, '/revise') ||
|
|
323
|
-
contains(github.event.comment.body, '/reject') ||
|
|
324
|
-
contains(github.event.comment.body, '/answer')
|
|
446
|
+
permissions:
|
|
447
|
+
contents: write
|
|
448
|
+
issues: write
|
|
449
|
+
pull-requests: write
|
|
325
450
|
|
|
451
|
+
jobs:
|
|
452
|
+
# Start workflow when a spets Issue is created
|
|
453
|
+
start-workflow:
|
|
454
|
+
if: github.event.action == 'opened' && contains(github.event.issue.labels.*.name, 'spets')
|
|
326
455
|
runs-on: ubuntu-latest
|
|
327
456
|
|
|
328
457
|
steps:
|
|
@@ -331,6 +460,30 @@ jobs:
|
|
|
331
460
|
with:
|
|
332
461
|
fetch-depth: 0
|
|
333
462
|
|
|
463
|
+
- name: Parse Issue body
|
|
464
|
+
id: parse
|
|
465
|
+
env:
|
|
466
|
+
ISSUE_BODY: ${gh("github.event.issue.body")}
|
|
467
|
+
ISSUE_NUMBER: ${gh("github.event.issue.number")}
|
|
468
|
+
run: |
|
|
469
|
+
# Parse task description
|
|
470
|
+
TASK=$(echo "$ISSUE_BODY" | sed -n '/### Task Description/,/###/{/###/!p;}' | sed '/^$/d' | head -1)
|
|
471
|
+
echo "task=$TASK" >> $GITHUB_OUTPUT
|
|
472
|
+
|
|
473
|
+
# Parse branch name (optional)
|
|
474
|
+
BRANCH=$(echo "$ISSUE_BODY" | sed -n '/### Branch Name/,/###/{/###/!p;}' | sed '/^$/d' | head -1)
|
|
475
|
+
if [ -z "$BRANCH" ]; then
|
|
476
|
+
BRANCH="spets/$ISSUE_NUMBER"
|
|
477
|
+
fi
|
|
478
|
+
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
|
479
|
+
|
|
480
|
+
- name: Create and checkout branch
|
|
481
|
+
run: |
|
|
482
|
+
git checkout -b ${gh("steps.parse.outputs.branch")}
|
|
483
|
+
git push -u origin ${gh("steps.parse.outputs.branch")}
|
|
484
|
+
env:
|
|
485
|
+
GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
|
|
486
|
+
|
|
334
487
|
- name: Setup Node.js
|
|
335
488
|
uses: actions/setup-node@v4
|
|
336
489
|
with:
|
|
@@ -342,44 +495,70 @@ jobs:
|
|
|
342
495
|
- name: Install dependencies
|
|
343
496
|
run: npm ci
|
|
344
497
|
|
|
345
|
-
- name:
|
|
346
|
-
id: context
|
|
498
|
+
- name: Start Spets workflow
|
|
347
499
|
run: |
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
500
|
+
npx spets start "$TASK" --github --issue ${gh("github.event.issue.number")}
|
|
501
|
+
env:
|
|
502
|
+
TASK: ${gh("steps.parse.outputs.task")}
|
|
503
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${gh("secrets.CLAUDE_CODE_OAUTH_TOKEN")}
|
|
504
|
+
GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
|
|
505
|
+
|
|
506
|
+
- name: Push changes
|
|
507
|
+
run: |
|
|
508
|
+
git config user.name "github-actions[bot]"
|
|
509
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
510
|
+
git add -A
|
|
511
|
+
git diff --staged --quiet || git commit -m "Spets: Start workflow for #${gh("github.event.issue.number")}"
|
|
512
|
+
git push
|
|
513
|
+
env:
|
|
514
|
+
GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
|
|
515
|
+
|
|
516
|
+
# Handle commands from Issue/PR comments
|
|
517
|
+
handle-command:
|
|
518
|
+
if: |
|
|
519
|
+
github.event.action == 'created' && (
|
|
520
|
+
contains(github.event.comment.body, '/approve') ||
|
|
521
|
+
contains(github.event.comment.body, '/revise') ||
|
|
522
|
+
contains(github.event.comment.body, '/reject')
|
|
523
|
+
)
|
|
524
|
+
runs-on: ubuntu-latest
|
|
525
|
+
|
|
526
|
+
steps:
|
|
527
|
+
- name: Find linked branch
|
|
528
|
+
id: branch
|
|
529
|
+
run: |
|
|
530
|
+
# Try to find branch linked to this issue
|
|
531
|
+
BRANCH=$(gh api repos/${gh("github.repository")}/issues/${gh("github.event.issue.number")} --jq '.body' | grep -oP 'spets/\\d+' || echo "")
|
|
532
|
+
if [ -z "$BRANCH" ]; then
|
|
533
|
+
BRANCH="spets/${gh("github.event.issue.number")}"
|
|
352
534
|
fi
|
|
353
|
-
echo "
|
|
535
|
+
echo "name=$BRANCH" >> $GITHUB_OUTPUT
|
|
536
|
+
env:
|
|
537
|
+
GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
|
|
538
|
+
|
|
539
|
+
- name: Checkout
|
|
540
|
+
uses: actions/checkout@v4
|
|
541
|
+
with:
|
|
542
|
+
ref: ${gh("steps.branch.outputs.name")}
|
|
543
|
+
fetch-depth: 0
|
|
544
|
+
|
|
545
|
+
- name: Setup Node.js
|
|
546
|
+
uses: actions/setup-node@v4
|
|
547
|
+
with:
|
|
548
|
+
node-version: '20'
|
|
549
|
+
|
|
550
|
+
- name: Install Claude Code
|
|
551
|
+
run: npm install -g @anthropic-ai/claude-code
|
|
354
552
|
|
|
355
|
-
- name:
|
|
553
|
+
- name: Install dependencies
|
|
554
|
+
run: npm ci
|
|
555
|
+
|
|
556
|
+
- name: Run Spets command
|
|
356
557
|
run: |
|
|
357
|
-
|
|
358
|
-
You are running a Spets workflow via GitHub Actions.
|
|
359
|
-
|
|
360
|
-
Context:
|
|
361
|
-
- Repository: ${gh("github.repository")}
|
|
362
|
-
- Issue/PR: #${gh("github.event.issue.number")}
|
|
363
|
-
- Comment author: ${gh("github.event.comment.user.login")}
|
|
364
|
-
- Command received: $COMMENT_BODY
|
|
365
|
-
|
|
366
|
-
Instructions:
|
|
367
|
-
1. Parse the command from the comment (/approve, /revise, /reject, or /answer)
|
|
368
|
-
2. Find the active spets task in .spets/outputs/
|
|
369
|
-
3. Execute the appropriate action:
|
|
370
|
-
- /approve: Mark current step as approved, generate next step
|
|
371
|
-
- /revise <feedback>: Regenerate current step with feedback
|
|
372
|
-
- /reject: Mark workflow as rejected
|
|
373
|
-
- /answer: Process answers and continue
|
|
374
|
-
4. If generating content (plan or implementation), actually write the code/changes
|
|
375
|
-
5. Post a summary comment to the PR/Issue using gh cli
|
|
376
|
-
6. Commit any changes with a descriptive message
|
|
377
|
-
|
|
378
|
-
Use 'gh issue comment' or 'gh pr comment' to post updates.
|
|
379
|
-
"
|
|
558
|
+
npx spets github --issue ${gh("github.event.issue.number")} --comment "$COMMENT"
|
|
380
559
|
env:
|
|
381
|
-
|
|
382
|
-
|
|
560
|
+
COMMENT: ${gh("github.event.comment.body")}
|
|
561
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${gh("secrets.CLAUDE_CODE_OAUTH_TOKEN")}
|
|
383
562
|
GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
|
|
384
563
|
|
|
385
564
|
- name: Push changes
|
|
@@ -387,7 +566,7 @@ jobs:
|
|
|
387
566
|
git config user.name "github-actions[bot]"
|
|
388
567
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
389
568
|
git add -A
|
|
390
|
-
git diff --staged --quiet || git commit -m "Spets: Update from
|
|
569
|
+
git diff --staged --quiet || git commit -m "Spets: Update from #${gh("github.event.issue.number")}"
|
|
391
570
|
git push
|
|
392
571
|
env:
|
|
393
572
|
GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
|
|
@@ -523,7 +702,7 @@ var Executor = class {
|
|
|
523
702
|
this.config = options.config;
|
|
524
703
|
this.cwd = options.cwd || process.cwd();
|
|
525
704
|
}
|
|
526
|
-
async executeWorkflow(taskId, userQuery, startIndex = 0) {
|
|
705
|
+
async executeWorkflow(taskId, userQuery, startIndex = 0, feedback) {
|
|
527
706
|
let previousOutput;
|
|
528
707
|
if (startIndex > 0) {
|
|
529
708
|
const prevStepName = this.config.steps[startIndex - 1];
|
|
@@ -543,7 +722,9 @@ var Executor = class {
|
|
|
543
722
|
instruction: stepDef.instruction,
|
|
544
723
|
template: stepDef.template,
|
|
545
724
|
previousOutput,
|
|
546
|
-
outputPath: getOutputPath(taskId, stepName, this.cwd)
|
|
725
|
+
outputPath: getOutputPath(taskId, stepName, this.cwd),
|
|
726
|
+
feedback: i === startIndex ? feedback : void 0
|
|
727
|
+
// Only apply feedback to the target step
|
|
547
728
|
};
|
|
548
729
|
if (this.config.hooks?.preStep) {
|
|
549
730
|
await runHook(this.config.hooks.preStep, {
|
|
@@ -653,7 +834,7 @@ var Executor = class {
|
|
|
653
834
|
};
|
|
654
835
|
|
|
655
836
|
// src/platform/cli.ts
|
|
656
|
-
import { spawn as spawn2, execSync } from "child_process";
|
|
837
|
+
import { spawn as spawn2, execSync as execSync2 } from "child_process";
|
|
657
838
|
import { input, select, confirm } from "@inquirer/prompts";
|
|
658
839
|
|
|
659
840
|
// src/platform/interface.ts
|
|
@@ -805,7 +986,7 @@ var CliPlatform = class extends BasePlatform {
|
|
|
805
986
|
isClaudeInstalled() {
|
|
806
987
|
try {
|
|
807
988
|
const command = process.platform === "win32" ? "where" : "which";
|
|
808
|
-
|
|
989
|
+
execSync2(`${command} claude`, { stdio: "ignore" });
|
|
809
990
|
return true;
|
|
810
991
|
} catch {
|
|
811
992
|
return false;
|
|
@@ -856,6 +1037,12 @@ function parseGitHubCommand(comment) {
|
|
|
856
1037
|
if (trimmed === "/approve") {
|
|
857
1038
|
return { command: "approve" };
|
|
858
1039
|
}
|
|
1040
|
+
if (trimmed === "/approve --pr") {
|
|
1041
|
+
return { command: "approve", createPR: true };
|
|
1042
|
+
}
|
|
1043
|
+
if (trimmed === "/approve --issue") {
|
|
1044
|
+
return { command: "approve", createIssue: true };
|
|
1045
|
+
}
|
|
859
1046
|
if (trimmed === "/reject") {
|
|
860
1047
|
return { command: "reject" };
|
|
861
1048
|
}
|
|
@@ -881,6 +1068,7 @@ function parseGitHubCommand(comment) {
|
|
|
881
1068
|
var GitHubPlatform = class extends BasePlatform {
|
|
882
1069
|
config;
|
|
883
1070
|
currentTaskId;
|
|
1071
|
+
currentOutputPath;
|
|
884
1072
|
constructor(config) {
|
|
885
1073
|
super();
|
|
886
1074
|
this.config = config;
|
|
@@ -890,6 +1078,7 @@ var GitHubPlatform = class extends BasePlatform {
|
|
|
890
1078
|
}
|
|
891
1079
|
async generateDocument(context) {
|
|
892
1080
|
this.currentTaskId = context.taskId;
|
|
1081
|
+
this.currentOutputPath = context.outputPath;
|
|
893
1082
|
const prompt = this.buildPrompt(context);
|
|
894
1083
|
const response = await this.callClaude(prompt);
|
|
895
1084
|
const { document, questions } = this.parseResponse(response);
|
|
@@ -903,7 +1092,7 @@ var GitHubPlatform = class extends BasePlatform {
|
|
|
903
1092
|
throw new PauseForInputError("questions", questions);
|
|
904
1093
|
}
|
|
905
1094
|
async requestApproval(doc, stepName) {
|
|
906
|
-
const comment = this.formatApprovalComment(doc, stepName, this.currentTaskId);
|
|
1095
|
+
const comment = this.formatApprovalComment(doc, stepName, this.currentTaskId, this.currentOutputPath);
|
|
907
1096
|
await this.postComment(comment);
|
|
908
1097
|
console.log("\n\u23F8\uFE0F Waiting for approval on GitHub...");
|
|
909
1098
|
console.log(" Comment /approve, /revise <feedback>, or /reject on the PR/Issue.");
|
|
@@ -936,20 +1125,31 @@ var GitHubPlatform = class extends BasePlatform {
|
|
|
936
1125
|
lines.push("");
|
|
937
1126
|
}
|
|
938
1127
|
lines.push("---");
|
|
939
|
-
lines.push("
|
|
1128
|
+
lines.push("");
|
|
1129
|
+
lines.push("**How to answer:**");
|
|
1130
|
+
lines.push("");
|
|
940
1131
|
lines.push("```");
|
|
941
1132
|
lines.push("/answer");
|
|
942
1133
|
for (let i = 0; i < questions.length; i++) {
|
|
943
|
-
lines.push(`Q${i + 1}: your answer
|
|
1134
|
+
lines.push(`Q${i + 1}: <your answer for question ${i + 1}>`);
|
|
944
1135
|
}
|
|
945
1136
|
lines.push("```");
|
|
1137
|
+
lines.push("");
|
|
1138
|
+
lines.push("Example:");
|
|
1139
|
+
lines.push("```");
|
|
1140
|
+
lines.push("/answer");
|
|
1141
|
+
lines.push("Q1: Use TypeScript");
|
|
1142
|
+
lines.push("Q2: Yes, add unit tests");
|
|
1143
|
+
lines.push("```");
|
|
946
1144
|
return lines.join("\n");
|
|
947
1145
|
}
|
|
948
|
-
formatApprovalComment(doc, stepName, taskId) {
|
|
1146
|
+
formatApprovalComment(doc, stepName, taskId, outputPath) {
|
|
1147
|
+
const relativePath = outputPath ? outputPath.replace(process.cwd() + "/", "") : `.spets/outputs/${taskId}/${stepName}.md`;
|
|
949
1148
|
const lines = [
|
|
950
1149
|
`## \u{1F4C4} Spets: ${stepName} - Review Required`,
|
|
951
1150
|
"",
|
|
952
1151
|
`> Task ID: \`${taskId}\``,
|
|
1152
|
+
`> Output: \`${relativePath}\``,
|
|
953
1153
|
"",
|
|
954
1154
|
"<details>",
|
|
955
1155
|
"<summary>\u{1F4DD} View Document</summary>",
|
|
@@ -966,8 +1166,12 @@ var GitHubPlatform = class extends BasePlatform {
|
|
|
966
1166
|
"| Command | Description |",
|
|
967
1167
|
"|---------|-------------|",
|
|
968
1168
|
"| `/approve` | Approve and continue to next step |",
|
|
1169
|
+
"| `/approve --pr` | Approve and create a Pull Request |",
|
|
1170
|
+
"| `/approve --issue` | Approve and create/update an Issue |",
|
|
969
1171
|
"| `/revise <feedback>` | Request changes with feedback |",
|
|
970
|
-
"| `/reject` | Reject and stop workflow |"
|
|
1172
|
+
"| `/reject` | Reject and stop workflow |",
|
|
1173
|
+
"",
|
|
1174
|
+
"Example: `/revise Please add more details about error handling`"
|
|
971
1175
|
];
|
|
972
1176
|
return lines.join("\n");
|
|
973
1177
|
}
|
|
@@ -1032,12 +1236,11 @@ var GitHubPlatform = class extends BasePlatform {
|
|
|
1032
1236
|
async callClaude(prompt) {
|
|
1033
1237
|
return new Promise((resolve, reject) => {
|
|
1034
1238
|
const proc = spawn3("claude", [
|
|
1035
|
-
"
|
|
1239
|
+
"-p",
|
|
1240
|
+
prompt,
|
|
1036
1241
|
"--permission-mode",
|
|
1037
1242
|
"bypassPermissions"
|
|
1038
|
-
], { stdio: ["
|
|
1039
|
-
proc.stdin.write(prompt);
|
|
1040
|
-
proc.stdin.end();
|
|
1243
|
+
], { stdio: ["ignore", "pipe", "pipe"] });
|
|
1041
1244
|
let stdout = "";
|
|
1042
1245
|
let stderr = "";
|
|
1043
1246
|
proc.stdout.on("data", (data) => {
|
|
@@ -1048,7 +1251,7 @@ var GitHubPlatform = class extends BasePlatform {
|
|
|
1048
1251
|
});
|
|
1049
1252
|
proc.on("close", (code) => {
|
|
1050
1253
|
if (code !== 0) {
|
|
1051
|
-
reject(new Error(`Claude CLI failed
|
|
1254
|
+
reject(new Error(`Claude CLI failed (code ${code}): stderr=${stderr}, stdout=${stdout}`));
|
|
1052
1255
|
} else {
|
|
1053
1256
|
resolve(stdout);
|
|
1054
1257
|
}
|
|
@@ -1080,6 +1283,83 @@ var GitHubPlatform = class extends BasePlatform {
|
|
|
1080
1283
|
};
|
|
1081
1284
|
|
|
1082
1285
|
// src/commands/start.ts
|
|
1286
|
+
import { execSync as execSync3 } from "child_process";
|
|
1287
|
+
function getGitHubInfo(cwd) {
|
|
1288
|
+
const config = getGitHubConfig(cwd);
|
|
1289
|
+
if (config?.owner && config?.repo) {
|
|
1290
|
+
return { owner: config.owner, repo: config.repo };
|
|
1291
|
+
}
|
|
1292
|
+
try {
|
|
1293
|
+
const remoteUrl = execSync3("git remote get-url origin", { encoding: "utf-8", cwd }).trim();
|
|
1294
|
+
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
|
|
1295
|
+
if (sshMatch) {
|
|
1296
|
+
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
1297
|
+
}
|
|
1298
|
+
const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/);
|
|
1299
|
+
if (httpsMatch) {
|
|
1300
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
1301
|
+
}
|
|
1302
|
+
} catch {
|
|
1303
|
+
}
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
function createGitHubIssue(info, query, taskId) {
|
|
1307
|
+
const body = `## Task
|
|
1308
|
+
${query}
|
|
1309
|
+
|
|
1310
|
+
## Spets Workflow
|
|
1311
|
+
- Task ID: \`${taskId}\`
|
|
1312
|
+
- Status: In Progress
|
|
1313
|
+
|
|
1314
|
+
---
|
|
1315
|
+
_Managed by [Spets](https://github.com/eatnug/spets)_`;
|
|
1316
|
+
const result = execSync3(
|
|
1317
|
+
`gh issue create --repo ${info.owner}/${info.repo} --title "${query.slice(0, 50)}" --body "${body.replace(/"/g, '\\"')}" --label spets`,
|
|
1318
|
+
{ encoding: "utf-8" }
|
|
1319
|
+
);
|
|
1320
|
+
const match = result.match(/\/issues\/(\d+)/);
|
|
1321
|
+
if (!match) {
|
|
1322
|
+
throw new Error("Failed to parse issue number from gh output");
|
|
1323
|
+
}
|
|
1324
|
+
return parseInt(match[1], 10);
|
|
1325
|
+
}
|
|
1326
|
+
function createGitHubPR(info, query, taskId) {
|
|
1327
|
+
const branchName = `spets/${taskId}`;
|
|
1328
|
+
execSync3(`git checkout -b ${branchName}`, { encoding: "utf-8" });
|
|
1329
|
+
execSync3(`git push -u origin ${branchName}`, { encoding: "utf-8" });
|
|
1330
|
+
const body = `## Task
|
|
1331
|
+
${query}
|
|
1332
|
+
|
|
1333
|
+
## Spets Workflow
|
|
1334
|
+
- Task ID: \`${taskId}\`
|
|
1335
|
+
- Status: In Progress
|
|
1336
|
+
|
|
1337
|
+
---
|
|
1338
|
+
_Managed by [Spets](https://github.com/eatnug/spets)_`;
|
|
1339
|
+
const result = execSync3(
|
|
1340
|
+
`gh pr create --repo ${info.owner}/${info.repo} --title "${query.slice(0, 50)}" --body "${body.replace(/"/g, '\\"')}" --label spets`,
|
|
1341
|
+
{ encoding: "utf-8" }
|
|
1342
|
+
);
|
|
1343
|
+
const match = result.match(/\/pull\/(\d+)/);
|
|
1344
|
+
if (!match) {
|
|
1345
|
+
throw new Error("Failed to parse PR number from gh output");
|
|
1346
|
+
}
|
|
1347
|
+
return parseInt(match[1], 10);
|
|
1348
|
+
}
|
|
1349
|
+
function findLinkedIssueOrPR(info) {
|
|
1350
|
+
try {
|
|
1351
|
+
const result = execSync3(
|
|
1352
|
+
`gh pr view --repo ${info.owner}/${info.repo} --json number`,
|
|
1353
|
+
{ encoding: "utf-8" }
|
|
1354
|
+
);
|
|
1355
|
+
const pr = JSON.parse(result);
|
|
1356
|
+
if (pr.number) {
|
|
1357
|
+
return { type: "pr", number: pr.number };
|
|
1358
|
+
}
|
|
1359
|
+
} catch {
|
|
1360
|
+
}
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1083
1363
|
async function startCommand(query, options) {
|
|
1084
1364
|
const cwd = process.cwd();
|
|
1085
1365
|
if (!spetsExists(cwd)) {
|
|
@@ -1088,35 +1368,67 @@ async function startCommand(query, options) {
|
|
|
1088
1368
|
}
|
|
1089
1369
|
const config = loadConfig(cwd);
|
|
1090
1370
|
const taskId = generateTaskId();
|
|
1091
|
-
console.log(`Starting new workflow: ${taskId}`);
|
|
1092
|
-
console.log(`Query: ${query}`);
|
|
1093
|
-
console.log(`Platform: ${options.platform || "cli"}`);
|
|
1094
|
-
console.log("");
|
|
1095
1371
|
saveTaskMetadata(taskId, query, cwd);
|
|
1096
1372
|
let platform;
|
|
1097
|
-
if (options.
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const pr = options.pr || (githubConfig?.defaultPr ? String(githubConfig.defaultPr) : void 0);
|
|
1102
|
-
const issue = options.issue || (githubConfig?.defaultIssue ? String(githubConfig.defaultIssue) : void 0);
|
|
1103
|
-
if (!owner || !repo) {
|
|
1104
|
-
console.error("GitHub platform requires --owner and --repo (or set in .spets/config.yml)");
|
|
1373
|
+
if (options.github || options.issue !== void 0 || options.pr !== void 0) {
|
|
1374
|
+
const githubInfo = getGitHubInfo(cwd);
|
|
1375
|
+
if (!githubInfo) {
|
|
1376
|
+
console.error("Could not determine GitHub owner/repo. Set in .spets/config.yml or add git remote.");
|
|
1105
1377
|
process.exit(1);
|
|
1106
1378
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1379
|
+
let issueNumber;
|
|
1380
|
+
let prNumber;
|
|
1381
|
+
if (options.issue !== void 0) {
|
|
1382
|
+
if (typeof options.issue === "string") {
|
|
1383
|
+
issueNumber = parseInt(options.issue, 10);
|
|
1384
|
+
console.log(`Using existing Issue #${issueNumber}`);
|
|
1385
|
+
} else {
|
|
1386
|
+
console.log("Creating new GitHub Issue...");
|
|
1387
|
+
issueNumber = createGitHubIssue(githubInfo, query, taskId);
|
|
1388
|
+
console.log(`Created Issue #${issueNumber}`);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
if (options.pr !== void 0) {
|
|
1392
|
+
if (typeof options.pr === "string") {
|
|
1393
|
+
prNumber = parseInt(options.pr, 10);
|
|
1394
|
+
console.log(`Using existing PR #${prNumber}`);
|
|
1395
|
+
} else {
|
|
1396
|
+
console.log("Creating new GitHub PR...");
|
|
1397
|
+
prNumber = createGitHubPR(githubInfo, query, taskId);
|
|
1398
|
+
console.log(`Created PR #${prNumber}`);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (issueNumber === void 0 && prNumber === void 0) {
|
|
1402
|
+
const linked = findLinkedIssueOrPR(githubInfo);
|
|
1403
|
+
if (linked) {
|
|
1404
|
+
console.log(`Found linked ${linked.type} #${linked.number}`);
|
|
1405
|
+
if (linked.type === "pr") {
|
|
1406
|
+
prNumber = linked.number;
|
|
1407
|
+
} else {
|
|
1408
|
+
issueNumber = linked.number;
|
|
1409
|
+
}
|
|
1410
|
+
} else {
|
|
1411
|
+
console.log("No linked Issue/PR found. Running in GitHub mode without Issue/PR.");
|
|
1412
|
+
}
|
|
1110
1413
|
}
|
|
1111
1414
|
platform = new GitHubPlatform({
|
|
1112
|
-
owner,
|
|
1113
|
-
repo,
|
|
1114
|
-
prNumber
|
|
1115
|
-
issueNumber
|
|
1415
|
+
owner: githubInfo.owner,
|
|
1416
|
+
repo: githubInfo.repo,
|
|
1417
|
+
prNumber,
|
|
1418
|
+
issueNumber
|
|
1116
1419
|
});
|
|
1420
|
+
console.log(`Starting workflow: ${taskId}`);
|
|
1421
|
+
console.log(`Query: ${query}`);
|
|
1422
|
+
console.log(`Platform: github (${githubInfo.owner}/${githubInfo.repo})`);
|
|
1423
|
+
if (issueNumber) console.log(`Issue: #${issueNumber}`);
|
|
1424
|
+
if (prNumber) console.log(`PR: #${prNumber}`);
|
|
1117
1425
|
} else {
|
|
1118
1426
|
platform = new CliPlatform();
|
|
1427
|
+
console.log(`Starting workflow: ${taskId}`);
|
|
1428
|
+
console.log(`Query: ${query}`);
|
|
1429
|
+
console.log(`Platform: cli`);
|
|
1119
1430
|
}
|
|
1431
|
+
console.log("");
|
|
1120
1432
|
const executor = new Executor({ platform, config, cwd });
|
|
1121
1433
|
try {
|
|
1122
1434
|
await executor.executeWorkflow(taskId, query);
|
|
@@ -1264,23 +1576,31 @@ function installClaudePlugin() {
|
|
|
1264
1576
|
const claudeDir = join3(homedir(), ".claude");
|
|
1265
1577
|
const commandsDir = join3(claudeDir, "commands");
|
|
1266
1578
|
mkdirSync2(commandsDir, { recursive: true });
|
|
1267
|
-
const skillPath = join3(commandsDir, "
|
|
1579
|
+
const skillPath = join3(commandsDir, "sdd-do.md");
|
|
1268
1580
|
writeFileSync2(skillPath, getClaudeSkillContent());
|
|
1269
1581
|
console.log("Installed Claude Code plugin.");
|
|
1270
1582
|
console.log(`Location: ${skillPath}`);
|
|
1271
1583
|
console.log("");
|
|
1272
1584
|
console.log("Usage in Claude Code:");
|
|
1273
|
-
console.log(' /
|
|
1274
|
-
console.log(" /spets status");
|
|
1275
|
-
console.log(" /spets resume");
|
|
1585
|
+
console.log(' /sdd-do "your task description"');
|
|
1276
1586
|
console.log("");
|
|
1277
|
-
console.log("
|
|
1587
|
+
console.log("This skill runs deterministically within your Claude Code session.");
|
|
1588
|
+
console.log("No additional Claude processes are spawned.");
|
|
1278
1589
|
}
|
|
1279
1590
|
async function uninstallPlugin(name) {
|
|
1280
1591
|
if (name === "claude") {
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1592
|
+
const oldSkillPath = join3(homedir(), ".claude", "commands", "spets.md");
|
|
1593
|
+
const newSkillPath = join3(homedir(), ".claude", "commands", "sdd-do.md");
|
|
1594
|
+
let uninstalled = false;
|
|
1595
|
+
if (existsSync3(oldSkillPath)) {
|
|
1596
|
+
rmSync(oldSkillPath);
|
|
1597
|
+
uninstalled = true;
|
|
1598
|
+
}
|
|
1599
|
+
if (existsSync3(newSkillPath)) {
|
|
1600
|
+
rmSync(newSkillPath);
|
|
1601
|
+
uninstalled = true;
|
|
1602
|
+
}
|
|
1603
|
+
if (uninstalled) {
|
|
1284
1604
|
console.log("Uninstalled Claude Code plugin.");
|
|
1285
1605
|
} else {
|
|
1286
1606
|
console.log("Claude Code plugin not installed.");
|
|
@@ -1293,10 +1613,11 @@ async function uninstallPlugin(name) {
|
|
|
1293
1613
|
async function listPlugins() {
|
|
1294
1614
|
console.log("Available plugins:");
|
|
1295
1615
|
console.log("");
|
|
1296
|
-
console.log(" claude - Claude Code
|
|
1616
|
+
console.log(" claude - Claude Code /sdd-do skill");
|
|
1297
1617
|
console.log("");
|
|
1298
|
-
const
|
|
1299
|
-
const
|
|
1618
|
+
const oldSkillPath = join3(homedir(), ".claude", "commands", "spets.md");
|
|
1619
|
+
const newSkillPath = join3(homedir(), ".claude", "commands", "sdd-do.md");
|
|
1620
|
+
const claudeInstalled = existsSync3(oldSkillPath) || existsSync3(newSkillPath);
|
|
1300
1621
|
console.log("Installed:");
|
|
1301
1622
|
if (claudeInstalled) {
|
|
1302
1623
|
console.log(" - claude");
|
|
@@ -1305,81 +1626,202 @@ async function listPlugins() {
|
|
|
1305
1626
|
}
|
|
1306
1627
|
}
|
|
1307
1628
|
function getClaudeSkillContent() {
|
|
1308
|
-
return `#
|
|
1629
|
+
return `# SDD-Do Skill
|
|
1630
|
+
|
|
1631
|
+
Spec-Driven Development workflow execution skill for Claude Code.
|
|
1632
|
+
|
|
1633
|
+
---
|
|
1634
|
+
|
|
1635
|
+
## When to Use This Skill
|
|
1309
1636
|
|
|
1310
|
-
|
|
1637
|
+
Automatically invoked when user uses:
|
|
1638
|
+
- \`/sdd-do\` - Run SDD workflow
|
|
1311
1639
|
|
|
1312
|
-
|
|
1640
|
+
---
|
|
1313
1641
|
|
|
1642
|
+
## CRITICAL: Execution Model
|
|
1643
|
+
|
|
1644
|
+
This skill runs **deterministically** within the current Claude Code session.
|
|
1645
|
+
- NO subprocess spawning for AI/LLM calls
|
|
1646
|
+
- The orchestrator manages state via JSON protocol
|
|
1647
|
+
- Each step follows a strict state machine flow
|
|
1648
|
+
|
|
1649
|
+
---
|
|
1650
|
+
|
|
1651
|
+
## Orchestrator Commands
|
|
1652
|
+
|
|
1653
|
+
All workflow state is managed by the orchestrator CLI:
|
|
1654
|
+
|
|
1655
|
+
\`\`\`bash
|
|
1656
|
+
npx spets orchestrate init "<description>" # Start new workflow
|
|
1657
|
+
npx spets orchestrate done <taskId> # Mark step complete
|
|
1658
|
+
npx spets orchestrate clarified <taskId> '<json>' # Submit answers
|
|
1659
|
+
npx spets orchestrate approve <taskId> # Approve and continue
|
|
1660
|
+
npx spets orchestrate revise <taskId> "<feedback>" # Request revision
|
|
1661
|
+
npx spets orchestrate reject <taskId> # Stop workflow
|
|
1662
|
+
npx spets orchestrate stop <taskId> # Pause (can resume)
|
|
1663
|
+
npx spets orchestrate status <taskId> # Get current status
|
|
1314
1664
|
\`\`\`
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1665
|
+
|
|
1666
|
+
---
|
|
1667
|
+
|
|
1668
|
+
## Execution Flow
|
|
1669
|
+
|
|
1670
|
+
### 1. Workflow Start (FIRST ACTION - NO EXCEPTIONS)
|
|
1671
|
+
|
|
1672
|
+
When skill is loaded, **immediately** run:
|
|
1673
|
+
|
|
1674
|
+
\`\`\`bash
|
|
1675
|
+
npx spets orchestrate init "{user_request}"
|
|
1676
|
+
\`\`\`
|
|
1677
|
+
|
|
1678
|
+
**JSON Output:**
|
|
1679
|
+
\`\`\`json
|
|
1680
|
+
{
|
|
1681
|
+
"type": "step",
|
|
1682
|
+
"step": "01-plan",
|
|
1683
|
+
"stepIndex": 1,
|
|
1684
|
+
"totalSteps": 2,
|
|
1685
|
+
"taskId": "mku3abc-xyz",
|
|
1686
|
+
"description": "User request here",
|
|
1687
|
+
"context": {
|
|
1688
|
+
"instruction": ".spets/steps/01-plan/instruction.md",
|
|
1689
|
+
"template": ".spets/steps/01-plan/template.md",
|
|
1690
|
+
"output": ".spets/outputs/mku3abc-xyz/01-plan.md",
|
|
1691
|
+
"revisionFeedback": null
|
|
1692
|
+
},
|
|
1693
|
+
"onComplete": "done mku3abc-xyz"
|
|
1694
|
+
}
|
|
1318
1695
|
\`\`\`
|
|
1319
1696
|
|
|
1320
|
-
|
|
1697
|
+
---
|
|
1321
1698
|
|
|
1322
|
-
|
|
1699
|
+
### 2. Document Generation (type: step)
|
|
1323
1700
|
|
|
1324
|
-
|
|
1325
|
-
2. Execute the appropriate spets CLI command using Bash
|
|
1326
|
-
3. For 'start', read the generated documents and help iterate
|
|
1701
|
+
When you receive \`type: "step"\`:
|
|
1327
1702
|
|
|
1328
|
-
|
|
1703
|
+
1. **Read the instruction file** using Read tool
|
|
1704
|
+
2. **Read the template file** (if exists) using Read tool
|
|
1705
|
+
3. **Read previous output** (if exists) using Read tool
|
|
1706
|
+
4. **Generate the document** following the instruction and template
|
|
1707
|
+
5. **Write the document** to context.output using Write tool
|
|
1708
|
+
6. **Mark step as done**:
|
|
1709
|
+
\`\`\`bash
|
|
1710
|
+
npx spets orchestrate done {taskId}
|
|
1711
|
+
\`\`\`
|
|
1329
1712
|
|
|
1330
|
-
|
|
1713
|
+
---
|
|
1331
1714
|
|
|
1332
|
-
|
|
1333
|
-
2. Read the generated plan document from .spets/outputs/<taskId>/
|
|
1334
|
-
3. Present the plan to the user
|
|
1335
|
-
4. If user approves, continue. If they want changes, provide feedback.
|
|
1715
|
+
### 3. Question Resolution (type: checkpoint, checkpoint: clarify)
|
|
1336
1716
|
|
|
1337
|
-
|
|
1717
|
+
When you receive \`type: "checkpoint"\` with \`checkpoint: "clarify"\`:
|
|
1338
1718
|
|
|
1339
|
-
1.
|
|
1340
|
-
2.
|
|
1719
|
+
1. **Use AskUserQuestion** tool to ask the questions
|
|
1720
|
+
2. **Collect answers** into JSON format
|
|
1721
|
+
3. **Submit answers**:
|
|
1722
|
+
\`\`\`bash
|
|
1723
|
+
npx spets orchestrate clarified {taskId} '[{"questionId":"q1","answer":"..."}]'
|
|
1724
|
+
\`\`\`
|
|
1341
1725
|
|
|
1342
|
-
|
|
1726
|
+
---
|
|
1343
1727
|
|
|
1344
|
-
|
|
1345
|
-
2. Continue the workflow from where it paused
|
|
1728
|
+
### 4. Approval Request (type: checkpoint, checkpoint: approve)
|
|
1346
1729
|
|
|
1347
|
-
|
|
1730
|
+
When you receive \`type: "checkpoint"\` with \`checkpoint: "approve"\`:
|
|
1731
|
+
|
|
1732
|
+
1. **Display document summary** from specPath using Read tool
|
|
1733
|
+
2. **Use AskUserQuestion** with options:
|
|
1734
|
+
- Approve & Continue
|
|
1735
|
+
- Revise (with feedback)
|
|
1736
|
+
- Reject
|
|
1737
|
+
- Stop (pause for later)
|
|
1738
|
+
|
|
1739
|
+
3. **Execute the selected action**:
|
|
1740
|
+
- **Approve**: \`npx spets orchestrate approve {taskId}\`
|
|
1741
|
+
- **Revise**: \`npx spets orchestrate revise {taskId} "{feedback}"\`
|
|
1742
|
+
- **Reject**: \`npx spets orchestrate reject {taskId}\`
|
|
1743
|
+
- **Stop**: \`npx spets orchestrate stop {taskId}\`
|
|
1744
|
+
|
|
1745
|
+
---
|
|
1746
|
+
|
|
1747
|
+
### 5. Workflow Complete (type: complete)
|
|
1748
|
+
|
|
1749
|
+
Display completion message based on status:
|
|
1750
|
+
- \`completed\`: Show success and list outputs
|
|
1751
|
+
- \`stopped\`: Show resume instructions
|
|
1752
|
+
- \`rejected\`: Show rejection message
|
|
1753
|
+
|
|
1754
|
+
---
|
|
1755
|
+
|
|
1756
|
+
## Response Type Actions Summary
|
|
1757
|
+
|
|
1758
|
+
| Type | Checkpoint | Action |
|
|
1759
|
+
|------|------------|--------|
|
|
1760
|
+
| step | - | Generate document, write to output, call done |
|
|
1761
|
+
| checkpoint | clarify | Ask questions, call clarified with answers |
|
|
1762
|
+
| checkpoint | approve | Show doc, ask for decision, call appropriate command |
|
|
1763
|
+
| complete | - | Show final message, end skill |
|
|
1764
|
+
| error | - | Show error, end skill |
|
|
1765
|
+
|
|
1766
|
+
---
|
|
1348
1767
|
|
|
1349
|
-
|
|
1768
|
+
## Critical Implementation Rules
|
|
1350
1769
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1770
|
+
1. **ORCHESTRATOR FIRST** - Always call orchestrator before any action
|
|
1771
|
+
2. **ONE STEP AT A TIME** - Complete one step, get approval, then next
|
|
1772
|
+
3. **DETERMINISTIC FLOW** - Follow the exact state machine transitions
|
|
1773
|
+
4. **NO PROCESS SPAWN FOR AI** - Generate documents in current session
|
|
1774
|
+
5. **WAIT FOR APPROVAL** - Never proceed without user approval
|
|
1775
|
+
|
|
1776
|
+
---
|
|
1777
|
+
|
|
1778
|
+
## Error Handling
|
|
1779
|
+
|
|
1780
|
+
- If \`.spets/\` not found, suggest running \`npx spets init\`
|
|
1781
|
+
- If orchestrator returns \`type: "error"\`, display the error and stop
|
|
1782
|
+
- If file read fails, show error and allow retry
|
|
1357
1783
|
|
|
1358
1784
|
$ARGUMENTS
|
|
1359
|
-
|
|
1360
|
-
args: Additional arguments for the command
|
|
1785
|
+
request: The user's task description for the workflow
|
|
1361
1786
|
`;
|
|
1362
1787
|
}
|
|
1363
1788
|
|
|
1364
1789
|
// src/commands/github.ts
|
|
1790
|
+
import { execSync as execSync4 } from "child_process";
|
|
1791
|
+
function getGitHubInfo2(cwd) {
|
|
1792
|
+
const config = getGitHubConfig(cwd);
|
|
1793
|
+
if (config?.owner && config?.repo) {
|
|
1794
|
+
return { owner: config.owner, repo: config.repo };
|
|
1795
|
+
}
|
|
1796
|
+
try {
|
|
1797
|
+
const remoteUrl = execSync4("git remote get-url origin", { encoding: "utf-8", cwd }).trim();
|
|
1798
|
+
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
|
|
1799
|
+
if (sshMatch) {
|
|
1800
|
+
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
1801
|
+
}
|
|
1802
|
+
const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/);
|
|
1803
|
+
if (httpsMatch) {
|
|
1804
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
1805
|
+
}
|
|
1806
|
+
} catch {
|
|
1807
|
+
}
|
|
1808
|
+
return null;
|
|
1809
|
+
}
|
|
1365
1810
|
async function githubCommand(options) {
|
|
1366
1811
|
const cwd = process.cwd();
|
|
1367
1812
|
if (!spetsExists(cwd)) {
|
|
1368
1813
|
console.error("Spets not initialized.");
|
|
1369
1814
|
process.exit(1);
|
|
1370
1815
|
}
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
const pr = options.pr || (configGitHub?.defaultPr ? String(configGitHub.defaultPr) : void 0);
|
|
1375
|
-
const issue = options.issue || (configGitHub?.defaultIssue ? String(configGitHub.defaultIssue) : void 0);
|
|
1376
|
-
const { comment, task } = options;
|
|
1377
|
-
if (!owner || !repo) {
|
|
1378
|
-
console.error("GitHub requires --owner and --repo (or set in .spets/config.yml)");
|
|
1816
|
+
const githubInfo = getGitHubInfo2(cwd);
|
|
1817
|
+
if (!githubInfo) {
|
|
1818
|
+
console.error("Could not determine GitHub owner/repo. Set in .spets/config.yml or add git remote.");
|
|
1379
1819
|
process.exit(1);
|
|
1380
1820
|
}
|
|
1821
|
+
const { owner, repo } = githubInfo;
|
|
1822
|
+
const { pr, issue, comment, task } = options;
|
|
1381
1823
|
if (!pr && !issue) {
|
|
1382
|
-
console.error("Either --pr or --issue is required
|
|
1824
|
+
console.error("Either --pr or --issue is required");
|
|
1383
1825
|
process.exit(1);
|
|
1384
1826
|
}
|
|
1385
1827
|
const parsed = parseGitHubCommand(comment);
|
|
@@ -1397,7 +1839,7 @@ async function githubCommand(options) {
|
|
|
1397
1839
|
}
|
|
1398
1840
|
if (!taskId) {
|
|
1399
1841
|
const config2 = loadConfig(cwd);
|
|
1400
|
-
const { listTasks: listTasks2 } = await import("./state-
|
|
1842
|
+
const { listTasks: listTasks2 } = await import("./state-JLYPJWUT.js");
|
|
1401
1843
|
const tasks = listTasks2(cwd);
|
|
1402
1844
|
for (const tid of tasks) {
|
|
1403
1845
|
const state2 = getWorkflowState(tid, config2, cwd);
|
|
@@ -1430,6 +1872,15 @@ async function githubCommand(options) {
|
|
|
1430
1872
|
case "approve": {
|
|
1431
1873
|
console.log(`Approving step: ${state.currentStepName}`);
|
|
1432
1874
|
updateDocumentStatus(outputPath, "approved");
|
|
1875
|
+
if (parsed.createPR) {
|
|
1876
|
+
console.log("Creating Pull Request...");
|
|
1877
|
+
const prNumber = await createPullRequest(githubConfig, taskId, userQuery, state.currentStepName);
|
|
1878
|
+
console.log(`Created PR #${prNumber}`);
|
|
1879
|
+
}
|
|
1880
|
+
if (parsed.createIssue) {
|
|
1881
|
+
console.log("Creating/Updating Issue...");
|
|
1882
|
+
await createOrUpdateIssue(githubConfig, taskId, userQuery, state.currentStepName);
|
|
1883
|
+
}
|
|
1433
1884
|
const nextIndex = state.currentStepIndex + 1;
|
|
1434
1885
|
if (nextIndex >= config.steps.length) {
|
|
1435
1886
|
console.log("\u2705 Workflow completed!");
|
|
@@ -1452,7 +1903,7 @@ async function githubCommand(options) {
|
|
|
1452
1903
|
const platform = new GitHubPlatform(githubConfig);
|
|
1453
1904
|
const executor = new Executor({ platform, config, cwd });
|
|
1454
1905
|
try {
|
|
1455
|
-
await executor.executeWorkflow(taskId, userQuery, state.currentStepIndex);
|
|
1906
|
+
await executor.executeWorkflow(taskId, userQuery, state.currentStepIndex, parsed.feedback);
|
|
1456
1907
|
} catch (error) {
|
|
1457
1908
|
if (error.name !== "PauseForInputError") {
|
|
1458
1909
|
throw error;
|
|
@@ -1521,14 +1972,527 @@ async function postComment(config, body) {
|
|
|
1521
1972
|
proc.on("error", reject);
|
|
1522
1973
|
});
|
|
1523
1974
|
}
|
|
1975
|
+
async function createPullRequest(config, taskId, userQuery, stepName) {
|
|
1976
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
1977
|
+
const { owner, repo } = config;
|
|
1978
|
+
const title = userQuery.slice(0, 50) + (userQuery.length > 50 ? "..." : "");
|
|
1979
|
+
const body = `## Spets Workflow
|
|
1980
|
+
|
|
1981
|
+
- Task ID: \`${taskId}\`
|
|
1982
|
+
- Current Step: **${stepName}**
|
|
1983
|
+
|
|
1984
|
+
### Description
|
|
1985
|
+
${userQuery}
|
|
1986
|
+
|
|
1987
|
+
---
|
|
1988
|
+
_Created by [Spets](https://github.com/eatnug/spets)_`;
|
|
1989
|
+
const result = execSync5(
|
|
1990
|
+
`gh pr create --repo ${owner}/${repo} --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}"`,
|
|
1991
|
+
{ encoding: "utf-8" }
|
|
1992
|
+
);
|
|
1993
|
+
const match = result.match(/\/pull\/(\d+)/);
|
|
1994
|
+
if (!match) {
|
|
1995
|
+
throw new Error("Failed to parse PR number from gh output");
|
|
1996
|
+
}
|
|
1997
|
+
return parseInt(match[1], 10);
|
|
1998
|
+
}
|
|
1999
|
+
async function createOrUpdateIssue(config, taskId, userQuery, stepName) {
|
|
2000
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
2001
|
+
const { owner, repo, issueNumber } = config;
|
|
2002
|
+
const body = `## Spets Workflow Update
|
|
2003
|
+
|
|
2004
|
+
- Task ID: \`${taskId}\`
|
|
2005
|
+
- Current Step: **${stepName}** \u2705 Approved
|
|
2006
|
+
|
|
2007
|
+
### Description
|
|
2008
|
+
${userQuery}
|
|
2009
|
+
|
|
2010
|
+
---
|
|
2011
|
+
_Updated by [Spets](https://github.com/eatnug/spets)_`;
|
|
2012
|
+
if (issueNumber) {
|
|
2013
|
+
execSync5(
|
|
2014
|
+
`gh issue comment ${issueNumber} --repo ${owner}/${repo} --body "${body.replace(/"/g, '\\"')}"`,
|
|
2015
|
+
{ encoding: "utf-8" }
|
|
2016
|
+
);
|
|
2017
|
+
} else {
|
|
2018
|
+
const title = userQuery.slice(0, 50) + (userQuery.length > 50 ? "..." : "");
|
|
2019
|
+
execSync5(
|
|
2020
|
+
`gh issue create --repo ${owner}/${repo} --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}" --label spets`,
|
|
2021
|
+
{ encoding: "utf-8" }
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// src/orchestrator/index.ts
|
|
2027
|
+
import { readFileSync, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
2028
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
2029
|
+
import matter from "gray-matter";
|
|
2030
|
+
var Orchestrator = class {
|
|
2031
|
+
cwd;
|
|
2032
|
+
constructor(cwd = process.cwd()) {
|
|
2033
|
+
this.cwd = cwd;
|
|
2034
|
+
}
|
|
2035
|
+
// ===========================================================================
|
|
2036
|
+
// Config & Path Helpers
|
|
2037
|
+
// ===========================================================================
|
|
2038
|
+
getSteps() {
|
|
2039
|
+
const config = loadConfig(this.cwd);
|
|
2040
|
+
return config.steps;
|
|
2041
|
+
}
|
|
2042
|
+
getOutputPath() {
|
|
2043
|
+
return getOutputsDir(this.cwd);
|
|
2044
|
+
}
|
|
2045
|
+
getStatePath(taskId) {
|
|
2046
|
+
return join4(this.getOutputPath(), taskId, ".state.json");
|
|
2047
|
+
}
|
|
2048
|
+
getSpecPath(taskId, step) {
|
|
2049
|
+
return join4(this.getOutputPath(), taskId, `${step}.md`);
|
|
2050
|
+
}
|
|
2051
|
+
getStepInstructionPath(step) {
|
|
2052
|
+
return join4(getStepsDir(this.cwd), step, "instruction.md");
|
|
2053
|
+
}
|
|
2054
|
+
getStepTemplatePath(step) {
|
|
2055
|
+
return join4(getStepsDir(this.cwd), step, "template.md");
|
|
2056
|
+
}
|
|
2057
|
+
// ===========================================================================
|
|
2058
|
+
// State Management
|
|
2059
|
+
// ===========================================================================
|
|
2060
|
+
loadState(taskId) {
|
|
2061
|
+
const statePath = this.getStatePath(taskId);
|
|
2062
|
+
if (!existsSync4(statePath)) {
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2065
|
+
const data = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
2066
|
+
return data;
|
|
2067
|
+
}
|
|
2068
|
+
saveState(state) {
|
|
2069
|
+
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2070
|
+
const statePath = this.getStatePath(state.taskId);
|
|
2071
|
+
const dir = dirname2(statePath);
|
|
2072
|
+
if (!existsSync4(dir)) {
|
|
2073
|
+
mkdirSync3(dir, { recursive: true });
|
|
2074
|
+
}
|
|
2075
|
+
writeFileSync3(statePath, JSON.stringify(state, null, 2));
|
|
2076
|
+
}
|
|
2077
|
+
// ===========================================================================
|
|
2078
|
+
// Spec Helpers
|
|
2079
|
+
// ===========================================================================
|
|
2080
|
+
checkUnresolvedQuestions(specPath) {
|
|
2081
|
+
if (!existsSync4(specPath)) {
|
|
2082
|
+
return [];
|
|
2083
|
+
}
|
|
2084
|
+
const content = readFileSync(specPath, "utf-8");
|
|
2085
|
+
const { data } = matter(content);
|
|
2086
|
+
const questions = [];
|
|
2087
|
+
if (data.open_questions && Array.isArray(data.open_questions)) {
|
|
2088
|
+
for (let i = 0; i < data.open_questions.length; i++) {
|
|
2089
|
+
const q = data.open_questions[i];
|
|
2090
|
+
if (!q.resolved) {
|
|
2091
|
+
questions.push({
|
|
2092
|
+
id: `q${i + 1}`,
|
|
2093
|
+
question: q.question || q.id || `Question ${i + 1}`,
|
|
2094
|
+
context: q.context,
|
|
2095
|
+
options: q.options,
|
|
2096
|
+
resolved: false
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
return questions;
|
|
2102
|
+
}
|
|
2103
|
+
generateTaskId(description) {
|
|
2104
|
+
const timestamp = Date.now().toString(36);
|
|
2105
|
+
const random = Math.random().toString(36).substring(2, 6);
|
|
2106
|
+
return `${timestamp}-${random}`;
|
|
2107
|
+
}
|
|
2108
|
+
// ===========================================================================
|
|
2109
|
+
// Protocol Response Builders
|
|
2110
|
+
// ===========================================================================
|
|
2111
|
+
responseStep(state) {
|
|
2112
|
+
const steps = this.getSteps();
|
|
2113
|
+
const outputPath = this.getOutputPath();
|
|
2114
|
+
let previousOutput;
|
|
2115
|
+
if (state.stepIndex > 1) {
|
|
2116
|
+
const prevStep = steps[state.stepIndex - 2];
|
|
2117
|
+
const prevPath = join4(outputPath, state.taskId, `${prevStep}.md`);
|
|
2118
|
+
if (existsSync4(prevPath)) {
|
|
2119
|
+
previousOutput = prevPath;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
const templatePath = this.getStepTemplatePath(state.currentStep);
|
|
2123
|
+
const hasTemplate = existsSync4(templatePath);
|
|
2124
|
+
return {
|
|
2125
|
+
type: "step",
|
|
2126
|
+
step: state.currentStep,
|
|
2127
|
+
stepIndex: state.stepIndex,
|
|
2128
|
+
totalSteps: state.totalSteps,
|
|
2129
|
+
taskId: state.taskId,
|
|
2130
|
+
description: state.description,
|
|
2131
|
+
context: {
|
|
2132
|
+
instruction: this.getStepInstructionPath(state.currentStep),
|
|
2133
|
+
template: hasTemplate ? templatePath : void 0,
|
|
2134
|
+
previousOutput,
|
|
2135
|
+
output: join4(outputPath, state.taskId, `${state.currentStep}.md`),
|
|
2136
|
+
revisionFeedback: state.revisionFeedback
|
|
2137
|
+
},
|
|
2138
|
+
onComplete: `done ${state.taskId}`
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
responseCheckpointClarify(state, questions) {
|
|
2142
|
+
return {
|
|
2143
|
+
type: "checkpoint",
|
|
2144
|
+
checkpoint: "clarify",
|
|
2145
|
+
taskId: state.taskId,
|
|
2146
|
+
step: state.currentStep,
|
|
2147
|
+
questions: questions.map((q) => ({
|
|
2148
|
+
id: q.id,
|
|
2149
|
+
question: q.question,
|
|
2150
|
+
context: q.context,
|
|
2151
|
+
options: q.options
|
|
2152
|
+
})),
|
|
2153
|
+
onComplete: `clarified ${state.taskId} '<answers_json>'`
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
responseCheckpointApprove(state) {
|
|
2157
|
+
const outputPath = this.getOutputPath();
|
|
2158
|
+
return {
|
|
2159
|
+
type: "checkpoint",
|
|
2160
|
+
checkpoint: "approve",
|
|
2161
|
+
taskId: state.taskId,
|
|
2162
|
+
step: state.currentStep,
|
|
2163
|
+
stepIndex: state.stepIndex,
|
|
2164
|
+
totalSteps: state.totalSteps,
|
|
2165
|
+
specPath: join4(outputPath, state.taskId, `${state.currentStep}.md`),
|
|
2166
|
+
options: ["approve", "revise", "reject", "stop"],
|
|
2167
|
+
onComplete: {
|
|
2168
|
+
approve: `approve ${state.taskId}`,
|
|
2169
|
+
revise: `revise ${state.taskId} '<feedback>'`,
|
|
2170
|
+
reject: `reject ${state.taskId}`,
|
|
2171
|
+
stop: `stop ${state.taskId}`
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
responseComplete(state, status) {
|
|
2176
|
+
const steps = this.getSteps();
|
|
2177
|
+
const outputPath = this.getOutputPath();
|
|
2178
|
+
const outputs = [];
|
|
2179
|
+
for (let i = 0; i < state.stepIndex; i++) {
|
|
2180
|
+
const specPath = join4(outputPath, state.taskId, `${steps[i]}.md`);
|
|
2181
|
+
if (existsSync4(specPath)) {
|
|
2182
|
+
outputs.push(specPath);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
const messages = {
|
|
2186
|
+
completed: "Workflow completed successfully",
|
|
2187
|
+
stopped: `Workflow stopped. Resume with: spets resume --task ${state.taskId}`,
|
|
2188
|
+
rejected: "Workflow rejected"
|
|
2189
|
+
};
|
|
2190
|
+
return {
|
|
2191
|
+
type: "complete",
|
|
2192
|
+
status,
|
|
2193
|
+
taskId: state.taskId,
|
|
2194
|
+
outputs,
|
|
2195
|
+
message: messages[status]
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
responseError(error, taskId, step) {
|
|
2199
|
+
const response = { type: "error", error };
|
|
2200
|
+
if (taskId) response.taskId = taskId;
|
|
2201
|
+
if (step) response.step = step;
|
|
2202
|
+
return response;
|
|
2203
|
+
}
|
|
2204
|
+
// ===========================================================================
|
|
2205
|
+
// Command Handlers
|
|
2206
|
+
// ===========================================================================
|
|
2207
|
+
/**
|
|
2208
|
+
* Initialize a new workflow
|
|
2209
|
+
*/
|
|
2210
|
+
cmdInit(description) {
|
|
2211
|
+
try {
|
|
2212
|
+
const steps = this.getSteps();
|
|
2213
|
+
const taskId = this.generateTaskId(description);
|
|
2214
|
+
const state = {
|
|
2215
|
+
taskId,
|
|
2216
|
+
description,
|
|
2217
|
+
currentStep: steps[0],
|
|
2218
|
+
stepIndex: 1,
|
|
2219
|
+
totalSteps: steps.length,
|
|
2220
|
+
status: "awaiting_spec",
|
|
2221
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2222
|
+
};
|
|
2223
|
+
this.saveState(state);
|
|
2224
|
+
return this.responseStep(state);
|
|
2225
|
+
} catch (e) {
|
|
2226
|
+
return this.responseError(e.message);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Mark current step as done, check for questions or go to approve
|
|
2231
|
+
*/
|
|
2232
|
+
cmdDone(taskId) {
|
|
2233
|
+
const state = this.loadState(taskId);
|
|
2234
|
+
if (!state) {
|
|
2235
|
+
return this.responseError(`No workflow found: ${taskId}`, taskId);
|
|
2236
|
+
}
|
|
2237
|
+
const specPath = this.getSpecPath(taskId, state.currentStep);
|
|
2238
|
+
if (!existsSync4(specPath)) {
|
|
2239
|
+
return this.responseError(`Spec not found: ${specPath}`, taskId, state.currentStep);
|
|
2240
|
+
}
|
|
2241
|
+
const questions = this.checkUnresolvedQuestions(specPath);
|
|
2242
|
+
if (questions.length > 0) {
|
|
2243
|
+
state.status = "awaiting_clarify";
|
|
2244
|
+
this.saveState(state);
|
|
2245
|
+
return this.responseCheckpointClarify(state, questions);
|
|
2246
|
+
}
|
|
2247
|
+
state.status = "awaiting_approve";
|
|
2248
|
+
this.saveState(state);
|
|
2249
|
+
return this.responseCheckpointApprove(state);
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* Submit clarification answers
|
|
2253
|
+
*/
|
|
2254
|
+
cmdClarified(taskId, answers) {
|
|
2255
|
+
const state = this.loadState(taskId);
|
|
2256
|
+
if (!state) {
|
|
2257
|
+
return this.responseError(`No workflow found: ${taskId}`, taskId);
|
|
2258
|
+
}
|
|
2259
|
+
const specPath = this.getSpecPath(taskId, state.currentStep);
|
|
2260
|
+
if (existsSync4(specPath)) {
|
|
2261
|
+
const content = readFileSync(specPath, "utf-8");
|
|
2262
|
+
const { content: body, data } = matter(content);
|
|
2263
|
+
if (data.open_questions && Array.isArray(data.open_questions)) {
|
|
2264
|
+
data.open_questions = data.open_questions.map((q, i) => ({
|
|
2265
|
+
...q,
|
|
2266
|
+
resolved: true,
|
|
2267
|
+
answer: answers.find((a) => a.questionId === `q${i + 1}`)?.answer
|
|
2268
|
+
}));
|
|
2269
|
+
}
|
|
2270
|
+
writeFileSync3(specPath, matter.stringify(body, data));
|
|
2271
|
+
}
|
|
2272
|
+
state.status = "awaiting_spec";
|
|
2273
|
+
state.revisionFeedback = `Answers provided: ${JSON.stringify(answers)}`;
|
|
2274
|
+
this.saveState(state);
|
|
2275
|
+
return this.responseStep(state);
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* Approve current step and move to next
|
|
2279
|
+
*/
|
|
2280
|
+
cmdApprove(taskId) {
|
|
2281
|
+
const state = this.loadState(taskId);
|
|
2282
|
+
if (!state) {
|
|
2283
|
+
return this.responseError(`No workflow found: ${taskId}`, taskId);
|
|
2284
|
+
}
|
|
2285
|
+
const steps = this.getSteps();
|
|
2286
|
+
const specPath = this.getSpecPath(taskId, state.currentStep);
|
|
2287
|
+
if (existsSync4(specPath)) {
|
|
2288
|
+
const content = readFileSync(specPath, "utf-8");
|
|
2289
|
+
const { content: body, data } = matter(content);
|
|
2290
|
+
data.status = "approved";
|
|
2291
|
+
data.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2292
|
+
writeFileSync3(specPath, matter.stringify(body, data));
|
|
2293
|
+
}
|
|
2294
|
+
if (state.stepIndex < state.totalSteps) {
|
|
2295
|
+
state.currentStep = steps[state.stepIndex];
|
|
2296
|
+
state.stepIndex += 1;
|
|
2297
|
+
state.status = "awaiting_spec";
|
|
2298
|
+
state.revisionFeedback = void 0;
|
|
2299
|
+
this.saveState(state);
|
|
2300
|
+
return this.responseStep(state);
|
|
2301
|
+
} else {
|
|
2302
|
+
state.status = "completed";
|
|
2303
|
+
this.saveState(state);
|
|
2304
|
+
return this.responseComplete(state, "completed");
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Request revision with feedback
|
|
2309
|
+
*/
|
|
2310
|
+
cmdRevise(taskId, feedback) {
|
|
2311
|
+
const state = this.loadState(taskId);
|
|
2312
|
+
if (!state) {
|
|
2313
|
+
return this.responseError(`No workflow found: ${taskId}`, taskId);
|
|
2314
|
+
}
|
|
2315
|
+
state.status = "awaiting_spec";
|
|
2316
|
+
state.revisionFeedback = feedback;
|
|
2317
|
+
this.saveState(state);
|
|
2318
|
+
return this.responseStep(state);
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Reject and stop workflow
|
|
2322
|
+
*/
|
|
2323
|
+
cmdReject(taskId) {
|
|
2324
|
+
const state = this.loadState(taskId);
|
|
2325
|
+
if (!state) {
|
|
2326
|
+
return this.responseError(`No workflow found: ${taskId}`, taskId);
|
|
2327
|
+
}
|
|
2328
|
+
const specPath = this.getSpecPath(taskId, state.currentStep);
|
|
2329
|
+
if (existsSync4(specPath)) {
|
|
2330
|
+
const content = readFileSync(specPath, "utf-8");
|
|
2331
|
+
const { content: body, data } = matter(content);
|
|
2332
|
+
data.status = "rejected";
|
|
2333
|
+
data.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2334
|
+
writeFileSync3(specPath, matter.stringify(body, data));
|
|
2335
|
+
}
|
|
2336
|
+
state.status = "rejected";
|
|
2337
|
+
this.saveState(state);
|
|
2338
|
+
return this.responseComplete(state, "rejected");
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Stop workflow (can resume later)
|
|
2342
|
+
*/
|
|
2343
|
+
cmdStop(taskId) {
|
|
2344
|
+
const state = this.loadState(taskId);
|
|
2345
|
+
if (!state) {
|
|
2346
|
+
return this.responseError(`No workflow found: ${taskId}`, taskId);
|
|
2347
|
+
}
|
|
2348
|
+
state.status = "stopped";
|
|
2349
|
+
this.saveState(state);
|
|
2350
|
+
return this.responseComplete(state, "stopped");
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Get current workflow status
|
|
2354
|
+
*/
|
|
2355
|
+
cmdStatus(taskId) {
|
|
2356
|
+
const state = this.loadState(taskId);
|
|
2357
|
+
if (!state) {
|
|
2358
|
+
return this.responseError(`No workflow found: ${taskId}`, taskId);
|
|
2359
|
+
}
|
|
2360
|
+
switch (state.status) {
|
|
2361
|
+
case "awaiting_spec":
|
|
2362
|
+
return this.responseStep(state);
|
|
2363
|
+
case "awaiting_clarify": {
|
|
2364
|
+
const questions = this.checkUnresolvedQuestions(this.getSpecPath(taskId, state.currentStep));
|
|
2365
|
+
return this.responseCheckpointClarify(state, questions);
|
|
2366
|
+
}
|
|
2367
|
+
case "awaiting_approve":
|
|
2368
|
+
return this.responseCheckpointApprove(state);
|
|
2369
|
+
case "completed":
|
|
2370
|
+
case "stopped":
|
|
2371
|
+
case "rejected":
|
|
2372
|
+
return this.responseComplete(state, state.status);
|
|
2373
|
+
default:
|
|
2374
|
+
return this.responseError(`Unknown status: ${state.status}`, taskId);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
};
|
|
2378
|
+
|
|
2379
|
+
// src/commands/orchestrate.ts
|
|
2380
|
+
function outputJSON(data) {
|
|
2381
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2382
|
+
}
|
|
2383
|
+
function outputError(error) {
|
|
2384
|
+
console.log(JSON.stringify({ type: "error", error }, null, 2));
|
|
2385
|
+
process.exit(1);
|
|
2386
|
+
}
|
|
2387
|
+
async function orchestrateCommand(action, args) {
|
|
2388
|
+
try {
|
|
2389
|
+
const orchestrator = new Orchestrator();
|
|
2390
|
+
switch (action) {
|
|
2391
|
+
case "init": {
|
|
2392
|
+
const description = args[0];
|
|
2393
|
+
if (!description) {
|
|
2394
|
+
outputError("Description is required for init");
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
const result = orchestrator.cmdInit(description);
|
|
2398
|
+
outputJSON(result);
|
|
2399
|
+
break;
|
|
2400
|
+
}
|
|
2401
|
+
case "done": {
|
|
2402
|
+
const taskId = args[0];
|
|
2403
|
+
if (!taskId) {
|
|
2404
|
+
outputError("Task ID is required for done");
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
const result = orchestrator.cmdDone(taskId);
|
|
2408
|
+
outputJSON(result);
|
|
2409
|
+
break;
|
|
2410
|
+
}
|
|
2411
|
+
case "clarified": {
|
|
2412
|
+
const taskId = args[0];
|
|
2413
|
+
const answersJson = args[1];
|
|
2414
|
+
if (!taskId || !answersJson) {
|
|
2415
|
+
outputError("Task ID and answers JSON are required for clarified");
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
let answers;
|
|
2419
|
+
try {
|
|
2420
|
+
answers = JSON.parse(answersJson);
|
|
2421
|
+
} catch {
|
|
2422
|
+
outputError("Invalid JSON for answers");
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
const result = orchestrator.cmdClarified(taskId, answers);
|
|
2426
|
+
outputJSON(result);
|
|
2427
|
+
break;
|
|
2428
|
+
}
|
|
2429
|
+
case "approve": {
|
|
2430
|
+
const taskId = args[0];
|
|
2431
|
+
if (!taskId) {
|
|
2432
|
+
outputError("Task ID is required for approve");
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
const result = orchestrator.cmdApprove(taskId);
|
|
2436
|
+
outputJSON(result);
|
|
2437
|
+
break;
|
|
2438
|
+
}
|
|
2439
|
+
case "revise": {
|
|
2440
|
+
const taskId = args[0];
|
|
2441
|
+
const feedback = args[1];
|
|
2442
|
+
if (!taskId || !feedback) {
|
|
2443
|
+
outputError("Task ID and feedback are required for revise");
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
const result = orchestrator.cmdRevise(taskId, feedback);
|
|
2447
|
+
outputJSON(result);
|
|
2448
|
+
break;
|
|
2449
|
+
}
|
|
2450
|
+
case "reject": {
|
|
2451
|
+
const taskId = args[0];
|
|
2452
|
+
if (!taskId) {
|
|
2453
|
+
outputError("Task ID is required for reject");
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
const result = orchestrator.cmdReject(taskId);
|
|
2457
|
+
outputJSON(result);
|
|
2458
|
+
break;
|
|
2459
|
+
}
|
|
2460
|
+
case "stop": {
|
|
2461
|
+
const taskId = args[0];
|
|
2462
|
+
if (!taskId) {
|
|
2463
|
+
outputError("Task ID is required for stop");
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
const result = orchestrator.cmdStop(taskId);
|
|
2467
|
+
outputJSON(result);
|
|
2468
|
+
break;
|
|
2469
|
+
}
|
|
2470
|
+
case "status": {
|
|
2471
|
+
const taskId = args[0];
|
|
2472
|
+
if (!taskId) {
|
|
2473
|
+
outputError("Task ID is required for status");
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
const result = orchestrator.cmdStatus(taskId);
|
|
2477
|
+
outputJSON(result);
|
|
2478
|
+
break;
|
|
2479
|
+
}
|
|
2480
|
+
default:
|
|
2481
|
+
outputError(`Unknown action: ${action}. Valid actions: init, done, clarified, approve, revise, reject, stop, status`);
|
|
2482
|
+
}
|
|
2483
|
+
} catch (e) {
|
|
2484
|
+
outputError(e.message);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
1524
2487
|
|
|
1525
2488
|
// src/index.ts
|
|
1526
2489
|
var program = new Command();
|
|
1527
|
-
program.name("spets").description("Spec Driven Development Execution Framework").version("0.1.
|
|
2490
|
+
program.name("spets").description("Spec Driven Development Execution Framework").version("0.1.3");
|
|
1528
2491
|
program.command("init").description("Initialize spets in current directory").option("-f, --force", "Overwrite existing config").option("--github", "Add GitHub Actions workflow for PR/Issue integration").action(initCommand);
|
|
1529
2492
|
program.command("status").description("Show current workflow status").option("-t, --task <taskId>", "Show status for specific task").action(statusCommand);
|
|
1530
|
-
program.command("start").description("Start a new workflow").argument("<query>", "User query describing the task").option("
|
|
2493
|
+
program.command("start").description("Start a new workflow").argument("<query>", "User query describing the task").option("--github", "Use GitHub platform (reads owner/repo from config or git remote)").option("--issue [number]", "Use or create GitHub Issue (optional: specify existing issue number)").option("--pr [number]", "Use or create GitHub PR (optional: specify existing PR number)").action(startCommand);
|
|
1531
2494
|
program.command("resume").description("Resume paused workflow").option("-t, --task <taskId>", "Resume specific task").option("--approve", "Approve current document and proceed").option("--revise <feedback>", "Request revision with feedback").action(resumeCommand);
|
|
1532
|
-
program.command("github").description("Handle GitHub Action callback (internal)").option("--
|
|
2495
|
+
program.command("github").description("Handle GitHub Action callback (internal)").option("--pr <number>", "PR number").option("--issue <number>", "Issue number").option("-t, --task <taskId>", "Task ID").requiredOption("--comment <comment>", "Comment body").action(githubCommand);
|
|
1533
2496
|
program.command("plugin").description("Manage plugins").argument("<action>", "Action: install, uninstall, list").argument("[name]", "Plugin name").action(pluginCommand);
|
|
2497
|
+
program.command("orchestrate").description("Workflow orchestration (JSON API for Claude Code)").argument("<action>", "Action: init, done, clarified, approve, revise, reject, stop, status").argument("[args...]", "Action arguments").action(orchestrateCommand);
|
|
1534
2498
|
program.parse();
|