merge: parse ticket frontmatter as yaml
This commit is contained in:
commit
10dc6da903
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260527-000004-manual-turn-rollback
|
id: 20260527-000004-manual-turn-rollback
|
||||||
slug: manual-turn-rollback
|
slug: manual-turn-rollback
|
||||||
title: Pod/TUI: 手動 rewind 導線
|
title: 'Pod/TUI: 手動 rewind 導線'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260527-000005-memory-tool-guidance-prompt
|
id: 20260527-000005-memory-tool-guidance-prompt
|
||||||
slug: memory-tool-guidance-prompt
|
slug: memory-tool-guidance-prompt
|
||||||
title: プロンプト: memory / knowledge tool 利用タイミングのガイダンス
|
title: 'プロンプト: memory / knowledge tool 利用タイミングのガイダンス'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260527-000008-pod-scope-persistence-authority
|
id: 20260527-000008-pod-scope-persistence-authority
|
||||||
slug: pod-scope-persistence-authority
|
slug: pod-scope-persistence-authority
|
||||||
title: Pod: scope 永続化 authority の整理
|
title: 'Pod: scope 永続化 authority の整理'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260527-000012-spawnpod-initial-run-confirmation
|
id: 20260527-000012-spawnpod-initial-run-confirmation
|
||||||
slug: spawnpod-initial-run-confirmation
|
slug: spawnpod-initial-run-confirmation
|
||||||
title: SpawnPod: initial Run delivery confirmation
|
title: 'SpawnPod: initial Run delivery confirmation'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260527-000013-tickets-sh-workitem-thread-mvp
|
id: 20260527-000013-tickets-sh-workitem-thread-mvp
|
||||||
slug: tickets-sh-workitem-thread-mvp
|
slug: tickets-sh-workitem-thread-mvp
|
||||||
title: Ticket 管理: tickets.sh による WorkItem / Thread MVP
|
title: 'Ticket 管理: tickets.sh による WorkItem / Thread MVP'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260527-000014-tui-actionbar-transient-notice-api
|
id: 20260527-000014-tui-actionbar-transient-notice-api
|
||||||
slug: tui-actionbar-transient-notice-api
|
slug: tui-actionbar-transient-notice-api
|
||||||
title: TUI: actionbar transient notice API
|
title: 'TUI: actionbar transient notice API'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260527-000016-tui-picker-live-pending-pods
|
id: 20260527-000016-tui-picker-live-pending-pods
|
||||||
slug: tui-picker-live-pending-pods
|
slug: tui-picker-live-pending-pods
|
||||||
title: TUI picker: live pending Pod の表示優先と状態補完
|
title: 'TUI picker: live pending Pod の表示優先と状態補完'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260527-000017-tui-spawned-pod-panel
|
id: 20260527-000017-tui-spawned-pod-panel
|
||||||
slug: tui-spawned-pod-panel
|
slug: tui-spawned-pod-panel
|
||||||
title: TUI: spawned child Pod の一覧と一時 attach
|
title: 'TUI: spawned child Pod の一覧と一時 attach'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260528-001748-compact-session-log-exploration
|
id: 20260528-001748-compact-session-log-exploration
|
||||||
slug: compact-session-log-exploration
|
slug: compact-session-log-exploration
|
||||||
title: Compact: session log 探索型の要約入力に変更する
|
title: 'Compact: session log 探索型の要約入力に変更する'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260529-145355-manifest-profile-encrypted-secrets
|
id: 20260529-145355-manifest-profile-encrypted-secrets
|
||||||
slug: manifest-profile-encrypted-secrets
|
slug: manifest-profile-encrypted-secrets
|
||||||
title: Manifest/Profile: local key-value secret store
|
title: 'Manifest/Profile: local key-value secret store'
|
||||||
status: closed
|
status: closed
|
||||||
kind: feature
|
kind: feature
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260530-062852-refresh-stale-docs
|
id: 20260530-062852-refresh-stale-docs
|
||||||
slug: refresh-stale-docs
|
slug: refresh-stale-docs
|
||||||
title: Docs: refresh stale architecture and operation docs
|
title: 'Docs: refresh stale architecture and operation docs'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260530-204045-webfetch-readable-extraction
|
id: 20260530-204045-webfetch-readable-extraction
|
||||||
slug: webfetch-readable-extraction
|
slug: webfetch-readable-extraction
|
||||||
title: WebFetch: extract main HTML content with lightweight readability
|
title: 'WebFetch: extract main HTML content with lightweight readability'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260530-215928-webfetch-local-reader-markdown
|
id: 20260530-215928-webfetch-local-reader-markdown
|
||||||
slug: webfetch-local-reader-markdown
|
slug: webfetch-local-reader-markdown
|
||||||
title: WebFetch: replace readability dependency with Markdown-preserving local reader
|
title: 'WebFetch: replace readability dependency with Markdown-preserving local reader'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-003743-codex-gpt55-effective-context-window
|
id: 20260531-003743-codex-gpt55-effective-context-window
|
||||||
slug: codex-gpt55-effective-context-window
|
slug: codex-gpt55-effective-context-window
|
||||||
title: Provider: make codex gpt-5.5 context window effective
|
title: 'Provider: make codex gpt-5.5 context window effective'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-005557-single-binary-insomnia-cli
|
id: 20260531-005557-single-binary-insomnia-cli
|
||||||
slug: single-binary-insomnia-cli
|
slug: single-binary-insomnia-cli
|
||||||
title: CLI: migrate toward a single insomnia binary
|
title: 'CLI: migrate toward a single insomnia binary'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-022821-pod-tool-surface-restore-list
|
id: 20260531-022821-pod-tool-surface-restore-list
|
||||||
slug: pod-tool-surface-restore-list
|
slug: pod-tool-surface-restore-list
|
||||||
title: Pod tools: unify pod listing and rename restore operation
|
title: 'Pod tools: unify pod listing and rename restore operation'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-043239-insomnia-pod-subcommand-runtime
|
id: 20260531-043239-insomnia-pod-subcommand-runtime
|
||||||
slug: insomnia-pod-subcommand-runtime
|
slug: insomnia-pod-subcommand-runtime
|
||||||
title: CLI: add insomnia pod runtime entrypoint
|
title: 'CLI: add insomnia pod runtime entrypoint'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-045034-spawn-through-insomnia-pod-subcommand
|
id: 20260531-045034-spawn-through-insomnia-pod-subcommand
|
||||||
slug: spawn-through-insomnia-pod-subcommand
|
slug: spawn-through-insomnia-pod-subcommand
|
||||||
title: CLI: spawn Pods through insomnia pod runtime
|
title: 'CLI: spawn Pods through insomnia pod runtime'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-054728-remove-insomnia-pod-binary
|
id: 20260531-054728-remove-insomnia-pod-binary
|
||||||
slug: remove-insomnia-pod-binary
|
slug: remove-insomnia-pod-binary
|
||||||
title: CLI: remove insomnia-pod installed/runtime alias
|
title: 'CLI: remove insomnia-pod installed/runtime alias'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-064550-rename-pod-command-crate-to-insomnia
|
id: 20260531-064550-rename-pod-command-crate-to-insomnia
|
||||||
slug: rename-pod-command-crate-to-insomnia
|
slug: rename-pod-command-crate-to-insomnia
|
||||||
title: CLI: rename pod-command crate to insomnia
|
title: 'CLI: rename pod-command crate to insomnia'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-074258-tui-extract-cli-parsing
|
id: 20260531-074258-tui-extract-cli-parsing
|
||||||
slug: tui-extract-cli-parsing
|
slug: tui-extract-cli-parsing
|
||||||
title: TUI: extract CLI parsing from main.rs
|
title: 'TUI: extract CLI parsing from main.rs'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-074258-tui-extract-single-pod-runtime
|
id: 20260531-074258-tui-extract-single-pod-runtime
|
||||||
slug: tui-extract-single-pod-runtime
|
slug: tui-extract-single-pod-runtime
|
||||||
title: TUI: extract single-Pod runtime loop from main.rs
|
title: 'TUI: extract single-Pod runtime loop from main.rs'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-074258-tui-move-view-mode-state
|
id: 20260531-074258-tui-move-view-mode-state
|
||||||
slug: tui-move-view-mode-state
|
slug: tui-move-view-mode-state
|
||||||
title: TUI: move view mode state out of ui module
|
title: 'TUI: move view mode state out of ui module'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-082646-document-env-var-policy
|
id: 20260531-082646-document-env-var-policy
|
||||||
slug: document-env-var-policy
|
slug: document-env-var-policy
|
||||||
title: Docs: document environment variable policy
|
title: 'Docs: document environment variable policy'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-085959-eliminate-test-only-env-vars
|
id: 20260531-085959-eliminate-test-only-env-vars
|
||||||
slug: eliminate-test-only-env-vars
|
slug: eliminate-test-only-env-vars
|
||||||
title: Tests: eliminate test-only environment variables
|
title: 'Tests: eliminate test-only environment variables'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-085959-remove-insomnia-pod-command-env
|
id: 20260531-085959-remove-insomnia-pod-command-env
|
||||||
slug: remove-insomnia-pod-command-env
|
slug: remove-insomnia-pod-command-env
|
||||||
title: CLI: remove INSOMNIA_POD_COMMAND override
|
title: 'CLI: remove INSOMNIA_POD_COMMAND override'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-104614-pure-path-fallback-tests
|
id: 20260531-104614-pure-path-fallback-tests
|
||||||
slug: pure-path-fallback-tests
|
slug: pure-path-fallback-tests
|
||||||
title: Tests: make path fallback tests independent from process env
|
title: 'Tests: make path fallback tests independent from process env'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-110818-remove-resource-dir
|
id: 20260531-110818-remove-resource-dir
|
||||||
slug: remove-resource-dir
|
slug: remove-resource-dir
|
||||||
title: Manifest: remove filesystem resource_dir dependency
|
title: 'Manifest: remove filesystem resource_dir dependency'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-111956-insomnia-crate-cli-owner
|
id: 20260531-111956-insomnia-crate-cli-owner
|
||||||
slug: insomnia-crate-cli-owner
|
slug: insomnia-crate-cli-owner
|
||||||
title: CLI: make insomnia crate own binary entrypoint and CLI dispatch
|
title: 'CLI: make insomnia crate own binary entrypoint and CLI dispatch'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-124040-dev-pod-runtime-command-env
|
id: 20260531-124040-dev-pod-runtime-command-env
|
||||||
slug: dev-pod-runtime-command-env
|
slug: dev-pod-runtime-command-env
|
||||||
title: Dev: add Pod runtime executable override env
|
title: 'Dev: add Pod runtime executable override env'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-223506-memory-prompt-conditional-lookup
|
id: 20260531-223506-memory-prompt-conditional-lookup
|
||||||
slug: memory-prompt-conditional-lookup
|
slug: memory-prompt-conditional-lookup
|
||||||
title: Memory prompt: conditional guidance and proactive lookup
|
title: 'Memory prompt: conditional guidance and proactive lookup'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260601-013132-tui-new-session-first-message-missing
|
id: 20260601-013132-tui-new-session-first-message-missing
|
||||||
slug: tui-new-session-first-message-missing
|
slug: tui-new-session-first-message-missing
|
||||||
title: TUI: first message missing when starting a new session
|
title: 'TUI: first message missing when starting a new session'
|
||||||
status: closed
|
status: closed
|
||||||
kind: bug
|
kind: bug
|
||||||
priority: P1
|
priority: P1
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260601-020202-tui-keys-inline-viewport-ui
|
id: 20260601-020202-tui-keys-inline-viewport-ui
|
||||||
slug: tui-keys-inline-viewport-ui
|
slug: tui-keys-inline-viewport-ui
|
||||||
title: TUI: align insomnia keys UI with inline viewport style
|
title: 'TUI: align insomnia keys UI with inline viewport style'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260601-132955-tui-peer-pod-handshake-command
|
id: 20260601-132955-tui-peer-pod-handshake-command
|
||||||
slug: tui-peer-pod-handshake-command
|
slug: tui-peer-pod-handshake-command
|
||||||
title: TUI: add peer Pod handshake and messaging command
|
title: 'TUI: add peer Pod handshake and messaging command'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260603-122317-hook-public-surface-hardening
|
id: 20260603-122317-hook-public-surface-hardening
|
||||||
slug: hook-public-surface-hardening
|
slug: hook-public-surface-hardening
|
||||||
title: Hook: harden public hook surface before plugin exposure
|
title: 'Hook: harden public hook surface before plugin exposure'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P1
|
priority: P1
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260603-122317-plugin-feature-contribution-registry
|
id: 20260603-122317-plugin-feature-contribution-registry
|
||||||
slug: plugin-feature-contribution-registry
|
slug: plugin-feature-contribution-registry
|
||||||
title: Plugin: feature contribution registry for built-in and external capabilities
|
title: 'Plugin: feature contribution registry for built-in and external capabilities'
|
||||||
status: closed
|
status: closed
|
||||||
kind: feature
|
kind: feature
|
||||||
priority: P1
|
priority: P1
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260604-223500-task-tools-builtin-plugin
|
id: 20260604-223500-task-tools-builtin-plugin
|
||||||
slug: task-tools-builtin-plugin
|
slug: task-tools-builtin-plugin
|
||||||
title: Feature: extract Task tools as builtin module
|
title: 'Feature: extract Task tools as builtin module'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P1
|
priority: P1
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260604-234844-feature-api-authority-separation
|
id: 20260604-234844-feature-api-authority-separation
|
||||||
slug: feature-api-authority-separation
|
slug: feature-api-authority-separation
|
||||||
title: Feature API: separate internal modules from external-plugin authority model
|
title: 'Feature API: separate internal modules from external-plugin authority model'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P1
|
priority: P1
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260605-004807-hook-context-system-item-sink
|
id: 20260605-004807-hook-context-system-item-sink
|
||||||
slug: hook-context-system-item-sink
|
slug: hook-context-system-item-sink
|
||||||
title: Hook: add context handles for host-mediated SystemItem append
|
title: 'Hook: add context handles for host-mediated SystemItem append'
|
||||||
status: closed
|
status: closed
|
||||||
kind: feature
|
kind: feature
|
||||||
priority: P1
|
priority: P1
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260605-004807-task-feature-own-store-reminder-hooks
|
id: 20260605-004807-task-feature-own-store-reminder-hooks
|
||||||
slug: task-feature-own-store-reminder-hooks
|
slug: task-feature-own-store-reminder-hooks
|
||||||
title: Task: move TaskStore and reminders into Task feature
|
title: 'Task: move TaskStore and reminders into Task feature'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P1
|
priority: P1
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260605-025100-task-domain-in-pod-feature
|
id: 20260605-025100-task-domain-in-pod-feature
|
||||||
slug: task-domain-in-pod-feature
|
slug: task-domain-in-pod-feature
|
||||||
title: Task: move Task domain out of tools into pod built-in feature
|
title: 'Task: move Task domain out of tools into pod built-in feature'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P1
|
priority: P1
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260606-210832-remove-tui-ticket-commands
|
id: 20260606-210832-remove-tui-ticket-commands
|
||||||
slug: remove-tui-ticket-commands
|
slug: remove-tui-ticket-commands
|
||||||
title: Remove obsolete TUI :ticket commands
|
title: 'Remove obsolete TUI :ticket commands'
|
||||||
status: closed
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260527-000006-permission-default-policy
|
id: 20260527-000006-permission-default-policy
|
||||||
slug: permission-default-policy
|
slug: permission-default-policy
|
||||||
title: Permission: allow-all 既定 policy への整理
|
title: 'Permission: allow-all 既定 policy への整理'
|
||||||
status: open
|
status: open
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260527-000009-pod-session-fork
|
id: 20260527-000009-pod-session-fork
|
||||||
slug: pod-session-fork
|
slug: pod-session-fork
|
||||||
title: Pod: 任意ターンからの Fork(複数ターン巻き戻し)
|
title: 'Pod: 任意ターンからの Fork(複数ターン巻き戻し)'
|
||||||
status: open
|
status: open
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260527-000015-tui-navigation-mode-design
|
id: 20260527-000015-tui-navigation-mode-design
|
||||||
slug: tui-navigation-mode-design
|
slug: tui-navigation-mode-design
|
||||||
title: TUI: navigation mode / block focus の設計
|
title: 'TUI: navigation mode / block focus の設計'
|
||||||
status: open
|
status: open
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260527-000018-tui-user-model-setup
|
id: 20260527-000018-tui-user-model-setup
|
||||||
slug: tui-user-model-setup
|
slug: tui-user-model-setup
|
||||||
title: TUI: ユーザーマニフェストのモデル設定 wizard
|
title: 'TUI: ユーザーマニフェストのモデル設定 wizard'
|
||||||
status: open
|
status: open
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260531-010005-plugin-extension-surface
|
id: 20260531-010005-plugin-extension-surface
|
||||||
slug: plugin-extension-surface
|
slug: plugin-extension-surface
|
||||||
title: Plugin: define extension surface for hooks and tools
|
title: 'Plugin: define extension surface for hooks and tools'
|
||||||
status: open
|
status: open
|
||||||
kind: feature
|
kind: feature
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: 20260601-021104-tui-composer-history-persistence
|
id: 20260601-021104-tui-composer-history-persistence
|
||||||
slug: tui-composer-history-persistence
|
slug: tui-composer-history-persistence
|
||||||
title: TUI: persist composer input recall history per workspace
|
title: 'TUI: persist composer input recall history per workspace'
|
||||||
status: open
|
status: open
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
|
|
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3632,6 +3632,7 @@ dependencies = [
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ llm-worker = { workspace = true }
|
||||||
schemars = { workspace = true }
|
schemars = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
serde_yaml = "0.9.34"
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use fs4::fs_std::FileExt;
|
use fs4::fs_std::FileExt;
|
||||||
|
use serde_yaml::{Mapping as YamlMapping, Value as YamlValue};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
@ -789,7 +790,7 @@ impl LocalTicketBackend {
|
||||||
let meta = ticket_meta(parsed.frontmatter.clone());
|
let meta = ticket_meta(parsed.frontmatter.clone());
|
||||||
let document = TicketDocument {
|
let document = TicketDocument {
|
||||||
body: MarkdownText::new(parsed.body),
|
body: MarkdownText::new(parsed.body),
|
||||||
raw_frontmatter: parsed.frontmatter,
|
raw_frontmatter: parsed.frontmatter.raw,
|
||||||
};
|
};
|
||||||
let thread_path = dir.join("thread.md");
|
let thread_path = dir.join("thread.md");
|
||||||
let events = if thread_path.exists() {
|
let events = if thread_path.exists() {
|
||||||
|
|
@ -1021,33 +1022,52 @@ impl TicketBackend for LocalTicketBackend {
|
||||||
fs::create_dir_all(dir.join("artifacts")).map_err(|e| io_err(&dir, e))?;
|
fs::create_dir_all(dir.join("artifacts")).map_err(|e| io_err(&dir, e))?;
|
||||||
atomic_write(&dir.join("artifacts/.gitkeep"), b"")?;
|
atomic_write(&dir.join("artifacts/.gitkeep"), b"")?;
|
||||||
let mut fields = Vec::new();
|
let mut fields = Vec::new();
|
||||||
fields.push(("id".to_string(), id.clone()));
|
fields.push(("id".to_string(), format_yaml_string_scalar(&id)));
|
||||||
fields.push(("slug".to_string(), slug.clone()));
|
fields.push(("slug".to_string(), format_yaml_string_scalar(&slug)));
|
||||||
fields.push(("title".to_string(), input.title));
|
fields.push((
|
||||||
fields.push(("status".to_string(), "open".to_string()));
|
"title".to_string(),
|
||||||
fields.push(("kind".to_string(), input.kind));
|
format_yaml_string_scalar(input.title.as_str()),
|
||||||
fields.push(("priority".to_string(), input.priority));
|
));
|
||||||
|
fields.push(("status".to_string(), format_yaml_string_scalar("open")));
|
||||||
|
fields.push((
|
||||||
|
"kind".to_string(),
|
||||||
|
format_yaml_string_scalar(input.kind.as_str()),
|
||||||
|
));
|
||||||
|
fields.push((
|
||||||
|
"priority".to_string(),
|
||||||
|
format_yaml_string_scalar(input.priority.as_str()),
|
||||||
|
));
|
||||||
fields.push(("labels".to_string(), labels_yaml(&input.labels)));
|
fields.push(("labels".to_string(), labels_yaml(&input.labels)));
|
||||||
fields.push((
|
fields.push((
|
||||||
"workflow_state".to_string(),
|
"workflow_state".to_string(),
|
||||||
input
|
format_yaml_string_scalar(
|
||||||
.workflow_state
|
input
|
||||||
.unwrap_or(TicketWorkflowState::Intake)
|
.workflow_state
|
||||||
.as_str()
|
.unwrap_or(TicketWorkflowState::Intake)
|
||||||
.to_string(),
|
.as_str(),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
fields.push((
|
||||||
|
"created_at".to_string(),
|
||||||
|
format_yaml_string_scalar(&created),
|
||||||
|
));
|
||||||
|
fields.push((
|
||||||
|
"updated_at".to_string(),
|
||||||
|
format_yaml_string_scalar(&created),
|
||||||
));
|
));
|
||||||
fields.push(("created_at".to_string(), created.clone()));
|
|
||||||
fields.push(("updated_at".to_string(), created.clone()));
|
|
||||||
fields.push((
|
fields.push((
|
||||||
"assignee".to_string(),
|
"assignee".to_string(),
|
||||||
input.assignee.unwrap_or_else(|| "null".to_string()),
|
yaml_string_or_null(input.assignee.as_deref()),
|
||||||
));
|
));
|
||||||
fields.push((
|
fields.push((
|
||||||
"legacy_ticket".to_string(),
|
"legacy_ticket".to_string(),
|
||||||
input.legacy_ticket.unwrap_or_else(|| "null".to_string()),
|
yaml_string_or_null(input.legacy_ticket.as_deref()),
|
||||||
));
|
));
|
||||||
if let Some(readiness) = input.readiness {
|
if let Some(readiness) = input.readiness {
|
||||||
fields.push(("readiness".to_string(), readiness));
|
fields.push((
|
||||||
|
"readiness".to_string(),
|
||||||
|
format_yaml_string_scalar(readiness.as_str()),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if let Some(needs_preflight) = input.needs_preflight {
|
if let Some(needs_preflight) = input.needs_preflight {
|
||||||
fields.push(("needs_preflight".to_string(), needs_preflight.to_string()));
|
fields.push(("needs_preflight".to_string(), needs_preflight.to_string()));
|
||||||
|
|
@ -1056,16 +1076,28 @@ impl TicketBackend for LocalTicketBackend {
|
||||||
fields.push(("risk_flags".to_string(), labels_yaml(&input.risk_flags)));
|
fields.push(("risk_flags".to_string(), labels_yaml(&input.risk_flags)));
|
||||||
}
|
}
|
||||||
if let Some(action_required) = input.action_required {
|
if let Some(action_required) = input.action_required {
|
||||||
fields.push(("action_required".to_string(), action_required));
|
fields.push((
|
||||||
|
"action_required".to_string(),
|
||||||
|
format_yaml_string_scalar(action_required.as_str()),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if let Some(attention_required) = input.attention_required {
|
if let Some(attention_required) = input.attention_required {
|
||||||
fields.push(("attention_required".to_string(), attention_required));
|
fields.push((
|
||||||
|
"attention_required".to_string(),
|
||||||
|
format_yaml_string_scalar(attention_required.as_str()),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if let Some(queued_by) = input.queued_by {
|
if let Some(queued_by) = input.queued_by {
|
||||||
fields.push(("queued_by".to_string(), queued_by));
|
fields.push((
|
||||||
|
"queued_by".to_string(),
|
||||||
|
format_yaml_string_scalar(queued_by.as_str()),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if let Some(queued_at) = input.queued_at {
|
if let Some(queued_at) = input.queued_at {
|
||||||
fields.push(("queued_at".to_string(), queued_at));
|
fields.push((
|
||||||
|
"queued_at".to_string(),
|
||||||
|
format_yaml_string_scalar(queued_at.as_str()),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let item = serialize_item(&fields, input.body.as_str());
|
let item = serialize_item(&fields, input.body.as_str());
|
||||||
atomic_write(&dir.join("item.md"), item.as_bytes())?;
|
atomic_write(&dir.join("item.md"), item.as_bytes())?;
|
||||||
|
|
@ -1520,10 +1552,41 @@ impl Drop for BackendLock {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct ParsedItem {
|
struct ParsedItem {
|
||||||
frontmatter: BTreeMap<String, String>,
|
frontmatter: TicketItemFrontmatter,
|
||||||
body: String,
|
body: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct TicketItemFrontmatter {
|
||||||
|
id: Option<String>,
|
||||||
|
slug: Option<String>,
|
||||||
|
title: Option<String>,
|
||||||
|
status: Option<String>,
|
||||||
|
kind: Option<String>,
|
||||||
|
priority: Option<String>,
|
||||||
|
labels: Vec<String>,
|
||||||
|
created_at: Option<String>,
|
||||||
|
updated_at: Option<String>,
|
||||||
|
assignee: Option<String>,
|
||||||
|
legacy_ticket: Option<String>,
|
||||||
|
readiness: Option<String>,
|
||||||
|
needs_preflight: Option<bool>,
|
||||||
|
risk_flags: Vec<String>,
|
||||||
|
action_required: Option<String>,
|
||||||
|
workflow_state: Option<TicketWorkflowState>,
|
||||||
|
workflow_state_explicit: bool,
|
||||||
|
attention_required: Option<String>,
|
||||||
|
queued_by: Option<String>,
|
||||||
|
queued_at: Option<String>,
|
||||||
|
raw: BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TicketItemFrontmatter {
|
||||||
|
fn get(&self, key: &str) -> Option<&String> {
|
||||||
|
self.raw.get(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn read_item_file(path: &Path) -> Result<ParsedItem> {
|
fn read_item_file(path: &Path) -> Result<ParsedItem> {
|
||||||
let content = fs::read_to_string(path).map_err(|e| io_err(path, e))?;
|
let content = fs::read_to_string(path).map_err(|e| io_err(path, e))?;
|
||||||
parse_item(&content).map_err(|message| TicketError::Parse {
|
parse_item(&content).map_err(|message| TicketError::Parse {
|
||||||
|
|
@ -1540,17 +1603,15 @@ fn parse_item(content: &str) -> std::result::Result<ParsedItem, String> {
|
||||||
if first != "---" {
|
if first != "---" {
|
||||||
return Err("item.md missing frontmatter opener".to_string());
|
return Err("item.md missing frontmatter opener".to_string());
|
||||||
}
|
}
|
||||||
let mut frontmatter = BTreeMap::new();
|
|
||||||
let mut found_close = false;
|
let mut found_close = false;
|
||||||
|
let mut frontmatter_lines = Vec::new();
|
||||||
let mut body = String::new();
|
let mut body = String::new();
|
||||||
for line in &mut lines {
|
for line in &mut lines {
|
||||||
if line == "---" {
|
if line == "---" {
|
||||||
found_close = true;
|
found_close = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if let Some((key, value)) = line.split_once(':') {
|
frontmatter_lines.push(line);
|
||||||
frontmatter.insert(key.trim().to_string(), value.trim_start().to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !found_close {
|
if !found_close {
|
||||||
return Err("item.md missing frontmatter closer".to_string());
|
return Err("item.md missing frontmatter closer".to_string());
|
||||||
|
|
@ -1562,89 +1623,204 @@ fn parse_item(content: &str) -> std::result::Result<ParsedItem, String> {
|
||||||
body.push('\n');
|
body.push('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let frontmatter = parse_ticket_frontmatter(&frontmatter_lines.join("\n"))?;
|
||||||
Ok(ParsedItem { frontmatter, body })
|
Ok(ParsedItem { frontmatter, body })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ticket_meta(frontmatter: BTreeMap<String, String>) -> TicketMeta {
|
fn parse_ticket_frontmatter(content: &str) -> std::result::Result<TicketItemFrontmatter, String> {
|
||||||
let id = frontmatter.get("id").cloned().unwrap_or_default();
|
let value: YamlValue =
|
||||||
let slug = frontmatter.get("slug").cloned().unwrap_or_default();
|
serde_yaml::from_str(content).map_err(|err| format!("invalid YAML frontmatter: {err}"))?;
|
||||||
let title = frontmatter.get("title").cloned().unwrap_or_default();
|
let mapping = match value {
|
||||||
let status = frontmatter
|
YamlValue::Mapping(mapping) => mapping,
|
||||||
.get("status")
|
YamlValue::Null => YamlMapping::new(),
|
||||||
.map(|value| ExtensibleTicketStatus::from(value.as_str()))
|
other => {
|
||||||
.unwrap_or_else(|| ExtensibleTicketStatus::Other(String::new()));
|
return Err(format!(
|
||||||
let kind = frontmatter.get("kind").cloned().unwrap_or_default();
|
"frontmatter must be a YAML mapping, found {}",
|
||||||
let priority = frontmatter.get("priority").cloned().unwrap_or_default();
|
yaml_kind(&other)
|
||||||
let labels = frontmatter
|
));
|
||||||
.get("labels")
|
}
|
||||||
.map(|value| parse_yaml_list(value))
|
};
|
||||||
.unwrap_or_default();
|
|
||||||
let risk_flags = frontmatter
|
let mut raw = BTreeMap::new();
|
||||||
.get("risk_flags")
|
for (key, value) in &mapping {
|
||||||
.or_else(|| frontmatter.get("risks"))
|
let YamlValue::String(key) = key else {
|
||||||
.map(|value| parse_yaml_list(value))
|
return Err("frontmatter keys must be strings".to_string());
|
||||||
.unwrap_or_default();
|
};
|
||||||
let workflow_state_explicit = frontmatter.contains_key("workflow_state");
|
raw.insert(key.clone(), raw_frontmatter_value(value)?);
|
||||||
let workflow_state = frontmatter
|
}
|
||||||
.get("workflow_state")
|
|
||||||
.and_then(|value| TicketWorkflowState::parse(value))
|
let workflow_state_explicit = mapping.contains_key(YamlValue::String("workflow_state".into()));
|
||||||
.unwrap_or_else(|| TicketWorkflowState::default_for_status(&status));
|
let workflow_state_value = yaml_string(&mapping, "workflow_state")?;
|
||||||
TicketMeta {
|
let workflow_state = match workflow_state_value.as_deref() {
|
||||||
id,
|
Some(value) => Some(TicketWorkflowState::parse(value).ok_or_else(|| {
|
||||||
slug,
|
format!("invalid workflow_state '{value}': expected intake, ready, queued, inprogress, or done")
|
||||||
title,
|
})?),
|
||||||
status,
|
None => None,
|
||||||
kind,
|
};
|
||||||
priority,
|
|
||||||
labels,
|
Ok(TicketItemFrontmatter {
|
||||||
created_at: frontmatter.get("created_at").cloned(),
|
id: yaml_string(&mapping, "id")?,
|
||||||
updated_at: frontmatter.get("updated_at").cloned(),
|
slug: yaml_string(&mapping, "slug")?,
|
||||||
assignee: frontmatter.get("assignee").cloned().filter(|v| v != "null"),
|
title: yaml_string(&mapping, "title")?,
|
||||||
legacy_ticket: frontmatter
|
status: yaml_string(&mapping, "status")?,
|
||||||
.get("legacy_ticket")
|
kind: yaml_string(&mapping, "kind")?,
|
||||||
.cloned()
|
priority: yaml_string(&mapping, "priority")?,
|
||||||
.filter(|v| v != "null"),
|
labels: yaml_string_list(&mapping, "labels")?,
|
||||||
readiness: frontmatter.get("readiness").cloned(),
|
created_at: yaml_string(&mapping, "created_at")?,
|
||||||
needs_preflight: frontmatter
|
updated_at: yaml_string(&mapping, "updated_at")?,
|
||||||
.get("needs_preflight")
|
assignee: yaml_string(&mapping, "assignee")?,
|
||||||
.or_else(|| frontmatter.get("needs-preflight"))
|
legacy_ticket: yaml_string(&mapping, "legacy_ticket")?,
|
||||||
.and_then(|value| parse_bool(value)),
|
readiness: yaml_string(&mapping, "readiness")?,
|
||||||
risk_flags,
|
needs_preflight: yaml_bool(&mapping, "needs_preflight")?,
|
||||||
action_required: frontmatter.get("action_required").cloned(),
|
risk_flags: yaml_string_list(&mapping, "risk_flags")?,
|
||||||
|
action_required: yaml_string(&mapping, "action_required")?,
|
||||||
workflow_state,
|
workflow_state,
|
||||||
workflow_state_explicit,
|
workflow_state_explicit,
|
||||||
attention_required: frontmatter.get("attention_required").cloned(),
|
attention_required: yaml_string(&mapping, "attention_required")?,
|
||||||
queued_by: frontmatter.get("queued_by").cloned(),
|
queued_by: yaml_string(&mapping, "queued_by")?,
|
||||||
queued_at: frontmatter.get("queued_at").cloned(),
|
queued_at: yaml_string(&mapping, "queued_at")?,
|
||||||
raw: frontmatter,
|
raw,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yaml_key(key: &str) -> YamlValue {
|
||||||
|
YamlValue::String(key.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yaml_get<'a>(mapping: &'a YamlMapping, key: &str) -> Option<&'a YamlValue> {
|
||||||
|
mapping.get(yaml_key(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yaml_string(mapping: &YamlMapping, key: &str) -> std::result::Result<Option<String>, String> {
|
||||||
|
match yaml_get(mapping, key) {
|
||||||
|
Some(YamlValue::Null) | None => Ok(None),
|
||||||
|
Some(YamlValue::String(value)) => Ok(Some(value.clone())),
|
||||||
|
Some(value) => Err(format!(
|
||||||
|
"frontmatter field `{key}` must be a YAML string or null, found {}",
|
||||||
|
yaml_kind(value)
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_bool(value: &str) -> Option<bool> {
|
fn yaml_bool(mapping: &YamlMapping, key: &str) -> std::result::Result<Option<bool>, String> {
|
||||||
match value.trim() {
|
match yaml_get(mapping, key) {
|
||||||
"true" | "yes" | "1" => Some(true),
|
Some(YamlValue::Null) | None => Ok(None),
|
||||||
"false" | "no" | "0" => Some(false),
|
Some(YamlValue::Bool(value)) => Ok(Some(*value)),
|
||||||
_ => None,
|
Some(value) => Err(format!(
|
||||||
|
"frontmatter field `{key}` must be a YAML boolean or null, found {}",
|
||||||
|
yaml_kind(value)
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_yaml_list(value: &str) -> Vec<String> {
|
fn yaml_string_list(mapping: &YamlMapping, key: &str) -> std::result::Result<Vec<String>, String> {
|
||||||
let trimmed = value.trim();
|
match yaml_get(mapping, key) {
|
||||||
if let Some(inner) = trimmed.strip_prefix('[').and_then(|v| v.strip_suffix(']')) {
|
Some(YamlValue::Null) | None => Ok(Vec::new()),
|
||||||
return inner
|
Some(YamlValue::Sequence(values)) => values
|
||||||
.split(',')
|
.iter()
|
||||||
.map(|part| part.trim().trim_matches('"').trim_matches('\''))
|
.enumerate()
|
||||||
.filter(|part| !part.is_empty())
|
.map(|(idx, value)| match value {
|
||||||
.map(ToOwned::to_owned)
|
YamlValue::String(value) => Ok(value.clone()),
|
||||||
.collect();
|
other => Err(format!(
|
||||||
|
"frontmatter field `{key}` item {idx} must be a YAML string, found {}",
|
||||||
|
yaml_kind(other)
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
Some(value) => Err(format!(
|
||||||
|
"frontmatter field `{key}` must be a YAML sequence or null, found {}",
|
||||||
|
yaml_kind(value)
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
if trimmed.is_empty() || trimmed == "null" {
|
}
|
||||||
Vec::new()
|
|
||||||
} else {
|
fn raw_frontmatter_value(value: &YamlValue) -> std::result::Result<String, String> {
|
||||||
vec![trimmed.to_string()]
|
match value {
|
||||||
|
YamlValue::Null => Ok("null".to_string()),
|
||||||
|
YamlValue::Bool(value) => Ok(value.to_string()),
|
||||||
|
YamlValue::Number(value) => Ok(value.to_string()),
|
||||||
|
YamlValue::String(value) => Ok(value.clone()),
|
||||||
|
YamlValue::Sequence(values) => values
|
||||||
|
.iter()
|
||||||
|
.map(|value| match value {
|
||||||
|
YamlValue::String(value) => Ok(format_yaml_string_scalar(value)),
|
||||||
|
other => Err(format!(
|
||||||
|
"frontmatter sequence values must be strings, found {}",
|
||||||
|
yaml_kind(other)
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
|
.map(|values| format!("[{}]", values.join(", "))),
|
||||||
|
YamlValue::Mapping(_) => Err("frontmatter nested mappings are not supported".to_string()),
|
||||||
|
YamlValue::Tagged(tagged) => raw_frontmatter_value(&tagged.value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn yaml_kind(value: &YamlValue) -> &'static str {
|
||||||
|
match value {
|
||||||
|
YamlValue::Null => "null",
|
||||||
|
YamlValue::Bool(_) => "boolean",
|
||||||
|
YamlValue::Number(_) => "number",
|
||||||
|
YamlValue::String(_) => "string",
|
||||||
|
YamlValue::Sequence(_) => "sequence",
|
||||||
|
YamlValue::Mapping(_) => "mapping",
|
||||||
|
YamlValue::Tagged(_) => "tagged value",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ticket_meta(frontmatter: TicketItemFrontmatter) -> TicketMeta {
|
||||||
|
let status = frontmatter
|
||||||
|
.status
|
||||||
|
.as_deref()
|
||||||
|
.map(ExtensibleTicketStatus::from)
|
||||||
|
.unwrap_or_else(|| ExtensibleTicketStatus::Other(String::new()));
|
||||||
|
let workflow_state = frontmatter
|
||||||
|
.workflow_state
|
||||||
|
.unwrap_or_else(|| TicketWorkflowState::default_for_status(&status));
|
||||||
|
TicketMeta {
|
||||||
|
id: frontmatter.id.unwrap_or_default(),
|
||||||
|
slug: frontmatter.slug.unwrap_or_default(),
|
||||||
|
title: frontmatter.title.unwrap_or_default(),
|
||||||
|
status,
|
||||||
|
kind: frontmatter.kind.unwrap_or_default(),
|
||||||
|
priority: frontmatter.priority.unwrap_or_default(),
|
||||||
|
labels: frontmatter.labels,
|
||||||
|
created_at: frontmatter.created_at,
|
||||||
|
updated_at: frontmatter.updated_at,
|
||||||
|
assignee: frontmatter.assignee,
|
||||||
|
legacy_ticket: frontmatter.legacy_ticket,
|
||||||
|
readiness: frontmatter.readiness,
|
||||||
|
needs_preflight: frontmatter.needs_preflight,
|
||||||
|
risk_flags: frontmatter.risk_flags,
|
||||||
|
action_required: frontmatter.action_required,
|
||||||
|
workflow_state,
|
||||||
|
workflow_state_explicit: frontmatter.workflow_state_explicit,
|
||||||
|
attention_required: frontmatter.attention_required,
|
||||||
|
queued_by: frontmatter.queued_by,
|
||||||
|
queued_at: frontmatter.queued_at,
|
||||||
|
raw: frontmatter.raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_yaml_string_scalar(value: &str) -> String {
|
||||||
|
let mut out = String::from("'");
|
||||||
|
for ch in value.chars() {
|
||||||
|
if ch == '\'' {
|
||||||
|
out.push_str("''");
|
||||||
|
} else {
|
||||||
|
out.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push('\'');
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yaml_string_or_null(value: Option<&str>) -> String {
|
||||||
|
value
|
||||||
|
.map(format_yaml_string_scalar)
|
||||||
|
.unwrap_or_else(|| "null".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn labels_yaml(labels: &[String]) -> String {
|
fn labels_yaml(labels: &[String]) -> String {
|
||||||
if labels.is_empty() {
|
if labels.is_empty() {
|
||||||
return "[]".to_string();
|
return "[]".to_string();
|
||||||
|
|
@ -1655,6 +1831,7 @@ fn labels_yaml(labels: &[String]) -> String {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|label| label.trim())
|
.map(|label| label.trim())
|
||||||
.filter(|label| !label.is_empty())
|
.filter(|label| !label.is_empty())
|
||||||
|
.map(format_yaml_string_scalar)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ")
|
.join(", ")
|
||||||
)
|
)
|
||||||
|
|
@ -1697,7 +1874,7 @@ fn replace_frontmatter_fields(
|
||||||
if let Some((key, _)) = line.split_once(':') {
|
if let Some((key, _)) = line.split_once(':') {
|
||||||
let key = key.trim().to_string();
|
let key = key.trim().to_string();
|
||||||
if let Some((_, value)) = updates.iter().find(|(update_key, _)| *update_key == key) {
|
if let Some((_, value)) = updates.iter().find(|(update_key, _)| *update_key == key) {
|
||||||
*line = format!("{key}: {value}");
|
*line = format!("{key}: {}", format_yaml_string_scalar(value));
|
||||||
seen.insert(key);
|
seen.insert(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1705,7 +1882,10 @@ fn replace_frontmatter_fields(
|
||||||
let mut insert_at = end;
|
let mut insert_at = end;
|
||||||
for (key, value) in updates {
|
for (key, value) in updates {
|
||||||
if !seen.contains(*key) {
|
if !seen.contains(*key) {
|
||||||
lines.insert(insert_at, format!("{key}: {value}"));
|
lines.insert(
|
||||||
|
insert_at,
|
||||||
|
format!("{key}: {}", format_yaml_string_scalar(value)),
|
||||||
|
);
|
||||||
insert_at += 1;
|
insert_at += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2237,6 +2417,63 @@ queued_at: 2026-06-05T00:01:00Z
|
||||||
assert_eq!(meta.queued_at.as_deref(), Some("2026-06-05T00:01:00Z"));
|
assert_eq!(meta.queued_at.as_deref(), Some("2026-06-05T00:01:00Z"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yaml_frontmatter_preserves_typed_nulls_lists_bools_and_quoted_strings() {
|
||||||
|
let frontmatter = parse_ticket_frontmatter(
|
||||||
|
r#"labels:
|
||||||
|
- ticket
|
||||||
|
- backend
|
||||||
|
risk_flags: [low, local]
|
||||||
|
assignee: ~
|
||||||
|
legacy_ticket:
|
||||||
|
attention_required: null
|
||||||
|
action_required: "null"
|
||||||
|
readiness: "~"
|
||||||
|
needs_preflight: false
|
||||||
|
workflow_state: intake
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let meta = ticket_meta(frontmatter);
|
||||||
|
assert_eq!(meta.labels, vec!["ticket", "backend"]);
|
||||||
|
assert_eq!(meta.risk_flags, vec!["low", "local"]);
|
||||||
|
assert_eq!(meta.assignee, None);
|
||||||
|
assert_eq!(meta.legacy_ticket, None);
|
||||||
|
assert_eq!(meta.attention_required, None);
|
||||||
|
assert_eq!(meta.action_required.as_deref(), Some("null"));
|
||||||
|
assert_eq!(meta.readiness.as_deref(), Some("~"));
|
||||||
|
assert_eq!(meta.needs_preflight, Some(false));
|
||||||
|
assert_eq!(meta.workflow_state, TicketWorkflowState::Intake);
|
||||||
|
assert!(meta.workflow_state_explicit);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yaml_frontmatter_rejects_legacy_raw_string_fallbacks() {
|
||||||
|
let labels_error = parse_ticket_frontmatter("labels: ticket").unwrap_err();
|
||||||
|
assert!(
|
||||||
|
labels_error.contains("must be a YAML sequence"),
|
||||||
|
"{labels_error}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let bool_error = parse_ticket_frontmatter("needs_preflight: 1").unwrap_err();
|
||||||
|
assert!(
|
||||||
|
bool_error.contains("must be a YAML boolean"),
|
||||||
|
"{bool_error}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let workflow_error = parse_ticket_frontmatter("workflow_state: almost").unwrap_err();
|
||||||
|
assert!(
|
||||||
|
workflow_error.contains("invalid workflow_state"),
|
||||||
|
"{workflow_error}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yaml_frontmatter_rejects_invalid_yaml() {
|
||||||
|
let err = parse_ticket_frontmatter("labels: [ticket").unwrap_err();
|
||||||
|
assert!(err.contains("invalid YAML frontmatter"), "{err}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_writes_local_ticket_layout() {
|
fn create_writes_local_ticket_layout() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
|
|
@ -2256,6 +2493,45 @@ queued_at: 2026-06-05T00:01:00Z
|
||||||
assert!(report.is_ok(), "{:?}", report.diagnostics);
|
assert!(report.is_ok(), "{:?}", report.diagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_round_trips_numeric_looking_string_frontmatter_values() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let backend = backend(&tmp);
|
||||||
|
let mut input = NewTicket::new("123");
|
||||||
|
input.slug = Some("numeric-looking-strings".to_string());
|
||||||
|
input.labels = vec!["123".into(), "01".into()];
|
||||||
|
input.risk_flags = vec!["1".into(), "42".into()];
|
||||||
|
input.assignee = Some("42".into());
|
||||||
|
input.attention_required = Some("0".into());
|
||||||
|
input.action_required = Some("true".into());
|
||||||
|
let ticket = backend.create(input).unwrap();
|
||||||
|
|
||||||
|
let record = backend.show(TicketIdOrSlug::Id(ticket.id.clone())).unwrap();
|
||||||
|
assert_eq!(record.meta.title, "123");
|
||||||
|
assert_eq!(record.meta.labels, vec!["123", "01"]);
|
||||||
|
assert_eq!(record.meta.risk_flags, vec!["1", "42"]);
|
||||||
|
assert_eq!(record.meta.assignee.as_deref(), Some("42"));
|
||||||
|
assert_eq!(record.meta.attention_required.as_deref(), Some("0"));
|
||||||
|
assert_eq!(record.meta.action_required.as_deref(), Some("true"));
|
||||||
|
|
||||||
|
let item = fs::read_to_string(
|
||||||
|
tmp.path()
|
||||||
|
.join("tickets/open")
|
||||||
|
.join(&ticket.id)
|
||||||
|
.join("item.md"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(item.contains("title: '123'"), "{item}");
|
||||||
|
assert!(item.contains("labels: ['123', '01']"), "{item}");
|
||||||
|
assert!(item.contains("risk_flags: ['1', '42']"), "{item}");
|
||||||
|
assert!(item.contains("assignee: '42'"), "{item}");
|
||||||
|
assert!(item.contains("attention_required: '0'"), "{item}");
|
||||||
|
assert!(item.contains("action_required: 'true'"), "{item}");
|
||||||
|
|
||||||
|
let report = backend.doctor().unwrap();
|
||||||
|
assert!(report.is_ok(), "{:?}", report.diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn add_event_review_status_and_close_preserve_local_layout() {
|
fn add_event_review_status_and_close_preserve_local_layout() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
|
|
@ -2468,15 +2744,14 @@ queued_at: 2026-06-05T00:01:00Z
|
||||||
fn workflow_state_defaults_and_queue_transition_round_trip() {
|
fn workflow_state_defaults_and_queue_transition_round_trip() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
let backend = backend(&tmp);
|
let backend = backend(&tmp);
|
||||||
let mut missing_frontmatter = BTreeMap::new();
|
let missing_meta = ticket_meta(
|
||||||
missing_frontmatter.insert("status".to_string(), "open".to_string());
|
parse_ticket_frontmatter("status: open").expect("missing workflow state parses"),
|
||||||
let missing_meta = ticket_meta(missing_frontmatter);
|
);
|
||||||
assert_eq!(missing_meta.workflow_state, TicketWorkflowState::Intake);
|
assert_eq!(missing_meta.workflow_state, TicketWorkflowState::Intake);
|
||||||
assert!(!missing_meta.workflow_state_explicit);
|
assert!(!missing_meta.workflow_state_explicit);
|
||||||
|
|
||||||
let mut closed_frontmatter = BTreeMap::new();
|
let closed_meta =
|
||||||
closed_frontmatter.insert("status".to_string(), "closed".to_string());
|
ticket_meta(parse_ticket_frontmatter("status: closed").expect("closed default parses"));
|
||||||
let closed_meta = ticket_meta(closed_frontmatter);
|
|
||||||
assert_eq!(closed_meta.workflow_state, TicketWorkflowState::Done);
|
assert_eq!(closed_meta.workflow_state, TicketWorkflowState::Done);
|
||||||
assert!(!closed_meta.workflow_state_explicit);
|
assert!(!closed_meta.workflow_state_explicit);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1139,6 +1139,46 @@ mod tests {
|
||||||
assert_eq!(queued.next_action, Some(NextUserAction::Wait));
|
assert_eq!(queued.next_action, Some(NextUserAction::Wait));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_panel_treats_yaml_null_attention_required_as_unblocked_intake() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
write_ticket_config(temp.path());
|
||||||
|
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||||
|
let ticket_ref = backend
|
||||||
|
.create({
|
||||||
|
let mut input = NewTicket::new("Null Attention Intake");
|
||||||
|
input.slug = Some("null-attention-intake".to_string());
|
||||||
|
input.workflow_state = Some(TicketWorkflowState::Intake);
|
||||||
|
input
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let item_path = temp
|
||||||
|
.path()
|
||||||
|
.join(".yoi/tickets/open")
|
||||||
|
.join(&ticket_ref.id)
|
||||||
|
.join("item.md");
|
||||||
|
let item = fs::read_to_string(&item_path).unwrap();
|
||||||
|
fs::write(
|
||||||
|
&item_path,
|
||||||
|
item.replace(
|
||||||
|
"workflow_state: intake\ncreated_at:",
|
||||||
|
"workflow_state: intake\nattention_required: null\ncreated_at:",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let model = build_workspace_panel(temp.path(), &empty_pods());
|
||||||
|
let row = model
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.find(|row| row.title == "Null Attention Intake")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(row.status, "intake");
|
||||||
|
assert_eq!(row.next_action, Some(NextUserAction::Clarify));
|
||||||
|
assert_eq!(row.priority, ActionPriority::Background);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn workspace_panel_defaults_missing_open_state_to_intake_and_displays_done_state() {
|
fn workspace_panel_defaults_missing_open_state_to_intake_and_displays_done_state() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec {
|
||||||
filter = sourceFilter;
|
filter = sourceFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
cargoHash = "sha256-aG07L64sHxGKYou7dzuNuYt6xoHjIgGhlsnI5kxGmUg=";
|
cargoHash = "sha256-uxmc3RsNb+ivbe9wnJcqLRWWRjU2uloF2HMvgZ6L0dI=";
|
||||||
|
|
||||||
depsExtraArgs = {
|
depsExtraArgs = {
|
||||||
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user