@pmelab/gtd 17.2.0 → 18.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -3
- package/claude/hooks/drive.ts +5 -0
- package/claude/hooks/persona.ts +28 -0
- package/claude/hooks/register.tsx +23 -19
- package/claude/hooks/wording.ts +3 -0
- package/claude/types/index.d.ts +5 -1
- package/dist/gtd.bundle.mjs +264 -270
- package/package.json +1 -1
- package/schema.json +1 -1
- package/skills/authoring/SKILL.md +6 -4
- package/src/flows/runtime.ts +6 -14
- package/src/workflows/prose.ts +10 -3
- package/src/workflows/review.test.ts +13 -13
- package/src/workflows/review.ts +4 -4
- package/src/workflows/skills.test.ts +67 -81
- package/src/workflows/skills.ts +33 -37
- package/src/workflows/steps.test.ts +56 -78
- package/src/workflows/steps.ts +10 -19
- package/src/workflows/text.fixture.ts +14 -29
- package/src/workflows/text.test.ts +25 -14
- package/src/workflows/text.ts +7 -16
- package/src/workflows/vars.ts +2 -4
package/README.md
CHANGED
|
@@ -28,8 +28,9 @@ Git history; keep secrets in an environment setting (see
|
|
|
28
28
|
Also install all three skill sources the bundled workflow names in its prompts —
|
|
29
29
|
none is optional. See
|
|
30
30
|
[Setup](https://github.com/pmelab/gtd/blob/main/docs/setup.md) for the list, the
|
|
31
|
-
install flags, and the never-install-the-plugin caveat.
|
|
32
|
-
|
|
31
|
+
install flags, and the never-install-the-plugin caveat. Skills are declared per
|
|
32
|
+
scope, and restricted per turn where the driver can; each bundled scope has its
|
|
33
|
+
own addressable `.gtdrc` `skills:` entry — see
|
|
33
34
|
[Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#the-skills-key)
|
|
34
35
|
to repoint one to a set your own harness has instead.
|
|
35
36
|
|
|
@@ -468,7 +469,8 @@ thread: a footnote holding a `H:`/`A:` conversation. Threads show as a
|
|
|
468
469
|
conversation under their block, question, chunk, or hunk, and one the agent
|
|
469
470
|
answered last carries a "waiting on you" badge. Tap it to reply — your reply is
|
|
470
471
|
added as a new `H:` entry; while the agent has yet to answer, reopening it edits
|
|
471
|
-
your last entry instead.
|
|
472
|
+
your last entry instead. Tap an already-answered question to leave a note on it;
|
|
473
|
+
the agent may overturn the answer. See
|
|
472
474
|
[Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md)
|
|
473
475
|
for its `ui:` settings.
|
|
474
476
|
|
package/claude/hooks/drive.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type Beat = {
|
|
|
13
13
|
memory?: string
|
|
14
14
|
model?: string
|
|
15
15
|
system?: string
|
|
16
|
+
skills?: string[]
|
|
16
17
|
validate?: string
|
|
17
18
|
judge?: unknown
|
|
18
19
|
changes?: { status: string; path: string }[]
|
|
@@ -33,6 +34,8 @@ export type Turn = {
|
|
|
33
34
|
label?: string
|
|
34
35
|
model?: string
|
|
35
36
|
system?: string
|
|
37
|
+
skills: readonly string[]
|
|
38
|
+
scope: string
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export type TurnEnd = { ok: true } | { ok: false; why: string }
|
|
@@ -131,6 +134,8 @@ async function agent(io: Io, b: Beat, landTurn: string | undefined): Promise<Act
|
|
|
131
134
|
label: b.label,
|
|
132
135
|
model: b.model || undefined,
|
|
133
136
|
system: b.system || undefined,
|
|
137
|
+
skills: b.skills ?? [],
|
|
138
|
+
scope: memory.split("#")[0]!,
|
|
134
139
|
})
|
|
135
140
|
if (!t.ok) return failed(b, t.why)
|
|
136
141
|
return b.validate ? validated(io, b, b.validate, memory) : {}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Turn } from "./drive"
|
|
2
|
+
|
|
3
|
+
const NEUTRAL = "You are a coding agent working on the task you are given."
|
|
4
|
+
|
|
5
|
+
// `general-purpose` cannot carry a restriction, so every prompt turn gets a
|
|
6
|
+
// registered persona. The name hashes everything the spec depends on: a
|
|
7
|
+
// `.gtdrc` change or gtd upgrade mid-session must register a fresh one.
|
|
8
|
+
export async function personaSpec(t: Pick<Turn, "scope" | "system" | "skills">) {
|
|
9
|
+
const key = JSON.stringify([t.scope, t.system ?? "", t.skills])
|
|
10
|
+
const digest = new Uint8Array(
|
|
11
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(key)),
|
|
12
|
+
)
|
|
13
|
+
const name =
|
|
14
|
+
"p-" + Array.from(digest.slice(0, 6), (b) => b.toString(16).padStart(2, "0")).join("")
|
|
15
|
+
return {
|
|
16
|
+
name,
|
|
17
|
+
prompt: t.system || NEUTRAL,
|
|
18
|
+
skills: [...t.skills],
|
|
19
|
+
disallowedTools: ["Skill"],
|
|
20
|
+
permissionMode: "bypassPermissions",
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resumable(entry: unknown, persona: string): string | undefined {
|
|
25
|
+
if (typeof entry !== "object" || entry === null) return undefined
|
|
26
|
+
const e = entry as { agentId?: unknown; persona?: unknown }
|
|
27
|
+
return typeof e.agentId === "string" && e.persona === persona ? e.agentId : undefined
|
|
28
|
+
}
|
|
@@ -3,9 +3,10 @@ import type { EngineInterface, Register } from "claude-code"
|
|
|
3
3
|
|
|
4
4
|
import type { Run, Stop } from "../types"
|
|
5
5
|
import { afterReload, drive, isTrue } from "./drive"
|
|
6
|
-
import type { Beat, Io, Landing, TurnEnd } from "./drive"
|
|
6
|
+
import type { Beat, Io, Landing, Turn, TurnEnd } from "./drive"
|
|
7
7
|
import { enter } from "./entry"
|
|
8
8
|
import { subagentModel } from "./models"
|
|
9
|
+
import { personaSpec, resumable } from "./persona"
|
|
9
10
|
import { JUDGE_SYSTEM, judgePrompt, toVerdicts } from "./judge"
|
|
10
11
|
import type { Judgment } from "./judge"
|
|
11
12
|
import { catchFrom, throwTo } from "./handoff"
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
pushText,
|
|
26
27
|
runningLine,
|
|
27
28
|
question,
|
|
29
|
+
restartLine,
|
|
28
30
|
RELOAD_CUT_SCRIPT,
|
|
29
31
|
SAFE,
|
|
30
32
|
SHIP,
|
|
@@ -37,7 +39,10 @@ type $ = EngineInterface
|
|
|
37
39
|
|
|
38
40
|
const run = atom({ plugin: "gtd", key: "run" } as const, { isRunning: false, beat: 0 } as Run)
|
|
39
41
|
// memory scope (`<scope>#<hash7>`) -> the subagent holding that conversation
|
|
40
|
-
const scopes = atom(
|
|
42
|
+
const scopes = atom(
|
|
43
|
+
{ plugin: "gtd", key: "scopes" } as const,
|
|
44
|
+
{} as Record<string, { agentId: string; persona: string }>,
|
|
45
|
+
)
|
|
41
46
|
const agents = atom({ plugin: "gtd", key: "agents" } as const, [] as string[])
|
|
42
47
|
|
|
43
48
|
const TEN_MINUTES = 600_000
|
|
@@ -82,23 +87,19 @@ async function gtd($: $, args: string[], stdin?: string) {
|
|
|
82
87
|
return r.stdout
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
// One agent type per system prompt. A registered type's
|
|
86
|
-
// session's system prompt, as `--system-prompt` does for
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
"p-" + Array.from(digest.slice(0, 6), (b) => b.toString(16).padStart(2, "0")).join("")
|
|
92
|
-
if (!personas.has(name)) {
|
|
90
|
+
// One agent type per scope, system prompt and skill list. A registered type's
|
|
91
|
+
// prompt replaces the session's system prompt, as `--system-prompt` does for
|
|
92
|
+
// the sh driver.
|
|
93
|
+
async function persona($: $, t: Pick<Turn, "scope" | "system" | "skills">) {
|
|
94
|
+
const spec = await personaSpec(t)
|
|
95
|
+
if (!personas.has(spec.name)) {
|
|
93
96
|
await $.agent.register({
|
|
94
|
-
|
|
97
|
+
...spec,
|
|
95
98
|
description: "gtd workflow persona, spawned by the gtd driver only",
|
|
96
|
-
prompt: system,
|
|
97
|
-
permissionMode: "bypassPermissions",
|
|
98
99
|
})
|
|
99
|
-
personas.add(name)
|
|
100
|
+
personas.add(spec.name)
|
|
100
101
|
}
|
|
101
|
-
return `gtd:${name}`
|
|
102
|
+
return `gtd:${spec.name}`
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
async function send($: $, agentId: string, text: string) {
|
|
@@ -162,7 +163,10 @@ function io($: $): Io {
|
|
|
162
163
|
}
|
|
163
164
|
},
|
|
164
165
|
turn: async (t) => {
|
|
165
|
-
const
|
|
166
|
+
const entry = (await read($, scopes))[t.memory]
|
|
167
|
+
const name = (await personaSpec(t)).name
|
|
168
|
+
const known = resumable(entry, name)
|
|
169
|
+
if (t.resume && entry && !known) $.ui.log(restartLine(t.scope))
|
|
166
170
|
if (t.resume && known) {
|
|
167
171
|
const end = send($, known, t.prompt)
|
|
168
172
|
await update($, run, (r) => ({ ...r, inflight: { agentId: known, memory: t.memory } }))
|
|
@@ -172,7 +176,7 @@ function io($: $): Io {
|
|
|
172
176
|
// No live conversation for this scope (fresh scope, or this session never
|
|
173
177
|
// held it): start one, as the sh driver falls back to `--session-id`.
|
|
174
178
|
const spawned = await $.agent.spawn({
|
|
175
|
-
subagentType:
|
|
179
|
+
subagentType: await persona($, t),
|
|
176
180
|
prompt: t.prompt,
|
|
177
181
|
description: t.label || "gtd",
|
|
178
182
|
model: subagentModel(t.model, {
|
|
@@ -185,12 +189,12 @@ function io($: $): Io {
|
|
|
185
189
|
if (!id) return { ok: false, why: spawned.deny ?? "subagent spawn refused" }
|
|
186
190
|
ours.add(id)
|
|
187
191
|
await update($, agents, (list) => [...list, id].slice(-200))
|
|
188
|
-
await update($, scopes, (s) => ({ ...s, [t.memory]: id }))
|
|
192
|
+
await update($, scopes, (s) => ({ ...s, [t.memory]: { agentId: id, persona: name } }))
|
|
189
193
|
await update($, run, (r) => ({ ...r, inflight: { agentId: id, memory: t.memory } }))
|
|
190
194
|
return settle($, await turnEnd(id))
|
|
191
195
|
},
|
|
192
196
|
resume: async (memory, text) => {
|
|
193
|
-
const id = (await read($, scopes))[memory]
|
|
197
|
+
const id = (await read($, scopes))[memory]?.agentId
|
|
194
198
|
const end = id ? await send($, id, text) : undefined
|
|
195
199
|
return end ?? { ok: false, why: `no live subagent for ${memory}` }
|
|
196
200
|
},
|
package/claude/hooks/wording.ts
CHANGED
|
@@ -77,6 +77,9 @@ export function runningLine(label: string, beat: number, elapsedMs: number) {
|
|
|
77
77
|
// One line per beat, for the status line and the transcript.
|
|
78
78
|
export const beatLine = (beat: number, b: Beat) => `▸ ${step(b)} · step ${beat}`
|
|
79
79
|
|
|
80
|
+
export const restartLine = (scope: string) =>
|
|
81
|
+
`${scope}: skills or system changed — started a fresh subagent`
|
|
82
|
+
|
|
80
83
|
// What a push to a phone says when gtd stops: the step, and where to act on it.
|
|
81
84
|
export function pushText(stop: Stop, repo: string, url?: string) {
|
|
82
85
|
const at = `${repo}: ${headline(stop).replace(/^\S+ /, "")}`
|
package/claude/types/index.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export type Run = {
|
|
|
28
28
|
|
|
29
29
|
declare module "claude-code" {
|
|
30
30
|
interface PluginState {
|
|
31
|
-
gtd: {
|
|
31
|
+
gtd: {
|
|
32
|
+
run: Run
|
|
33
|
+
scopes: Record<string, { agentId: string; persona: string }>
|
|
34
|
+
agents: string[]
|
|
35
|
+
}
|
|
32
36
|
}
|
|
33
37
|
}
|