sequant 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +125 -160
- package/dist/bin/cli.js +59 -4
- package/dist/dashboard/server.js +1 -0
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +2 -2
- package/dist/marketplace/external_plugins/sequant/README.md +6 -3
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +92 -0
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +18 -9
- package/dist/marketplace/external_plugins/sequant/hooks/relay-check.sh +107 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/behavior-rule-detection.md +205 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +21 -8
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +302 -86
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +109 -0
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +141 -22
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +83 -78
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +377 -137
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +28 -0
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +621 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +741 -232
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +47 -1
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +12 -6
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +217 -964
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/parallel-groups.md +7 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/quality-checklist.md +75 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +4 -2
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +0 -27
- package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +24 -44
- package/dist/src/commands/abort.d.ts +36 -0
- package/dist/src/commands/abort.js +138 -0
- package/dist/src/commands/prompt.d.ts +7 -0
- package/dist/src/commands/prompt.js +101 -7
- package/dist/src/commands/ready-tui-adapter.d.ts +59 -0
- package/dist/src/commands/ready-tui-adapter.js +130 -0
- package/dist/src/commands/ready.d.ts +49 -0
- package/dist/src/commands/ready.js +243 -0
- package/dist/src/commands/run-progress.d.ts +11 -1
- package/dist/src/commands/run-progress.js +20 -3
- package/dist/src/commands/run.js +12 -2
- package/dist/src/commands/status.js +4 -0
- package/dist/src/commands/watch.d.ts +2 -0
- package/dist/src/commands/watch.js +67 -3
- package/dist/src/lib/assess-collision-detect.js +1 -1
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +39 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +34 -2
- package/dist/src/lib/cli-ui/run-renderer.js +250 -33
- package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
- package/dist/src/lib/cli-ui/scrollback-harness.js +294 -0
- package/dist/src/lib/merge-check/types.js +1 -1
- package/dist/src/lib/relay/archive.js +6 -0
- package/dist/src/lib/relay/types.d.ts +2 -0
- package/dist/src/lib/relay/types.js +9 -0
- package/dist/src/lib/settings.d.ts +34 -0
- package/dist/src/lib/settings.js +23 -1
- package/dist/src/lib/workflow/batch-executor.js +34 -18
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
- package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
- package/dist/src/lib/workflow/drivers/aider.js +9 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
- package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
- package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
- package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
- package/dist/src/lib/workflow/event-emitter.js +102 -0
- package/dist/src/lib/workflow/notice.d.ts +32 -0
- package/dist/src/lib/workflow/notice.js +38 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +9 -21
- package/dist/src/lib/workflow/phase-executor.js +105 -117
- package/dist/src/lib/workflow/phase-mapper.d.ts +26 -13
- package/dist/src/lib/workflow/phase-mapper.js +55 -33
- package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
- package/dist/src/lib/workflow/phase-registry.js +233 -0
- package/dist/src/lib/workflow/platforms/github.d.ts +6 -0
- package/dist/src/lib/workflow/platforms/github.js +17 -0
- package/dist/src/lib/workflow/ready-gate.d.ts +155 -0
- package/dist/src/lib/workflow/ready-gate.js +374 -0
- package/dist/src/lib/workflow/reconcile.js +6 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
- package/dist/src/lib/workflow/run-orchestrator.d.ts +32 -2
- package/dist/src/lib/workflow/run-orchestrator.js +125 -11
- package/dist/src/lib/workflow/state-manager.d.ts +19 -1
- package/dist/src/lib/workflow/state-manager.js +27 -1
- package/dist/src/lib/workflow/state-schema.d.ts +23 -35
- package/dist/src/lib/workflow/state-schema.js +29 -3
- package/dist/src/lib/workflow/types.d.ts +74 -15
- package/dist/src/lib/workflow/types.js +18 -13
- package/dist/src/ui/tui/App.js +8 -2
- package/dist/src/ui/tui/IssueBox.js +3 -4
- package/dist/src/ui/tui/index.d.ts +13 -4
- package/dist/src/ui/tui/index.js +19 -5
- package/dist/src/ui/tui/row-cap.d.ts +51 -0
- package/dist/src/ui/tui/row-cap.js +76 -0
- package/dist/src/ui/tui/teardown.d.ts +20 -0
- package/dist/src/ui/tui/teardown.js +29 -0
- package/dist/src/ui/tui/theme.d.ts +3 -0
- package/dist/src/ui/tui/theme.js +3 -0
- package/package.json +23 -11
- package/templates/hooks/post-tool.sh +81 -0
- package/templates/skills/assess/SKILL.md +28 -28
- package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
- package/templates/skills/qa/SKILL.md +5 -2
- package/templates/skills/setup/SKILL.md +6 -6
|
@@ -25,6 +25,13 @@ When the implementation involves 3+ independent tasks that could be parallelized
|
|
|
25
25
|
|
|
26
26
|
## Model Selection
|
|
27
27
|
|
|
28
|
+
> **Note:** Per anthropics/claude-code#43869, the `[model: ...]` annotation
|
|
29
|
+
> below and the `model=` parameter `/exec` passes to `Agent(...)` are currently
|
|
30
|
+
> ignored — every spawned subagent inherits the parent session's model. The
|
|
31
|
+
> guidance here reflects the *intended* tier for each task once upstream fixes
|
|
32
|
+
> ship; the parser in `exec/SKILL.md` is kept intact so it reactivates
|
|
33
|
+
> automatically.
|
|
34
|
+
|
|
28
35
|
Include a `[model: haiku]` or `[model: sonnet]` annotation at the end of each task line:
|
|
29
36
|
|
|
30
37
|
| Task Type | Recommended Model |
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Feature Quality Planning — Full Checklist
|
|
2
|
+
|
|
3
|
+
Use this checklist for **Complex** tier issues or when the exception-based summary in SKILL.md flags significant gaps. For Simple/Standard tiers, the exception-based approach in the main prompt is sufficient.
|
|
4
|
+
|
|
5
|
+
## Section Applicability
|
|
6
|
+
|
|
7
|
+
| Issue Type | Sections Required |
|
|
8
|
+
|------------|-------------------|
|
|
9
|
+
| Bug fix | Completeness, Error Handling, Test Coverage |
|
|
10
|
+
| New feature | All sections |
|
|
11
|
+
| Refactor | Completeness, Code Quality, Test Coverage |
|
|
12
|
+
| UI change | All sections including Polish |
|
|
13
|
+
| Backend/API | Completeness, Error Handling, Code Quality, Test Coverage, Best Practices |
|
|
14
|
+
| CLI/Script | Completeness, Error Handling, Test Coverage, Best Practices |
|
|
15
|
+
| Docs only | Completeness only |
|
|
16
|
+
|
|
17
|
+
## Completeness Check
|
|
18
|
+
|
|
19
|
+
- [ ] All AC items have corresponding implementation steps
|
|
20
|
+
- [ ] Integration points with existing features identified
|
|
21
|
+
- [ ] No partial implementations or TODOs planned
|
|
22
|
+
- [ ] State management considered (if applicable)
|
|
23
|
+
- [ ] Data flow is complete end-to-end
|
|
24
|
+
|
|
25
|
+
## Error Handling
|
|
26
|
+
|
|
27
|
+
- [ ] Invalid input scenarios identified
|
|
28
|
+
- [ ] API/external service failures handled
|
|
29
|
+
- [ ] Edge cases documented (empty, null, max values)
|
|
30
|
+
- [ ] Error messages are user-friendly
|
|
31
|
+
- [ ] Graceful degradation planned
|
|
32
|
+
|
|
33
|
+
## Code Quality
|
|
34
|
+
|
|
35
|
+
- [ ] Types fully defined (no `any` planned)
|
|
36
|
+
- [ ] Follows existing patterns in codebase
|
|
37
|
+
- [ ] Error boundaries where needed
|
|
38
|
+
- [ ] No magic strings/numbers
|
|
39
|
+
- [ ] Consistent naming conventions
|
|
40
|
+
|
|
41
|
+
## Test Coverage Plan
|
|
42
|
+
|
|
43
|
+
- [ ] Unit tests for business logic
|
|
44
|
+
- [ ] Integration tests for data flow
|
|
45
|
+
- [ ] Edge case tests identified
|
|
46
|
+
- [ ] Mocking strategy appropriate
|
|
47
|
+
- [ ] Critical paths have test coverage
|
|
48
|
+
|
|
49
|
+
## Best Practices
|
|
50
|
+
|
|
51
|
+
- [ ] Logging for debugging/observability
|
|
52
|
+
- [ ] Accessibility considerations (if UI)
|
|
53
|
+
- [ ] Performance implications considered
|
|
54
|
+
- [ ] Security reviewed (auth, validation, sanitization)
|
|
55
|
+
- [ ] Documentation updated (if behavior changes)
|
|
56
|
+
|
|
57
|
+
## Polish (UI features only)
|
|
58
|
+
|
|
59
|
+
- [ ] Loading states planned
|
|
60
|
+
- [ ] Error states have UI
|
|
61
|
+
- [ ] Empty states handled
|
|
62
|
+
- [ ] Responsive design considered
|
|
63
|
+
- [ ] Keyboard navigation works
|
|
64
|
+
|
|
65
|
+
## Derived ACs
|
|
66
|
+
|
|
67
|
+
Based on quality planning, identify additional ACs:
|
|
68
|
+
|
|
69
|
+
| Source | Derived AC | Priority |
|
|
70
|
+
|--------|-----------|----------|
|
|
71
|
+
| Error Handling | AC-N: Handle [specific error] with [specific response] | High/Medium/Low |
|
|
72
|
+
| Test Coverage | AC-N+1: Add tests for [specific scenario] | High/Medium/Low |
|
|
73
|
+
| Best Practices | AC-N+2: Add logging for [specific operation] | High/Medium/Low |
|
|
74
|
+
|
|
75
|
+
Derived ACs are numbered sequentially after original ACs.
|
package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md
CHANGED
|
@@ -14,16 +14,18 @@ This document shows the expected output format for the `## Recommended Workflow`
|
|
|
14
14
|
|
|
15
15
|
## Examples
|
|
16
16
|
|
|
17
|
-
### Simple Bug Fix
|
|
17
|
+
### Simple Bug Fix (spec confirms straightforward scope)
|
|
18
18
|
|
|
19
19
|
```markdown
|
|
20
20
|
## Recommended Workflow
|
|
21
21
|
|
|
22
22
|
**Phases:** exec → qa
|
|
23
23
|
**Quality Loop:** disabled
|
|
24
|
-
**Reasoning:**
|
|
24
|
+
**Reasoning:** This spec pass confirmed a clear root cause and narrow scope — no testgen or additional phases required; proceed to exec.
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
*Note:* Since #533, spec always runs by default. `**Phases:**` lists phases **after** spec — use `exec → qa` here to indicate "spec is done; only exec and qa remain."
|
|
28
|
+
|
|
27
29
|
### Standard Feature
|
|
28
30
|
|
|
29
31
|
```markdown
|
|
@@ -805,33 +805,6 @@ Both can be used together:
|
|
|
805
805
|
|
|
806
806
|
---
|
|
807
807
|
|
|
808
|
-
## State Tracking
|
|
809
|
-
|
|
810
|
-
**IMPORTANT:** Update workflow state when running standalone (not orchestrated).
|
|
811
|
-
|
|
812
|
-
### State Updates (Standalone Only)
|
|
813
|
-
|
|
814
|
-
When NOT orchestrated (`SEQUANT_ORCHESTRATOR` is not set):
|
|
815
|
-
|
|
816
|
-
**At skill start:**
|
|
817
|
-
```bash
|
|
818
|
-
npx tsx scripts/state/update.ts start <issue-number> test
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
**On successful completion:**
|
|
822
|
-
```bash
|
|
823
|
-
npx tsx scripts/state/update.ts complete <issue-number> test
|
|
824
|
-
```
|
|
825
|
-
|
|
826
|
-
**On failure:**
|
|
827
|
-
```bash
|
|
828
|
-
npx tsx scripts/state/update.ts fail <issue-number> test "X/Y tests failed"
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
**Why this matters:** State tracking enables dashboard visibility, resume capability, and workflow orchestration. Skills update state when standalone; orchestrators handle state when running workflows.
|
|
832
|
-
|
|
833
|
-
---
|
|
834
|
-
|
|
835
808
|
## Output Verification
|
|
836
809
|
|
|
837
810
|
**Before responding, verify your output includes ALL of these:**
|
|
@@ -39,9 +39,16 @@ When invoked as `/testgen <issue-number>`, your job is to:
|
|
|
39
39
|
- `/testgen 123` - Generate test stubs for issue #123 based on /spec comment
|
|
40
40
|
- `/testgen` - Generate stubs for the most recently discussed issue in conversation
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## Sub-Agent Delegation for Stub Generation
|
|
43
43
|
|
|
44
|
-
**Purpose:** Test stub generation is highly mechanical and
|
|
44
|
+
**Purpose:** Test stub generation is highly mechanical and is delegated to `sequant-testgen` so the main agent focuses on orchestration.
|
|
45
|
+
|
|
46
|
+
> **Upstream caveat:** `sequant-testgen` declares `model: haiku`, but per
|
|
47
|
+
> anthropics/claude-code#43869 that declaration is currently ignored — the
|
|
48
|
+
> subagent inherits the parent session's model. Older versions of this doc
|
|
49
|
+
> claimed concrete token-cost savings from haiku. Those numbers are not
|
|
50
|
+
> achievable until the upstream fix ships; treat the haiku claim as the
|
|
51
|
+
> *intended* tier, not the runtime one.
|
|
45
52
|
|
|
46
53
|
**Pattern:** Use `Agent(subagent_type="sequant-testgen")` for:
|
|
47
54
|
1. Parsing verification criteria from /spec comments
|
|
@@ -49,13 +56,12 @@ When invoked as `/testgen <issue-number>`, your job is to:
|
|
|
49
56
|
3. Writing test file content
|
|
50
57
|
|
|
51
58
|
**Benefits:**
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
- Main agent focuses on orchestration and decisions
|
|
59
|
+
- Main agent focuses on orchestration and decisions, not stub templating
|
|
60
|
+
- Designated tier (`haiku`) will yield token savings once anthropics/claude-code#43869 is fixed; today subagents inherit the parent's model
|
|
55
61
|
|
|
56
62
|
### Sub-Agent Usage
|
|
57
63
|
|
|
58
|
-
**Step 1: Parse Verification Criteria (
|
|
64
|
+
**Step 1: Parse Verification Criteria (designated haiku — currently inert per anthropics/claude-code#43869)**
|
|
59
65
|
|
|
60
66
|
```javascript
|
|
61
67
|
Agent(subagent_type="sequant-testgen", prompt=`
|
|
@@ -87,7 +93,7 @@ ${specComment}
|
|
|
87
93
|
`)
|
|
88
94
|
```
|
|
89
95
|
|
|
90
|
-
**Step 2: Generate Test Stubs (
|
|
96
|
+
**Step 2: Generate Test Stubs (designated haiku — currently inert per anthropics/claude-code#43869)**
|
|
91
97
|
|
|
92
98
|
```javascript
|
|
93
99
|
// For each AC with Unit Test or Integration Test verification method
|
|
@@ -122,16 +128,16 @@ The main agent handles file operations to ensure proper coordination:
|
|
|
122
128
|
|
|
123
129
|
| Task | Agent | Reasoning |
|
|
124
130
|
|------|-------|-----------|
|
|
125
|
-
| Parse /spec comment | haiku | Mechanical text extraction |
|
|
126
|
-
| Generate test stub code | haiku | Templated generation |
|
|
127
|
-
| Identify failure scenarios | haiku | Pattern matching |
|
|
131
|
+
| Parse /spec comment | `sequant-testgen` (declared haiku, inert per #43869) | Mechanical text extraction |
|
|
132
|
+
| Generate test stub code | `sequant-testgen` (declared haiku, inert per #43869) | Templated generation |
|
|
133
|
+
| Identify failure scenarios | `sequant-testgen` (declared haiku, inert per #43869) | Pattern matching |
|
|
128
134
|
| Decide file locations | main | Requires codebase context |
|
|
129
135
|
| Write files | main | File system coordination |
|
|
130
136
|
| Post GitHub comment | main | Session context needed |
|
|
131
137
|
|
|
132
138
|
### Parallel Sub-Agent Execution
|
|
133
139
|
|
|
134
|
-
When multiple ACs need test stubs, spawn
|
|
140
|
+
When multiple ACs need test stubs, spawn `sequant-testgen` agents in parallel (declared haiku tier, currently inert per anthropics/claude-code#43869):
|
|
135
141
|
|
|
136
142
|
```javascript
|
|
137
143
|
// Spawn all stub generation agents in a single message
|
|
@@ -143,11 +149,12 @@ const stubPromises = criteria
|
|
|
143
149
|
// Main agent writes all files
|
|
144
150
|
```
|
|
145
151
|
|
|
146
|
-
**Cost savings
|
|
147
|
-
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
**Cost savings (when upstream lands):**
|
|
153
|
+
Once anthropics/claude-code#43869 is fixed and the declared haiku tier takes
|
|
154
|
+
effect, delegating mechanical stub generation to `sequant-testgen` will
|
|
155
|
+
substantially reduce token cost vs. having the main agent generate every stub.
|
|
156
|
+
Concrete savings are not measured here because the declaration is currently
|
|
157
|
+
inert.
|
|
151
158
|
|
|
152
159
|
## Workflow
|
|
153
160
|
|
|
@@ -626,7 +633,7 @@ Cannot generate test files - no feature worktree exists for Issue #<N>.
|
|
|
626
633
|
|
|
627
634
|
**Options:**
|
|
628
635
|
1. Run `/exec <issue>` first (creates worktree automatically)
|
|
629
|
-
2. Create worktree manually: `./scripts/
|
|
636
|
+
2. Create worktree manually: `./scripts/new-feature.sh <issue>`
|
|
630
637
|
3. Use the browser/manual test scenarios from this comment
|
|
631
638
|
```
|
|
632
639
|
|
|
@@ -672,33 +679,6 @@ Generated with [Claude Code](https://claude.com/claude-code)
|
|
|
672
679
|
|
|
673
680
|
---
|
|
674
681
|
|
|
675
|
-
## State Tracking
|
|
676
|
-
|
|
677
|
-
**IMPORTANT:** Update workflow state when running standalone (not orchestrated).
|
|
678
|
-
|
|
679
|
-
### State Updates (Standalone Only)
|
|
680
|
-
|
|
681
|
-
When NOT orchestrated (`SEQUANT_ORCHESTRATOR` is not set):
|
|
682
|
-
|
|
683
|
-
**At skill start:**
|
|
684
|
-
```bash
|
|
685
|
-
npx tsx scripts/state/update.ts start <issue-number> testgen
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
**On successful completion:**
|
|
689
|
-
```bash
|
|
690
|
-
npx tsx scripts/state/update.ts complete <issue-number> testgen
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
**On failure:**
|
|
694
|
-
```bash
|
|
695
|
-
npx tsx scripts/state/update.ts fail <issue-number> testgen "Failed to generate test stubs"
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
**Note:** `/testgen` is an optional skill that generates test stubs. State tracking is informational - it doesn't block subsequent phases.
|
|
699
|
-
|
|
700
|
-
---
|
|
701
|
-
|
|
702
682
|
## Output Verification
|
|
703
683
|
|
|
704
684
|
**Before responding, verify your output includes ALL of these:**
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sequant abort <issue>` — out-of-band escape hatch for a running headless
|
|
3
|
+
* session (#645, Gap 7).
|
|
4
|
+
*
|
|
5
|
+
* `sequant prompt --type abort` queues an abort message into the inbox, which
|
|
6
|
+
* the agent must read via the PostToolUse hook chain. When that chain is
|
|
7
|
+
* broken (the bug originally reported in #645), no in-band abort can land.
|
|
8
|
+
*
|
|
9
|
+
* This command bypasses the inbox entirely: it locates the orchestrator PID
|
|
10
|
+
* via `state.json.relay.pid` (with the per-issue pidfile as fallback) and
|
|
11
|
+
* sends signals directly. The receiving end is the existing ShutdownManager
|
|
12
|
+
* in `sequant run`, which already performs a clean teardown on SIGINT/SIGTERM.
|
|
13
|
+
*/
|
|
14
|
+
export interface AbortCommandOptions {
|
|
15
|
+
/** Skip the SIGINT grace period; SIGTERM immediately. */
|
|
16
|
+
force?: boolean;
|
|
17
|
+
/** Seconds to wait after SIGINT before escalating. Default 10. */
|
|
18
|
+
graceSeconds?: number;
|
|
19
|
+
json?: boolean;
|
|
20
|
+
/** Test seam: override the system kill function. */
|
|
21
|
+
killFn?: (pid: number, signal: NodeJS.Signals) => void;
|
|
22
|
+
/** Test seam: override the liveness check. */
|
|
23
|
+
isAlive?: (pid: number) => boolean;
|
|
24
|
+
/** Test seam: override the poll interval (ms). Default 250. */
|
|
25
|
+
pollIntervalMs?: number;
|
|
26
|
+
/** Test seam: override SIGTERM grace before SIGKILL (ms). Default 3000. */
|
|
27
|
+
sigtermTimeoutMs?: number;
|
|
28
|
+
/** Test seam: override SIGKILL final wait (ms). Default 2000. */
|
|
29
|
+
sigkillTimeoutMs?: number;
|
|
30
|
+
/** Test seam: override cwd for pidfile resolution. */
|
|
31
|
+
cwd?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function abortCommand(argsAndOptions: {
|
|
34
|
+
args: string[];
|
|
35
|
+
options: AbortCommandOptions;
|
|
36
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sequant abort <issue>` — out-of-band escape hatch for a running headless
|
|
3
|
+
* session (#645, Gap 7).
|
|
4
|
+
*
|
|
5
|
+
* `sequant prompt --type abort` queues an abort message into the inbox, which
|
|
6
|
+
* the agent must read via the PostToolUse hook chain. When that chain is
|
|
7
|
+
* broken (the bug originally reported in #645), no in-band abort can land.
|
|
8
|
+
*
|
|
9
|
+
* This command bypasses the inbox entirely: it locates the orchestrator PID
|
|
10
|
+
* via `state.json.relay.pid` (with the per-issue pidfile as fallback) and
|
|
11
|
+
* sends signals directly. The receiving end is the existing ShutdownManager
|
|
12
|
+
* in `sequant run`, which already performs a clean teardown on SIGINT/SIGTERM.
|
|
13
|
+
*/
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
import { isPidAlive, readPidFile } from "../lib/relay/pid.js";
|
|
16
|
+
import { StateManager } from "../lib/workflow/state-manager.js";
|
|
17
|
+
import { findActiveIssues, resolveTargetIssue } from "./prompt.js";
|
|
18
|
+
function delay(ms) {
|
|
19
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Send `signal` and wait up to `timeoutMs` for the PID to exit. Returns true
|
|
23
|
+
* if the process died, false if still alive at the deadline.
|
|
24
|
+
*/
|
|
25
|
+
async function signalAndWait(pid, signal, timeoutMs, isAlive, killFn, pollIntervalMs) {
|
|
26
|
+
try {
|
|
27
|
+
killFn(pid, signal);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// ESRCH (process already dead) → success.
|
|
31
|
+
if (!isAlive(pid))
|
|
32
|
+
return true;
|
|
33
|
+
throw new Error(`Failed to send ${signal} to PID ${pid} (signal not delivered)`);
|
|
34
|
+
}
|
|
35
|
+
const deadline = Date.now() + timeoutMs;
|
|
36
|
+
while (Date.now() < deadline) {
|
|
37
|
+
if (!isAlive(pid))
|
|
38
|
+
return true;
|
|
39
|
+
await delay(pollIntervalMs);
|
|
40
|
+
}
|
|
41
|
+
return !isAlive(pid);
|
|
42
|
+
}
|
|
43
|
+
export async function abortCommand(argsAndOptions) {
|
|
44
|
+
const { args, options } = argsAndOptions;
|
|
45
|
+
const json = Boolean(options.json);
|
|
46
|
+
const killFn = options.killFn ??
|
|
47
|
+
((pid, signal) => {
|
|
48
|
+
process.kill(pid, signal);
|
|
49
|
+
});
|
|
50
|
+
const isAlive = options.isAlive ?? isPidAlive;
|
|
51
|
+
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
52
|
+
const cwd = options.cwd ?? process.cwd();
|
|
53
|
+
// Resolve target issue: explicit arg or single-active auto-resolve.
|
|
54
|
+
const stateManager = new StateManager();
|
|
55
|
+
let issueNumber;
|
|
56
|
+
const issueArg = args[0];
|
|
57
|
+
if (issueArg !== undefined) {
|
|
58
|
+
const n = Number.parseInt(issueArg, 10);
|
|
59
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
60
|
+
throw new Error(`Invalid issue number: '${issueArg}'`);
|
|
61
|
+
}
|
|
62
|
+
issueNumber = n;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const all = stateManager.stateExists()
|
|
66
|
+
? Object.values(await stateManager.getAllIssueStates())
|
|
67
|
+
: [];
|
|
68
|
+
const active = findActiveIssues(all, isAlive, cwd);
|
|
69
|
+
const target = resolveTargetIssue({ explicit: null, activeIssues: active });
|
|
70
|
+
issueNumber = target.issue;
|
|
71
|
+
}
|
|
72
|
+
const issueState = await stateManager.getIssueState(issueNumber);
|
|
73
|
+
const pid = issueState?.relay?.pid ?? readPidFile(issueNumber, cwd) ?? null;
|
|
74
|
+
if (pid === null) {
|
|
75
|
+
const msg = `No relay PID found for #${issueNumber}. Is the run active?`;
|
|
76
|
+
if (json) {
|
|
77
|
+
console.log(JSON.stringify({ ok: false, issue: issueNumber, error: msg }));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
console.error(chalk.yellow(msg));
|
|
81
|
+
}
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!isAlive(pid)) {
|
|
86
|
+
const msg = `PID ${pid} for #${issueNumber} is already dead.`;
|
|
87
|
+
if (json) {
|
|
88
|
+
console.log(JSON.stringify({ ok: true, issue: issueNumber, pid, signal: null }));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.log(chalk.gray(msg));
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const graceMs = Math.max(0, (options.graceSeconds ?? 10) * 1000);
|
|
96
|
+
const force = Boolean(options.force);
|
|
97
|
+
let delivered = "SIGINT";
|
|
98
|
+
let died = false;
|
|
99
|
+
if (!force) {
|
|
100
|
+
if (!json) {
|
|
101
|
+
console.log(chalk.gray(`Sending SIGINT to PID ${pid} (#${issueNumber}); waiting up to ${graceMs / 1000}s for graceful exit…`));
|
|
102
|
+
}
|
|
103
|
+
died = await signalAndWait(pid, "SIGINT", graceMs, isAlive, killFn, pollIntervalMs);
|
|
104
|
+
}
|
|
105
|
+
if (!died) {
|
|
106
|
+
delivered = "SIGTERM";
|
|
107
|
+
if (!json) {
|
|
108
|
+
console.log(chalk.yellow(force
|
|
109
|
+
? `Sending SIGTERM to PID ${pid} (#${issueNumber}) (--force)…`
|
|
110
|
+
: `Grace expired; escalating to SIGTERM…`));
|
|
111
|
+
}
|
|
112
|
+
died = await signalAndWait(pid, "SIGTERM", options.sigtermTimeoutMs ?? 3000, isAlive, killFn, pollIntervalMs);
|
|
113
|
+
}
|
|
114
|
+
if (!died) {
|
|
115
|
+
delivered = "SIGKILL";
|
|
116
|
+
if (!json) {
|
|
117
|
+
console.log(chalk.red(`SIGTERM ignored; escalating to SIGKILL on PID ${pid}…`));
|
|
118
|
+
}
|
|
119
|
+
died = await signalAndWait(pid, "SIGKILL", options.sigkillTimeoutMs ?? 2000, isAlive, killFn, pollIntervalMs);
|
|
120
|
+
}
|
|
121
|
+
if (!died) {
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
}
|
|
124
|
+
if (json) {
|
|
125
|
+
console.log(JSON.stringify({
|
|
126
|
+
ok: died,
|
|
127
|
+
issue: issueNumber,
|
|
128
|
+
pid,
|
|
129
|
+
signal: delivered,
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
else if (died) {
|
|
133
|
+
console.log(chalk.green(`Aborted #${issueNumber} (PID ${pid}, ${delivered}).`));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.error(chalk.red(`Failed to abort #${issueNumber}: PID ${pid} still alive after ${delivered}.`));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -7,6 +7,13 @@ import type { IssueState } from "../lib/workflow/state-schema.js";
|
|
|
7
7
|
export interface PromptCommandOptions {
|
|
8
8
|
type?: string;
|
|
9
9
|
json?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* If set, poll the outbox for a reply that matches the new message ID and
|
|
12
|
+
* print it inline. Exits 0 on reply, 1 on timeout (#645, Gap 4).
|
|
13
|
+
*/
|
|
14
|
+
waitSeconds?: number;
|
|
15
|
+
/** Test seam: override the poll interval (ms). Default 250. */
|
|
16
|
+
waitPollIntervalMs?: number;
|
|
10
17
|
}
|
|
11
18
|
export interface ParsedPromptArgs {
|
|
12
19
|
issue: number | null;
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* `sequant prompt <issue> "<message>"` — send a message into a running
|
|
3
3
|
* headless `sequant run` session via the interactive relay (#383).
|
|
4
4
|
*/
|
|
5
|
+
import { existsSync, statSync, readFileSync } from "fs";
|
|
5
6
|
import chalk from "chalk";
|
|
6
|
-
import { RelayMessageTypeSchema, } from "../lib/relay/types.js";
|
|
7
|
+
import { RelayMessageTypeSchema, RelayResponseSchema, } from "../lib/relay/types.js";
|
|
7
8
|
import { appendInboxMessage } from "../lib/relay/writer.js";
|
|
8
9
|
import { cleanupStalePid, readPidFile } from "../lib/relay/pid.js";
|
|
10
|
+
import { outboxPathFor } from "../lib/relay/paths.js";
|
|
9
11
|
import { StateManager } from "../lib/workflow/state-manager.js";
|
|
10
12
|
/** Validate raw CLI args. Throws on invalid type or empty message. */
|
|
11
13
|
export function parseRelayPromptArgs(args, options = {}) {
|
|
@@ -150,26 +152,118 @@ export async function promptCommand(argsAndOptions) {
|
|
|
150
152
|
catch {
|
|
151
153
|
/* swallow */
|
|
152
154
|
}
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
+
// Re-fetch state with a fresh manager to bypass any cached snapshot from
|
|
156
|
+
// the start-of-command read. Without this, `currentPhase` shown in the
|
|
157
|
+
// confirmation can be a phase-old (#645, Gap 6: user reported "exec phase"
|
|
158
|
+
// while state.json had advanced to qa).
|
|
159
|
+
let freshPhase;
|
|
160
|
+
let freshStartedAt;
|
|
161
|
+
try {
|
|
162
|
+
const freshState = await new StateManager().getIssueState(issueNumber);
|
|
163
|
+
freshPhase = freshState?.currentPhase;
|
|
164
|
+
freshStartedAt = freshState?.relay?.startedAt;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Fall back to the issueState we already have.
|
|
168
|
+
freshPhase = issueState?.currentPhase;
|
|
169
|
+
freshStartedAt = issueState?.relay?.startedAt;
|
|
170
|
+
}
|
|
155
171
|
let elapsedSegment = "";
|
|
156
|
-
if (
|
|
157
|
-
const ms = Date.now() - new Date(
|
|
172
|
+
if (freshStartedAt) {
|
|
173
|
+
const ms = Date.now() - new Date(freshStartedAt).getTime();
|
|
158
174
|
elapsedSegment = `, ${formatElapsed(ms)} elapsed`;
|
|
159
175
|
}
|
|
160
|
-
|
|
176
|
+
// Omit the phase label when we don't have a fresh reading (#645, Gap 6) —
|
|
177
|
+
// a wrong phase is worse than no phase. Callers asking for JSON still get
|
|
178
|
+
// an explicit `phase: null` for that case.
|
|
179
|
+
const phaseSegment = freshPhase
|
|
180
|
+
? ` (${freshPhase} phase${elapsedSegment})`
|
|
181
|
+
: "";
|
|
182
|
+
const confirmation = `Message sent to #${issueNumber}${phaseSegment}`;
|
|
161
183
|
if (options.json) {
|
|
162
184
|
console.log(JSON.stringify({
|
|
163
185
|
ok: true,
|
|
164
186
|
issue: issueNumber,
|
|
165
187
|
messageId: message.id,
|
|
166
188
|
type: parsed.type,
|
|
167
|
-
phase,
|
|
189
|
+
phase: freshPhase ?? null,
|
|
168
190
|
}));
|
|
169
191
|
}
|
|
170
192
|
else {
|
|
171
193
|
console.log(chalk.green(confirmation));
|
|
172
194
|
}
|
|
195
|
+
// Optional --wait: poll outbox for a reply that references our message id.
|
|
196
|
+
// Times out with exit 1 if no matching reply lands within the window.
|
|
197
|
+
if (typeof options.waitSeconds === "number" &&
|
|
198
|
+
options.waitSeconds > 0 &&
|
|
199
|
+
parsed.type !== "abort") {
|
|
200
|
+
const reply = await waitForReply({
|
|
201
|
+
issueNumber,
|
|
202
|
+
worktreePath: issueState?.worktree,
|
|
203
|
+
inReplyTo: message.id,
|
|
204
|
+
timeoutMs: options.waitSeconds * 1000,
|
|
205
|
+
pollIntervalMs: options.waitPollIntervalMs ?? 250,
|
|
206
|
+
});
|
|
207
|
+
if (reply) {
|
|
208
|
+
if (options.json) {
|
|
209
|
+
console.log(JSON.stringify({ ok: true, reply }));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
console.log(chalk.cyan(`Reply: ${reply.message}`));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const msg = `No reply received within ${options.waitSeconds}s. Message may be archived without a response.`;
|
|
217
|
+
if (options.json) {
|
|
218
|
+
console.log(JSON.stringify({ ok: false, timeout: true, error: msg }));
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
console.error(chalk.yellow(msg));
|
|
222
|
+
}
|
|
223
|
+
process.exitCode = 1;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function waitForReply(options) {
|
|
228
|
+
const outboxPath = outboxPathFor(options.issueNumber, {
|
|
229
|
+
worktreePath: options.worktreePath,
|
|
230
|
+
});
|
|
231
|
+
// Seed offset at current EOF: only NEW replies count for this prompt's wait.
|
|
232
|
+
let offset = existsSync(outboxPath) ? statSync(outboxPath).size : 0;
|
|
233
|
+
let partial = "";
|
|
234
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
235
|
+
while (Date.now() < deadline) {
|
|
236
|
+
if (existsSync(outboxPath)) {
|
|
237
|
+
try {
|
|
238
|
+
const size = statSync(outboxPath).size;
|
|
239
|
+
if (size > offset) {
|
|
240
|
+
const chunk = readFileSync(outboxPath, "utf-8").slice(offset);
|
|
241
|
+
offset = size;
|
|
242
|
+
const lines = (partial + chunk).split("\n");
|
|
243
|
+
partial = lines.pop() ?? "";
|
|
244
|
+
for (const line of lines) {
|
|
245
|
+
if (line.trim() === "")
|
|
246
|
+
continue;
|
|
247
|
+
try {
|
|
248
|
+
const parsed = RelayResponseSchema.safeParse(JSON.parse(line));
|
|
249
|
+
if (parsed.success &&
|
|
250
|
+
parsed.data.inReplyTo === options.inReplyTo) {
|
|
251
|
+
return parsed.data;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
/* skip malformed */
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
/* transient — try again */
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
await new Promise((r) => setTimeout(r, options.pollIntervalMs));
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
173
267
|
}
|
|
174
268
|
function formatElapsed(ms) {
|
|
175
269
|
const totalSec = Math.max(0, Math.floor(ms / 1000));
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-issue snapshot adapter for `sequant ready` (#699 Part A).
|
|
3
|
+
*
|
|
4
|
+
* The Ink TUI is pull-based: `App` polls `getSnapshot(): RunSnapshot` at 10 Hz
|
|
5
|
+
* (see `src/ui/tui/index.ts`). But `ready` has no `RunOrchestrator` — its
|
|
6
|
+
* progress arrives push-style via the gate's `onProgress` hook (#697). This
|
|
7
|
+
* adapter bridges the two: a small mutable tracker the gate feeds, exposing a
|
|
8
|
+
* `getSnapshot()` that returns a one-issue `RunSnapshot` for the TUI to mount.
|
|
9
|
+
*
|
|
10
|
+
* The gate fires `start`/`complete`/`failed` around each `qa`/`loop` pass; we
|
|
11
|
+
* model those passes as the phase row, with a coarse `nowLine`
|
|
12
|
+
* (`formatCoarseNowLine`) — no agent-stream enrichment (a stated Non-Goal).
|
|
13
|
+
*/
|
|
14
|
+
import type { ProgressCallback } from "../lib/workflow/types.js";
|
|
15
|
+
import { type RunSnapshot } from "../lib/workflow/run-state.js";
|
|
16
|
+
export interface ReadySnapshotAdapterOptions {
|
|
17
|
+
issueNumber: number;
|
|
18
|
+
title: string;
|
|
19
|
+
branch: string;
|
|
20
|
+
/** Surfaced in the snapshot config header; `ready` always loops. */
|
|
21
|
+
qualityLoop?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Mutable single-issue runtime tracker that doubles as a TUI snapshot provider.
|
|
25
|
+
*
|
|
26
|
+
* Lifecycle: construct → mount `renderTui(adapter)` → pass `adapter.onProgress`
|
|
27
|
+
* to the gate → on gate resolution call `markDone(ready)` so the polling `App`
|
|
28
|
+
* sees `done` and unmounts.
|
|
29
|
+
*/
|
|
30
|
+
export declare class ReadySnapshotAdapter {
|
|
31
|
+
private readonly issueNumber;
|
|
32
|
+
private readonly title;
|
|
33
|
+
private readonly branch;
|
|
34
|
+
private readonly qualityLoop;
|
|
35
|
+
private status;
|
|
36
|
+
private readonly phases;
|
|
37
|
+
private currentPhase;
|
|
38
|
+
private startedAt;
|
|
39
|
+
private completedAt;
|
|
40
|
+
private finished;
|
|
41
|
+
constructor(opts: ReadySnapshotAdapterOptions);
|
|
42
|
+
/**
|
|
43
|
+
* `ProgressCallback`-shaped sink wired into the gate's `onProgress`.
|
|
44
|
+
*
|
|
45
|
+
* - `start` → append a running phase + set `currentPhase` (coarse nowLine).
|
|
46
|
+
* - `complete` → mark the active phase done, record elapsed, clear nowLine.
|
|
47
|
+
* - `failed` → mark the active phase failed, flip issue status to failed.
|
|
48
|
+
* - `activity` → refresh the activity stamp / nowLine if a finer signal lands.
|
|
49
|
+
*/
|
|
50
|
+
readonly onProgress: ProgressCallback;
|
|
51
|
+
/**
|
|
52
|
+
* Mark the run finished after `runReadyGate` resolves. Flips the snapshot's
|
|
53
|
+
* `done` flag so the polling `App` unmounts, and sets a terminal status
|
|
54
|
+
* (failed wins if a phase already failed).
|
|
55
|
+
*/
|
|
56
|
+
markDone(ready: boolean): void;
|
|
57
|
+
/** Pull-based snapshot consumed by the TUI's 10 Hz poll loop. */
|
|
58
|
+
getSnapshot(): RunSnapshot;
|
|
59
|
+
}
|