@pmelab/gtd 22.0.0 → 23.1.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 +25 -13
- package/claude/hooks/persona.ts +1 -1
- package/dist/gtd.bundle.mjs +669 -7862
- package/dist/workflows.mjs +2 -2
- package/package.json +4 -5
- package/skills/authoring/SKILL.md +9 -5
- package/src/flows/config.test.ts +66 -0
- package/src/flows/config.ts +141 -0
- package/src/flows/index.ts +1 -0
- package/src/workflows/access.ts +1 -1
- package/src/workflows/health.ts +1 -1
- package/src/workflows/skills.ts +1 -1
- package/src/workflows/text.fixture.ts +3 -3
- package/src/workflows/text.ts +1 -1
- package/src/workflows/vars.ts +1 -1
- package/schema.json +0 -136
package/dist/workflows.mjs
CHANGED
|
@@ -1273,7 +1273,7 @@ every commit is machine-authored.
|
|
|
1273
1273
|
load (a stricter validation, an escape a value must now spell differently)
|
|
1274
1274
|
— a public surface, not an internal one, either way — add a literal
|
|
1275
1275
|
\`BREAKING CHANGE:\` footer naming what breaks and, for the second case,
|
|
1276
|
-
the exact escape or rewrite a committed
|
|
1276
|
+
the exact escape or rewrite a committed config needs. A \`!\` on the
|
|
1277
1277
|
type prefix alone (\`feat!:\`/\`refactor!:\`) is NOT enough: this repo's
|
|
1278
1278
|
own commit-analyzer has missed that marker before, cutting no release
|
|
1279
1279
|
until a follow-up commit carried the literal footer instead
|
|
@@ -1468,7 +1468,7 @@ const collecting = (capture) => agentWithSkills("review.collecting", buildReview
|
|
|
1468
1468
|
//#region src/workflows/health.ts
|
|
1469
1469
|
/** Written while `fastTestCommand` is unset; its presence rests the process at the check. */
|
|
1470
1470
|
const SETUP = ".gtd/SETUP.md";
|
|
1471
|
-
const SETUP_TEXT = "The bundled workflow needs the `fastTestCommand` setting: the fast suite (everything but e2e).\nSet it
|
|
1471
|
+
const SETUP_TEXT = "The bundled workflow needs the `fastTestCommand` setting: the fast suite (everything but e2e).\nSet it as a workflow option in `gtd.config.ts` or as GTD_FASTTESTCOMMAND.";
|
|
1472
1472
|
/** Shell lines for a check's preamble: unset (blank after trim) setting writes `SETUP` and skips the suite. */
|
|
1473
1473
|
const fastSuiteGuard = () => (env.fastTestCommand ?? "").trim() === "" ? [
|
|
1474
1474
|
`mkdir -p ${quote(dirname(SETUP))}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "23.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"src/workflows/",
|
|
26
26
|
"README.md",
|
|
27
27
|
"LICENSE",
|
|
28
|
-
"schema.json",
|
|
29
28
|
".claude-plugin/plugin.json",
|
|
30
29
|
"hooks/",
|
|
31
30
|
"bin/",
|
|
@@ -52,7 +51,7 @@
|
|
|
52
51
|
"prepare": "husky",
|
|
53
52
|
"dev": "node dev/run.mjs",
|
|
54
53
|
"build": "tsdown --filter web && npx @tailwindcss/cli -i src/web/styles.css -o dist/web/main.css --minify && node scripts/inline-web-client.mjs && tsdown --filter gtd && tsdown --filter workflows",
|
|
55
|
-
"postbuild": "
|
|
54
|
+
"postbuild": "node scripts/assert-no-test-doubles.mjs",
|
|
56
55
|
"watch": "tsdown --watch",
|
|
57
56
|
"bench:replay": "node scripts/bench-replay.mjs",
|
|
58
57
|
"test": "turbo run format:check typecheck lint lint:sh lint:boundaries test:unit test:e2e:inmem test:e2e:live test:web analyze",
|
|
@@ -126,6 +125,7 @@
|
|
|
126
125
|
"tsdown": "^0.22.8",
|
|
127
126
|
"turbo": "^2.10.9",
|
|
128
127
|
"vitest": "^3.2.0",
|
|
128
|
+
"yaml": "^2.8.2",
|
|
129
129
|
"zustand": "^5.0.15"
|
|
130
130
|
},
|
|
131
131
|
"peerDependencies": {
|
|
@@ -148,7 +148,6 @@
|
|
|
148
148
|
"micromark-extension-gfm-strikethrough": "^2.1.0",
|
|
149
149
|
"micromark-extension-gfm-task-list-item": "^2.1.0",
|
|
150
150
|
"vscode-languageserver": "^10.1.0",
|
|
151
|
-
"vscode-languageserver-textdocument": "^1.0.13"
|
|
152
|
-
"yaml": "^2.8.2"
|
|
151
|
+
"vscode-languageserver-textdocument": "^1.0.13"
|
|
153
152
|
}
|
|
154
153
|
}
|
|
@@ -109,7 +109,7 @@ workflow that fails to load breaks every gtd command in the repository.
|
|
|
109
109
|
step you add must fit one of them; there is no sixth.
|
|
110
110
|
|
|
111
111
|
Options (all optional): `label`, `file` (a `.gtd/` path), `mode` (needs `file`;
|
|
112
|
-
`qa`, `review`, or a
|
|
112
|
+
`qa`, `review`, or a `config.modes` name), `message` (human/judge), `model`,
|
|
113
113
|
`system` (agent), `allowEmpty` (agent), `acceptClean` (human), `base` (the
|
|
114
114
|
commit the step reviews since — what `gtd base` prints).
|
|
115
115
|
|
|
@@ -150,7 +150,7 @@ the pending landing — call it right after the step whose turn you reject.
|
|
|
150
150
|
`export const base = (workflow, vars) => commitish | undefined` fixes a
|
|
151
151
|
started process's diff base (blank = default-branch merge-base).
|
|
152
152
|
`--var <name>=<value>` only pins process settings: names the workflow's
|
|
153
|
-
`defaults`
|
|
153
|
+
`defaults` declare — never an environment setting.
|
|
154
154
|
|
|
155
155
|
## Landing rules you are designing for
|
|
156
156
|
|
|
@@ -186,9 +186,13 @@ certainly a bug.
|
|
|
186
186
|
## Rules for flow code
|
|
187
187
|
|
|
188
188
|
Flow code is replayed on every command, so it must reach the same steps every
|
|
189
|
-
time it sees the same history. `run()` bodies
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
time it sees the same history. Only `run()` bodies are exempt: module top-level
|
|
190
|
+
code and every local import are evaluated on every command too, so they read no
|
|
191
|
+
`process.env`, clock, network or files either (a per-machine value is an
|
|
192
|
+
`envDefaults` setting). A process records the config's hashes when it starts and
|
|
193
|
+
warns whenever it differs. gtd does not read the source ahead of time: breaking
|
|
194
|
+
a rule shows up when replay runs, as an error or, for nondeterminism, as a
|
|
195
|
+
divergence later.
|
|
192
196
|
|
|
193
197
|
- No IO or nondeterminism in flow code — no clock, randomness, environment,
|
|
194
198
|
network or filesystem. Read the tree through the helpers; do IO inside a
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { configProblems, defineConfig } from "./config.js"
|
|
3
|
+
|
|
4
|
+
describe("defineConfig", () => {
|
|
5
|
+
it("returns a valid config as given", () => {
|
|
6
|
+
const config = {
|
|
7
|
+
modes: { qa: { format: "fmt" }, adr: { validate: "check" } },
|
|
8
|
+
ui: { port: 8443, format: "fmt" },
|
|
9
|
+
skills: { "feature.plan": ["tdd"] },
|
|
10
|
+
access: { "feature.plan": { write: [".gtd/**"] } },
|
|
11
|
+
}
|
|
12
|
+
expect(defineConfig(config)).toBe(config)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("rejects a removed key at type level and at runtime", () => {
|
|
16
|
+
// @ts-expect-error -- `vars` is no `gtd.config.ts` setting
|
|
17
|
+
expect(() => defineConfig({ vars: { testCommand: "npm test" } })).toThrow(
|
|
18
|
+
"config.vars: is not a known key",
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("rejects an unknown nested key at type level and at runtime", () => {
|
|
23
|
+
// @ts-expect-error -- a mode has only format and validate
|
|
24
|
+
expect(() => defineConfig({ modes: { qa: { formatter: "fmt" } } })).toThrow(
|
|
25
|
+
"config.modes.qa.formatter: is not a known key",
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe("configProblems", () => {
|
|
31
|
+
it("finds every malformed value", () => {
|
|
32
|
+
expect(
|
|
33
|
+
configProblems({
|
|
34
|
+
modes: { empty: {}, scalar: "fmt", blank: { format: " " } },
|
|
35
|
+
ui: { port: "80", host: 1 },
|
|
36
|
+
skills: { a: "tdd" },
|
|
37
|
+
access: { a: { read: "*", other: [] } },
|
|
38
|
+
}),
|
|
39
|
+
).toEqual([
|
|
40
|
+
{ path: ["modes", "scalar"], message: 'must be an object with "format" and/or "validate"' },
|
|
41
|
+
{ path: ["modes", "blank", "format"], message: "must be a non-empty shell command" },
|
|
42
|
+
{ path: ["ui", "port"], message: "must be an integer" },
|
|
43
|
+
{ path: ["ui", "host"], message: "must be a string" },
|
|
44
|
+
{ path: ["skills", "a"], message: "must be an array of skill names" },
|
|
45
|
+
{ path: ["access", "a", "other"], message: "is not a known key" },
|
|
46
|
+
{ path: ["access", "a", "read"], message: "must be an array of glob strings" },
|
|
47
|
+
])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("rejects a container of the wrong shape", () => {
|
|
51
|
+
expect(configProblems({ modes: { "": {} }, skills: [], ui: "x", access: { a: "x" } })).toEqual([
|
|
52
|
+
{ path: ["modes", ""], message: "is a mode without a name" },
|
|
53
|
+
{ path: ["ui"], message: "must be an object" },
|
|
54
|
+
{ path: ["skills"], message: "must be an object of skill-name arrays" },
|
|
55
|
+
{ path: ["access", "a"], message: "must be an object of { read?, write? } glob arrays" },
|
|
56
|
+
])
|
|
57
|
+
expect(configProblems({ modes: "x", access: [] })).toEqual([
|
|
58
|
+
{ path: ["modes"], message: "must be an object of modes" },
|
|
59
|
+
{ path: ["access"], message: "must be an object of scope access" },
|
|
60
|
+
])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("rejects a config that is no object", () => {
|
|
64
|
+
expect(configProblems([])).toEqual([{ path: [], message: "must be an object" }])
|
|
65
|
+
})
|
|
66
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { ScopeAccess } from "./runtime.js"
|
|
2
|
+
|
|
3
|
+
/** One steering-file mode's shell commands; each reads the file from `$GTD_FILE`. */
|
|
4
|
+
export interface ModeCommands {
|
|
5
|
+
readonly format?: string
|
|
6
|
+
readonly validate?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** `gtd ui` settings; its `--host`/`--port` flags outrank `host`/`port`. */
|
|
10
|
+
export interface UiSettings {
|
|
11
|
+
readonly port?: number
|
|
12
|
+
readonly host?: string
|
|
13
|
+
readonly cert?: string
|
|
14
|
+
readonly key?: string
|
|
15
|
+
readonly format?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The repository settings a `gtd.config.ts` exports as `config`. */
|
|
19
|
+
export interface GtdConfig {
|
|
20
|
+
readonly modes?: Readonly<Record<string, ModeCommands>>
|
|
21
|
+
readonly ui?: UiSettings
|
|
22
|
+
readonly skills?: Readonly<Record<string, readonly string[]>>
|
|
23
|
+
readonly access?: Readonly<Record<string, ScopeAccess>>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ConfigProblem {
|
|
27
|
+
readonly path: readonly string[]
|
|
28
|
+
readonly message: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const CONFIG_KEYS = ["modes", "ui", "skills", "access"] as const
|
|
32
|
+
const MODE_KEYS = ["format", "validate"] as const
|
|
33
|
+
const UI_KEYS = ["port", "host", "cert", "key", "format"] as const
|
|
34
|
+
|
|
35
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
36
|
+
typeof v === "object" && v !== null && !Array.isArray(v)
|
|
37
|
+
|
|
38
|
+
const isStringArray = (v: unknown): boolean =>
|
|
39
|
+
Array.isArray(v) && v.every((entry) => typeof entry === "string")
|
|
40
|
+
|
|
41
|
+
const unknownKeys = (
|
|
42
|
+
value: Record<string, unknown>,
|
|
43
|
+
known: readonly string[],
|
|
44
|
+
path: readonly string[],
|
|
45
|
+
): ConfigProblem[] =>
|
|
46
|
+
Object.keys(value)
|
|
47
|
+
.filter((key) => !known.includes(key))
|
|
48
|
+
.map((key) => ({ path: [...path, key], message: "is not a known key" }))
|
|
49
|
+
|
|
50
|
+
const recordOf = (
|
|
51
|
+
value: unknown,
|
|
52
|
+
path: readonly string[],
|
|
53
|
+
shape: string,
|
|
54
|
+
entry: (value: unknown, path: readonly string[]) => ConfigProblem[],
|
|
55
|
+
): ConfigProblem[] =>
|
|
56
|
+
isRecord(value)
|
|
57
|
+
? Object.entries(value).flatMap(([key, v]) => entry(v, [...path, key]))
|
|
58
|
+
: [{ path, message: `must be an object of ${shape}` }]
|
|
59
|
+
|
|
60
|
+
const modeProblems = (value: unknown, path: readonly string[]): ConfigProblem[] => {
|
|
61
|
+
if (path.at(-1) === "") return [{ path, message: "is a mode without a name" }]
|
|
62
|
+
// An empty mode still registers its name: a step may use it, unformatted and unvalidated.
|
|
63
|
+
if (!isRecord(value))
|
|
64
|
+
return [{ path, message: 'must be an object with "format" and/or "validate"' }]
|
|
65
|
+
return [
|
|
66
|
+
...unknownKeys(value, MODE_KEYS, path),
|
|
67
|
+
...MODE_KEYS.flatMap((key) => {
|
|
68
|
+
const command = value[key]
|
|
69
|
+
return command === undefined || (typeof command === "string" && command.trim() !== "")
|
|
70
|
+
? []
|
|
71
|
+
: [{ path: [...path, key], message: "must be a non-empty shell command" }]
|
|
72
|
+
}),
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const uiProblems = (value: unknown, path: readonly string[]): ConfigProblem[] => {
|
|
77
|
+
if (!isRecord(value)) return [{ path, message: "must be an object" }]
|
|
78
|
+
return [
|
|
79
|
+
...unknownKeys(value, UI_KEYS, path),
|
|
80
|
+
...UI_KEYS.flatMap((key) => {
|
|
81
|
+
const v = value[key]
|
|
82
|
+
if (v === undefined) return []
|
|
83
|
+
const ok = key === "port" ? Number.isInteger(v) : typeof v === "string"
|
|
84
|
+
return ok
|
|
85
|
+
? []
|
|
86
|
+
: [
|
|
87
|
+
{
|
|
88
|
+
path: [...path, key],
|
|
89
|
+
message: key === "port" ? "must be an integer" : "must be a string",
|
|
90
|
+
},
|
|
91
|
+
]
|
|
92
|
+
}),
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const accessProblems = (value: unknown, path: readonly string[]): ConfigProblem[] => {
|
|
97
|
+
if (!isRecord(value))
|
|
98
|
+
return [{ path, message: "must be an object of { read?, write? } glob arrays" }]
|
|
99
|
+
return [
|
|
100
|
+
...unknownKeys(value, ["read", "write"], path),
|
|
101
|
+
...(["read", "write"] as const).flatMap((side) =>
|
|
102
|
+
value[side] === undefined || isStringArray(value[side])
|
|
103
|
+
? []
|
|
104
|
+
: [{ path: [...path, side], message: "must be an array of glob strings" }],
|
|
105
|
+
),
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Every way `value` is not a `GtdConfig`; an unknown key is one, so a removed key fails loudly. */
|
|
110
|
+
export const configProblems = (value: unknown): readonly ConfigProblem[] => {
|
|
111
|
+
if (!isRecord(value)) return [{ path: [], message: "must be an object" }]
|
|
112
|
+
return [
|
|
113
|
+
...unknownKeys(value, CONFIG_KEYS, []),
|
|
114
|
+
...(value.modes === undefined ? [] : recordOf(value.modes, ["modes"], "modes", modeProblems)),
|
|
115
|
+
...(value.ui === undefined ? [] : uiProblems(value.ui, ["ui"])),
|
|
116
|
+
...(value.skills === undefined
|
|
117
|
+
? []
|
|
118
|
+
: recordOf(value.skills, ["skills"], "skill-name arrays", (v, path) =>
|
|
119
|
+
isStringArray(v) ? [] : [{ path, message: "must be an array of skill names" }],
|
|
120
|
+
)),
|
|
121
|
+
...(value.access === undefined
|
|
122
|
+
? []
|
|
123
|
+
: recordOf(value.access, ["access"], "scope access", accessProblems)),
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The loader's diagnostic format, so a problem reads the same whether
|
|
128
|
+
// `defineConfig` throws it or the loader finds it in a plain object.
|
|
129
|
+
const formatConfigProblem = ({ path, message }: ConfigProblem): string =>
|
|
130
|
+
`${["config", ...path].join(".")}: ${message}`
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Type and check the `config` export. Not generic on purpose: a literal argument
|
|
134
|
+
* then gets TypeScript's excess-property check at every depth, so an unknown key
|
|
135
|
+
* is a type error — and it throws at runtime too.
|
|
136
|
+
*/
|
|
137
|
+
export const defineConfig = (config: GtdConfig): GtdConfig => {
|
|
138
|
+
const problems = configProblems(config)
|
|
139
|
+
if (problems.length > 0) throw new Error(problems.map(formatConfigProblem).join("; "))
|
|
140
|
+
return config
|
|
141
|
+
}
|
package/src/flows/index.ts
CHANGED
package/src/workflows/access.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ScopeAccess } from "../flows/index.js"
|
|
|
2
2
|
import { lensesOf } from "./review.js"
|
|
3
3
|
|
|
4
4
|
// The bundled workflow's default file access, keyed by scope full name — the
|
|
5
|
-
// name a
|
|
5
|
+
// name a `config.access` entry addresses. Only planning and review scopes
|
|
6
6
|
// are restricted, and only on writes; no scope restricts reads. A step's own
|
|
7
7
|
// steering file is folded in per step, so it is not listed here. Build and fix
|
|
8
8
|
// scopes carry no entry: no project layout is assumed.
|
package/src/workflows/health.ts
CHANGED
|
@@ -19,7 +19,7 @@ import * as t from "./text.js"
|
|
|
19
19
|
export const SETUP = ".gtd/SETUP.md"
|
|
20
20
|
|
|
21
21
|
const SETUP_TEXT =
|
|
22
|
-
"The bundled workflow needs the `fastTestCommand` setting: the fast suite (everything but e2e).\nSet it
|
|
22
|
+
"The bundled workflow needs the `fastTestCommand` setting: the fast suite (everything but e2e).\nSet it as a workflow option in `gtd.config.ts` or as GTD_FASTTESTCOMMAND."
|
|
23
23
|
|
|
24
24
|
/** Shell lines for a check's preamble: unset (blank after trim) setting writes `SETUP` and skips the suite. */
|
|
25
25
|
export const fastSuiteGuard = (): string[] =>
|
package/src/workflows/skills.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { builtInLenses } from "./steps.js"
|
|
|
2
2
|
import { lensesOf } from "./review.js"
|
|
3
3
|
|
|
4
4
|
// The bundled workflow's default skills, one list per SCOPE, keyed by the
|
|
5
|
-
// scope's full name — the same name a
|
|
5
|
+
// scope's full name — the same name a `config.skills` entry addresses. A
|
|
6
6
|
// nested scope with no entry inherits its parent's; group scopes
|
|
7
7
|
// (`packages.item.fix`, `build.fix`, `build.review.fix`, `build.quality`) run
|
|
8
8
|
// no turn and carry no key. `build.quality.<lens>` is one key per
|
|
@@ -20,9 +20,9 @@ export interface TextContext {
|
|
|
20
20
|
readonly read?: (path: string) => string | undefined
|
|
21
21
|
/** The scope the step runs in (`"build.review"`); the fixture pushes no scopes, so this stands in for the caller's. */
|
|
22
22
|
readonly scope?: string
|
|
23
|
-
/** Stands in for
|
|
23
|
+
/** Stands in for `config.skills`, keyed by scope full name. */
|
|
24
24
|
readonly skills?: Readonly<Record<string, readonly string[]>>
|
|
25
|
-
/** Stands in for
|
|
25
|
+
/** Stands in for `config.access`, keyed by scope full name. */
|
|
26
26
|
readonly access?: Readonly<Record<string, ScopeAccess>>
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -30,7 +30,7 @@ const unavailable = (): never => {
|
|
|
30
30
|
throw new Error("not available while rendering a text outside a replay")
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** Mirrors `Replay.ts`'s scope walk: for each prefix of the step's memory scope, innermost first, a
|
|
33
|
+
/** Mirrors `Replay.ts`'s scope walk: for each prefix of the step's memory scope, innermost first, a `config` entry, then the bundled export. */
|
|
34
34
|
const skillsForOf =
|
|
35
35
|
(context: TextContext) =>
|
|
36
36
|
(localName: string): readonly string[] => {
|
package/src/workflows/text.ts
CHANGED
|
@@ -869,7 +869,7 @@ every commit is machine-authored.
|
|
|
869
869
|
load (a stricter validation, an escape a value must now spell differently)
|
|
870
870
|
— a public surface, not an internal one, either way — add a literal
|
|
871
871
|
\`BREAKING CHANGE:\` footer naming what breaks and, for the second case,
|
|
872
|
-
the exact escape or rewrite a committed
|
|
872
|
+
the exact escape or rewrite a committed config needs. A \`!\` on the
|
|
873
873
|
type prefix alone (\`feat!:\`/\`refactor!:\`) is NOT enough: this repo's
|
|
874
874
|
own commit-analyzer has missed that marker before, cutting no release
|
|
875
875
|
until a follow-up commit carried the literal footer instead
|
package/src/workflows/vars.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The bundled workflow's settings, in two kinds. A process setting changes
|
|
2
2
|
// which step comes next, so it is pinned for the whole process at its start;
|
|
3
3
|
// an environment setting only changes how a step runs on this machine, so it
|
|
4
|
-
// is read live on every invocation.
|
|
4
|
+
// is read live on every invocation. A factory option, `--var` (process
|
|
5
5
|
// settings only) and `GTD_<NAME>` override either.
|
|
6
6
|
|
|
7
7
|
/** Process settings. */
|
package/schema.json
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"type": "object",
|
|
4
|
-
"required": [],
|
|
5
|
-
"properties": {
|
|
6
|
-
"vars": {
|
|
7
|
-
"type": "object",
|
|
8
|
-
"description": "Flat name -> scalar map merged into the workflow's process settings (defaults), pinned for the whole process at its start. A workflow built by a factory receives these entries as options and validates them itself. Scalars are coerced to strings.",
|
|
9
|
-
"additionalProperties": {
|
|
10
|
-
"type": [
|
|
11
|
-
"string",
|
|
12
|
-
"number",
|
|
13
|
-
"boolean"
|
|
14
|
-
]
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
"env": {
|
|
18
|
-
"type": "object",
|
|
19
|
-
"description": "Flat name -> scalar map merged into the workflow's environment settings (envDefaults) — values that change how a step runs on this machine. A workflow built by a factory receives these entries as options and validates them itself. Read fresh on every gtd call, never pinned to a process. GTD_<NAME> environment variables override these entries. Scalars are coerced to strings.",
|
|
20
|
-
"additionalProperties": {
|
|
21
|
-
"type": [
|
|
22
|
-
"string",
|
|
23
|
-
"number",
|
|
24
|
-
"boolean"
|
|
25
|
-
]
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
"judge": {
|
|
29
|
-
"type": "object",
|
|
30
|
-
"required": [],
|
|
31
|
-
"properties": {
|
|
32
|
-
"provider": {
|
|
33
|
-
"type": "string",
|
|
34
|
-
"enum": [
|
|
35
|
-
"fixed",
|
|
36
|
-
"jev",
|
|
37
|
-
"llm"
|
|
38
|
-
],
|
|
39
|
-
"description": "Judge provider `gtd judge run` uses when no --provider flag, no GTD_JUDGE_PROVIDER and no user-level judge.json says. Absent means auto: jev when TYPESAFE_API_KEY is set, otherwise llm."
|
|
40
|
-
},
|
|
41
|
-
"model": {
|
|
42
|
-
"type": "string",
|
|
43
|
-
"description": "Model for provider llm, used when no --model flag, no GTD_JUDGE_MODEL and no user-level judge.json says. Pairing it with another provider is an error."
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"additionalProperties": false,
|
|
47
|
-
"description": "Which judge `gtd judge run` uses, when no flag, GTD_JUDGE_* variable or user-level judge.json says."
|
|
48
|
-
},
|
|
49
|
-
"modes": {
|
|
50
|
-
"type": "object",
|
|
51
|
-
"description": "Steering-file modes a workflow step's mode: may name. Each entry declares at least one of format/validate: shell commands gtd runs via bash with $GTD_FILE set to the steering file's path. format rewrites the file in place; validate exits 0 when valid, non-zero with findings on stdout/stderr otherwise. The halves layer independently, so naming a built-in mode (qa/review) and declaring only format: adds formatting while keeping gtd's own validation. gtd ships no formatter — bring your own (prettier, dprint, a script).",
|
|
52
|
-
"additionalProperties": {
|
|
53
|
-
"type": "object",
|
|
54
|
-
"description": "One mode: at least one of format/validate.",
|
|
55
|
-
"additionalProperties": false,
|
|
56
|
-
"minProperties": 1,
|
|
57
|
-
"properties": {
|
|
58
|
-
"format": {
|
|
59
|
-
"type": "string",
|
|
60
|
-
"description": "Shell command that rewrites the steering file in place before validation ($GTD_FILE is the file path). A non-zero exit is a hard error."
|
|
61
|
-
},
|
|
62
|
-
"validate": {
|
|
63
|
-
"type": "string",
|
|
64
|
-
"description": "Shell command that validates the steering file ($GTD_FILE is the file path). Exit 0 = valid; non-zero = invalid, with its output reported as the findings."
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
"ui": {
|
|
70
|
-
"type": "object",
|
|
71
|
-
"required": [],
|
|
72
|
-
"properties": {
|
|
73
|
-
"port": {
|
|
74
|
-
"type": "integer",
|
|
75
|
-
"description": "For ui, the tailscale serve port (default: the first free of 8443, 10000, 443), or the bind port when --host opts out of serve (default: a free port).",
|
|
76
|
-
"title": "int"
|
|
77
|
-
},
|
|
78
|
-
"host": {
|
|
79
|
-
"type": "string",
|
|
80
|
-
"description": "Host/interface `gtd ui` binds to."
|
|
81
|
-
},
|
|
82
|
-
"cert": {
|
|
83
|
-
"type": "string",
|
|
84
|
-
"description": "Path to a TLS certificate file, enabling HTTPS. Requires `key` too."
|
|
85
|
-
},
|
|
86
|
-
"key": {
|
|
87
|
-
"type": "string",
|
|
88
|
-
"description": "Path to a TLS private key file, enabling HTTPS. Requires `cert` too."
|
|
89
|
-
},
|
|
90
|
-
"format": {
|
|
91
|
-
"type": "string",
|
|
92
|
-
"description": "Shell command run after every UI write, before it resolves ($GTD_FILE is the written file's absolute path). A non-zero exit or missing binary never reverts the write or refuses it — it's reported to the client as a notice naming the command and its exit code. Absent means no command runs at all."
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
"additionalProperties": false,
|
|
96
|
-
"description": "Settings for `gtd ui`: where it listens, and optional TLS."
|
|
97
|
-
},
|
|
98
|
-
"skills": {
|
|
99
|
-
"type": "object",
|
|
100
|
-
"description": "Flat scope full-name -> skill-name array map. Each entry REPLACES the named scope's skill list wholesale (never merges into it) and reaches every nested scope that sets no list of its own; [] means no skills at all for that scope, and no preamble. A key that is not a scope running a turn is a load error listing the known scopes — the schema itself cannot validate a key, only a value's shape.",
|
|
101
|
-
"additionalProperties": {
|
|
102
|
-
"type": "array",
|
|
103
|
-
"items": {
|
|
104
|
-
"type": "string"
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
"access": {
|
|
109
|
-
"type": "object",
|
|
110
|
-
"description": "Flat scope full-name -> { read?, write? } map of glob arrays. Each entry REPLACES the named scope's declared access wholesale (never merges into it) and reaches every nested scope that sets none of its own; a missing side is unrestricted, [] allows nothing, and {} lifts every restriction. read and write are independent: write grants no read. A key that is not a scope running a turn is a load error listing the known scopes — the schema itself cannot validate a key, only a value's shape.",
|
|
111
|
-
"additionalProperties": {
|
|
112
|
-
"type": "object",
|
|
113
|
-
"additionalProperties": false,
|
|
114
|
-
"properties": {
|
|
115
|
-
"read": {
|
|
116
|
-
"type": "array",
|
|
117
|
-
"items": {
|
|
118
|
-
"type": "string"
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
"write": {
|
|
122
|
-
"type": "array",
|
|
123
|
-
"items": {
|
|
124
|
-
"type": "string"
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
"$schema": {
|
|
131
|
-
"type": "string",
|
|
132
|
-
"description": "Editor-only pointer at this schema. Stripped by gtd before validation."
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
"additionalProperties": false
|
|
136
|
-
}
|