@pmelab/gtd 21.0.0 → 22.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 +15 -10
- package/claude/hooks/drive.ts +3 -1
- package/claude/hooks/judge.ts +18 -4
- package/claude/hooks/register.tsx +10 -7
- package/dist/gtd.bundle.mjs +344 -75
- package/dist/workflows.mjs +19415 -47
- package/package.json +2 -1
- package/schema.json +5 -5
- package/src/flows/factory.test.ts +94 -0
- package/src/flows/factory.ts +116 -0
- package/src/flows/index.ts +1 -0
- package/src/workflows/bundled.ts +9 -4
- package/src/workflows/options.test.ts +52 -0
- package/src/workflows/options.ts +38 -0
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -32693,6 +32693,71 @@ const requireRevert = (edited, base) => {
|
|
|
32693
32693
|
refuse(`gtd land: require-revert: ${residue.join(", ")} still differ from ${base}~1 — the revert did not take. Run \`git checkout ${base}~1 -- ${quoted}\`, then \`gtd land\` again.`);
|
|
32694
32694
|
};
|
|
32695
32695
|
//#endregion
|
|
32696
|
+
//#region src/flows/factory.ts
|
|
32697
|
+
const FACTORY = Symbol.for("@pmelab/gtd/workflow-factory");
|
|
32698
|
+
/**
|
|
32699
|
+
* Define a workflow configured by options. `schema` is any Standard Schema
|
|
32700
|
+
* (https://standardschema.dev); it should reject unknown keys, so a removed or
|
|
32701
|
+
* misspelled option fails at load. Exported from `gtd.config.ts`, the factory
|
|
32702
|
+
* itself and a call's result are both workflows.
|
|
32703
|
+
*/
|
|
32704
|
+
const defineWorkflow = (schema, build) => {
|
|
32705
|
+
const internals = {
|
|
32706
|
+
schema,
|
|
32707
|
+
build
|
|
32708
|
+
};
|
|
32709
|
+
const factory = (options) => ({
|
|
32710
|
+
[FACTORY]: {
|
|
32711
|
+
...internals,
|
|
32712
|
+
options
|
|
32713
|
+
},
|
|
32714
|
+
then: () => {
|
|
32715
|
+
throw new Error("a configured workflow is not a flow — compose fixFlow/reviewFlow instead");
|
|
32716
|
+
}
|
|
32717
|
+
});
|
|
32718
|
+
return Object.assign(factory, { [FACTORY]: internals });
|
|
32719
|
+
};
|
|
32720
|
+
const internalsOf = (value) => (typeof value === "function" || typeof value === "object" && value !== null) && FACTORY in value ? value[FACTORY] : void 0;
|
|
32721
|
+
const isFactoryWorkflow = (value) => internalsOf(value) !== void 0;
|
|
32722
|
+
const segment = (key) => {
|
|
32723
|
+
const raw = typeof key === "object" ? key.key : key;
|
|
32724
|
+
return typeof raw === "number" ? raw : String(raw);
|
|
32725
|
+
};
|
|
32726
|
+
/**
|
|
32727
|
+
* Validate a factory workflow's options — those it was called with, `overrides`
|
|
32728
|
+
* on top — and build it. `undefined` for an export that is no factory workflow.
|
|
32729
|
+
*/
|
|
32730
|
+
const configure$1 = (value, overrides = {}) => {
|
|
32731
|
+
const internals = internalsOf(value);
|
|
32732
|
+
if (internals === void 0) return void 0;
|
|
32733
|
+
const options = {
|
|
32734
|
+
...internals.options,
|
|
32735
|
+
...overrides
|
|
32736
|
+
};
|
|
32737
|
+
const result = internals.schema["~standard"].validate(options);
|
|
32738
|
+
if (result instanceof Promise) return {
|
|
32739
|
+
ok: false,
|
|
32740
|
+
issues: [{
|
|
32741
|
+
path: [],
|
|
32742
|
+
message: "the options schema validates asynchronously — gtd needs a synchronous one"
|
|
32743
|
+
}]
|
|
32744
|
+
};
|
|
32745
|
+
if (result.issues !== void 0) return {
|
|
32746
|
+
ok: false,
|
|
32747
|
+
issues: result.issues.length === 0 ? [{
|
|
32748
|
+
path: [],
|
|
32749
|
+
message: "the options schema rejected them without saying why"
|
|
32750
|
+
}] : result.issues.map((issue) => ({
|
|
32751
|
+
path: (issue.path ?? []).map(segment),
|
|
32752
|
+
message: issue.message
|
|
32753
|
+
}))
|
|
32754
|
+
};
|
|
32755
|
+
return {
|
|
32756
|
+
ok: true,
|
|
32757
|
+
build: internals.build(result.value)
|
|
32758
|
+
};
|
|
32759
|
+
};
|
|
32760
|
+
//#endregion
|
|
32696
32761
|
//#region src/flows/index.ts
|
|
32697
32762
|
var flows_exports = /* @__PURE__ */ __exportAll({
|
|
32698
32763
|
FLOWS_PROTOCOL: () => 1,
|
|
@@ -32706,12 +32771,15 @@ var flows_exports = /* @__PURE__ */ __exportAll({
|
|
|
32706
32771
|
checkScript: () => checkScript,
|
|
32707
32772
|
codeChanges: () => codeChanges,
|
|
32708
32773
|
codeThreads: () => codeThreads,
|
|
32774
|
+
configure: () => configure$1,
|
|
32775
|
+
defineWorkflow: () => defineWorkflow,
|
|
32709
32776
|
env: () => env,
|
|
32710
32777
|
glob: () => glob,
|
|
32711
32778
|
hasThreadFor: () => hasThreadFor,
|
|
32712
32779
|
head: () => head,
|
|
32713
32780
|
human: () => human,
|
|
32714
32781
|
installContext: () => installContext,
|
|
32782
|
+
isFactoryWorkflow: () => isFactoryWorkflow,
|
|
32715
32783
|
judge: () => judge,
|
|
32716
32784
|
moveScript: () => moveScript,
|
|
32717
32785
|
numeric: () => numeric,
|
|
@@ -49589,10 +49657,10 @@ const replay = async (input) => {
|
|
|
49589
49657
|
};
|
|
49590
49658
|
//#endregion
|
|
49591
49659
|
//#region src/workflow/ConfigSchema.ts
|
|
49592
|
-
/** The `vars:` shape: a flat name -> scalar map (`compileVarsMap` coerces every scalar to a string). */
|
|
49660
|
+
/** The `vars:` shape: a flat name -> scalar map (`compileVarsMap` coerces every scalar to a string). Its names are the workflow's business, never core's. */
|
|
49593
49661
|
const varsJsonSchema = {
|
|
49594
49662
|
type: "object",
|
|
49595
|
-
description: "Flat name -> scalar map merged into the workflow's process settings (defaults), pinned for the whole process at its start. Scalars are coerced to strings.",
|
|
49663
|
+
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.",
|
|
49596
49664
|
additionalProperties: { type: [
|
|
49597
49665
|
"string",
|
|
49598
49666
|
"number",
|
|
@@ -49602,7 +49670,7 @@ const varsJsonSchema = {
|
|
|
49602
49670
|
/** The `env:` shape: environment settings, read live on every invocation and never pinned to a process. */
|
|
49603
49671
|
const envJsonSchema = {
|
|
49604
49672
|
type: "object",
|
|
49605
|
-
description: "Flat name -> scalar map merged into the workflow's environment settings (envDefaults) — values that change how a step runs on this machine
|
|
49673
|
+
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.",
|
|
49606
49674
|
additionalProperties: { type: [
|
|
49607
49675
|
"string",
|
|
49608
49676
|
"number",
|
|
@@ -49676,9 +49744,9 @@ const UiSchema = Struct({
|
|
|
49676
49744
|
format: optional$2(String$.annotations({ 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." }))
|
|
49677
49745
|
}).annotations({ description: "Settings for `gtd ui`: where it listens, and optional TLS." });
|
|
49678
49746
|
const JudgeSchema = Struct({
|
|
49679
|
-
provider: optional$2(Literal("fixed", "jev", "llm").annotations({ description: "Judge provider `gtd judge run` uses when no --provider flag and no
|
|
49680
|
-
model: optional$2(String$.annotations({ description: "Model for provider llm, used when no --model flag and no
|
|
49681
|
-
}).annotations({ description: "Which judge `gtd judge run` uses, when no flag
|
|
49747
|
+
provider: optional$2(Literal("fixed", "jev", "llm").annotations({ 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." })),
|
|
49748
|
+
model: optional$2(String$.annotations({ 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." }))
|
|
49749
|
+
}).annotations({ description: "Which judge `gtd judge run` uses, when no flag, GTD_JUDGE_* variable or user-level judge.json says." });
|
|
49682
49750
|
const ConfigSchema = Struct({
|
|
49683
49751
|
vars: optional$2(Unknown.annotations({ jsonSchema: varsJsonSchema })),
|
|
49684
49752
|
env: optional$2(Unknown.annotations({ jsonSchema: envJsonSchema })),
|
|
@@ -57008,11 +57076,12 @@ const load = gen(function* () {
|
|
|
57008
57076
|
catch: (e) => new GtdError(`gtd: ${e instanceof Error ? e.message : String(e)}`)
|
|
57009
57077
|
});
|
|
57010
57078
|
const loaded = yield* try_({
|
|
57011
|
-
try: () => loadWorkflows(module, shipped),
|
|
57079
|
+
try: () => loadWorkflows(module, shipped, compiled),
|
|
57012
57080
|
catch: (e) => new GtdError(`gtd config:\n - ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`)
|
|
57013
57081
|
});
|
|
57014
|
-
const diagnostics = report(wrongKindDiagnostics(loaded.
|
|
57082
|
+
const diagnostics = report([...loaded.diagnostics, ...wrongKindDiagnostics(loaded.settings, compiled)], [...rcOrder, module.filepath]);
|
|
57015
57083
|
yield* failOnErrors(diagnostics);
|
|
57084
|
+
if (loaded.default === void 0) return yield* die("unreachable: a default fails to build only with an error reported");
|
|
57016
57085
|
const initial = yield* firstStep(loaded.default, headTree(yield* Workspace));
|
|
57017
57086
|
const steering = loaded.file.steering;
|
|
57018
57087
|
const define = (entry) => ({
|
|
@@ -57034,8 +57103,8 @@ const load = gen(function* () {
|
|
|
57034
57103
|
const resolve = (name, entry) => ({
|
|
57035
57104
|
name,
|
|
57036
57105
|
def: define(entry),
|
|
57037
|
-
vars: { ...entry.
|
|
57038
|
-
env: { ...entry.
|
|
57106
|
+
vars: { ...entry.defaults },
|
|
57107
|
+
env: { ...entry.envDefaults }
|
|
57039
57108
|
});
|
|
57040
57109
|
const defaultWorkflow = resolve(loaded.default.name, loaded.default);
|
|
57041
57110
|
return {
|
|
@@ -57056,9 +57125,9 @@ const load = gen(function* () {
|
|
|
57056
57125
|
};
|
|
57057
57126
|
});
|
|
57058
57127
|
/** A name under the wrong key (or under both) in a `.gtdrc` layer: each is an error at its own origin. */
|
|
57059
|
-
const wrongKindDiagnostics = (
|
|
57060
|
-
const isEnvOnly = (key) => Object.hasOwn(
|
|
57061
|
-
const isVarOnly = (key) => Object.hasOwn(
|
|
57128
|
+
const wrongKindDiagnostics = (settings, compiled) => {
|
|
57129
|
+
const isEnvOnly = (key) => Object.hasOwn(settings.envDefaults, key);
|
|
57130
|
+
const isVarOnly = (key) => Object.hasOwn(settings.defaults, key);
|
|
57062
57131
|
const error = (path, message, origin) => ({
|
|
57063
57132
|
severity: "error",
|
|
57064
57133
|
path,
|
|
@@ -57179,16 +57248,20 @@ const RESERVED_EXPORTS = /* @__PURE__ */ new Set([
|
|
|
57179
57248
|
"access",
|
|
57180
57249
|
"doors"
|
|
57181
57250
|
]);
|
|
57251
|
+
/** The rules every declared setting meets, whether a file's export or a factory's build. */
|
|
57252
|
+
const checkSettings = (defaults, envDefaults, declares) => {
|
|
57253
|
+
for (const [kind, record] of [["defaults", defaults], ["envDefaults", envDefaults]]) for (const [name, setting] of Object.entries(record)) {
|
|
57254
|
+
if (!isSettingName(name)) throw new Error(`${declares(kind)} ${JSON.stringify(name)}, not a valid setting name — ${SETTING_NAME_RULE}`);
|
|
57255
|
+
if (typeof setting !== "string") throw new Error(`${declares(kind)} ${JSON.stringify(name)}, whose value is not a string`);
|
|
57256
|
+
}
|
|
57257
|
+
const both = Object.keys(defaults).filter((name) => Object.hasOwn(envDefaults, name));
|
|
57258
|
+
if (both.length > 0) throw new Error(`${both.map((name) => `"${name}"`).join(", ")} declared in both "defaults" and "envDefaults" — a setting is a process setting or an environment setting, not both`);
|
|
57259
|
+
};
|
|
57182
57260
|
/** A module's per-file exports: `defaults`, `envDefaults`, `steering`, `skills`, `access`, `summary`, `base`. */
|
|
57183
57261
|
const fileFrom = (exports, origin, dependencyFiles) => {
|
|
57184
57262
|
const defaults = stringRecord(exports, "defaults");
|
|
57185
57263
|
const envDefaults = stringRecord(exports, "envDefaults");
|
|
57186
|
-
|
|
57187
|
-
const bad = Object.keys(record).find((name) => !isSettingName(name));
|
|
57188
|
-
if (bad !== void 0) throw new Error(`the "${exportName}" export declares ${JSON.stringify(bad)}, not a valid setting name — ${SETTING_NAME_RULE}`);
|
|
57189
|
-
}
|
|
57190
|
-
const both = Object.keys(defaults).filter((name) => Object.hasOwn(envDefaults, name));
|
|
57191
|
-
if (both.length > 0) throw new Error(`${both.map((name) => `"${name}"`).join(", ")} declared in both "defaults" and "envDefaults" — a setting is a process setting or an environment setting, not both`);
|
|
57264
|
+
checkSettings(defaults, envDefaults, (kind) => `the "${kind}" export declares`);
|
|
57192
57265
|
return {
|
|
57193
57266
|
defaults,
|
|
57194
57267
|
envDefaults,
|
|
@@ -57243,43 +57316,172 @@ const checkDoors = (declared, catalogue) => {
|
|
|
57243
57316
|
};
|
|
57244
57317
|
const namespaceOf = (exported) => typeof exported === "object" && exported !== null ? exported : {};
|
|
57245
57318
|
/**
|
|
57246
|
-
* Evaluate `gtd.config.ts`. Every exported function
|
|
57247
|
-
* a workflow named by its export. `cache` is jiti's OWN
|
|
57248
|
-
* (unrelated to the instance's `moduleCache: false`) —
|
|
57249
|
-
* in has the side effect of collecting every real file
|
|
57250
|
-
* evaluating this module, module AND every relative import it
|
|
57251
|
-
* transitively.
|
|
57319
|
+
* Evaluate `gtd.config.ts`. Every exported function or factory workflow but
|
|
57320
|
+
* the reserved names is a workflow named by its export. `cache` is jiti's OWN
|
|
57321
|
+
* per-call module cache (unrelated to the instance's `moduleCache: false`) —
|
|
57322
|
+
* passing an empty object in has the side effect of collecting every real file
|
|
57323
|
+
* jiti resolved evaluating this module, module AND every relative import it
|
|
57324
|
+
* pulled in, transitively.
|
|
57252
57325
|
*/
|
|
57253
|
-
const loadWorkflows = (module, shipped) => {
|
|
57326
|
+
const loadWorkflows = (module, shipped, compiled) => {
|
|
57254
57327
|
const cache = {};
|
|
57255
57328
|
const exports = namespaceOf(jiti(shipped).evalModule(module.source, {
|
|
57256
57329
|
filename: module.filepath,
|
|
57257
57330
|
cache
|
|
57258
57331
|
}));
|
|
57259
57332
|
const file = fileFrom(exports, module.filepath, Object.keys(cache));
|
|
57260
|
-
const
|
|
57261
|
-
|
|
57262
|
-
|
|
57263
|
-
|
|
57264
|
-
|
|
57265
|
-
|
|
57333
|
+
const exported = Object.entries(exports).filter(([name, value]) => !RESERVED_EXPORTS.has(name) && isWorkflow(value));
|
|
57334
|
+
const value = defaultExport(exports);
|
|
57335
|
+
const defaultName = exported.find(([, other]) => other === value)?.[0] ?? "default";
|
|
57336
|
+
const built = /* @__PURE__ */ new Map();
|
|
57337
|
+
const rejections = [];
|
|
57338
|
+
const declarers = declarersOf([...exported, [defaultName, value]], file, compiled);
|
|
57339
|
+
for (const [name, entry] of [...exported, [defaultName, value]]) if (!built.has(entry)) built.set(entry, buildWorkflow(name, entry, file, compiled, rejections, declarers));
|
|
57340
|
+
const catalogue = new Map(exported.flatMap(([name, entry]) => {
|
|
57341
|
+
const workflow = built.get(entry);
|
|
57342
|
+
return workflow === void 0 ? [] : [[name, workflow]];
|
|
57343
|
+
}));
|
|
57344
|
+
const defaultWorkflow = built.get(value);
|
|
57345
|
+
return {
|
|
57346
|
+
file,
|
|
57347
|
+
catalogue,
|
|
57348
|
+
doors: rejections.length > 0 ? /* @__PURE__ */ new Map() : checkDoors(doorsFrom(exports), catalogue),
|
|
57349
|
+
default: defaultWorkflow && {
|
|
57350
|
+
...defaultWorkflow,
|
|
57351
|
+
name: defaultName
|
|
57352
|
+
},
|
|
57353
|
+
settings: mergeSettings([...built.values()].filter((w) => w !== void 0)),
|
|
57354
|
+
diagnostics: nameRejections(rejections)
|
|
57355
|
+
};
|
|
57356
|
+
};
|
|
57357
|
+
const isWorkflow = (value) => typeof value === "function" || isFactoryWorkflow(value);
|
|
57358
|
+
const defaultExport = (exports) => {
|
|
57359
|
+
const value = exports.default;
|
|
57360
|
+
if (value === void 0) throw new Error("no default export — export the workflow an ordinary start runs (`gtd install` scaffolds one)");
|
|
57361
|
+
if (!isWorkflow(value)) throw new Error("the default export is not a function");
|
|
57362
|
+
return value;
|
|
57363
|
+
};
|
|
57364
|
+
const mergeSettings = (all) => ({
|
|
57365
|
+
defaults: Object.assign({}, ...all.map((s) => s.defaults)),
|
|
57366
|
+
envDefaults: Object.assign({}, ...all.map((s) => s.envDefaults))
|
|
57367
|
+
});
|
|
57368
|
+
/** Each setting name, with who declares it: the file's exports, or a factory's own build. */
|
|
57369
|
+
const declarersOf = (exported, file, compiled) => {
|
|
57370
|
+
const declarers = /* @__PURE__ */ new Map();
|
|
57371
|
+
const declare = (key, who) => {
|
|
57372
|
+
declarers.set(key, (declarers.get(key) ?? /* @__PURE__ */ new Set()).add(who));
|
|
57373
|
+
};
|
|
57374
|
+
for (const key of [...Object.keys(file.defaults), ...Object.keys(file.envDefaults)]) declare(key, file);
|
|
57375
|
+
for (const [, value] of exported) {
|
|
57376
|
+
const own = ownBuild(value, {
|
|
57377
|
+
...compiled.rcVars,
|
|
57378
|
+
...compiled.rcEnv
|
|
57266
57379
|
});
|
|
57380
|
+
if (own?.ok) for (const key of [...Object.keys(own.build.defaults ?? {}), ...Object.keys(own.build.envDefaults ?? {})]) declare(key, value);
|
|
57381
|
+
}
|
|
57382
|
+
return declarers;
|
|
57383
|
+
};
|
|
57384
|
+
/**
|
|
57385
|
+
* A factory's build as its call alone gives it — or, when the call fails, with
|
|
57386
|
+
* the `.gtdrc` values for just the options it flagged, since a name the factory
|
|
57387
|
+
* itself rejects is its own.
|
|
57388
|
+
*/
|
|
57389
|
+
const ownBuild = (value, rc) => {
|
|
57390
|
+
const bare = configure$1(value);
|
|
57391
|
+
if (bare === void 0 || bare.ok) return bare;
|
|
57392
|
+
const flagged = new Set(bare.issues.map((issue) => issue.path[0]));
|
|
57393
|
+
return configure$1(value, Object.fromEntries(Object.entries(rc).filter(([key]) => flagged.has(key))));
|
|
57394
|
+
};
|
|
57395
|
+
/**
|
|
57396
|
+
* One exported workflow: a plain flow as-is, a factory validated and built.
|
|
57397
|
+
* `.gtdrc` `vars:`/`env:` values reach a factory as options, so the factory
|
|
57398
|
+
* judges them — except a name some other loaded workflow declares and this one
|
|
57399
|
+
* does not, which belongs to that workflow.
|
|
57400
|
+
*/
|
|
57401
|
+
const buildWorkflow = (name, value, file, compiled, rejections, declarers) => {
|
|
57402
|
+
if (configure$1(value) === void 0) return {
|
|
57403
|
+
flow: value,
|
|
57404
|
+
file,
|
|
57405
|
+
defaults: file.defaults,
|
|
57406
|
+
envDefaults: file.envDefaults
|
|
57407
|
+
};
|
|
57408
|
+
const belongsElsewhere = (key) => {
|
|
57409
|
+
const who = declarers.get(key);
|
|
57410
|
+
return who !== void 0 && !who.has(value);
|
|
57411
|
+
};
|
|
57412
|
+
const overrides = Object.fromEntries(Object.entries({
|
|
57413
|
+
...compiled.rcVars,
|
|
57414
|
+
...compiled.rcEnv
|
|
57415
|
+
}).filter(([key]) => !belongsElsewhere(key)));
|
|
57416
|
+
const result = configure$1(value, overrides);
|
|
57417
|
+
if (!result.ok) {
|
|
57418
|
+
for (const issue of result.issues) rejections.push(optionDiagnostic(name, issue, overrides, file, compiled));
|
|
57419
|
+
return;
|
|
57267
57420
|
}
|
|
57268
|
-
const flow = exports.default;
|
|
57269
|
-
if (flow === void 0) throw new Error("no default export — export the workflow an ordinary start runs (`gtd install` scaffolds one)");
|
|
57270
|
-
if (typeof flow !== "function") throw new Error("the default export is not a function");
|
|
57271
|
-
const name = [...catalogue].find(([, entry]) => entry.flow === flow)?.[0] ?? "default";
|
|
57272
57421
|
return {
|
|
57422
|
+
flow: result.build.flow,
|
|
57273
57423
|
file,
|
|
57274
|
-
|
|
57275
|
-
|
|
57276
|
-
|
|
57277
|
-
|
|
57278
|
-
|
|
57279
|
-
|
|
57280
|
-
|
|
57424
|
+
...builtSettings(name, file, result.build)
|
|
57425
|
+
};
|
|
57426
|
+
};
|
|
57427
|
+
/** A factory's settings over its file's, held to the rules a file's own exports are. */
|
|
57428
|
+
const builtSettings = (name, file, build) => {
|
|
57429
|
+
checkSettings(build.defaults ?? {}, build.envDefaults ?? {}, (kind) => `workflow "${name}" builds ${kind}`);
|
|
57430
|
+
const defaults = {
|
|
57431
|
+
...file.defaults,
|
|
57432
|
+
...build.defaults
|
|
57433
|
+
};
|
|
57434
|
+
const envDefaults = {
|
|
57435
|
+
...file.envDefaults,
|
|
57436
|
+
...build.envDefaults
|
|
57437
|
+
};
|
|
57438
|
+
checkSettings(defaults, envDefaults, (kind) => `workflow "${name}" builds ${kind}`);
|
|
57439
|
+
return {
|
|
57440
|
+
defaults,
|
|
57441
|
+
envDefaults
|
|
57281
57442
|
};
|
|
57282
57443
|
};
|
|
57444
|
+
/** A rejected option, reported where its value came from: the innermost `.gtdrc` that set it, else `gtd.config.ts`. */
|
|
57445
|
+
const optionDiagnostic = (workflow, issue, overrides, file, compiled) => {
|
|
57446
|
+
const [key] = issue.path;
|
|
57447
|
+
const message = `${issue.path.length > 0 ? `option "${issue.path.join(".")}"` : "options"}: ${issue.message}`;
|
|
57448
|
+
if (!(typeof key === "string" && Object.hasOwn(overrides, key))) return {
|
|
57449
|
+
workflow,
|
|
57450
|
+
severity: "error",
|
|
57451
|
+
path: issue.path,
|
|
57452
|
+
message,
|
|
57453
|
+
origin: file.origin
|
|
57454
|
+
};
|
|
57455
|
+
const kind = Object.hasOwn(compiled.rcEnv, key) ? "env" : "vars";
|
|
57456
|
+
const origin = [...kind === "env" ? compiled.envKeys : compiled.varsKeys].reverse().find((entry) => entry.key === key)?.origin;
|
|
57457
|
+
return {
|
|
57458
|
+
workflow,
|
|
57459
|
+
severity: "error",
|
|
57460
|
+
path: [kind, ...issue.path],
|
|
57461
|
+
message,
|
|
57462
|
+
origin: origin ?? file.origin
|
|
57463
|
+
};
|
|
57464
|
+
};
|
|
57465
|
+
const nameRejections = (rejections) => {
|
|
57466
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
57467
|
+
for (const rejection of rejections) {
|
|
57468
|
+
const key = JSON.stringify([
|
|
57469
|
+
rejection.origin,
|
|
57470
|
+
rejection.path,
|
|
57471
|
+
rejection.message
|
|
57472
|
+
]);
|
|
57473
|
+
const group = grouped.get(key);
|
|
57474
|
+
if (group === void 0) grouped.set(key, {
|
|
57475
|
+
first: rejection,
|
|
57476
|
+
workflows: [rejection.workflow]
|
|
57477
|
+
});
|
|
57478
|
+
else group.workflows.push(rejection.workflow);
|
|
57479
|
+
}
|
|
57480
|
+
return [...grouped.values()].map(({ first: { workflow: _, ...diagnostic }, workflows }) => ({
|
|
57481
|
+
...diagnostic,
|
|
57482
|
+
message: `${workflows.length > 1 ? "workflows" : "workflow"} ${workflows.map((w) => `"${w}"`).join(", ")} ${diagnostic.message}`
|
|
57483
|
+
}));
|
|
57484
|
+
};
|
|
57283
57485
|
const headTree = (workspace) => {
|
|
57284
57486
|
let entries;
|
|
57285
57487
|
const list = () => entries ??= workspace.treeSync("HEAD");
|
|
@@ -57748,23 +57950,73 @@ const envProvider = (env) => {
|
|
|
57748
57950
|
return value;
|
|
57749
57951
|
};
|
|
57750
57952
|
const checkPairing = (resolved) => resolved.model !== void 0 && resolved.provider !== void 0 && resolved.provider !== "llm" ? fail(`a model only applies to provider llm, not ${resolved.provider}`) : resolved;
|
|
57953
|
+
const userConfigPath = (env) => {
|
|
57954
|
+
const xdg = env["XDG_CONFIG_HOME"];
|
|
57955
|
+
if (xdg) return `${xdg}/gtd/judge.json`;
|
|
57956
|
+
const home = env["HOME"];
|
|
57957
|
+
return home ? `${home}/.config/gtd/judge.json` : void 0;
|
|
57958
|
+
};
|
|
57959
|
+
const readText = async (path) => {
|
|
57960
|
+
try {
|
|
57961
|
+
return await readFile(path, "utf8");
|
|
57962
|
+
} catch (e) {
|
|
57963
|
+
if (e.code === "ENOENT") return void 0;
|
|
57964
|
+
return fail(`cannot read ${path}: ${e instanceof Error ? e.message : String(e)}`);
|
|
57965
|
+
}
|
|
57966
|
+
};
|
|
57967
|
+
const parseObject = (path, text) => {
|
|
57968
|
+
let json;
|
|
57969
|
+
try {
|
|
57970
|
+
json = JSON.parse(text);
|
|
57971
|
+
} catch {
|
|
57972
|
+
return fail(`${path} is not valid JSON`);
|
|
57973
|
+
}
|
|
57974
|
+
return typeof json === "object" && json !== null && !Array.isArray(json) ? json : fail(`${path} must be a JSON object`);
|
|
57975
|
+
};
|
|
57976
|
+
const decodeUserConfig = (path, text) => {
|
|
57977
|
+
const { provider, model, ...rest } = parseObject(path, text);
|
|
57978
|
+
const [extra] = Object.keys(rest);
|
|
57979
|
+
if (extra !== void 0) return fail(`${path}: unknown key "${extra}"`);
|
|
57980
|
+
if (provider !== void 0 && !PROVIDERS.includes(provider)) return fail(`${path}: "provider" must be one of ${PROVIDERS.join(", ")}`);
|
|
57981
|
+
if (model !== void 0 && typeof model !== "string") return fail(`${path}: "model" must be a string`);
|
|
57982
|
+
return {
|
|
57983
|
+
provider,
|
|
57984
|
+
model
|
|
57985
|
+
};
|
|
57986
|
+
};
|
|
57987
|
+
const readUserConfig = async (env) => {
|
|
57988
|
+
const path = userConfigPath(env);
|
|
57989
|
+
const text = path === void 0 ? void 0 : await readText(path);
|
|
57990
|
+
return path === void 0 || text === void 0 ? void 0 : decodeUserConfig(path, text);
|
|
57991
|
+
};
|
|
57751
57992
|
/**
|
|
57752
57993
|
* Per field, the first one set wins: flag, then `GTD_JUDGE_PROVIDER` /
|
|
57753
|
-
* `GTD_JUDGE_MODEL` (empty counts as unset), then `.gtdrc`
|
|
57754
|
-
* provider/model pairing is checked on the resolved pair,
|
|
57755
|
-
* supplied each half.
|
|
57994
|
+
* `GTD_JUDGE_MODEL` (empty counts as unset), then the user file, then `.gtdrc`
|
|
57995
|
+
* `judge:`. The provider/model pairing is checked on the resolved pair,
|
|
57996
|
+
* whichever layer supplied each half.
|
|
57756
57997
|
*/
|
|
57757
|
-
const selectJudge = ({ provider, model, configured, env }) => {
|
|
57758
|
-
const
|
|
57759
|
-
|
|
57760
|
-
|
|
57761
|
-
|
|
57762
|
-
|
|
57998
|
+
const selectJudge = ({ provider, model, configured, env }, user) => {
|
|
57999
|
+
const layers = [
|
|
58000
|
+
{
|
|
58001
|
+
provider,
|
|
58002
|
+
model
|
|
58003
|
+
},
|
|
58004
|
+
{
|
|
58005
|
+
provider: envProvider(env),
|
|
58006
|
+
model: env["GTD_JUDGE_MODEL"] || void 0
|
|
58007
|
+
},
|
|
58008
|
+
user ?? {},
|
|
58009
|
+
configured ?? {}
|
|
58010
|
+
];
|
|
58011
|
+
return checkPairing({
|
|
58012
|
+
provider: layers.find((l) => l.provider !== void 0)?.provider,
|
|
58013
|
+
model: layers.find((l) => l.model !== void 0)?.model
|
|
58014
|
+
});
|
|
57763
58015
|
};
|
|
57764
58016
|
/** The only place auto selection lives; it never falls back across providers. */
|
|
57765
|
-
const answererFor = (opts) => {
|
|
58017
|
+
const answererFor = async (opts) => {
|
|
57766
58018
|
const { answers, env, cwd } = opts;
|
|
57767
|
-
const { provider, model } = selectJudge(opts);
|
|
58019
|
+
const { provider, model } = selectJudge(opts, await readUserConfig(env));
|
|
57768
58020
|
if (provider === "fixed") return fixed({
|
|
57769
58021
|
answersPath: answers,
|
|
57770
58022
|
env
|
|
@@ -57785,7 +58037,7 @@ const runJudge = async (opts) => {
|
|
|
57785
58037
|
}
|
|
57786
58038
|
const { questions, state } = doc ?? {};
|
|
57787
58039
|
if (!Array.isArray(questions)) return fail("stdin has no \"questions\" array");
|
|
57788
|
-
const verdicts = await answererFor(opts)(questions, typeof state === "object" && state !== null ? state : {});
|
|
58040
|
+
const verdicts = await (await answererFor(opts))(questions, typeof state === "object" && state !== null ? state : {});
|
|
57789
58041
|
return JSON.stringify(verdicts) + "\n";
|
|
57790
58042
|
};
|
|
57791
58043
|
//#endregion
|
|
@@ -75762,7 +76014,8 @@ const Beat = Struct({
|
|
|
75762
76014
|
model: optional$2(String$),
|
|
75763
76015
|
system: optional$2(String$),
|
|
75764
76016
|
validate: optional$2(String$),
|
|
75765
|
-
label: optional$2(String$)
|
|
76017
|
+
label: optional$2(String$),
|
|
76018
|
+
judge: optional$2(String$)
|
|
75766
76019
|
});
|
|
75767
76020
|
const Land = Struct({
|
|
75768
76021
|
schema: Number$,
|
|
@@ -75958,10 +76211,11 @@ const gitRoot = gen(function* () {
|
|
|
75958
76211
|
if (exit.code !== 0) return yield* fail$7(/* @__PURE__ */ new Error("not inside a git repository"));
|
|
75959
76212
|
return exit.stdout.trim();
|
|
75960
76213
|
});
|
|
75961
|
-
const gtd = (ctx, args) => flatMap$4(ctx.host.spawn({
|
|
76214
|
+
const gtd = (ctx, args, stdin) => flatMap$4(ctx.host.spawn({
|
|
75962
76215
|
command: ctx.gtd[0],
|
|
75963
76216
|
args: [...ctx.gtd.slice(1), ...args],
|
|
75964
76217
|
cwd: ctx.root,
|
|
76218
|
+
...stdin === void 0 ? {} : { stdin },
|
|
75965
76219
|
output: "capture"
|
|
75966
76220
|
}), (exit) => exit.code === 0 ? succeed$3(exit.stdout) : fail$7(/* @__PURE__ */ new Error(`\`gtd ${args.join(" ")}\` failed (exit ${exit.code})\n${exit.stderr.trimEnd()}`)));
|
|
75967
76221
|
const readBeat = (ctx) => flatMap$4(gtd(ctx, ["next", "--json"]), (stdout) => fromEither(decodeBeat(stdout)));
|
|
@@ -76044,29 +76298,41 @@ const runScript = (ctx, content) => as(ctx.host.spawn({
|
|
|
76044
76298
|
output: { log: ctx.log }
|
|
76045
76299
|
}), LAND);
|
|
76046
76300
|
const stop = (ctx, sgr, headline, prose) => as(zipRight$1(ctx.host.appendLog(ctx.log, prose), rest(ctx, sgr, headline)), "stop");
|
|
76047
|
-
const
|
|
76048
|
-
|
|
76301
|
+
const judgeGate = (ctx, doc) => gtd(ctx, ["judge", "run"], doc.judge).pipe(flatMap$4((verdict) => gtd(ctx, [
|
|
76302
|
+
"judge",
|
|
76303
|
+
"answer",
|
|
76304
|
+
"--json"
|
|
76305
|
+
], verdict)), tap(() => ctx.host.say(` ${paint(ctx.host, "2", "⚖ judged")}\n`)), catchAll((error) => as(ctx.host.appendLog(ctx.log, `${error.message}\n`), void 0)));
|
|
76306
|
+
const messageBeat = (ctx, beat, doc, judged) => gen(function* () {
|
|
76307
|
+
const isJudge = doc.judge !== void 0 && doc.judge !== "";
|
|
76308
|
+
const landing = isJudge && judged !== doc.state ? yield* judgeGate(ctx, doc) : void 0;
|
|
76309
|
+
if (landing !== void 0) return {
|
|
76310
|
+
...LAND,
|
|
76311
|
+
landing
|
|
76312
|
+
};
|
|
76313
|
+
if (beat === 1 && !isJudge) return LAND;
|
|
76049
76314
|
const label = doc.label === void 0 ? "" : ` (${doc.label})`;
|
|
76050
|
-
return stop(ctx, "33", `your turn — ${doc.state}${label}`, doc.content);
|
|
76051
|
-
};
|
|
76052
|
-
const kindBeat = (ctx, beat, doc) => {
|
|
76315
|
+
return yield* stop(ctx, "33", `your turn — ${doc.state}${label}`, doc.content);
|
|
76316
|
+
});
|
|
76317
|
+
const kindBeat = (ctx, beat, doc, judged) => {
|
|
76053
76318
|
switch (doc.kind) {
|
|
76054
76319
|
case "stalled": return zipRight$1(ctx.host.appendLog(ctx.log, doc.content), fail$7(new Error(doc.content.trimEnd())));
|
|
76055
|
-
case "message": return messageBeat(ctx, beat, doc);
|
|
76320
|
+
case "message": return messageBeat(ctx, beat, doc, judged);
|
|
76056
76321
|
case "capture": return succeed$3(LAND);
|
|
76057
76322
|
case "script": return runScript(ctx, doc.content);
|
|
76058
76323
|
case "prompt": return promptBeat(ctx, doc);
|
|
76059
76324
|
default: return fail$7(/* @__PURE__ */ new Error(`unknown beat kind "${doc.kind}"`));
|
|
76060
76325
|
}
|
|
76061
76326
|
};
|
|
76062
|
-
const runBeat = (ctx, beat, doc) => beat > 1 && doc.idle ? flatMap$4(gtd(ctx, ["next"]), (message) => stop(ctx, "32", `done — ${doc.state}`, message)) : kindBeat(ctx, beat, doc);
|
|
76327
|
+
const runBeat = (ctx, beat, doc, judged) => beat > 1 && doc.idle ? flatMap$4(gtd(ctx, ["next"]), (message) => stop(ctx, "32", `done — ${doc.state}`, message)) : kindBeat(ctx, beat, doc, judged);
|
|
76063
76328
|
const costFlags = (outcome) => outcome.cost > 0 ? [`--cost=${outcome.cost.toFixed(6)}`, ...outcome.model === void 0 ? [] : [`--model=${outcome.model}`]] : [];
|
|
76064
76329
|
const land = (ctx, outcome) => gen(function* () {
|
|
76065
|
-
const
|
|
76330
|
+
const landed = outcome.landing ?? (yield* gtd(ctx, [
|
|
76066
76331
|
"land",
|
|
76067
76332
|
"--json",
|
|
76068
76333
|
...costFlags(outcome)
|
|
76069
|
-
]))
|
|
76334
|
+
]));
|
|
76335
|
+
const doc = yield* fromEither(decodeLand(landed));
|
|
76070
76336
|
const exit = yield* ctx.host.spawn({
|
|
76071
76337
|
command: "sh",
|
|
76072
76338
|
args: [],
|
|
@@ -76106,11 +76372,13 @@ const runLoop = (options) => gen(function* () {
|
|
|
76106
76372
|
log: resolve(root, doc.log)
|
|
76107
76373
|
};
|
|
76108
76374
|
yield* host.resetLog(ctx.log);
|
|
76375
|
+
let judged;
|
|
76109
76376
|
for (let beat = 1;; beat++) {
|
|
76110
76377
|
if (beat > 1) doc = yield* readBeat(ctx);
|
|
76111
76378
|
yield* header(ctx, beat, doc);
|
|
76112
|
-
const outcome = yield* runBeat(ctx, beat, doc);
|
|
76379
|
+
const outcome = yield* runBeat(ctx, beat, doc, judged);
|
|
76113
76380
|
if (outcome === "stop" || (yield* land(ctx, outcome))) return;
|
|
76381
|
+
judged = outcome.landing === void 0 ? void 0 : doc.state;
|
|
76114
76382
|
}
|
|
76115
76383
|
});
|
|
76116
76384
|
//#endregion
|
|
@@ -76897,8 +77165,8 @@ const FLAGS = [
|
|
|
76897
77165
|
valueHint: "<name>",
|
|
76898
77166
|
help: [
|
|
76899
77167
|
"(gtd judge run only) the answerer: fixed | jev | llm;",
|
|
76900
|
-
"omitted: GTD_JUDGE_PROVIDER, then .
|
|
76901
|
-
"when TYPESAFE_API_KEY is set, else llm"
|
|
77168
|
+
"omitted: GTD_JUDGE_PROVIDER, then judge.json, then .gtdrc",
|
|
77169
|
+
"judge:, then jev when TYPESAFE_API_KEY is set, else llm"
|
|
76902
77170
|
]
|
|
76903
77171
|
},
|
|
76904
77172
|
{
|
|
@@ -76936,7 +77204,8 @@ const FLAGS = [
|
|
|
76936
77204
|
help: [
|
|
76937
77205
|
"(gtd land, with --cost) tag that cost's model",
|
|
76938
77206
|
"(gtd judge run, llm answerer) the claude model; omitted:",
|
|
76939
|
-
"GTD_JUDGE_MODEL, then .gtdrc judge:, then
|
|
77207
|
+
"GTD_JUDGE_MODEL, then judge.json, then .gtdrc judge:, then",
|
|
77208
|
+
"haiku"
|
|
76940
77209
|
]
|
|
76941
77210
|
},
|
|
76942
77211
|
{
|
|
@@ -77336,11 +77605,11 @@ const COMMAND_ROWS = [
|
|
|
77336
77605
|
"does not cover are left out. --provider jev asks TypeSafe",
|
|
77337
77606
|
"(TYPESAFE_API_KEY). --provider llm asks the `claude` CLI",
|
|
77338
77607
|
"on PATH (Claude Code only), model haiku unless --model.",
|
|
77339
|
-
"No --provider: GTD_JUDGE_PROVIDER, then
|
|
77340
|
-
"provider, else jev when
|
|
77341
|
-
"model is given, else llm;
|
|
77342
|
-
"
|
|
77343
|
-
"
|
|
77608
|
+
"No --provider: GTD_JUDGE_PROVIDER, then ~/.config/gtd/",
|
|
77609
|
+
"judge.json, then .gtdrc judge: provider, else jev when",
|
|
77610
|
+
"TYPESAFE_API_KEY is set and no model is given, else llm;",
|
|
77611
|
+
"--model likewise falls back to GTD_JUDGE_MODEL, then the",
|
|
77612
|
+
"file's and judge:'s model. Needs no repository. jev and",
|
|
77344
77613
|
"llm exit 1 when they cannot answer every question, with",
|
|
77345
77614
|
"nothing on stdout"
|
|
77346
77615
|
]
|