chore(repo): reinitialize repository
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package humantask
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusPending Status = "pending"
|
||||
StatusAnswered Status = "answered"
|
||||
StatusCancelled Status = "cancelled"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
TopicID string `json:"topic_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
PromptMessageID string `json:"prompt_message_id"`
|
||||
Status Status `json:"status"`
|
||||
AnsweredMessageID string `json:"answered_message_id,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (r Record) Validate() error {
|
||||
if r.WorkspaceID == "" {
|
||||
return fmt.Errorf("workspace id is required")
|
||||
}
|
||||
if r.TopicID == "" {
|
||||
return fmt.Errorf("topic id is required")
|
||||
}
|
||||
if r.RoleName == "" {
|
||||
return fmt.Errorf("role name is required")
|
||||
}
|
||||
if r.PromptMessageID == "" {
|
||||
return fmt.Errorf("prompt message id is required")
|
||||
}
|
||||
switch r.Status {
|
||||
case StatusPending, StatusAnswered, StatusCancelled:
|
||||
default:
|
||||
return fmt.Errorf("invalid human task status %q", r.Status)
|
||||
}
|
||||
if r.Status == StatusAnswered && r.AnsweredMessageID == "" {
|
||||
return fmt.Errorf("answered message id is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package lane
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusDraft Status = "draft"
|
||||
StatusReady Status = "ready"
|
||||
StatusRunning Status = "running"
|
||||
StatusBlocked Status = "blocked"
|
||||
StatusSucceeded Status = "succeeded"
|
||||
StatusFailed Status = "failed"
|
||||
StatusCancelled Status = "cancelled"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
TopicID string `json:"topic_id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Purpose string `json:"purpose,omitempty"`
|
||||
Status Status `json:"status"`
|
||||
BaseBranch string `json:"base_branch"`
|
||||
BranchName string `json:"branch_name"`
|
||||
HeadCommit string `json:"head_commit,omitempty"`
|
||||
WorktreePath string `json:"worktree_path"`
|
||||
ContainerName string `json:"container_name"`
|
||||
RuntimeEndpoint string `json:"runtime_endpoint"`
|
||||
CreatedByRoleName string `json:"created_by_role_name"`
|
||||
ResultSummaryMarkdown string `json:"result_summary_markdown,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
StartedAt string `json:"started_at,omitempty"`
|
||||
CompletedAt string `json:"completed_at,omitempty"`
|
||||
}
|
||||
|
||||
func ValidateStatus(value Status) error {
|
||||
switch value {
|
||||
case StatusDraft, StatusReady, StatusRunning, StatusBlocked, StatusSucceeded, StatusFailed, StatusCancelled:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid lane status %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r Record) Validate() error {
|
||||
if r.WorkspaceID == "" {
|
||||
return fmt.Errorf("workspace id is required")
|
||||
}
|
||||
if r.TopicID == "" {
|
||||
return fmt.Errorf("topic id is required")
|
||||
}
|
||||
if r.Name == "" {
|
||||
return fmt.Errorf("lane name is required")
|
||||
}
|
||||
if r.Slug == "" {
|
||||
return fmt.Errorf("lane slug is required")
|
||||
}
|
||||
if r.CreatedByRoleName == "" {
|
||||
return fmt.Errorf("created by role is required")
|
||||
}
|
||||
return ValidateStatus(r.Status)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package lane
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"inbox/internal/base/slug"
|
||||
)
|
||||
|
||||
func DefaultBranchName(workspaceSlug, topicID, laneSlug string) string {
|
||||
return "lane/" + runtimeWorkspaceSlug(workspaceSlug) + "/" + runtimeLaneSlug(laneSlug) + "-" + runtimeTopicSuffix(topicID)
|
||||
}
|
||||
|
||||
func DefaultWorktreePath(workspaceRoot, workspaceSlug, topicID, laneSlug string) string {
|
||||
return filepath.Join(filepath.Dir(workspaceRoot), runtimeWorkspaceSlug(workspaceSlug)+"--"+runtimeLaneSlug(laneSlug)+"--"+runtimeTopicSuffix(topicID))
|
||||
}
|
||||
|
||||
func DefaultContainerName(workspaceSlug, topicID, laneSlug string) string {
|
||||
return "lane-" + runtimeWorkspaceSlug(workspaceSlug) + "-" + runtimeLaneSlug(laneSlug) + "-" + runtimeTopicSuffix(topicID)
|
||||
}
|
||||
|
||||
func runtimeWorkspaceSlug(value string) string {
|
||||
if normalized := slug.Normalize(value); normalized != "" {
|
||||
return normalized
|
||||
}
|
||||
return "workspace"
|
||||
}
|
||||
|
||||
func runtimeLaneSlug(value string) string {
|
||||
if normalized := slug.Normalize(value); normalized != "" {
|
||||
return normalized
|
||||
}
|
||||
return "lane"
|
||||
}
|
||||
|
||||
func runtimeTopicSuffix(value string) string {
|
||||
normalized := slug.Normalize(strings.TrimPrefix(strings.TrimSpace(value), "topic-"))
|
||||
if normalized == "" {
|
||||
return "topic"
|
||||
}
|
||||
if len(normalized) > 12 {
|
||||
return normalized[len(normalized)-12:]
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package lane
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRuntimeNamesIncludeTopicSuffix(t *testing.T) {
|
||||
topicID := "topic-20260317T030141Z-05c4bd6c9a45"
|
||||
|
||||
branch := DefaultBranchName("todo", topicID, "frontend-app")
|
||||
if branch != "lane/todo/frontend-app-05c4bd6c9a45" {
|
||||
t.Fatalf("unexpected branch name: %s", branch)
|
||||
}
|
||||
|
||||
worktree := DefaultWorktreePath("/tmp/inbox-worktrees/todo", "todo", topicID, "frontend-app")
|
||||
if worktree != "/tmp/inbox-worktrees/todo--frontend-app--05c4bd6c9a45" {
|
||||
t.Fatalf("unexpected worktree path: %s", worktree)
|
||||
}
|
||||
|
||||
container := DefaultContainerName("todo", topicID, "frontend-app")
|
||||
if container != "lane-todo-frontend-app-05c4bd6c9a45" {
|
||||
t.Fatalf("unexpected container name: %s", container)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package lanesync
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusApplied Status = "applied"
|
||||
StatusSkipped Status = "skipped"
|
||||
StatusFailed Status = "failed"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
TopicID string `json:"topic_id"`
|
||||
DownstreamLaneID string `json:"downstream_lane_id"`
|
||||
UpstreamLaneID string `json:"upstream_lane_id"`
|
||||
TaskID string `json:"task_id"`
|
||||
UpstreamCommit string `json:"upstream_commit"`
|
||||
MergeCommit string `json:"merge_commit,omitempty"`
|
||||
Status Status `json:"status"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
func ValidateStatus(value Status) error {
|
||||
switch value {
|
||||
case StatusApplied, StatusSkipped, StatusFailed:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid lane sync status %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r Record) Validate() error {
|
||||
if r.WorkspaceID == "" {
|
||||
return fmt.Errorf("workspace id is required")
|
||||
}
|
||||
if r.TopicID == "" {
|
||||
return fmt.Errorf("topic id is required")
|
||||
}
|
||||
if r.DownstreamLaneID == "" {
|
||||
return fmt.Errorf("downstream lane id is required")
|
||||
}
|
||||
if r.UpstreamLaneID == "" {
|
||||
return fmt.Errorf("upstream lane id is required")
|
||||
}
|
||||
if r.TaskID == "" {
|
||||
return fmt.Errorf("task id is required")
|
||||
}
|
||||
if r.UpstreamCommit == "" {
|
||||
return fmt.Errorf("upstream commit is required")
|
||||
}
|
||||
if err := ValidateStatus(r.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Status == StatusFailed && r.ErrorMessage == "" {
|
||||
return fmt.Errorf("failed lane sync must include error_message")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package message
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Type string
|
||||
|
||||
const (
|
||||
TypeChat Type = "chat"
|
||||
TypeProposal Type = "proposal"
|
||||
TypeQuestion Type = "question"
|
||||
TypeDecision Type = "decision"
|
||||
TypeSummary Type = "summary"
|
||||
)
|
||||
|
||||
type DeliveryState string
|
||||
|
||||
const (
|
||||
DeliveryPending DeliveryState = "pending"
|
||||
DeliveryReceived DeliveryState = "received"
|
||||
DeliveryArchived DeliveryState = "archived"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
TopicID string `json:"topic_id"`
|
||||
FromRoleName string `json:"from_role_name"`
|
||||
ToExpr string `json:"to_expr"`
|
||||
Type Type `json:"type"`
|
||||
Stage string `json:"stage"`
|
||||
ReplyToMessageID string `json:"reply_to_message_id,omitempty"`
|
||||
BodyMarkdown string `json:"body_markdown"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type DeliveryClaim struct {
|
||||
Message Record `json:"message"`
|
||||
RecipientRoleName string `json:"recipient_role_name"`
|
||||
State DeliveryState `json:"state"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type PendingDelivery struct {
|
||||
TopicID string `json:"topic_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
Count int `json:"count"`
|
||||
LastUpdated string `json:"last_updated"`
|
||||
}
|
||||
|
||||
func (r Record) Validate() error {
|
||||
if r.WorkspaceID == "" {
|
||||
return fmt.Errorf("workspace id is required")
|
||||
}
|
||||
if r.TopicID == "" {
|
||||
return fmt.Errorf("topic id is required")
|
||||
}
|
||||
if r.FromRoleName == "" {
|
||||
return fmt.Errorf("from role is required")
|
||||
}
|
||||
if r.ToExpr == "" {
|
||||
return fmt.Errorf("to expr is required")
|
||||
}
|
||||
switch r.Type {
|
||||
case TypeChat, TypeProposal, TypeQuestion, TypeDecision, TypeSummary:
|
||||
default:
|
||||
return fmt.Errorf("invalid message type %q", r.Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ExecutorKind string
|
||||
|
||||
const (
|
||||
ExecutorKindCodex ExecutorKind = "codex"
|
||||
ExecutorKindHuman ExecutorKind = "human"
|
||||
)
|
||||
|
||||
type PromptKind string
|
||||
|
||||
const (
|
||||
PromptSystem PromptKind = "system"
|
||||
)
|
||||
|
||||
type Definition struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
ExecutorKind ExecutorKind `json:"executor_kind"`
|
||||
Description string `json:"description"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
IsBuiltin bool `json:"is_builtin"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (d Definition) Validate() error {
|
||||
if d.Name == "" {
|
||||
return errors.New("role name is required")
|
||||
}
|
||||
if d.Title == "" {
|
||||
return errors.New("role title is required")
|
||||
}
|
||||
switch d.ExecutorKind {
|
||||
case ExecutorKindCodex, ExecutorKindHuman:
|
||||
default:
|
||||
return fmt.Errorf("invalid role executor kind %q", d.ExecutorKind)
|
||||
}
|
||||
if d.Name == "user" && d.ExecutorKind != ExecutorKindHuman {
|
||||
return fmt.Errorf("user role must use human executor")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DefaultExecutorKind(roleName string) ExecutorKind {
|
||||
if roleName == "user" {
|
||||
return ExecutorKindHuman
|
||||
}
|
||||
return ExecutorKindCodex
|
||||
}
|
||||
|
||||
func NormalizeDefinition(value Definition) Definition {
|
||||
if value.ExecutorKind == "" {
|
||||
value.ExecutorKind = DefaultExecutorKind(value.Name)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
RoleName string `json:"role_name"`
|
||||
ConfigTOML string `json:"config_toml"`
|
||||
AuthJSON string `json:"auth_json"`
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if strings.TrimSpace(c.RoleName) == "" {
|
||||
return errors.New("role name is required")
|
||||
}
|
||||
if strings.TrimSpace(c.AuthJSON) == "" {
|
||||
return nil
|
||||
}
|
||||
if !json.Valid([]byte(c.AuthJSON)) {
|
||||
return fmt.Errorf("role auth_json must be valid json")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NormalizeConfig(value Config) Config {
|
||||
value.RoleName = strings.TrimSpace(value.RoleName)
|
||||
value.ConfigTOML = strings.TrimSpace(value.ConfigTOML)
|
||||
if strings.TrimSpace(value.AuthJSON) == "" {
|
||||
value.AuthJSON = "{}"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
type Prompt struct {
|
||||
ID string `json:"id"`
|
||||
RoleName string `json:"role_name"`
|
||||
WorkspaceID string `json:"workspace_id,omitempty"`
|
||||
PromptKind PromptKind `json:"prompt_kind"`
|
||||
ContentMarkdown string `json:"content_markdown"`
|
||||
Version int `json:"version"`
|
||||
UpdatedBy string `json:"updated_by"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (p Prompt) Validate() error {
|
||||
if p.RoleName == "" {
|
||||
return errors.New("role name is required")
|
||||
}
|
||||
if p.ContentMarkdown == "" {
|
||||
return errors.New("prompt content is required")
|
||||
}
|
||||
switch p.PromptKind {
|
||||
case PromptSystem:
|
||||
default:
|
||||
return fmt.Errorf("invalid prompt kind %q", p.PromptKind)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SkillBinding struct {
|
||||
ID string `json:"id"`
|
||||
RoleName string `json:"role_name"`
|
||||
WorkspaceID string `json:"workspace_id,omitempty"`
|
||||
SkillID string `json:"skill_id"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
Config map[string]any `json:"config,omitempty"`
|
||||
Version int `json:"version"`
|
||||
UpdatedBy string `json:"updated_by"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (b SkillBinding) Validate() error {
|
||||
if b.RoleName == "" {
|
||||
return errors.New("role name is required")
|
||||
}
|
||||
if b.SkillID == "" {
|
||||
return errors.New("skill id is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package skill
|
||||
|
||||
import "errors"
|
||||
|
||||
type Definition struct {
|
||||
ID string `json:"id"`
|
||||
SkillKey string `json:"skill_key"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
SourceType string `json:"source_type"`
|
||||
ContentMarkdown string `json:"content_markdown"`
|
||||
Status string `json:"status"`
|
||||
Version int `json:"version"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (s Definition) Validate() error {
|
||||
if s.SkillKey == "" {
|
||||
return errors.New("skill key is required")
|
||||
}
|
||||
if s.Name == "" {
|
||||
return errors.New("skill name is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package task
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusDraft Status = "draft"
|
||||
StatusReady Status = "ready"
|
||||
StatusRunning Status = "running"
|
||||
StatusBlocked Status = "blocked"
|
||||
StatusSucceeded Status = "succeeded"
|
||||
StatusFailed Status = "failed"
|
||||
StatusCancelled Status = "cancelled"
|
||||
)
|
||||
|
||||
type Kind string
|
||||
|
||||
const (
|
||||
KindExecution Kind = "execution"
|
||||
KindGate Kind = "gate"
|
||||
KindVerification Kind = "verification"
|
||||
KindMilestone Kind = "milestone"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
TopicID string `json:"topic_id"`
|
||||
LaneID string `json:"lane_id"`
|
||||
Title string `json:"title"`
|
||||
BodyMarkdown string `json:"body_markdown"`
|
||||
AcceptanceMarkdown string `json:"acceptance_markdown,omitempty"`
|
||||
Kind Kind `json:"kind"`
|
||||
Deliverables []string `json:"deliverables,omitempty"`
|
||||
BatchKey string `json:"batch_key,omitempty"`
|
||||
Status Status `json:"status"`
|
||||
Priority int `json:"priority"`
|
||||
TaskOrder int `json:"task_order"`
|
||||
CreatedByRoleName string `json:"created_by_role_name"`
|
||||
BlockingReasonMarkdown string `json:"blocking_reason_markdown,omitempty"`
|
||||
ResultSummaryMarkdown string `json:"result_summary_markdown,omitempty"`
|
||||
AssignedRunID string `json:"assigned_run_id,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
StartedAt string `json:"started_at,omitempty"`
|
||||
CompletedAt string `json:"completed_at,omitempty"`
|
||||
}
|
||||
|
||||
type Dependency struct {
|
||||
TaskID string `json:"task_id"`
|
||||
DependsOnTaskID string `json:"depends_on_task_id"`
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
ID string `json:"id"`
|
||||
TaskID string `json:"task_id"`
|
||||
EventType string `json:"event_type"`
|
||||
BodyMarkdown string `json:"body_markdown,omitempty"`
|
||||
CreatedByRoleName string `json:"created_by_role_name"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func ValidateStatus(value Status) error {
|
||||
switch value {
|
||||
case StatusDraft, StatusReady, StatusRunning, StatusBlocked, StatusSucceeded, StatusFailed, StatusCancelled:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid task status %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateKind(value Kind) error {
|
||||
switch value {
|
||||
case KindExecution, KindGate, KindVerification, KindMilestone:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid task kind %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeRecord(value Record) Record {
|
||||
if value.Kind == "" {
|
||||
value.Kind = KindExecution
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (r Record) Validate() error {
|
||||
r = NormalizeRecord(r)
|
||||
if r.WorkspaceID == "" {
|
||||
return fmt.Errorf("workspace id is required")
|
||||
}
|
||||
if r.TopicID == "" {
|
||||
return fmt.Errorf("topic id is required")
|
||||
}
|
||||
if r.LaneID == "" {
|
||||
return fmt.Errorf("lane id is required")
|
||||
}
|
||||
if r.Title == "" {
|
||||
return fmt.Errorf("task title is required")
|
||||
}
|
||||
if r.BodyMarkdown == "" {
|
||||
return fmt.Errorf("task body is required")
|
||||
}
|
||||
if r.CreatedByRoleName == "" {
|
||||
return fmt.Errorf("created by role is required")
|
||||
}
|
||||
if err := ValidateKind(r.Kind); err != nil {
|
||||
return err
|
||||
}
|
||||
return ValidateStatus(r.Status)
|
||||
}
|
||||
|
||||
func (d Dependency) Validate() error {
|
||||
if d.TaskID == "" {
|
||||
return fmt.Errorf("task id is required")
|
||||
}
|
||||
if d.DependsOnTaskID == "" {
|
||||
return fmt.Errorf("depends_on task id is required")
|
||||
}
|
||||
if d.TaskID == d.DependsOnTaskID {
|
||||
return fmt.Errorf("task cannot depend on itself")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e Event) Validate() error {
|
||||
if e.TaskID == "" {
|
||||
return fmt.Errorf("task id is required")
|
||||
}
|
||||
if e.EventType == "" {
|
||||
return fmt.Errorf("event type is required")
|
||||
}
|
||||
if e.CreatedByRoleName == "" {
|
||||
return fmt.Errorf("created by role is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package taskgraph
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusDraft Status = "draft"
|
||||
StatusActive Status = "active"
|
||||
StatusSuperseded Status = "superseded"
|
||||
StatusCancelled Status = "cancelled"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
ID string `json:"id"`
|
||||
TopicID string `json:"topic_id"`
|
||||
Version int `json:"version"`
|
||||
Status Status `json:"status"`
|
||||
PlanJSON string `json:"plan_json"`
|
||||
PlanSummaryMarkdown string `json:"plan_summary_markdown"`
|
||||
CreatedByRoleName string `json:"created_by_role_name"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ConfirmedAt string `json:"confirmed_at,omitempty"`
|
||||
SupersedesGraphVersionID string `json:"supersedes_graph_version_id,omitempty"`
|
||||
}
|
||||
|
||||
func ValidateStatus(value Status) error {
|
||||
switch value {
|
||||
case StatusDraft, StatusActive, StatusSuperseded, StatusCancelled:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid task graph status %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r Record) Validate() error {
|
||||
if r.TopicID == "" {
|
||||
return fmt.Errorf("topic id is required")
|
||||
}
|
||||
if r.Version <= 0 {
|
||||
return fmt.Errorf("version must be greater than zero")
|
||||
}
|
||||
if r.PlanJSON == "" {
|
||||
return fmt.Errorf("plan json is required")
|
||||
}
|
||||
if r.CreatedByRoleName == "" {
|
||||
return fmt.Errorf("created by role is required")
|
||||
}
|
||||
return ValidateStatus(r.Status)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package topic
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Space string
|
||||
|
||||
const (
|
||||
SpaceClarify Space = "clarify"
|
||||
SpaceWorkflow Space = "workflow"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Space Space `json:"space"`
|
||||
Status string `json:"status"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
ClosedAt string `json:"closed_at,omitempty"`
|
||||
}
|
||||
|
||||
func (r Record) Validate() error {
|
||||
if r.WorkspaceID == "" {
|
||||
return fmt.Errorf("workspace id is required")
|
||||
}
|
||||
if r.Slug == "" {
|
||||
return fmt.Errorf("topic slug is required")
|
||||
}
|
||||
if r.Title == "" {
|
||||
return fmt.Errorf("topic title is required")
|
||||
}
|
||||
switch r.Space {
|
||||
case SpaceClarify, SpaceWorkflow:
|
||||
default:
|
||||
return fmt.Errorf("invalid topic space %q", r.Space)
|
||||
}
|
||||
if r.Status == "" {
|
||||
return fmt.Errorf("topic status is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package workflow
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Stage string
|
||||
|
||||
const (
|
||||
StageClarification Stage = "clarification"
|
||||
StageFreeze Stage = "freeze"
|
||||
StagePlan Stage = "plan"
|
||||
StageReview Stage = "review"
|
||||
StageExecution Stage = "execution"
|
||||
StageVerification Stage = "verification"
|
||||
)
|
||||
|
||||
type RunStatus string
|
||||
|
||||
const (
|
||||
RunStatusRunning RunStatus = "running"
|
||||
RunStatusSucceeded RunStatus = "succeeded"
|
||||
RunStatusFailed RunStatus = "failed"
|
||||
RunStatusCancelled RunStatus = "cancelled"
|
||||
)
|
||||
|
||||
type LogStream string
|
||||
|
||||
const (
|
||||
LogStreamStdout LogStream = "stdout"
|
||||
LogStreamStderr LogStream = "stderr"
|
||||
LogStreamSystem LogStream = "system"
|
||||
)
|
||||
|
||||
var stageTransitions = map[Stage][]Stage{
|
||||
StageClarification: {StagePlan, StageFreeze},
|
||||
StageFreeze: {StageExecution},
|
||||
StagePlan: {StageReview, StageExecution},
|
||||
StageReview: {StageExecution},
|
||||
StageExecution: {StageVerification},
|
||||
StageVerification: {},
|
||||
}
|
||||
|
||||
func ValidateRunStatus(status RunStatus) error {
|
||||
switch status {
|
||||
case RunStatusRunning, RunStatusSucceeded, RunStatusFailed, RunStatusCancelled:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid run status %q", status)
|
||||
}
|
||||
}
|
||||
|
||||
func CanTransition(from, to Stage) bool {
|
||||
for _, candidate := range stageTransitions[from] {
|
||||
if candidate == to {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ValidateStage(stage Stage) error {
|
||||
switch stage {
|
||||
case StageClarification, StageFreeze, StagePlan, StageReview, StageExecution, StageVerification:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid workflow stage %q", stage)
|
||||
}
|
||||
}
|
||||
|
||||
type Run struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
TopicID string `json:"topic_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
Stage Stage `json:"stage"`
|
||||
Mode string `json:"mode"`
|
||||
Status RunStatus `json:"status"`
|
||||
RequestMessageID string `json:"request_message_id,omitempty"`
|
||||
ConfigSnapshotJSON string `json:"config_snapshot_json"`
|
||||
CommandJSON string `json:"command_json"`
|
||||
ReplyMessageID string `json:"reply_message_id,omitempty"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
StartedAt string `json:"started_at"`
|
||||
CompletedAt string `json:"completed_at,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
}
|
||||
|
||||
func (r Run) Validate() error {
|
||||
if r.WorkspaceID == "" {
|
||||
return fmt.Errorf("workspace id is required")
|
||||
}
|
||||
if r.TopicID == "" {
|
||||
return fmt.Errorf("topic id is required")
|
||||
}
|
||||
if r.RoleName == "" {
|
||||
return fmt.Errorf("role name is required")
|
||||
}
|
||||
if err := ValidateStage(r.Stage); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ValidateRunStatus(r.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RunLog struct {
|
||||
RunID string `json:"run_id"`
|
||||
Seq int `json:"seq"`
|
||||
Stream LogStream `json:"stream"`
|
||||
Content string `json:"content"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func ValidateLogStream(stream LogStream) error {
|
||||
switch stream {
|
||||
case LogStreamStdout, LogStreamStderr, LogStreamSystem:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid log stream %q", stream)
|
||||
}
|
||||
}
|
||||
|
||||
func (l RunLog) Validate() error {
|
||||
if l.RunID == "" {
|
||||
return fmt.Errorf("run id is required")
|
||||
}
|
||||
if err := ValidateLogStream(l.Stream); err != nil {
|
||||
return err
|
||||
}
|
||||
if l.Content == "" {
|
||||
return fmt.Errorf("log content is required")
|
||||
}
|
||||
if l.Seq < 0 {
|
||||
return fmt.Errorf("log seq must be >= 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package workflow
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCanTransition(t *testing.T) {
|
||||
if !CanTransition(StagePlan, StageReview) {
|
||||
t.Fatal("expected plan -> review to be allowed")
|
||||
}
|
||||
if CanTransition(StageVerification, StagePlan) {
|
||||
t.Fatal("expected verification -> plan to be disallowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunValidateRejectsInvalidStatus(t *testing.T) {
|
||||
run := Run{
|
||||
WorkspaceID: "ws_1",
|
||||
TopicID: "topic_1",
|
||||
RoleName: "backend",
|
||||
Stage: StageExecution,
|
||||
Status: RunStatus("broken"),
|
||||
}
|
||||
if err := run.Validate(); err == nil {
|
||||
t.Fatal("Validate() expected invalid status error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLogValidate(t *testing.T) {
|
||||
log := RunLog{
|
||||
RunID: "run_1",
|
||||
Stream: LogStreamStdout,
|
||||
Content: "hello",
|
||||
}
|
||||
if err := log.Validate(); err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package workspace
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const DefaultBranch = "main"
|
||||
const ActiveStatus = "active"
|
||||
const ManagedRuntimeBackend = "host"
|
||||
const PendingProvisionState = "pending"
|
||||
|
||||
type Project struct {
|
||||
ID string `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
RootPath string `json:"root_path"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (p Project) Validate() error {
|
||||
if p.Slug == "" {
|
||||
return fmt.Errorf("project slug is required")
|
||||
}
|
||||
if p.Name == "" {
|
||||
return fmt.Errorf("project name is required")
|
||||
}
|
||||
if p.RootPath == "" {
|
||||
return fmt.Errorf("project root path is required")
|
||||
}
|
||||
if p.DefaultBranch == "" {
|
||||
return fmt.Errorf("project default branch is required")
|
||||
}
|
||||
if p.Status == "" {
|
||||
return fmt.Errorf("project status is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NormalizeProjectForCreate(value Project) Project {
|
||||
if strings.TrimSpace(value.DefaultBranch) == "" {
|
||||
value.DefaultBranch = DefaultBranch
|
||||
}
|
||||
if strings.TrimSpace(value.Status) == "" {
|
||||
value.Status = ActiveStatus
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
type Workspace struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
RootPath string `json:"root_path"`
|
||||
BaseBranch string `json:"base_branch"`
|
||||
WorktreeBranch string `json:"worktree_branch"`
|
||||
RuntimeBackend string `json:"runtime_backend"`
|
||||
Status string `json:"status"`
|
||||
ProvisionState string `json:"provision_state"`
|
||||
ProvisionError string `json:"provision_error"`
|
||||
LastProvisionedAt string `json:"last_provisioned_at,omitempty"`
|
||||
ContainerState string `json:"container_state,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (w Workspace) Validate() error {
|
||||
if w.ProjectID == "" {
|
||||
return fmt.Errorf("workspace project id is required")
|
||||
}
|
||||
if w.Slug == "" {
|
||||
return fmt.Errorf("workspace slug is required")
|
||||
}
|
||||
if w.Name == "" {
|
||||
return fmt.Errorf("workspace name is required")
|
||||
}
|
||||
if w.RootPath == "" {
|
||||
return fmt.Errorf("workspace root path is required")
|
||||
}
|
||||
if w.BaseBranch == "" {
|
||||
return fmt.Errorf("workspace base branch is required")
|
||||
}
|
||||
if w.WorktreeBranch == "" {
|
||||
return fmt.Errorf("workspace worktree branch is required")
|
||||
}
|
||||
if w.RuntimeBackend == "" {
|
||||
return fmt.Errorf("workspace runtime backend is required")
|
||||
}
|
||||
if w.Status == "" {
|
||||
return fmt.Errorf("workspace status is required")
|
||||
}
|
||||
if w.ProvisionState == "" {
|
||||
return fmt.Errorf("workspace provision state is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NormalizeWorkspaceForCreate(value Workspace) Workspace {
|
||||
if strings.TrimSpace(value.BaseBranch) == "" {
|
||||
value.BaseBranch = DefaultBranch
|
||||
}
|
||||
if strings.TrimSpace(value.WorktreeBranch) == "" {
|
||||
value.WorktreeBranch = DefaultWorktreeBranch(value.Slug)
|
||||
}
|
||||
if strings.TrimSpace(value.RuntimeBackend) == "" {
|
||||
value.RuntimeBackend = ManagedRuntimeBackend
|
||||
}
|
||||
if strings.TrimSpace(value.Status) == "" {
|
||||
value.Status = ActiveStatus
|
||||
}
|
||||
if strings.TrimSpace(value.ProvisionState) == "" {
|
||||
value.ProvisionState = PendingProvisionState
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func ApplyManagedRuntimeConfig(value Workspace, workspacesDir, baseBranch string) Workspace {
|
||||
if dir := strings.TrimSpace(workspacesDir); dir != "" {
|
||||
value.RootPath = filepath.Join(dir, value.Slug)
|
||||
}
|
||||
value.BaseBranch = strings.TrimSpace(baseBranch)
|
||||
value.WorktreeBranch = DefaultWorktreeBranch(value.Slug)
|
||||
value.RuntimeBackend = ManagedRuntimeBackend
|
||||
value.Status = ActiveStatus
|
||||
return value
|
||||
}
|
||||
|
||||
func DefaultWorktreeBranch(slug string) string {
|
||||
return "worktree/" + strings.TrimSpace(slug)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package workspace
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeProjectForCreateAppliesDefaults(t *testing.T) {
|
||||
project := NormalizeProjectForCreate(Project{})
|
||||
if project.DefaultBranch != DefaultBranch {
|
||||
t.Fatalf("default branch = %q", project.DefaultBranch)
|
||||
}
|
||||
if project.Status != ActiveStatus {
|
||||
t.Fatalf("status = %q", project.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeWorkspaceForCreateAppliesDefaults(t *testing.T) {
|
||||
ws := NormalizeWorkspaceForCreate(Workspace{Slug: "blog"})
|
||||
if ws.BaseBranch != DefaultBranch {
|
||||
t.Fatalf("base branch = %q", ws.BaseBranch)
|
||||
}
|
||||
if ws.WorktreeBranch != "worktree/blog" {
|
||||
t.Fatalf("worktree branch = %q", ws.WorktreeBranch)
|
||||
}
|
||||
if ws.RuntimeBackend != ManagedRuntimeBackend {
|
||||
t.Fatalf("runtime backend = %q", ws.RuntimeBackend)
|
||||
}
|
||||
if ws.Status != ActiveStatus {
|
||||
t.Fatalf("status = %q", ws.Status)
|
||||
}
|
||||
if ws.ProvisionState != PendingProvisionState {
|
||||
t.Fatalf("provision state = %q", ws.ProvisionState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyManagedRuntimeConfig(t *testing.T) {
|
||||
ws := ApplyManagedRuntimeConfig(Workspace{Slug: "blog"}, "/tmp/workspaces", "release")
|
||||
if ws.RootPath != "/tmp/workspaces/blog" {
|
||||
t.Fatalf("root path = %q", ws.RootPath)
|
||||
}
|
||||
if ws.BaseBranch != "release" {
|
||||
t.Fatalf("base branch = %q", ws.BaseBranch)
|
||||
}
|
||||
if ws.WorktreeBranch != "worktree/blog" {
|
||||
t.Fatalf("worktree branch = %q", ws.WorktreeBranch)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user