pikuri-tasks 0.0.4

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 532d85628fc253786ae595ad4a26e47ecf6c7b0e116f14d41d211a32ebaeb084
4
+ data.tar.gz: 840465bdd7eed7f31a28b3f59ee2b1784f27cf07e2819323d66e60c1f4e4a99e
5
+ SHA512:
6
+ metadata.gz: 1ea43022b727733934abb762cf62d604c3fabde847ca7e10c0fdbd2cb53000f41c4fc83b822e023e41555eb9b610cbfd5a249decf4352bda53c447f0ea5cd050
7
+ data.tar.gz: c6f8e656609113785b062f197e79b87b5753c5cc6290b05f5442b6b1652ede03530f9ffd39f39b14ae542b64ddaf469c82e7d5cd7383999d2a3531d346da1e32
data/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # pikuri-tasks
2
+
3
+ In-memory task list + four LLM-facing tools for the
4
+ [pikuri](https://codeberg.org/mvysny/pikuri) AI-assistant toolkit.
5
+
6
+ Provides:
7
+ - `Pikuri::Tasks::List` — per-Agent in-memory list of
8
+ `(content, status)` items. Status is one of `pending`,
9
+ `in_progress`, `completed`. Nothing is written to disk.
10
+ - Four tool classes, all sharing one `List` instance:
11
+ - `Pikuri::Tasks::Create` (`task_create`) — mass-create pending
12
+ items from a newline-separated `items` string. Atomic: if any
13
+ line is a duplicate (within the batch or already on the list),
14
+ nothing is added.
15
+ - `Pikuri::Tasks::InProgress` (`task_in_progress`) — mark an item
16
+ as `in_progress` by content.
17
+ - `Pikuri::Tasks::Completed` (`task_completed`) — mark an item as
18
+ `completed` by content.
19
+ - `Pikuri::Tasks::Delete` (`task_delete`) — remove an item by
20
+ content.
21
+ - `Pikuri::Tasks::Extension` — wires the four tools + a brief
22
+ `<tasks_usage>` workflow snippet into a `Pikuri::Agent` via the
23
+ `c.add_extension(...)` block API.
24
+
25
+ Two shape choices worth flagging:
26
+ - **Status is baked into the tool name** (no `status:` parameter
27
+ with an enum). Removes the `"in-progress"` vs `"in_progress"`
28
+ vs `"inprogress"` typo failure mode on smaller models.
29
+ - **Content doubles as identifier** across the three update tools
30
+ (no item IDs to bookkeep). Duplicates are rejected on
31
+ `task_create` so the identifier stays unique.
32
+
33
+ ## Install
34
+
35
+ ```ruby
36
+ # Gemfile
37
+ gem 'pikuri-tasks'
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```ruby
43
+ require 'pikuri-core'
44
+ require 'pikuri-tasks'
45
+
46
+ agent = Pikuri::Agent.new(transport: ..., system_prompt: ...) do |c|
47
+ c.add_extension(Pikuri::Tasks::Extension.new)
48
+ end
49
+ ```
50
+
51
+ The extension's `configure` constructs a fresh `Tasks::List`,
52
+ registers all four task tools against it (so they mutate the same
53
+ instance), and appends a short `<tasks_usage>` snippet to the system
54
+ prompt explaining the workflow (create at the start, exactly one
55
+ `in_progress` at a time, `completed` only after verification).
56
+
57
+ Every mutation returns the full rendered list as its observation,
58
+ so the LLM always sees fresh state without a separate read tool:
59
+
60
+ ```
61
+ <tasks>
62
+ - [pending] Add dark mode toggle
63
+ - [in_progress] Write unit tests
64
+ - [completed] Update README
65
+ </tasks>
66
+ ```
67
+
68
+ Empty renders as `<tasks>(empty)</tasks>` so the LLM gets an
69
+ unambiguous "the call worked and the list is now empty" signal.
70
+
71
+ Sub-agents do **not** inherit extensions — a sub-agent spawned by
72
+ the `agent` tool gets a fresh persona and no task list. The
73
+ parent's plan stays private to the parent.
74
+
75
+ ## Further reading
76
+
77
+ - **API reference:** browse the YARD docs at
78
+ <https://rubydoc.info/gems/pikuri-tasks> (once published), or
79
+ run `bundle exec yard` in this directory for a local copy.
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pikuri
4
+ module Tasks
5
+ # The +task_completed+ tool: mark the item whose +content+ exactly
6
+ # matches as +completed+. Same shape and rationale as
7
+ # {InProgress} — the status is baked into the tool name to remove
8
+ # the enum-typo failure mode small models hit on
9
+ # +"completed"+ / +"complete"+ / +"done"+.
10
+ #
11
+ # Returns the rendered current list via {List#render} on success,
12
+ # or +"Error: no such task: '<content>'"+ when the content does
13
+ # not match.
14
+ class Completed < Pikuri::Tool
15
+ # @return [String]
16
+ DESCRIPTION = <<~DESC
17
+ Mark a task as `completed` once the work — including any required verification — is actually done.
18
+
19
+ Usage:
20
+ - Pass the exact `content` string the task was created with.
21
+ - Do NOT mark `completed` based on intent; mark it only after the underlying work is verified.
22
+ - If the work is partially done or blocked, leave the task `in_progress` and add a follow-up via `task_create`.
23
+ - On `Error: no such task: ...` the call did nothing — read the returned list in any subsequent tool's output to pick the right name.
24
+ - On success the full current list is returned for you to read back.
25
+ DESC
26
+
27
+ # @param list [List]
28
+ # @return [Completed]
29
+ def initialize(list:)
30
+ super(
31
+ name: 'task_completed',
32
+ description: DESCRIPTION,
33
+ parameters: Pikuri::Tool::Parameters.build { |p|
34
+ p.required_string :content,
35
+ 'Exact content of the existing task to mark as ' \
36
+ 'completed, e.g. "Add dark mode toggle".'
37
+ },
38
+ execute: lambda { |content:|
39
+ Completed.execute(list: list, content: content)
40
+ }
41
+ )
42
+ end
43
+
44
+ # @param list [List]
45
+ # @param content [String]
46
+ # @return [String]
47
+ def self.execute(list:, content:)
48
+ list.set_status(content: content, status: 'completed')
49
+ list.render
50
+ rescue ItemNotFound
51
+ "Error: no such task: '#{content}'"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pikuri
4
+ module Tasks
5
+ # The +task_create+ tool: mass-create pending items in a single
6
+ # call from a newline-separated +items+ string. Why
7
+ # newline-separated rather than a JSON array: it stays within
8
+ # pikuri's scalar-only +Tool::Parameters+ DSL (no array support
9
+ # to extend), and a smaller model never has to balance brackets
10
+ # or escape quotes — fewer formatting failure modes on the burst
11
+ # of items that opens most multi-step work.
12
+ #
13
+ # Each line is right- and left-stripped; blank lines are skipped.
14
+ # If any input is a duplicate (within the batch, or already in
15
+ # the list), the whole call aborts with an +"Error: ..."+ string
16
+ # and nothing is added — the LLM resends a corrected batch on the
17
+ # next turn. Atomic semantics keep the list in a coherent state
18
+ # the LLM doesn't have to reconcile.
19
+ #
20
+ # On success returns the rendered current list via {List#render},
21
+ # so the LLM always sees fresh state without a separate read tool.
22
+ class Create < Pikuri::Tool
23
+ # @return [String] static description shown to the LLM,
24
+ # opencode-shape (summary + +Usage:+ bullets).
25
+ DESCRIPTION = <<~DESC
26
+ Create one or more new task items in a single call. All items start as `pending`.
27
+
28
+ Usage:
29
+ - Use at the start of a multi-step task to capture the plan.
30
+ - `items` is a single newline-separated string — one task per line. Blank lines are ignored.
31
+ - Duplicate content (within the batch or already on the list) aborts the whole call with `Error: ...` and adds nothing — resend a corrected batch.
32
+ - Empty input is rejected the same way.
33
+ - On success the full current list is returned for you to read back.
34
+ DESC
35
+
36
+ # @param list [List] the shared per-Agent list, captured by
37
+ # closure so every tool in the {Extension}'s set mutates the
38
+ # same instance.
39
+ # @return [Create]
40
+ def initialize(list:)
41
+ super(
42
+ name: 'task_create',
43
+ description: DESCRIPTION,
44
+ parameters: Pikuri::Tool::Parameters.build { |p|
45
+ p.required_string :items,
46
+ 'Newline-separated list of task contents, e.g. ' \
47
+ '"Add dark mode toggle\nWrite unit tests\nUpdate README". ' \
48
+ 'Blank lines are ignored.'
49
+ },
50
+ execute: lambda { |items:|
51
+ Create.execute(list: list, items: items)
52
+ }
53
+ )
54
+ end
55
+
56
+ # Validate and apply the batch. Public so specs can drive it
57
+ # without constructing a tool instance.
58
+ #
59
+ # @param list [List]
60
+ # @param items [String] raw +items+ argument from the LLM.
61
+ # @return [String] either {List#render} on success or an
62
+ # +"Error: ..."+ string the LLM can react to.
63
+ def self.execute(list:, items:)
64
+ cleaned = items.lines.map(&:strip).reject(&:empty?)
65
+ return 'Error: task_create requires at least one non-blank item' if cleaned.empty?
66
+
67
+ seen_in_batch = {}
68
+ cleaned.each do |c|
69
+ return "Error: duplicate item in batch: '#{c}'" if seen_in_batch[c]
70
+
71
+ seen_in_batch[c] = true
72
+ end
73
+
74
+ existing = list.items.map(&:content)
75
+ clash = cleaned.find { |c| existing.include?(c) }
76
+ return "Error: task already exists: '#{clash}'" if clash
77
+
78
+ cleaned.each { |c| list.add(c) }
79
+ list.render
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pikuri
4
+ module Tasks
5
+ # The +task_delete+ tool: remove the item whose +content+ exactly
6
+ # matches from the list. Used to drop items that turned out to be
7
+ # unnecessary, were created in error, or have been superseded —
8
+ # rather than leaving them sitting in +pending+ as visual noise.
9
+ #
10
+ # Distinct from {Completed}: +completed+ means the work was
11
+ # actually done; +delete+ means the task should never have been
12
+ # there. The list itself draws no such distinction once an item
13
+ # is gone, but the LLM picks the right verb because the tool
14
+ # names make the intent clear.
15
+ #
16
+ # Returns the rendered current list via {List#render} on success,
17
+ # or +"Error: no such task: '<content>'"+ when the content does
18
+ # not match.
19
+ class Delete < Pikuri::Tool
20
+ # @return [String]
21
+ DESCRIPTION = <<~DESC
22
+ Remove a task from the list. Use this for items that turn out not to be needed, were created in error, or have been superseded.
23
+
24
+ Usage:
25
+ - Pass the exact `content` string the task was created with.
26
+ - Use `task_completed` (not this) when the work was actually done.
27
+ - On `Error: no such task: ...` the call did nothing — read the returned list in any subsequent tool's output to pick the right name.
28
+ - On success the full current list is returned for you to read back.
29
+ DESC
30
+
31
+ # @param list [List]
32
+ # @return [Delete]
33
+ def initialize(list:)
34
+ super(
35
+ name: 'task_delete',
36
+ description: DESCRIPTION,
37
+ parameters: Pikuri::Tool::Parameters.build { |p|
38
+ p.required_string :content,
39
+ 'Exact content of the existing task to remove, ' \
40
+ 'e.g. "Add dark mode toggle".'
41
+ },
42
+ execute: lambda { |content:|
43
+ Delete.execute(list: list, content: content)
44
+ }
45
+ )
46
+ end
47
+
48
+ # @param list [List]
49
+ # @param content [String]
50
+ # @return [String]
51
+ def self.execute(list:, content:)
52
+ list.delete(content)
53
+ list.render
54
+ rescue ItemNotFound
55
+ "Error: no such task: '#{content}'"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pikuri
4
+ # Namespace for the in-memory task-list feature. Holds the
5
+ # {List} value type, the four task tool classes ({Create},
6
+ # {InProgress}, {Completed}, {Delete}), and the {Extension} that
7
+ # wires them into an {Pikuri::Agent}.
8
+ module Tasks
9
+ # An {Pikuri::Agent::Extension} that auto-wires an in-memory
10
+ # task list onto an agent: constructs a fresh {List}, registers
11
+ # the four task tool classes against it, and appends a brief
12
+ # workflow snippet to the system prompt.
13
+ #
14
+ # == Usage
15
+ #
16
+ # Pikuri::Agent.new(transport: ..., system_prompt: ...) do |c|
17
+ # c.add_extension Pikuri::Tasks::Extension.new
18
+ # end
19
+ #
20
+ # The list is per-Agent and in-memory. It is dropped when the
21
+ # agent is garbage-collected — nothing is written to disk.
22
+ #
23
+ # == Sub-agents
24
+ #
25
+ # Sub-agents do not inherit extensions (see CLAUDE.md's "Seams").
26
+ # Concretely: a sub-agent spawned by the +agent+ tool gets a
27
+ # fresh persona, fresh toolset, no task list. That keeps the
28
+ # parent's plan private to the parent and avoids the
29
+ # who-owns-which-task confusion a shared list would produce.
30
+ # If a host wants the sub-agent to also have a task list, it
31
+ # adds the extension to the sub-agent's own configurator —
32
+ # cleanly opt-in, no implicit sharing.
33
+ #
34
+ # == Empty default
35
+ #
36
+ # No +catalog+-style empty state: registering the extension
37
+ # always installs the four tools and the snippet. A host that
38
+ # doesn't want tasks simply omits the extension.
39
+ class Extension
40
+ include Pikuri::Agent::Extension
41
+
42
+ # System-prompt snippet appended once per agent. Short by
43
+ # design: rules-of-thumb only, no inventory (the tool
44
+ # descriptions cover their own usage). Mirrors the shape of
45
+ # opencode's +todowrite.txt+ but condensed to fit pikuri's
46
+ # "short prose over abstract framing" docs convention.
47
+ #
48
+ # @return [String]
49
+ PROMPT_SNIPPET = <<~PROMPT
50
+ <tasks_usage>
51
+ You have an in-memory task list. Use it to plan and track multi-step work.
52
+
53
+ Workflow:
54
+ - When a task has 3+ steps, call `task_create` once with the full plan (newline-separated items, all start as `pending`).
55
+ - Before starting an item, call `task_in_progress` with its exact content. Keep exactly one item `in_progress` at a time.
56
+ - When an item is fully done (including any required verification), call `task_completed` with its exact content.
57
+ - Use `task_delete` to remove items that turn out not to be needed.
58
+
59
+ Skip task tracking entirely for single-step or purely informational requests — it adds noise, not value.
60
+
61
+ Every mutation returns the full current list, so you do not need a separate read tool. Content doubles as identifier across the four tools: spelling and capitalization must match exactly.
62
+ </tasks_usage>
63
+ PROMPT
64
+
65
+ # Tool classes the extension auto-registers. Used both for
66
+ # construction and for the duplicate-registration guard below.
67
+ TOOL_CLASSES = [Create, InProgress, Completed, Delete].freeze
68
+
69
+ # @return [Extension]
70
+ def initialize
71
+ @list = List.new
72
+ end
73
+
74
+ # @return [List] the per-agent list, exposed for tests and for
75
+ # hosts that want to render it in a UI (a future TUI could
76
+ # surface +list.items+ in a sidebar).
77
+ attr_reader :list
78
+
79
+ # Construct the four tools (each sharing +@list+) and register
80
+ # them, then append {PROMPT_SNIPPET}. Raises if any of the four
81
+ # tool classes have been pre-registered via +c.add_tool+ — the
82
+ # whole point of the extension is to be the single owner of the
83
+ # shared list, and a manually pre-registered tool would bind to
84
+ # a different list.
85
+ #
86
+ # @param c [Pikuri::Agent::Configurator]
87
+ # @return [void]
88
+ def configure(c)
89
+ TOOL_CLASSES.each do |cls|
90
+ if c.tools.any?(cls)
91
+ raise "#{cls} cannot be pre-registered (in tools: or via c.add_tool) " \
92
+ 'when adding Pikuri::Tasks::Extension — the extension auto-registers all four task tools ' \
93
+ 'so they share the same in-memory list.'
94
+ end
95
+ end
96
+
97
+ TOOL_CLASSES.each { |cls| c.add_tool(cls.new(list: @list)) }
98
+ c.append_system_prompt(PROMPT_SNIPPET)
99
+ nil
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pikuri
4
+ module Tasks
5
+ # The +task_in_progress+ tool: mark the item whose +content+
6
+ # exactly matches as +in_progress+. The status name is baked into
7
+ # the tool name rather than passed as a parameter — that takes
8
+ # one degree of freedom away from the LLM (no +"in-progress"+ vs
9
+ # +"in_progress"+ vs +"inprogress"+ typos) at the cost of one
10
+ # extra tool class.
11
+ #
12
+ # Returns the rendered current list via {List#render} on success,
13
+ # or +"Error: no such task: '<content>'"+ when the content does
14
+ # not match — the LLM can read the returned list to find the
15
+ # closest match and re-call.
16
+ class InProgress < Pikuri::Tool
17
+ # @return [String]
18
+ DESCRIPTION = <<~DESC
19
+ Mark a task as `in_progress` immediately before you start working on it.
20
+
21
+ Usage:
22
+ - Pass the exact `content` string the task was created with — content doubles as identifier; spelling and capitalization must match.
23
+ - Keep exactly one task `in_progress` at a time. Finish (or revert) the current one before starting another.
24
+ - On `Error: no such task: ...` the call did nothing — read the returned list in any subsequent tool's output to pick the right name.
25
+ - On success the full current list is returned for you to read back.
26
+ DESC
27
+
28
+ # @param list [List]
29
+ # @return [InProgress]
30
+ def initialize(list:)
31
+ super(
32
+ name: 'task_in_progress',
33
+ description: DESCRIPTION,
34
+ parameters: Pikuri::Tool::Parameters.build { |p|
35
+ p.required_string :content,
36
+ 'Exact content of the existing task to mark as ' \
37
+ 'in_progress, e.g. "Add dark mode toggle".'
38
+ },
39
+ execute: lambda { |content:|
40
+ InProgress.execute(list: list, content: content)
41
+ }
42
+ )
43
+ end
44
+
45
+ # @param list [List]
46
+ # @param content [String]
47
+ # @return [String]
48
+ def self.execute(list:, content:)
49
+ list.set_status(content: content, status: 'in_progress')
50
+ list.render
51
+ rescue ItemNotFound
52
+ "Error: no such task: '#{content}'"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pikuri
4
+ module Tasks
5
+ # Raised by {List#add} when a new item's +content+ already appears
6
+ # in the list. Content doubles as identifier in the task tools, so
7
+ # duplicates would make +task_in_progress+ / +task_completed+ /
8
+ # +task_delete+ ambiguous. The exception carries the offending
9
+ # content string so the tool layer can surface it verbatim in the
10
+ # +"Error: ..."+ observation.
11
+ class DuplicateItem < StandardError; end
12
+
13
+ # Raised by {List#set_status} and {List#delete} when no item with
14
+ # the given +content+ exists. The four task tools rescue this and
15
+ # report +"Error: no such task: <content>"+ so the LLM can correct
16
+ # the name on the next turn.
17
+ class ItemNotFound < StandardError; end
18
+
19
+ # Allowed values for {Item#status}. Kept deliberately small — three
20
+ # states match the workflow the prompt advertises (pending → in_progress
21
+ # → completed) without inviting a +cancelled+ vs +deleted+ debate.
22
+ # +task_delete+ removes items outright rather than introducing a
23
+ # fourth status.
24
+ STATUSES = %w[pending in_progress completed].freeze
25
+
26
+ # One row in a {List}. +content+ is the LLM-visible string that
27
+ # also acts as identifier across the four task tools; +status+ is
28
+ # one of {STATUSES}.
29
+ Item = Data.define(:content, :status)
30
+
31
+ # An in-memory ordered list of {Item}s, scoped to a single
32
+ # {Pikuri::Agent}. Held inside {Extension} and captured by closure
33
+ # into the +execute+ block of each of the four task tool classes,
34
+ # so every mutation hits the same instance.
35
+ #
36
+ # == Concurrency
37
+ #
38
+ # The agent loop is single-threaded with respect to tool calls
39
+ # (ruby_llm dispatches them sequentially), so no locking. A future
40
+ # parallel-tool-execution feature would need a +Mutex+ here.
41
+ #
42
+ # == Persistence
43
+ #
44
+ # None. The list is dropped when the +Agent+ is garbage-collected.
45
+ # That matches the gem's stated scope: "in-memory only, no
46
+ # session-state-on-disk."
47
+ class List
48
+ # @return [List]
49
+ def initialize
50
+ @items = []
51
+ end
52
+
53
+ # @return [Array<Item>] a frozen snapshot of the current items,
54
+ # in insertion order. Callers cannot mutate the internal
55
+ # storage through this accessor.
56
+ def items
57
+ @items.dup.freeze
58
+ end
59
+
60
+ # @return [Integer]
61
+ def size
62
+ @items.size
63
+ end
64
+
65
+ # @return [Boolean]
66
+ def empty?
67
+ @items.empty?
68
+ end
69
+
70
+ # Append a new item with status +pending+. Content matching is
71
+ # exact (no case- or whitespace-folding) since the tools quote
72
+ # the content back to the LLM as the identifier.
73
+ #
74
+ # @param content [String] non-empty content; whitespace is the
75
+ # caller's responsibility.
76
+ # @return [Item] the newly added item
77
+ # @raise [DuplicateItem] if an item with the same +content+
78
+ # already exists.
79
+ def add(content)
80
+ raise DuplicateItem, content if find(content)
81
+
82
+ item = Item.new(content: content, status: 'pending')
83
+ @items << item
84
+ item
85
+ end
86
+
87
+ # Update the status of the item whose +content+ matches.
88
+ #
89
+ # @param content [String]
90
+ # @param status [String] one of {STATUSES}.
91
+ # @return [Item] the updated item (a fresh frozen +Data+
92
+ # instance — the old one is replaced in place).
93
+ # @raise [ItemNotFound] if no matching item exists.
94
+ # @raise [ArgumentError] if +status+ is not in {STATUSES}.
95
+ def set_status(content:, status:)
96
+ unless STATUSES.include?(status)
97
+ raise ArgumentError, "invalid status: #{status.inspect} (allowed: #{STATUSES.join(', ')})"
98
+ end
99
+
100
+ idx = @items.index { |i| i.content == content }
101
+ raise ItemNotFound, content if idx.nil?
102
+
103
+ @items[idx] = Item.new(content: content, status: status)
104
+ @items[idx]
105
+ end
106
+
107
+ # Remove the item whose +content+ matches.
108
+ #
109
+ # @param content [String]
110
+ # @return [Item] the removed item.
111
+ # @raise [ItemNotFound] if no matching item exists.
112
+ def delete(content)
113
+ idx = @items.index { |i| i.content == content }
114
+ raise ItemNotFound, content if idx.nil?
115
+
116
+ @items.delete_at(idx)
117
+ end
118
+
119
+ # The canonical rendering returned as the observation by every
120
+ # task tool, so the LLM sees the latest full state on each call
121
+ # without needing a separate read tool. Format:
122
+ #
123
+ # <tasks>
124
+ # - [pending] Add dark mode toggle
125
+ # - [in_progress] Write unit tests
126
+ # - [completed] Update README
127
+ # </tasks>
128
+ #
129
+ # Empty list renders as +<tasks>(empty)</tasks>+ so the LLM gets
130
+ # an unambiguous "yes, the call worked and the list is now empty"
131
+ # signal rather than an ambiguous blank block.
132
+ #
133
+ # @return [String]
134
+ def render
135
+ return '<tasks>(empty)</tasks>' if @items.empty?
136
+
137
+ lines = @items.map { |i| "- [#{i.status}] #{i.content}" }
138
+ "<tasks>\n#{lines.join("\n")}\n</tasks>"
139
+ end
140
+
141
+ private
142
+
143
+ def find(content)
144
+ @items.find { |i| i.content == content }
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pikuri-core'
4
+
5
+ # Entry file for the pikuri-tasks gem. Sets up a dedicated Zeitwerk
6
+ # loader rooted at this gem's +lib/+, contributing to the shared
7
+ # +Pikuri::+ namespace alongside pikuri-core. After +require
8
+ # 'pikuri-tasks'+, +Pikuri::Tasks::List+, the four task tool classes
9
+ # (+Create+, +InProgress+, +Completed+, +Delete+), and
10
+ # +Pikuri::Tasks::Extension+ are all defined.
11
+ #
12
+ # The loader is per-gem (not shared with pikuri-core's loader) so each
13
+ # gem owns its own +lib/+ tree and the cooperation between gems is via
14
+ # the Pikuri namespace alone.
15
+ module Pikuri
16
+ module Tasks
17
+ LOADER = Zeitwerk::Loader.new
18
+ LOADER.tag = 'pikuri-tasks'
19
+ LOADER.push_dir(File.expand_path('.', __dir__))
20
+ LOADER.ignore(__FILE__)
21
+ LOADER.setup
22
+ LOADER.eager_load
23
+ end
24
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pikuri-tasks
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Martin Vysny
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-05-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pikuri-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.4
27
+ description: |
28
+ pikuri-tasks gives a pikuri-core agent an in-memory task list it
29
+ can use to plan and track multi-step work. A +Pikuri::Tasks::List+
30
+ holds the per-Agent state; four tools (+task_create+,
31
+ +task_in_progress+, +task_completed+, +task_delete+) mutate it via
32
+ content-as-identifier (no item IDs to hallucinate).
33
+ +Pikuri::Tasks::Extension+ wires the list and tools onto an
34
+ +Pikuri::Agent+ via +c.add_extension(...)+ inside the +Agent.new+
35
+ block.
36
+
37
+ The list lives in process memory only — nothing is written to disk.
38
+ Sub-agents do not inherit the parent's list (consistent with the
39
+ "sub-agents do not inherit extensions" rule).
40
+ email:
41
+ - martin@vysny.me
42
+ executables: []
43
+ extensions: []
44
+ extra_rdoc_files: []
45
+ files:
46
+ - README.md
47
+ - lib/pikuri-tasks.rb
48
+ - lib/pikuri/tasks/completed.rb
49
+ - lib/pikuri/tasks/create.rb
50
+ - lib/pikuri/tasks/delete.rb
51
+ - lib/pikuri/tasks/extension.rb
52
+ - lib/pikuri/tasks/in_progress.rb
53
+ - lib/pikuri/tasks/list.rb
54
+ homepage: https://codeberg.org/mvysny/pikuri
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ source_code_uri: https://codeberg.org/mvysny/pikuri/src/branch/master
59
+ changelog_uri: https://codeberg.org/mvysny/pikuri/src/branch/master/CHANGELOG.md
60
+ bug_tracker_uri: https://codeberg.org/mvysny/pikuri/issues
61
+ rubygems_mfa_required: 'true'
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '3.3'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 3.5.22
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: Per-session in-memory task list + tools for pikuri.
81
+ test_files: []