@pmelab/gtd 18.0.0 → 19.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 +34 -30
- package/claude/hooks/access.ts +58 -0
- package/claude/hooks/drive.ts +42 -4
- package/claude/hooks/register.tsx +12 -0
- package/dist/gtd.bundle.mjs +779 -668
- package/package.json +2 -1
- package/schema.json +22 -0
- package/skills/authoring/SKILL.md +6 -7
- package/src/flows/helpers.ts +4 -2
- package/src/flows/runtime.ts +20 -0
- package/src/flows/scripts.test.ts +28 -1
- package/src/flows/scripts.ts +3 -0
- package/src/workflows/access.test.ts +40 -0
- package/src/workflows/access.ts +22 -0
- package/src/workflows/health.test.ts +21 -1
- package/src/workflows/health.ts +47 -8
- package/src/workflows/packages.test.ts +91 -0
- package/src/workflows/packages.ts +96 -76
- package/src/workflows/planning.ts +5 -49
- package/src/workflows/prose.ts +18 -9
- package/src/workflows/review.test.ts +88 -1
- package/src/workflows/review.ts +41 -16
- package/src/workflows/scenarios.test.ts +256 -0
- package/src/workflows/scenarios.ts +137 -0
- package/src/workflows/skills.test.ts +0 -4
- package/src/workflows/skills.ts +0 -2
- package/src/workflows/steps.test.ts +0 -10
- package/src/workflows/steps.ts +2 -20
- package/src/workflows/text.fixture.ts +26 -0
- package/src/workflows/text.test.ts +101 -0
- package/src/workflows/text.ts +125 -97
- package/src/workflows/unified.ts +9 -4
- package/src/workflows/vars.ts +1 -2
- package/src/workflows/diff.test.ts +0 -115
- package/src/workflows/diff.ts +0 -306
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -30000,7 +30000,7 @@ const handleBadArgument = (method) => (cause) => new BadArgument({
|
|
|
30000
30000
|
method,
|
|
30001
30001
|
cause
|
|
30002
30002
|
});
|
|
30003
|
-
const access = /*#__PURE__*/ (() => {
|
|
30003
|
+
const access$1 = /*#__PURE__*/ (() => {
|
|
30004
30004
|
const nodeAccess = /*#__PURE__*/ effectify(NFS.access, /*#__PURE__*/ handleErrnoException("FileSystem", "access"), /*#__PURE__*/ handleBadArgument("access"));
|
|
30005
30005
|
return (path, options) => {
|
|
30006
30006
|
let mode = NFS.constants.F_OK;
|
|
@@ -30285,7 +30285,7 @@ const writeFile$1 = (path, data, options) => async((resume, signal) => {
|
|
|
30285
30285
|
* @category layer
|
|
30286
30286
|
*/
|
|
30287
30287
|
const layer$7 = /* @__PURE__ */ effect$1(FileSystem, /* @__PURE__ */ map$4(/*#__PURE__*/ serviceOption(WatchBackend), (backend) => make$6({
|
|
30288
|
-
access,
|
|
30288
|
+
access: access$1,
|
|
30289
30289
|
chmod,
|
|
30290
30290
|
chown,
|
|
30291
30291
|
copy,
|
|
@@ -30968,6 +30968,8 @@ const head = () => ctx().head();
|
|
|
30968
30968
|
const start = () => ctx().start();
|
|
30969
30969
|
/** The skill list `localName` (scoped from here, same as `agent()`) resolves to — for a prompt preamble. */
|
|
30970
30970
|
const skillsFor = (localName) => ctx().skillsFor(localName);
|
|
30971
|
+
/** The folded access `localName` (scoped from here, same as `agent()`) resolves to — for a prompt preamble. */
|
|
30972
|
+
const accessFor = (localName, file) => ctx().accessFor(localName, file);
|
|
30971
30973
|
const settingsProxy = (read) => new Proxy({}, {
|
|
30972
30974
|
get: (_target, key) => typeof key === "string" ? read()[key] : void 0,
|
|
30973
30975
|
has: (_target, key) => typeof key === "string" && key in read(),
|
|
@@ -31011,6 +31013,7 @@ const checkScript = (command, options) => {
|
|
|
31011
31013
|
return [
|
|
31012
31014
|
"#!/usr/bin/env sh",
|
|
31013
31015
|
"set +e",
|
|
31016
|
+
...options.preamble ?? [],
|
|
31014
31017
|
...removal(options.sweep ?? []),
|
|
31015
31018
|
`mkdir -p "$(dirname ${report})"`,
|
|
31016
31019
|
"(",
|
|
@@ -31104,12 +31107,13 @@ const wrote = (path) => changes(path).some((c) => c.status !== "deleted");
|
|
|
31104
31107
|
* command passed.
|
|
31105
31108
|
*/
|
|
31106
31109
|
const check = async (name, command, options) => {
|
|
31107
|
-
const { report, label, sweep, sweepOnGreen } = options;
|
|
31110
|
+
const { report, label, sweep, sweepOnGreen, preamble } = options;
|
|
31108
31111
|
await run$3(name, checkScript(command, {
|
|
31109
31112
|
report,
|
|
31110
31113
|
stamp: head().slice(0, 7),
|
|
31111
31114
|
sweep,
|
|
31112
|
-
sweepOnGreen
|
|
31115
|
+
sweepOnGreen,
|
|
31116
|
+
preamble
|
|
31113
31117
|
}), { label });
|
|
31114
31118
|
return !wrote(report);
|
|
31115
31119
|
};
|
|
@@ -31161,6 +31165,7 @@ const requireRevert = (edited, base) => {
|
|
|
31161
31165
|
//#endregion
|
|
31162
31166
|
//#region src/flows/index.ts
|
|
31163
31167
|
var flows_exports = /* @__PURE__ */ __exportAll({
|
|
31168
|
+
accessFor: () => accessFor,
|
|
31164
31169
|
agent: () => agent,
|
|
31165
31170
|
answered: () => answered,
|
|
31166
31171
|
changes: () => changes,
|
|
@@ -47480,6 +47485,34 @@ const viewOf = (format, content) => ({
|
|
|
47480
47485
|
/** Every OPEN thread — last entry is the agent's — as `{ name, line, firstMe }` (0-based line). `[]` for a format with no thread concept. */
|
|
47481
47486
|
const openThreads = (format, content) => format.openThreads?.(content) ?? [];
|
|
47482
47487
|
//#endregion
|
|
47488
|
+
//#region src/replay/Access.ts
|
|
47489
|
+
const UNRESTRICTED = {};
|
|
47490
|
+
const withExtras = (globs, extras) => globs === void 0 ? null : [.../* @__PURE__ */ new Set([...globs, ...extras])];
|
|
47491
|
+
/**
|
|
47492
|
+
* Fold a step's own paths into a scope's access: each restricted side gets the
|
|
47493
|
+
* steering `file`, then the code threads waiting on the agent. An unrestricted
|
|
47494
|
+
* side stays `null`.
|
|
47495
|
+
*/
|
|
47496
|
+
const foldAccess = (scopeAccess, file, threadPaths) => {
|
|
47497
|
+
const access = scopeAccess ?? UNRESTRICTED;
|
|
47498
|
+
const extras = [...file === void 0 ? [] : [file], ...threadPaths];
|
|
47499
|
+
return {
|
|
47500
|
+
read: withExtras(access.read, extras),
|
|
47501
|
+
write: withExtras(access.write, extras)
|
|
47502
|
+
};
|
|
47503
|
+
};
|
|
47504
|
+
/** The shape fault of a `scope()` `access` value, or `undefined` when well-formed. */
|
|
47505
|
+
const accessShapeFault = (value) => {
|
|
47506
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return "access must be an object of { read?, write? } glob arrays";
|
|
47507
|
+
for (const [side, globs] of Object.entries(value)) {
|
|
47508
|
+
if (side !== "read" && side !== "write") return `unknown access key "${side}"`;
|
|
47509
|
+
if (globs === void 0) continue;
|
|
47510
|
+
if (!Array.isArray(globs) || globs.some((g) => typeof g !== "string")) return `access.${side} must be an array of glob strings`;
|
|
47511
|
+
}
|
|
47512
|
+
};
|
|
47513
|
+
const sameGlobs = (a, b) => a === void 0 || b === void 0 ? a === b : a.length === b.length && a.every((g, i) => g === b[i]);
|
|
47514
|
+
const sameAccess = (a, b) => sameGlobs(a?.read, b?.read) && sameGlobs(a?.write, b?.write);
|
|
47515
|
+
//#endregion
|
|
47483
47516
|
//#region src/replay/Glob.ts
|
|
47484
47517
|
const ESCAPE_RE = /[.+^${}()|[\]\\?]/g;
|
|
47485
47518
|
const compile = (glob) => {
|
|
@@ -47701,7 +47734,8 @@ const budgeted = (evidence, budgetBytes) => {
|
|
|
47701
47734
|
truncated
|
|
47702
47735
|
};
|
|
47703
47736
|
};
|
|
47704
|
-
const
|
|
47737
|
+
const sameSkills = (a, b) => (a?.length ?? 0) === (b?.length ?? 0) && (a ?? []).every((skill, i) => skill === b?.[i]);
|
|
47738
|
+
const samePersona = (a, b) => a.model === b.model && a.system === b.system && sameSkills(a.skills, b.skills) && sameAccess(a.access, b.access);
|
|
47705
47739
|
/**
|
|
47706
47740
|
* Replay one episode: run its entry flow, answering each step from the
|
|
47707
47741
|
* episode's commits in order, until a step has no commit left — the rest.
|
|
@@ -47791,17 +47825,34 @@ const replay = async (input) => {
|
|
|
47791
47825
|
truncated: judgeCuts.get(request) ?? []
|
|
47792
47826
|
} : void 0;
|
|
47793
47827
|
const judgeCuts = /* @__PURE__ */ new WeakMap();
|
|
47794
|
-
const
|
|
47828
|
+
const resolveScoped = (memory, pick, configured, bundled) => {
|
|
47795
47829
|
const own = /* @__PURE__ */ new Map();
|
|
47796
47830
|
const names = [];
|
|
47797
47831
|
for (const s of scopes) {
|
|
47798
47832
|
if (s.name !== void 0) names.push(s.name);
|
|
47799
|
-
|
|
47833
|
+
const v = pick(s);
|
|
47834
|
+
if (v !== void 0) own.set(names.join("."), v);
|
|
47800
47835
|
}
|
|
47801
47836
|
const parts = memory === "" ? [] : memory.split(".");
|
|
47802
|
-
|
|
47837
|
+
return parts.map((_, i) => parts.slice(0, parts.length - i).join(".")).concat("").map((p) => configured?.[p] ?? own.get(p) ?? bundled?.[p]).find((r) => r !== void 0);
|
|
47838
|
+
};
|
|
47839
|
+
const resolveSkills = (memory) => {
|
|
47840
|
+
const hit = resolveScoped(memory, (s) => s.skills, input.configuredSkills, input.skills);
|
|
47803
47841
|
return hit === void 0 || hit.length === 0 ? void 0 : hit;
|
|
47804
47842
|
};
|
|
47843
|
+
const resolveAccess = (memory) => resolveScoped(memory, (s) => s.access, input.configuredAccess, input.access);
|
|
47844
|
+
const foldedAccessFor = (scopeAccess, file) => {
|
|
47845
|
+
return foldAccess(scopeAccess, file, scopeAccess?.read !== void 0 || scopeAccess?.write !== void 0 ? context.codeThreads().filter((t) => t.waitingOn === "agent").map((t) => t.path) : []);
|
|
47846
|
+
};
|
|
47847
|
+
const scopeAccessFault = () => {
|
|
47848
|
+
for (const [i, s] of scopes.entries()) {
|
|
47849
|
+
if (s.access === void 0) continue;
|
|
47850
|
+
const fault = accessShapeFault(s.access);
|
|
47851
|
+
if (fault === void 0) continue;
|
|
47852
|
+
return `gtd: scope "${scopes.slice(0, i + 1).flatMap((o) => o.name === void 0 ? [] : [o.name]).join(".") || "root"}": ${fault}`;
|
|
47853
|
+
}
|
|
47854
|
+
};
|
|
47855
|
+
const unfoldedAccess = /* @__PURE__ */ new WeakMap();
|
|
47805
47856
|
const resolve = (request) => {
|
|
47806
47857
|
if (request.kind === "agent") {
|
|
47807
47858
|
const persona = {};
|
|
@@ -47833,6 +47884,8 @@ const replay = async (input) => {
|
|
|
47833
47884
|
const occurrence = (occurrences.get(name) ?? 0) + 1;
|
|
47834
47885
|
occurrences.set(name, occurrence);
|
|
47835
47886
|
const resolved = resolve(request);
|
|
47887
|
+
const memory = memoryScopeOf(name);
|
|
47888
|
+
const scopeAccess = resolved.kind === "agent" ? resolveAccess(memory) : void 0;
|
|
47836
47889
|
const reached = {
|
|
47837
47890
|
id: {
|
|
47838
47891
|
name,
|
|
@@ -47843,11 +47896,15 @@ const replay = async (input) => {
|
|
|
47843
47896
|
actor: actorOfKind(resolved.kind),
|
|
47844
47897
|
request: resolved,
|
|
47845
47898
|
memoryScope: memoryScopeOf(name),
|
|
47846
|
-
...resolved.kind === "agent" ? {
|
|
47899
|
+
...resolved.kind === "agent" ? {
|
|
47900
|
+
skills: resolveSkills(memory),
|
|
47901
|
+
access: foldedAccessFor(scopeAccess, resolved.options.file)
|
|
47902
|
+
} : {},
|
|
47847
47903
|
enteredAt: position.hash,
|
|
47848
47904
|
truncated: (judgeCuts.get(resolved) ?? []).length > 0,
|
|
47849
47905
|
scopeCalls: namedScopeCalls()
|
|
47850
47906
|
};
|
|
47907
|
+
if (resolved.kind === "agent") unfoldedAccess.set(reached, scopeAccess);
|
|
47851
47908
|
trace.push(reached);
|
|
47852
47909
|
return reached;
|
|
47853
47910
|
};
|
|
@@ -47856,14 +47913,15 @@ const replay = async (input) => {
|
|
|
47856
47913
|
const identity = {
|
|
47857
47914
|
model: step.request.options.model,
|
|
47858
47915
|
system: step.request.options.system,
|
|
47859
|
-
skills: step.skills
|
|
47916
|
+
skills: step.skills,
|
|
47917
|
+
access: unfoldedAccess.get(step)
|
|
47860
47918
|
};
|
|
47861
47919
|
const seen = personas.get(step.memoryScope);
|
|
47862
47920
|
if (seen === void 0) {
|
|
47863
47921
|
personas.set(step.memoryScope, identity);
|
|
47864
47922
|
return;
|
|
47865
47923
|
}
|
|
47866
|
-
return samePersona(seen, identity) ? void 0 : `gtd: "${step.name}" runs with a different model, system prompt or
|
|
47924
|
+
return samePersona(seen, identity) ? void 0 : `gtd: "${step.name}" runs with a different model, system prompt, skills or file access than an earlier agent step in memory scope "${step.memoryScope || "root"}" — one scope is one conversation, and a conversation has one identity`;
|
|
47867
47925
|
};
|
|
47868
47926
|
const DIVERGED = "the workflow changed under this process; run `gtd abandon` to start over";
|
|
47869
47927
|
const blockerAt = (step) => {
|
|
@@ -47917,6 +47975,10 @@ const replay = async (input) => {
|
|
|
47917
47975
|
});
|
|
47918
47976
|
return replyFor(step.request, input.pending.verdicts ?? []);
|
|
47919
47977
|
};
|
|
47978
|
+
const requestFault = (request) => {
|
|
47979
|
+
if (request.kind === "run" && typeof request.body !== "string") return `gtd: step "${scoped(request.name)}": a run() body is a shell script string — decide in flow code, then render the script (see checkScript and friends)`;
|
|
47980
|
+
return request.kind === "agent" ? scopeAccessFault() : void 0;
|
|
47981
|
+
};
|
|
47920
47982
|
const handleStep = async (request) => {
|
|
47921
47983
|
if (outcome !== void 0) throw new Stop();
|
|
47922
47984
|
if (request.kind === "restart") return finish(endOfFlow("restart"));
|
|
@@ -47924,9 +47986,10 @@ const replay = async (input) => {
|
|
|
47924
47986
|
kind: "failed",
|
|
47925
47987
|
message: "gtd: a step was called without a name"
|
|
47926
47988
|
});
|
|
47927
|
-
|
|
47989
|
+
const fault = requestFault(request);
|
|
47990
|
+
if (fault !== void 0) return finish({
|
|
47928
47991
|
kind: "failed",
|
|
47929
|
-
message:
|
|
47992
|
+
message: fault
|
|
47930
47993
|
});
|
|
47931
47994
|
const step = reach(request);
|
|
47932
47995
|
const blocker = blockerAt(step);
|
|
@@ -47946,7 +48009,7 @@ const replay = async (input) => {
|
|
|
47946
48009
|
if (tree === void 0) throw new Error(`gtd: changesSince(${hash}): ${hash} is not the episode base or one of its commits — pass a hash this run read from head() or start(), not one captured earlier, read from state, or from another branch`);
|
|
47947
48010
|
return changesBetween(tree, position.tree);
|
|
47948
48011
|
};
|
|
47949
|
-
|
|
48012
|
+
const context = {
|
|
47950
48013
|
step: handleStep,
|
|
47951
48014
|
refuse: (message) => {
|
|
47952
48015
|
outcome ??= {
|
|
@@ -48001,11 +48064,13 @@ const replay = async (input) => {
|
|
|
48001
48064
|
env: input.env,
|
|
48002
48065
|
start: () => input.start,
|
|
48003
48066
|
skillsFor: (localName) => resolveSkills(memoryScopeOf(scoped(localName))) ?? [],
|
|
48067
|
+
accessFor: (localName, file) => foldedAccessFor(resolveAccess(memoryScopeOf(scoped(localName))), file),
|
|
48004
48068
|
head: () => {
|
|
48005
48069
|
const rest = commits.slice(cursor);
|
|
48006
48070
|
return rest.length > 0 && rest.every((c) => c.parsed.step === void 0) ? rest[rest.length - 1].hash : position.hash;
|
|
48007
48071
|
}
|
|
48008
|
-
}
|
|
48072
|
+
};
|
|
48073
|
+
installContext(context);
|
|
48009
48074
|
try {
|
|
48010
48075
|
const flowDone = input.flow(args).then(() => {
|
|
48011
48076
|
if (outcome === void 0 && trace.length === 0) outcome = {
|
|
@@ -48045,6 +48110,11 @@ const skillsPreamble = `- Load whatever's listed here that your harness actually
|
|
|
48045
48110
|
prompt states first
|
|
48046
48111
|
- Never let a loaded skill turn this turn interactive — answer nothing,
|
|
48047
48112
|
ask nothing; this runs unattended, with no one at a keyboard`;
|
|
48113
|
+
/** One bullet per restricted side; empty when neither is. An empty glob list reads "nothing". */
|
|
48114
|
+
const accessPreamble = (access) => {
|
|
48115
|
+
const globs = (list) => list.length === 0 ? "nothing" : list.join(", ");
|
|
48116
|
+
return [...access.read === null ? [] : [`- This turn may read only: ${globs(access.read)}`], ...access.write === null ? [] : [`- This turn may write only: ${globs(access.write)} — anything else is refused when the turn lands`]].join("\n");
|
|
48117
|
+
};
|
|
48048
48118
|
const styleBlock = `- A deliverable, not a chat reply — size follows the work; cut padding
|
|
48049
48119
|
- Lead with the answer; never circle back to restate it
|
|
48050
48120
|
- Flat, commanding sentences — commit to the claim, never hedge
|
|
@@ -48089,8 +48159,7 @@ const architectPersona = `You are the technical planning voice in gtd's build pi
|
|
|
48089
48159
|
once product concerns are settled, reading them cold, no carried
|
|
48090
48160
|
conversation. Work out the *how* per concern — structure, data models,
|
|
48091
48161
|
tech-stack choices, error handling — raise the technical open
|
|
48092
|
-
questions, then
|
|
48093
|
-
judgement call: the grouping is already decided.`;
|
|
48162
|
+
questions, then pin interfaces and tests in the architecture document.`;
|
|
48094
48163
|
const reviewerPersona = `You are the independent reviewing mind in gtd's build pipeline —
|
|
48095
48164
|
deliberately separate from whoever wrote the code, with no attachment
|
|
48096
48165
|
to it. Write a structured review document grouping a diff into
|
|
@@ -48099,12 +48168,6 @@ just approving; answer the human's questions inline in the review;
|
|
|
48099
48168
|
and, when asked, fix the small nits the human flagged and the risks
|
|
48100
48169
|
you marked yourself, each in one batch.
|
|
48101
48170
|
Beyond that you never fix or build anything yourself.`;
|
|
48102
|
-
const specReviewerPersona = `You are the adversarial spec-conformance checker in gtd's build
|
|
48103
|
-
pipeline, checking one freshly-built package against its spec. Verify
|
|
48104
|
-
only: tasks done, criteria met, code sound and consistent with the
|
|
48105
|
-
codebase. Write feedback only when something is genuinely wrong —
|
|
48106
|
-
otherwise write nothing; a clean turn IS the approval. Never fix what
|
|
48107
|
-
you find — naming it precisely enough for a fix turn is the whole job.`;
|
|
48108
48171
|
const builderPersona = `You are the TDD implementer in gtd's build pipeline: build one
|
|
48109
48172
|
package's declared scope end to end, tests first, and come back to fix
|
|
48110
48173
|
things when redirected — a failing check, or reviewer feedback. Stay
|
|
@@ -48282,8 +48345,12 @@ const withSkills = (skills, prompt) => {
|
|
|
48282
48345
|
if (skills === void 0 || skills.trim() === "") return prompt;
|
|
48283
48346
|
return `${skillsPreamble.replaceAll("{skills}", skills)}\n\n${prompt}`;
|
|
48284
48347
|
};
|
|
48285
|
-
/** An `agent()` step whose prompt opens with the preamble naming its scope's skills. */
|
|
48286
|
-
const agentWithSkills = (name, prompt, options = {}) =>
|
|
48348
|
+
/** An `agent()` step whose prompt opens with the preamble naming its scope's skills, then its restricted file access. */
|
|
48349
|
+
const agentWithSkills = (name, prompt, options = {}) => {
|
|
48350
|
+
const access = accessPreamble(accessFor(name, options.file));
|
|
48351
|
+
const body = access === "" ? prompt : `${access}\n\n${prompt}`;
|
|
48352
|
+
return agent(name, withSkills(skillsFor(name).join(", "), body), options);
|
|
48353
|
+
};
|
|
48287
48354
|
const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
|
|
48288
48355
|
const idleMessage = () => `No active gtd process.
|
|
48289
48356
|
|
|
@@ -48310,14 +48377,6 @@ carries it.
|
|
|
48310
48377
|
What each change does next (then run \`gtd land\`):
|
|
48311
48378
|
- **Continue** — having undone the sketch by hand, check the test baseline is green and start triage (**start-gate.check**).
|
|
48312
48379
|
`;
|
|
48313
|
-
const architecturePreMessage = () => `Judging whether \`.gtd/REQUIREMENTS.md\`'s settled concerns need a
|
|
48314
|
-
dedicated architecture pass — real structural decisions, multiple
|
|
48315
|
-
integration points, or a non-obvious tradeoff — before packages
|
|
48316
|
-
are written. Run \`gtd judge answer\` and pipe a verdict for
|
|
48317
|
-
\`architectureWarranted\` — or land untouched to run the full pass
|
|
48318
|
-
(the conservative default; a skipped judgment never suppresses
|
|
48319
|
-
it).
|
|
48320
|
-
`;
|
|
48321
48380
|
const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
|
|
48322
48381
|
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
48323
48382
|
|
|
@@ -48482,22 +48541,46 @@ ${footnoteFoldIn}${codeThreadReplies()}
|
|
|
48482
48541
|
authority is to merge only, never to split — the whole problem
|
|
48483
48542
|
is over-granularity
|
|
48484
48543
|
- Record every merge under \`## Merged Concerns\`,
|
|
48485
|
-
carrying both merged requirements verbatim so
|
|
48486
|
-
|
|
48544
|
+
carrying both merged requirements verbatim so each requirement
|
|
48545
|
+
stays traceable to its tests
|
|
48487
48546
|
- A merge raises no open question and stops for no
|
|
48488
48547
|
human — do not route it to \`architecture.gate\` for a veto; the
|
|
48489
|
-
human sees it when reviewing the plan
|
|
48490
|
-
real safety net
|
|
48548
|
+
human sees it when reviewing the plan
|
|
48491
48549
|
- Prefer fewer, larger packages — the smallest independently
|
|
48492
48550
|
valuable change, not the smallest change that compiles
|
|
48493
48551
|
- Every open point here is TECHNICAL — triage already resolved
|
|
48494
48552
|
the product ones, one phase earlier. PERMISSIVE: answer it
|
|
48495
48553
|
yourself unless you genuinely cannot defend a default; a wrong
|
|
48496
|
-
technical call is still caught
|
|
48554
|
+
technical call is still caught by the full run and the quality lap
|
|
48497
48555
|
- The narrow exception: a simplification that drops something
|
|
48498
48556
|
\`.gtd/REQUIREMENTS.md\` mentions is an open question, not a
|
|
48499
48557
|
silent default
|
|
48500
48558
|
|
|
48559
|
+
- \`.gtd/ARCHITECTURE.md\` is the acceptance spec the human signs
|
|
48560
|
+
off. It carries these sections, in this order:
|
|
48561
|
+
1. \`## Open Questions\` (qa format, first, only when present)
|
|
48562
|
+
2. \`## Interfaces\` — new and changed signatures/contracts
|
|
48563
|
+
only, as fenced TypeScript (or the repository's language)
|
|
48564
|
+
3. \`## Call Stacks\` — seam level, entry point → module
|
|
48565
|
+
boundary; one line per hop, never per line of code
|
|
48566
|
+
4. \`## E2E Scenarios\` — Gherkin in fenced \`gherkin\` blocks
|
|
48567
|
+
(whatever e2e framework the repository uses), each preceded
|
|
48568
|
+
by a line \`- e2e: <path>\`. With no user-visible
|
|
48569
|
+
behaviour change, the literal line \`No e2e change.\` plus a
|
|
48570
|
+
one-line reason — never empty or missing
|
|
48571
|
+
5. \`## Unit Tests\` — one \`### <interface>\` per entry of
|
|
48572
|
+
\`## Interfaces\`, each test a line
|
|
48573
|
+
\`- unit: <path> — <behaviour> (covers <concern>)\`;
|
|
48574
|
+
never against an internal helper. A requirement no test can
|
|
48575
|
+
cover (docs, README, deletions, config) goes under a
|
|
48576
|
+
\`### Chores\` heading in this section as
|
|
48577
|
+
\`- chore: <path> — <what> (covers <concern>)\`
|
|
48578
|
+
6. \`## Merged Concerns\` — kept, when any merge happened
|
|
48579
|
+
7. \`## Answered Questions\` — last
|
|
48580
|
+
- Coverage rule: every concern in \`.gtd/REQUIREMENTS.md\` appears
|
|
48581
|
+
in at least one \`(covers …)\` tail, as a test or a chore. Each
|
|
48582
|
+
test entry carries its file path and level (unit / e2e)
|
|
48583
|
+
|
|
48501
48584
|
${questionBar}
|
|
48502
48585
|
## Return lap
|
|
48503
48586
|
|
|
@@ -48509,26 +48592,41 @@ const architectureDecomposePrompt = () => `${styleBlock}
|
|
|
48509
48592
|
|
|
48510
48593
|
${stateFileRules}
|
|
48511
48594
|
- The only state files this turn touches are the package files
|
|
48512
|
-
under \`.gtd/packages/\`
|
|
48513
|
-
|
|
48595
|
+
under \`.gtd/packages/\` — no other files for notes or output.
|
|
48596
|
+
Never delete \`.gtd/ARCHITECTURE.md\`: the build tail's full run
|
|
48597
|
+
still needs it, and package files may reference it
|
|
48514
48598
|
- Work from \`.gtd/ARCHITECTURE.md\` if you wrote it earlier this
|
|
48515
48599
|
conversation, otherwise read it (the converged technical
|
|
48516
|
-
plan). It already lists
|
|
48517
|
-
|
|
48518
|
-
|
|
48519
|
-
|
|
48520
|
-
|
|
48521
|
-
|
|
48522
|
-
|
|
48523
|
-
|
|
48524
|
-
|
|
48525
|
-
|
|
48526
|
-
|
|
48527
|
-
-
|
|
48528
|
-
|
|
48529
|
-
|
|
48530
|
-
|
|
48531
|
-
|
|
48600
|
+
plan). It already lists the concerns, interfaces, e2e scenarios
|
|
48601
|
+
and unit tests — this turn groups them into packages. A
|
|
48602
|
+
\`## Merged Concerns\` heading there records merges, never a
|
|
48603
|
+
concern of its own: write no package file for it
|
|
48604
|
+
- A package is a consecutive run of \`## Unit Tests\` entries that go
|
|
48605
|
+
green together on the fast suite, at most ~8 unit-test entries
|
|
48606
|
+
each. Keep the merge rule: concerns whose footprints centre on the
|
|
48607
|
+
same files stay in one package unless the later one only consumes
|
|
48608
|
+
an interface the earlier one creates. Prefer fewer, larger
|
|
48609
|
+
packages. Order them so each stays green on the fast suite —
|
|
48610
|
+
interface-introducing packages first
|
|
48611
|
+
- Write the packages under \`.gtd/packages/\`, each carrying its
|
|
48612
|
+
requirement(s) — both, independently, if merged — its tasks, and
|
|
48613
|
+
each task's acceptance criteria as \`- [ ]\` checkboxes and
|
|
48614
|
+
relevant paths:
|
|
48615
|
+
- \`.gtd/packages/00-e2e-scenarios.md\` — package 0: it writes
|
|
48616
|
+
\`## E2E Scenarios\` as tests in the repository's own e2e
|
|
48617
|
+
framework and leaves them red. Write no such file when
|
|
48618
|
+
\`## E2E Scenarios\` says "No e2e change."
|
|
48619
|
+
- \`01-…\` onward — the unit-test packages. A requirement no test
|
|
48620
|
+
covers (docs, README, deletions, config) becomes a chore
|
|
48621
|
+
package, or a task of one; chore packages declare no tests
|
|
48622
|
+
- the last numbered package is the wiring package: CLI entry,
|
|
48623
|
+
workflow composition, config. A red e2e after it is a bug, not
|
|
48624
|
+
missing work
|
|
48625
|
+
- Every package with tests has a \`## Tests\` section, one line per
|
|
48626
|
+
declared test, copied from the architecture entries:
|
|
48627
|
+
\`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
|
|
48628
|
+
none
|
|
48629
|
+
- Leave everything uncommitted and finish
|
|
48532
48630
|
`;
|
|
48533
48631
|
const architectureGateAnswerMessage = () => `This gate stops on every process, even with no open question, so
|
|
48534
48632
|
you can discuss \`.gtd/ARCHITECTURE.md\`; changing nothing and re-running
|
|
@@ -48557,7 +48655,7 @@ What each change does next (then run \`gtd land\`):
|
|
|
48557
48655
|
`;
|
|
48558
48656
|
const packagesItemBuildingPrompt = (pkg) => `${stateFileRules}
|
|
48559
48657
|
- The only state file this turn may write is \`.gtd/SATISFIED.md\`;
|
|
48560
|
-
never delete the package file (the
|
|
48658
|
+
never delete the package file (the declared-tests guard reads it
|
|
48561
48659
|
after you)
|
|
48562
48660
|
- The package to implement is \`${pkg}\`
|
|
48563
48661
|
- First check its acceptance criteria against the current tree —
|
|
@@ -48566,6 +48664,13 @@ const packagesItemBuildingPrompt = (pkg) => `${stateFileRules}
|
|
|
48566
48664
|
with each criterion's concrete evidence (commit, file, or
|
|
48567
48665
|
symbol), change nothing else, and finish. Otherwise implement
|
|
48568
48666
|
normally and skip that file
|
|
48667
|
+
- Write the tests the package declares under \`## Tests\` first,
|
|
48668
|
+
then the code that makes them pass; the turn is refused unless
|
|
48669
|
+
every declared test is in your diff
|
|
48670
|
+
- Package 0 (\`.gtd/packages/00-e2e-scenarios.md\`) writes the scenarios verbatim
|
|
48671
|
+
from \`## E2E Scenarios\` of \`.gtd/ARCHITECTURE.md\`, with only
|
|
48672
|
+
step code that typechecks, and leaves them red — no
|
|
48673
|
+
implementation
|
|
48569
48674
|
- Implement every task the package describes, no more, no less,
|
|
48570
48675
|
fanning independent ones out to parallel subagents where your
|
|
48571
48676
|
harness supports it; leave other package files untouched
|
|
@@ -48582,15 +48687,6 @@ ${fixFeedbackPrompt}
|
|
|
48582
48687
|
then legitimately report itself already satisfied later
|
|
48583
48688
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
48584
48689
|
`;
|
|
48585
|
-
const packagesItemFixSpecPrompt = (pkg) => `${stateFileRules}
|
|
48586
|
-
- The only state file this turn touches is
|
|
48587
|
-
\`.gtd/SPEC_FEEDBACK.md\` — address it, then delete it
|
|
48588
|
-
- Read it (the reviewer's concerns) and the package spec
|
|
48589
|
-
(\`${pkg}\`), then fix the code to
|
|
48590
|
-
resolve every concern
|
|
48591
|
-
- Delete \`.gtd/SPEC_FEEDBACK.md\` once resolved; leave everything
|
|
48592
|
-
else uncommitted and finish your turn
|
|
48593
|
-
`;
|
|
48594
48690
|
const healthJudgeMessage = () => `The check is still red, and this isn't the first round —
|
|
48595
48691
|
\`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
|
|
48596
48692
|
is in history. Run \`gtd judge answer\` and pipe a
|
|
@@ -48628,45 +48724,22 @@ Edit \`.gtd/ESCALATION.md\` with fresh instructions for the next fix
|
|
|
48628
48724
|
turn, or land untouched to give it one more attempt at the same
|
|
48629
48725
|
analysis.
|
|
48630
48726
|
`;
|
|
48631
|
-
const
|
|
48632
|
-
|
|
48633
|
-
|
|
48634
|
-
|
|
48635
|
-
|
|
48727
|
+
const buildContextPrompt = (built) => {
|
|
48728
|
+
const list = (paths) => paths.length === 0 ? " (none)" : paths.map((path) => ` - ${path}`).join("\n");
|
|
48729
|
+
return `- Read \`.gtd/ARCHITECTURE.md\` — the plan every package was built from
|
|
48730
|
+
- A red scenario that is NEW in this plan likely means missing wiring
|
|
48731
|
+
between packages; new scenarios:
|
|
48732
|
+
${list(built.scenarios.added)}
|
|
48733
|
+
- A red CHANGED or existing scenario or test is a regression a package
|
|
48734
|
+
introduced; scenarios this plan changed:
|
|
48735
|
+
${list(built.scenarios.changed)}
|
|
48736
|
+
- Each package's commits, to bisect a break:
|
|
48737
|
+
${built.ranges.map((r) => ` git log --oneline ${r.from}..${r.to} # ${r.pkg}`).join("\n")}
|
|
48636
48738
|
`;
|
|
48637
|
-
|
|
48638
|
-
const
|
|
48639
|
-
your review to only these sections:
|
|
48640
|
-
${failing.map((title) => ` - ${title}\n`).join("")}` : "";
|
|
48641
|
-
const packagesItemSpecReviewPrompt = (pkg, failing = []) => `${styleBlock}
|
|
48642
|
-
|
|
48643
|
-
You are reviewing a freshly-built work package against its own
|
|
48644
|
-
spec.
|
|
48645
|
-
|
|
48646
|
-
${stateFileRules}
|
|
48647
|
-
- The only state file this turn touches is
|
|
48648
|
-
\`.gtd/SPEC_FEEDBACK.md\` — write it only when you find problems
|
|
48649
|
-
- The package spec is \`${pkg}\`
|
|
48650
|
-
${specScope(failing)}- Verify the implementation against it: tasks done, criteria
|
|
48651
|
-
met, code sound and consistent with the codebase. No diff is
|
|
48652
|
-
given — read the range yourself, from \`${start()}\`
|
|
48653
|
-
to the working tree, process-wide (it can span earlier
|
|
48654
|
-
packages)
|
|
48655
|
-
- You own that bar; nothing downstream re-weighs your findings
|
|
48656
|
-
- Write nothing when the package fully satisfies its spec —
|
|
48657
|
-
silence is your approval. Otherwise write
|
|
48658
|
-
\`.gtd/SPEC_FEEDBACK.md\` listing what would violate the spec if
|
|
48659
|
-
it shipped unaddressed, specific enough to act on, each as its
|
|
48660
|
-
own \`## \` heading
|
|
48661
|
-
- Never fix anything yourself and never delete the package
|
|
48662
|
-
file — a later step owns that
|
|
48663
|
-
`;
|
|
48664
|
-
const specReviewerSystem = () => `${specReviewerPersona}
|
|
48665
|
-
|
|
48666
|
-
${agentConduct}`;
|
|
48667
|
-
const buildFixPrompt = () => `${stateFileRules}
|
|
48739
|
+
};
|
|
48740
|
+
const buildFixPrompt = (built) => `${stateFileRules}
|
|
48668
48741
|
${fixFeedbackPrompt}
|
|
48669
|
-
- Leave everything uncommitted — do not commit
|
|
48742
|
+
${built === void 0 ? "" : buildContextPrompt(built)}- Leave everything uncommitted — do not commit
|
|
48670
48743
|
`;
|
|
48671
48744
|
const finisherSystem = () => `${finisherPersona}
|
|
48672
48745
|
|
|
@@ -48999,6 +49072,20 @@ ${it.processCostByModel.map((m) => `- ${m.model}: ${m.cost}\n`).join("")}
|
|
|
48999
49072
|
Print the closing message and stop — this writes nothing itself.
|
|
49000
49073
|
`;
|
|
49001
49074
|
};
|
|
49075
|
+
const scenarioWordingMessage = (at, drifted) => `The e2e scenario wording was frozen when package 0 landed, and the last
|
|
49076
|
+
turn changed it. Removed lines are marked \`-\`, added ones \`+\`:
|
|
49077
|
+
|
|
49078
|
+
${drifted.map(({ path, lines }) => `${path}\n${lines.join("\n")}`).join("\n\n")}
|
|
49079
|
+
|
|
49080
|
+
To accept the change, land untouched.
|
|
49081
|
+
|
|
49082
|
+
To reject it, run
|
|
49083
|
+
|
|
49084
|
+
git checkout ${at} -- ${drifted.map(({ path }) => quote(path)).join(" ")}
|
|
49085
|
+
|
|
49086
|
+
and land. Restoring only some of the files is a partial accept: every file
|
|
49087
|
+
still differing from the frozen wording is accepted.
|
|
49088
|
+
`;
|
|
49002
49089
|
//#endregion
|
|
49003
49090
|
//#region src/workflows/steps.ts
|
|
49004
49091
|
const FEEDBACK = ".gtd/FEEDBACK.md";
|
|
@@ -49007,7 +49094,6 @@ const REQUIREMENTS = ".gtd/REQUIREMENTS.md";
|
|
|
49007
49094
|
const ARCHITECTURE = ".gtd/ARCHITECTURE.md";
|
|
49008
49095
|
const REVIEW = ".gtd/REVIEW.md";
|
|
49009
49096
|
const QUALITY = ".gtd/QUALITY.md";
|
|
49010
|
-
const SPEC_FEEDBACK = ".gtd/SPEC_FEEDBACK.md";
|
|
49011
49097
|
const planner = () => env.plannerModel ?? "";
|
|
49012
49098
|
const coder = () => env.coderModel ?? "";
|
|
49013
49099
|
/** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
|
|
@@ -49055,20 +49141,7 @@ const fixSuite = () => agentWithSkills("fix.suite.fixing", packagesItemFixSuiteP
|
|
|
49055
49141
|
model: coder(),
|
|
49056
49142
|
system: builderSystem()
|
|
49057
49143
|
});
|
|
49058
|
-
const
|
|
49059
|
-
label: "Fixing review feedback",
|
|
49060
|
-
file: SPEC_FEEDBACK,
|
|
49061
|
-
model: coder(),
|
|
49062
|
-
system: builderSystem()
|
|
49063
|
-
});
|
|
49064
|
-
/** Review `pkg` against its spec, focused on the `failing` sections the pre-judge could not clear. */
|
|
49065
|
-
const reviewPackage = (pkg, failing = []) => agentWithSkills("spec.review", packagesItemSpecReviewPrompt(pkg, failing), {
|
|
49066
|
-
label: "Reviewing the package",
|
|
49067
|
-
model: planner(),
|
|
49068
|
-
system: specReviewerSystem(),
|
|
49069
|
-
allowEmpty: true
|
|
49070
|
-
});
|
|
49071
|
-
const fix = () => agentWithSkills("fix", buildFixPrompt(), {
|
|
49144
|
+
const fix = (built) => agentWithSkills("fix", buildFixPrompt(built), {
|
|
49072
49145
|
label: "Fixing the check",
|
|
49073
49146
|
file: FEEDBACK,
|
|
49074
49147
|
model: coder(),
|
|
@@ -49176,11 +49249,29 @@ const collecting = (capture) => agentWithSkills("review.collecting", buildReview
|
|
|
49176
49249
|
system: reviewerSystem(),
|
|
49177
49250
|
allowEmpty: true
|
|
49178
49251
|
});
|
|
49252
|
+
//#endregion
|
|
49253
|
+
//#region src/workflows/health.ts
|
|
49254
|
+
/** Written while `fastTestCommand` is unset; its presence rests the process at the check. */
|
|
49255
|
+
const SETUP = ".gtd/SETUP.md";
|
|
49256
|
+
const SETUP_TEXT = "The bundled workflow needs the `fastTestCommand` setting: the fast suite (everything but e2e).\nSet it under `env:` in `.gtdrc` or as GTD_FASTTESTCOMMAND.";
|
|
49257
|
+
/** Shell lines for a check's preamble: unset (blank after trim) setting writes `SETUP` and skips the suite. */
|
|
49258
|
+
const fastSuiteGuard = () => (env.fastTestCommand ?? "").trim() === "" ? [
|
|
49259
|
+
`mkdir -p ${quote(dirname(SETUP))}`,
|
|
49260
|
+
`printf '%s\\n' ${quote(SETUP_TEXT)} > ${quote(SETUP)}`,
|
|
49261
|
+
`printf '%s\\n' ${quote(SETUP_TEXT)} >&2`,
|
|
49262
|
+
"exit 0"
|
|
49263
|
+
] : [`rm -f ${quote(SETUP)}`];
|
|
49179
49264
|
/** Run the suite as step `name`; resolves `true` when it passed. A failure is in `.gtd/FEEDBACK.md`. */
|
|
49180
|
-
const baseline = (name, label = "Checking the baseline") =>
|
|
49181
|
-
|
|
49182
|
-
|
|
49183
|
-
|
|
49265
|
+
const baseline = async (name, label = "Checking the baseline") => {
|
|
49266
|
+
for (;;) {
|
|
49267
|
+
const green = await check(name, env.testCommand ?? "", {
|
|
49268
|
+
report: FEEDBACK,
|
|
49269
|
+
label,
|
|
49270
|
+
preamble: fastSuiteGuard()
|
|
49271
|
+
});
|
|
49272
|
+
if (read(".gtd/SETUP.md") === void 0) return green;
|
|
49273
|
+
}
|
|
49274
|
+
};
|
|
49184
49275
|
/**
|
|
49185
49276
|
* Hand a red suite that repeated attempts could not fix to a person: an
|
|
49186
49277
|
* agent writes `.gtd/ESCALATION.md`, a human narrows it. From the third round
|
|
@@ -49219,6 +49310,15 @@ const sameFailure = async (previous, current) => {
|
|
|
49219
49310
|
});
|
|
49220
49311
|
return comparable && truncated.length === 0 && answered(answers.verdict, "identical", numeric(vars.judgeIdenticalMinP, Infinity));
|
|
49221
49312
|
};
|
|
49313
|
+
const runSuite = (options) => {
|
|
49314
|
+
const fast = options.suite === "fast";
|
|
49315
|
+
return check("health.check", (fast ? env.fastTestCommand : env.testCommand) ?? "", {
|
|
49316
|
+
report: FEEDBACK,
|
|
49317
|
+
label: "Running checks",
|
|
49318
|
+
sweepOnGreen: [ESCALATION, ...options.sweepOnGreen ?? []],
|
|
49319
|
+
...fast ? { preamble: fastSuiteGuard() } : {}
|
|
49320
|
+
});
|
|
49321
|
+
};
|
|
49222
49322
|
/**
|
|
49223
49323
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
49224
49324
|
* first once `FIX_CAP` fixes are spent, or a judge calls the failure
|
|
@@ -49229,11 +49329,9 @@ const healthy = async (fix, options = {}) => {
|
|
|
49229
49329
|
let fixes = options.fixesSoFar ?? 0;
|
|
49230
49330
|
let previous;
|
|
49231
49331
|
for (;;) {
|
|
49232
|
-
|
|
49233
|
-
|
|
49234
|
-
|
|
49235
|
-
sweepOnGreen: [".gtd/ESCALATION.md"]
|
|
49236
|
-
})) {
|
|
49332
|
+
const green = await runSuite(options);
|
|
49333
|
+
if (options.suite === "fast" && read(".gtd/SETUP.md") !== void 0) continue;
|
|
49334
|
+
if (green) {
|
|
49237
49335
|
escalations.rounds = 0;
|
|
49238
49336
|
return;
|
|
49239
49337
|
}
|
|
@@ -49257,324 +49355,184 @@ const gate = (name, message) => scope(name, async () => {
|
|
|
49257
49355
|
});
|
|
49258
49356
|
});
|
|
49259
49357
|
//#endregion
|
|
49260
|
-
//#region src/workflows/
|
|
49261
|
-
const
|
|
49262
|
-
|
|
49263
|
-
|
|
49264
|
-
|
|
49265
|
-
|
|
49266
|
-
|
|
49267
|
-
|
|
49268
|
-
|
|
49269
|
-
|
|
49270
|
-
|
|
49271
|
-
|
|
49272
|
-
|
|
49273
|
-
|
|
49274
|
-
|
|
49275
|
-
|
|
49276
|
-
|
|
49277
|
-
|
|
49278
|
-
|
|
49279
|
-
|
|
49280
|
-
|
|
49281
|
-
|
|
49282
|
-
|
|
49283
|
-
|
|
49284
|
-
"dist/**",
|
|
49285
|
-
"build/**",
|
|
49286
|
-
"out/**",
|
|
49287
|
-
"coverage/**",
|
|
49288
|
-
"node_modules/**",
|
|
49289
|
-
"vendor/**",
|
|
49290
|
-
".turbo/**",
|
|
49291
|
-
"**/__snapshots__/**",
|
|
49292
|
-
"**/*.snap",
|
|
49293
|
-
"**/*.min.js",
|
|
49294
|
-
"**/*.min.css",
|
|
49295
|
-
"**/*.map"
|
|
49296
|
-
];
|
|
49297
|
-
const BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
49298
|
-
"png",
|
|
49299
|
-
"jpg",
|
|
49300
|
-
"jpeg",
|
|
49301
|
-
"gif",
|
|
49302
|
-
"webp",
|
|
49303
|
-
"ico",
|
|
49304
|
-
"pdf",
|
|
49305
|
-
"zip",
|
|
49306
|
-
"gz",
|
|
49307
|
-
"tar",
|
|
49308
|
-
"woff",
|
|
49309
|
-
"woff2",
|
|
49310
|
-
"ttf",
|
|
49311
|
-
"otf",
|
|
49312
|
-
"mp4",
|
|
49313
|
-
"wasm",
|
|
49314
|
-
"bin",
|
|
49315
|
-
"exe",
|
|
49316
|
-
"so",
|
|
49317
|
-
"dylib"
|
|
49318
|
-
]);
|
|
49319
|
-
const globCache = /* @__PURE__ */ new Map();
|
|
49320
|
-
const compileGlob = (glob) => {
|
|
49321
|
-
let pattern = "^";
|
|
49322
|
-
let i = 0;
|
|
49323
|
-
while (i < glob.length) {
|
|
49324
|
-
const char = glob[i];
|
|
49325
|
-
if (char !== "*") {
|
|
49326
|
-
pattern += char.replace(/[.+^${}()|[\]\\?]/g, "\\$&");
|
|
49327
|
-
i += 1;
|
|
49328
|
-
} else if (glob[i + 1] !== "*") {
|
|
49329
|
-
pattern += "[^/]*";
|
|
49330
|
-
i += 1;
|
|
49331
|
-
} else if (glob[i + 2] === "/") {
|
|
49332
|
-
pattern += "(?:.*/)?";
|
|
49333
|
-
i += 3;
|
|
49358
|
+
//#region src/workflows/scenarios.ts
|
|
49359
|
+
const DOC_STRING = /^(\s*)("""|```)/;
|
|
49360
|
+
/**
|
|
49361
|
+
* Gherkin wording only: lines trimmed; blank, `#` comment and `@` tag lines
|
|
49362
|
+
* dropped — except inside a doc string, whose content is test input: kept
|
|
49363
|
+
* whole, indented relative to its opening delimiter (as Gherkin reads it).
|
|
49364
|
+
*/
|
|
49365
|
+
const scenarioText = (source) => {
|
|
49366
|
+
const out = [];
|
|
49367
|
+
let doc;
|
|
49368
|
+
for (const line of source.split("\n")) {
|
|
49369
|
+
const delimiter = DOC_STRING.exec(line);
|
|
49370
|
+
if (doc === void 0 && delimiter !== null) {
|
|
49371
|
+
doc = {
|
|
49372
|
+
indent: delimiter[1].length,
|
|
49373
|
+
fence: delimiter[2]
|
|
49374
|
+
};
|
|
49375
|
+
out.push(line.trim());
|
|
49376
|
+
} else if (doc !== void 0 && line.trim().startsWith(doc.fence)) {
|
|
49377
|
+
doc = void 0;
|
|
49378
|
+
out.push(line.trim());
|
|
49379
|
+
} else if (doc !== void 0) {
|
|
49380
|
+
const lead = line.length - line.trimStart().length;
|
|
49381
|
+
out.push(line.slice(Math.min(lead, doc.indent)).trimEnd());
|
|
49334
49382
|
} else {
|
|
49335
|
-
|
|
49336
|
-
|
|
49383
|
+
const trimmed = line.trim();
|
|
49384
|
+
if (trimmed !== "" && !trimmed.startsWith("#") && !trimmed.startsWith("@")) out.push(trimmed);
|
|
49337
49385
|
}
|
|
49338
49386
|
}
|
|
49339
|
-
return
|
|
49387
|
+
return out.join("\n");
|
|
49340
49388
|
};
|
|
49341
|
-
const
|
|
49342
|
-
|
|
49343
|
-
|
|
49344
|
-
regex = compileGlob(glob);
|
|
49345
|
-
globCache.set(glob, regex);
|
|
49346
|
-
}
|
|
49347
|
-
return regex.test(path);
|
|
49389
|
+
const textOf = (path) => {
|
|
49390
|
+
const source = read(path);
|
|
49391
|
+
return source === void 0 ? void 0 : scenarioText(source);
|
|
49348
49392
|
};
|
|
49349
|
-
|
|
49350
|
-
|
|
49351
|
-
|
|
49352
|
-
|
|
49353
|
-
|
|
49354
|
-
const
|
|
49355
|
-
|
|
49356
|
-
|
|
49357
|
-
|
|
49358
|
-
|
|
49359
|
-
|
|
49360
|
-
|
|
49361
|
-
if (lines[lines.length - 1] === "") lines.pop();
|
|
49362
|
-
return lines;
|
|
49393
|
+
/** Snapshot `.feature` paths at `head()`, merged over `previous`; `undefined` when nothing is frozen. */
|
|
49394
|
+
const freezeScenarios = (paths, previous) => {
|
|
49395
|
+
if (paths.length === 0) return previous;
|
|
49396
|
+
const texts = { ...previous?.texts };
|
|
49397
|
+
for (const path of paths) {
|
|
49398
|
+
const text = textOf(path);
|
|
49399
|
+
if (text !== void 0) texts[path] = text;
|
|
49400
|
+
}
|
|
49401
|
+
return Object.keys(texts).length === 0 ? previous : {
|
|
49402
|
+
at: head(),
|
|
49403
|
+
texts
|
|
49404
|
+
};
|
|
49363
49405
|
};
|
|
49364
|
-
/**
|
|
49406
|
+
/** Paths whose wording differs from the snapshot (a deleted file counts). */
|
|
49407
|
+
const driftedScenarios = (frozen) => Object.entries(frozen.texts).filter(([path, text]) => textOf(path) !== text).map(([path]) => path);
|
|
49408
|
+
const lines = (text) => text === void 0 || text === "" ? [] : text.split("\n");
|
|
49409
|
+
/** `table[i][j]`: the longest common subsequence of `a[i..]` and `b[j..]`. */
|
|
49365
49410
|
const lcsTable = (a, b) => {
|
|
49366
|
-
const
|
|
49367
|
-
|
|
49368
|
-
|
|
49369
|
-
|
|
49370
|
-
|
|
49371
|
-
|
|
49372
|
-
|
|
49373
|
-
const
|
|
49374
|
-
const ops = [];
|
|
49411
|
+
const table = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
49412
|
+
for (let i = a.length - 1; i >= 0; i--) for (let j = b.length - 1; j >= 0; j--) table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
|
|
49413
|
+
return table;
|
|
49414
|
+
};
|
|
49415
|
+
/** Changed lines in file order, `- ` / `+ `-prefixed, so a moved or repeated step still shows. */
|
|
49416
|
+
const lineDiff = (a, b) => {
|
|
49417
|
+
const table = lcsTable(a, b);
|
|
49418
|
+
const out = [];
|
|
49375
49419
|
let i = 0;
|
|
49376
49420
|
let j = 0;
|
|
49377
49421
|
while (i < a.length && j < b.length) if (a[i] === b[j]) {
|
|
49378
|
-
ops.push({
|
|
49379
|
-
type: "eq",
|
|
49380
|
-
line: a[i]
|
|
49381
|
-
});
|
|
49382
|
-
i++;
|
|
49383
|
-
j++;
|
|
49384
|
-
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
49385
|
-
ops.push({
|
|
49386
|
-
type: "del",
|
|
49387
|
-
line: a[i]
|
|
49388
|
-
});
|
|
49389
49422
|
i++;
|
|
49390
|
-
} else {
|
|
49391
|
-
ops.push({
|
|
49392
|
-
type: "add",
|
|
49393
|
-
line: b[j]
|
|
49394
|
-
});
|
|
49395
49423
|
j++;
|
|
49396
|
-
}
|
|
49397
|
-
|
|
49398
|
-
|
|
49399
|
-
|
|
49400
|
-
|
|
49401
|
-
|
|
49402
|
-
|
|
49403
|
-
line: b[j++]
|
|
49404
|
-
});
|
|
49405
|
-
return ops;
|
|
49406
|
-
};
|
|
49407
|
-
/** Longest-common-subsequence edit script between two (already trimmed) line arrays. */
|
|
49408
|
-
const lcsOps = (a, b) => backtrack(a, b, lcsTable(a, b));
|
|
49409
|
-
const commonAffixes = (before, after) => {
|
|
49410
|
-
const maxPrefix = Math.min(before.length, after.length);
|
|
49411
|
-
let prefix = 0;
|
|
49412
|
-
while (prefix < maxPrefix && before[prefix] === after[prefix]) prefix++;
|
|
49413
|
-
const maxSuffix = maxPrefix - prefix;
|
|
49414
|
-
let suffix = 0;
|
|
49415
|
-
while (suffix < maxSuffix && before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) suffix++;
|
|
49416
|
-
return {
|
|
49417
|
-
prefix,
|
|
49418
|
-
suffix
|
|
49419
|
-
};
|
|
49424
|
+
} else if (table[i + 1][j] >= table[i][j + 1]) out.push(`- ${a[i++]}`);
|
|
49425
|
+
else out.push(`+ ${b[j++]}`);
|
|
49426
|
+
return [
|
|
49427
|
+
...out,
|
|
49428
|
+
...a.slice(i).map((l) => `- ${l}`),
|
|
49429
|
+
...b.slice(j).map((l) => `+ ${l}`)
|
|
49430
|
+
];
|
|
49420
49431
|
};
|
|
49421
|
-
const
|
|
49422
|
-
|
|
49423
|
-
|
|
49424
|
-
|
|
49425
|
-
|
|
49426
|
-
|
|
49427
|
-
|
|
49428
|
-
|
|
49429
|
-
|
|
49430
|
-
|
|
49431
|
-
|
|
49432
|
-
|
|
49433
|
-
|
|
49434
|
-
|
|
49435
|
-
const start = Math.max(0, idx - CONTEXT);
|
|
49436
|
-
const end = Math.min(ops.length - 1, idx + CONTEXT);
|
|
49437
|
-
const last = ranges[ranges.length - 1];
|
|
49438
|
-
if (last !== void 0 && start <= last[1] + 1) last[1] = Math.max(last[1], end);
|
|
49439
|
-
else ranges.push([start, end]);
|
|
49440
|
-
});
|
|
49441
|
-
return ranges.map(([start, end]) => {
|
|
49442
|
-
const slice = ops.slice(start, end + 1);
|
|
49443
|
-
return {
|
|
49444
|
-
beforeStart: beforeLineAt[start],
|
|
49445
|
-
beforeCount: slice.filter((op) => op.type !== "add").length,
|
|
49446
|
-
afterStart: afterLineAt[start],
|
|
49447
|
-
afterCount: slice.filter((op) => op.type !== "del").length,
|
|
49448
|
-
lines: slice.map((op) => `${op.type === "add" ? "+" : op.type === "del" ? "-" : " "}${op.line}`)
|
|
49449
|
-
};
|
|
49432
|
+
const wordingDrift = (frozen, path) => ({
|
|
49433
|
+
path,
|
|
49434
|
+
lines: lineDiff(lines(frozen.texts[path]), lines(textOf(path)))
|
|
49435
|
+
});
|
|
49436
|
+
/** After a turn: stop at `scenario-wording` while wording drifted; accept moves the snapshot. */
|
|
49437
|
+
const holdWording = async (frozen) => {
|
|
49438
|
+
if (frozen === void 0) return;
|
|
49439
|
+
const drifted = driftedScenarios(frozen);
|
|
49440
|
+
if (drifted.length === 0) return;
|
|
49441
|
+
await human("scenario-wording", {
|
|
49442
|
+
message: scenarioWordingMessage(frozen.at, drifted.map((path) => wordingDrift(frozen, path))),
|
|
49443
|
+
label: "Scenario wording changed",
|
|
49444
|
+
acceptClean: true,
|
|
49445
|
+
base: frozen.at
|
|
49450
49446
|
});
|
|
49447
|
+
const accepted = driftedScenarios(frozen);
|
|
49448
|
+
if (accepted.length === 0) return;
|
|
49449
|
+
for (const path of accepted) {
|
|
49450
|
+
const text = textOf(path);
|
|
49451
|
+
if (text === void 0) delete frozen.texts[path];
|
|
49452
|
+
else frozen.texts[path] = text;
|
|
49453
|
+
}
|
|
49454
|
+
frozen.at = head();
|
|
49451
49455
|
};
|
|
49452
|
-
|
|
49453
|
-
|
|
49454
|
-
|
|
49455
|
-
|
|
49456
|
-
|
|
49457
|
-
const { prefix, suffix } = commonAffixes(before, after);
|
|
49458
|
-
const beforeMiddle = before.slice(prefix, before.length - suffix);
|
|
49459
|
-
const afterMiddle = after.slice(prefix, after.length - suffix);
|
|
49460
|
-
if (beforeMiddle.length > TOO_LARGE_LINES || afterMiddle.length > TOO_LARGE_LINES) return `${change.path}: +${afterMiddle.length}/-${beforeMiddle.length} lines, too large to inline`;
|
|
49461
|
-
const prefixOps = before.slice(0, prefix).map((line) => ({
|
|
49462
|
-
type: "eq",
|
|
49463
|
-
line
|
|
49464
|
-
}));
|
|
49465
|
-
const suffixOps = before.slice(before.length - suffix).map((line) => ({
|
|
49466
|
-
type: "eq",
|
|
49467
|
-
line
|
|
49468
|
-
}));
|
|
49469
|
-
const ops = [
|
|
49470
|
-
...prefixOps,
|
|
49471
|
-
...lcsOps(beforeMiddle, afterMiddle),
|
|
49472
|
-
...suffixOps
|
|
49473
|
-
];
|
|
49474
|
-
const beforePath = change.status === "added" ? "/dev/null" : `a/${change.path}`;
|
|
49475
|
-
const afterPath = change.status === "deleted" ? "/dev/null" : `b/${change.path}`;
|
|
49476
|
-
return [
|
|
49477
|
-
`--- ${beforePath}`,
|
|
49478
|
-
`+++ ${afterPath}`,
|
|
49479
|
-
...buildHunks(ops).flatMap(renderHunk)
|
|
49480
|
-
].join("\n");
|
|
49481
|
-
};
|
|
49482
|
-
const omissionTrailer = (paths) => `${paths.length} file(s) omitted for the judge's byte budget: ${paths.join(", ")}`;
|
|
49483
|
-
/** The byte size of keeping `files`' first `keepCount` entries plus the trailer the rest would need — the trailer counts against the cap too, not just the kept text. */
|
|
49484
|
-
const sizeWithTrailer = (files, keepCount) => {
|
|
49485
|
-
let used = 0;
|
|
49486
|
-
for (let i = 0; i < keepCount; i++) used += Buffer.byteLength(files[i].text, "utf8") + (i > 0 ? 1 : 0);
|
|
49487
|
-
if (files.length - keepCount === 0) return used;
|
|
49488
|
-
const trailer = omissionTrailer(files.slice(keepCount).map((f) => f.path));
|
|
49489
|
-
return used + (keepCount > 0 ? 1 : 0) + Buffer.byteLength(trailer, "utf8");
|
|
49490
|
-
};
|
|
49491
|
-
/**
|
|
49492
|
-
* The filtered `changes`, rendered as one unified diff (files in path order)
|
|
49493
|
-
* and bounded to `capBytes`: over the cap, whole files are dropped from the
|
|
49494
|
-
* end — never bytes off a hunk — and named in a trailer line, so the
|
|
49495
|
-
* judge is told what it did not see rather than handed a silently short diff.
|
|
49496
|
-
* The trailer itself is sized against the cap along with the files it keeps,
|
|
49497
|
-
* so a package with many touched files can never push the total past it.
|
|
49498
|
-
*/
|
|
49499
|
-
const packageDiff = (changes, capBytes) => {
|
|
49500
|
-
const files = filterChanges(changes).slice().sort((a, b) => a.path.localeCompare(b.path)).map((change) => ({
|
|
49501
|
-
path: change.path,
|
|
49502
|
-
text: renderFileDiff(change)
|
|
49503
|
-
}));
|
|
49504
|
-
let keepCount = files.length;
|
|
49505
|
-
while (keepCount > 0 && sizeWithTrailer(files, keepCount) > capBytes) keepCount--;
|
|
49506
|
-
const dropped = files.slice(keepCount).map((f) => f.path);
|
|
49507
|
-
return [...files.slice(0, keepCount).map((f) => f.text), ...dropped.length > 0 ? [omissionTrailer(dropped)] : []].join("\n");
|
|
49456
|
+
/** `turn`, then `holdWording`; `within` names the scope the gate rests in. */
|
|
49457
|
+
const guarded = (frozen, turn, within) => async () => {
|
|
49458
|
+
await turn();
|
|
49459
|
+
if (within === void 0) await holdWording(frozen);
|
|
49460
|
+
else await scope(within, () => holdWording(frozen));
|
|
49508
49461
|
};
|
|
49509
49462
|
//#endregion
|
|
49510
49463
|
//#region src/workflows/packages.ts
|
|
49511
|
-
|
|
49512
|
-
const
|
|
49513
|
-
const
|
|
49514
|
-
|
|
49515
|
-
|
|
49516
|
-
|
|
49517
|
-
|
|
49518
|
-
|
|
49519
|
-
|
|
49520
|
-
|
|
49521
|
-
|
|
49522
|
-
|
|
49523
|
-
|
|
49524
|
-
* package's commits. Resolves `true` when approved.
|
|
49525
|
-
*/
|
|
49526
|
-
const specReview = async (pkg, since) => {
|
|
49527
|
-
const found = sectionBodies(read(pkg) ?? "");
|
|
49528
|
-
const titles = found.map((section) => section.title);
|
|
49529
|
-
const judged = titles.length > 0 && titles.length <= MAX_SECTIONS;
|
|
49530
|
-
const evidence = {};
|
|
49531
|
-
const questions = [];
|
|
49532
|
-
if (judged) {
|
|
49533
|
-
found.forEach(({ title, body }, i) => {
|
|
49534
|
-
evidence[`section-${i + 1}`] = body;
|
|
49535
|
-
questions.push(sectionQuestion(`section-${i + 1}`, title));
|
|
49464
|
+
/** Package 0: the e2e scenarios, written red. */
|
|
49465
|
+
const SCENARIO_PACKAGE = ".gtd/packages/00-e2e-scenarios.md";
|
|
49466
|
+
const TEST_LINE = /^\s*-\s+(unit|e2e):\s+`([^`]+)`/;
|
|
49467
|
+
/** The `## Tests` entries of a package file; malformed lines are ignored. */
|
|
49468
|
+
const declaredTests = (packageText) => {
|
|
49469
|
+
const found = [];
|
|
49470
|
+
let inTests = false;
|
|
49471
|
+
for (const line of packageText.split("\n")) if (/^##\s/.test(line)) inTests = /^##\s+Tests\s*$/.test(line);
|
|
49472
|
+
else if (inTests) {
|
|
49473
|
+
const match = TEST_LINE.exec(line);
|
|
49474
|
+
if (match) found.push({
|
|
49475
|
+
level: match[1],
|
|
49476
|
+
path: match[2]
|
|
49536
49477
|
});
|
|
49537
|
-
const capBytes = Math.floor(numeric(vars.judgeBudgetBytes, 32768) / (found.length + 1));
|
|
49538
|
-
evidence[DIFF_KEY] = packageDiff(changesSince(since), capBytes);
|
|
49539
|
-
}
|
|
49540
|
-
const { answers, truncated } = await judge("spec.pre", {
|
|
49541
|
-
questions,
|
|
49542
|
-
evidence,
|
|
49543
|
-
message: packagesItemSpecPreMessage(),
|
|
49544
|
-
label: "Judging spec coverage"
|
|
49545
|
-
});
|
|
49546
|
-
const clearMinP = numeric(vars.specPreJudge, Infinity);
|
|
49547
|
-
const failing = titles.filter((_, i) => {
|
|
49548
|
-
const id = `section-${i + 1}`;
|
|
49549
|
-
return !judged || truncated.includes(id) || truncated.includes(DIFF_KEY) || !answered(answers[id], "yes", clearMinP);
|
|
49550
|
-
});
|
|
49551
|
-
if (titles.length > 0 && failing.length === 0) return true;
|
|
49552
|
-
await reviewPackage(pkg, failing);
|
|
49553
|
-
if (wrote(".gtd/SPEC_FEEDBACK.md")) return false;
|
|
49554
|
-
if (changes(".gtd/SPEC_FEEDBACK.md").length > 0 || changes().length === 0) return true;
|
|
49555
|
-
return refuse("gtd land: no declared pattern matches the pending changes — write .gtd/SPEC_FEEDBACK.md to request changes, or change nothing to approve");
|
|
49556
|
-
};
|
|
49557
|
-
/** Build `pkg`, keep the suite green, review it against its spec, and close it out, which removes it. */
|
|
49558
|
-
const packageItem = async (pkg) => {
|
|
49559
|
-
const since = head();
|
|
49560
|
-
await build(pkg);
|
|
49561
|
-
for (;;) {
|
|
49562
|
-
await healthy(fixSuite);
|
|
49563
|
-
if (await specReview(pkg, since)) break;
|
|
49564
|
-
await fixSpec(pkg);
|
|
49565
49478
|
}
|
|
49566
|
-
|
|
49479
|
+
return found;
|
|
49480
|
+
};
|
|
49481
|
+
/**
|
|
49482
|
+
* Refuse a build or fix turn unless every test `pkg` declares exists and is in the
|
|
49483
|
+
* diff since `since`. The package text is read as of `since`, so a builder
|
|
49484
|
+
* editing its own `## Tests` cannot dodge the guard; with `.gtd/SATISFIED.md`
|
|
49485
|
+
* written, existing in the tree suffices.
|
|
49486
|
+
*/
|
|
49487
|
+
const requireDeclaredTests = (pkg, since) => {
|
|
49488
|
+
const range = changesSince(since);
|
|
49489
|
+
const text = range.get(pkg)?.before ?? read(pkg) ?? "";
|
|
49490
|
+
const satisfied = read(".gtd/SATISFIED.md") !== void 0;
|
|
49491
|
+
const missing = declaredTests(text).map((test) => test.path).filter((path) => satisfied ? read(path) === void 0 : range.get(path) === void 0 || range.get(path)?.status === "deleted" || read(path) === void 0);
|
|
49492
|
+
if (missing.length === 0) return;
|
|
49493
|
+
refuse(`gtd land: declared-tests: ${pkg} declares tests that are ${satisfied ? "missing from the tree" : "not in the package's diff"} — write them first:\n${missing.map((path) => ` - ${path}`).join("\n")}`);
|
|
49494
|
+
};
|
|
49495
|
+
/** Build `pkg`, check its declared tests, keep the fast suite green, and close it out, which removes it. */
|
|
49496
|
+
const packageItem = async (pkg, frozen) => {
|
|
49497
|
+
const from = head();
|
|
49498
|
+
const declared = (turn) => async () => {
|
|
49499
|
+
await turn();
|
|
49500
|
+
requireDeclaredTests(pkg, from);
|
|
49501
|
+
};
|
|
49502
|
+
await guarded(frozen, declared(() => build(pkg)))();
|
|
49503
|
+
await healthy(guarded(frozen, declared(fixSuite)), { suite: "fast" });
|
|
49504
|
+
await run$3("closing", removeScript([pkg, ".gtd/SATISFIED.md"]), { label: "Closing out the package" });
|
|
49505
|
+
return {
|
|
49567
49506
|
pkg,
|
|
49568
|
-
|
|
49569
|
-
|
|
49570
|
-
|
|
49571
|
-
]), { label: "Closing out the package" });
|
|
49507
|
+
from,
|
|
49508
|
+
to: head()
|
|
49509
|
+
};
|
|
49572
49510
|
};
|
|
49573
49511
|
/** The first queued package — the one a package step works on. */
|
|
49574
49512
|
const nextPackage = () => [...glob(".gtd/packages/*.md")].sort()[0];
|
|
49513
|
+
const outsideGtd = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
49575
49514
|
/** Build every package file under `.gtd/packages/`, in name order. */
|
|
49576
|
-
const packages = () => scope("packages", async () => {
|
|
49577
|
-
|
|
49515
|
+
const packages = (carried) => scope("packages", async () => {
|
|
49516
|
+
let frozen = carried;
|
|
49517
|
+
const ranges = [];
|
|
49518
|
+
let touched = [];
|
|
49519
|
+
for (let pkg = nextPackage(); pkg !== void 0; pkg = nextPackage()) {
|
|
49520
|
+
const range = await scope("item", () => packageItem(pkg, pkg === ".gtd/packages/00-e2e-scenarios.md" ? void 0 : frozen));
|
|
49521
|
+
ranges.push(range);
|
|
49522
|
+
if (pkg === ".gtd/packages/00-e2e-scenarios.md") {
|
|
49523
|
+
touched = changesSince(range.from).filter((c) => outsideGtd(c.path) && c.status !== "deleted");
|
|
49524
|
+
frozen = freezeScenarios(touched.filter((c) => c.path.endsWith(".feature")).map((c) => c.path), frozen);
|
|
49525
|
+
}
|
|
49526
|
+
}
|
|
49527
|
+
const paths = (status) => touched.filter((c) => c.status === status).map((c) => c.path);
|
|
49528
|
+
return {
|
|
49529
|
+
ranges,
|
|
49530
|
+
scenarios: {
|
|
49531
|
+
added: paths("added"),
|
|
49532
|
+
changed: paths("modified")
|
|
49533
|
+
},
|
|
49534
|
+
frozen
|
|
49535
|
+
};
|
|
49578
49536
|
});
|
|
49579
49537
|
//#endregion
|
|
49580
49538
|
//#region src/workflows/planning.ts
|
|
@@ -49607,30 +49565,8 @@ const architecture = () => scope("architecture", async () => {
|
|
|
49607
49565
|
} while (await noteGate(ARCHITECTURE, answerTechnicalQuestions));
|
|
49608
49566
|
await decompose();
|
|
49609
49567
|
if (changes(".gtd/packages/**").length === 0) refuse("gtd land: decompose: write at least one package under .gtd/packages/");
|
|
49568
|
+
if (changes(".gtd/ARCHITECTURE.md").some((c) => c.status === "deleted")) refuse("gtd land: decompose: keep .gtd/ARCHITECTURE.md — the build tail's full run needs it; it is removed only once that run is green");
|
|
49610
49569
|
});
|
|
49611
|
-
/** A package file name from a plan's first heading. */
|
|
49612
|
-
const slug = (title) => title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "package";
|
|
49613
|
-
/** Whether the settled plan needs its own architecture pass, or goes straight to one package. */
|
|
49614
|
-
const architecturePass = async () => {
|
|
49615
|
-
const { answers, truncated } = await judge("architecture-pre", {
|
|
49616
|
-
questions: [{
|
|
49617
|
-
id: "architectureWarranted",
|
|
49618
|
-
primitive: "noul",
|
|
49619
|
-
instructions: "Given the settled concerns in `.gtd/REQUIREMENTS.md` (in state), does this plan warrant a dedicated architecture pass — real structural decisions, multiple integration points, or a non-obvious tradeoff — before packages are written?",
|
|
49620
|
-
criteria: "Answer yes if uncertain; a trivial, single-concern, mechanical plan with no real design decision answers no."
|
|
49621
|
-
}],
|
|
49622
|
-
evidence: { requirements: read(".gtd/REQUIREMENTS.md") ?? "" },
|
|
49623
|
-
message: architecturePreMessage(),
|
|
49624
|
-
label: "Judging whether this plan warrants an architecture pass"
|
|
49625
|
-
});
|
|
49626
|
-
const skip = truncated.length === 0 && answered(answers.architectureWarranted, "no", numeric(vars.architectureSkipMinP, Infinity));
|
|
49627
|
-
const plan = read(REQUIREMENTS);
|
|
49628
|
-
if (skip && plan !== void 0) {
|
|
49629
|
-
await run$3("architecture-promote", moveScript(REQUIREMENTS, `.gtd/packages/01-${slug(sections(plan)[0] ?? "")}.md`), { label: "Promoting the plan straight to a package" });
|
|
49630
|
-
return;
|
|
49631
|
-
}
|
|
49632
|
-
await architecture();
|
|
49633
|
-
};
|
|
49634
49570
|
//#endregion
|
|
49635
49571
|
//#region src/workflows/review.ts
|
|
49636
49572
|
/** Split a `qualityReviews` value into its lenses: on `,`, trimmed, blanks dropped. */
|
|
@@ -49645,10 +49581,10 @@ const qualityLap = async () => {
|
|
|
49645
49581
|
return (read(".gtd/QUALITY.md") ?? "").length > 0 ? "findings" : "clean";
|
|
49646
49582
|
};
|
|
49647
49583
|
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
49648
|
-
const fixQualityFindings = async (escalations) => {
|
|
49584
|
+
const fixQualityFindings = async (escalations, frozen) => {
|
|
49649
49585
|
for (let turns = 0; turns < 3; turns++) {
|
|
49650
|
-
await fixQuality();
|
|
49651
|
-
if (
|
|
49586
|
+
await guarded(frozen, fixQuality)();
|
|
49587
|
+
if (read(".gtd/QUALITY.md") === void 0) return true;
|
|
49652
49588
|
}
|
|
49653
49589
|
await escalation(escalations);
|
|
49654
49590
|
return false;
|
|
@@ -49750,8 +49686,8 @@ const routeNotes = async (notes, r) => {
|
|
|
49750
49686
|
answeredAt = head();
|
|
49751
49687
|
}
|
|
49752
49688
|
if (nits.length > 0) {
|
|
49753
|
-
await fixNits(nits);
|
|
49754
|
-
await healthy(fix, { escalations: r.escalations });
|
|
49689
|
+
await guarded(r.frozen, () => fixNits(nits), "review")();
|
|
49690
|
+
await healthy(guarded(r.frozen, fix, "review"), { escalations: r.escalations });
|
|
49755
49691
|
}
|
|
49756
49692
|
if (edits.length > 0) {
|
|
49757
49693
|
await r.close();
|
|
@@ -49769,7 +49705,7 @@ const routeNotes = async (notes, r) => {
|
|
|
49769
49705
|
reviewed: answeredAt
|
|
49770
49706
|
};
|
|
49771
49707
|
};
|
|
49772
|
-
const finish = async (reviewed, collectedAt, escalations) => {
|
|
49708
|
+
const finish = async (reviewed, collectedAt, escalations, frozen) => {
|
|
49773
49709
|
const round = head();
|
|
49774
49710
|
const since = changesSince(reviewed);
|
|
49775
49711
|
const edited = since.filter(isCodeEdit);
|
|
@@ -49802,18 +49738,19 @@ const finish = async (reviewed, collectedAt, escalations) => {
|
|
|
49802
49738
|
return routeNotes(notes, {
|
|
49803
49739
|
round,
|
|
49804
49740
|
escalations,
|
|
49741
|
+
frozen,
|
|
49805
49742
|
close,
|
|
49806
49743
|
outcome,
|
|
49807
49744
|
unfolded
|
|
49808
49745
|
});
|
|
49809
49746
|
};
|
|
49810
49747
|
/** Write the review; if it marks risks, fix them, keep green, and write it again — once, so the re-review's own risks reach the human unfixed. */
|
|
49811
|
-
const reviewOnce = async (base, carry, escalations) => {
|
|
49748
|
+
const reviewOnce = async (base, carry, escalations, frozen) => {
|
|
49812
49749
|
await reviewing(base, carry);
|
|
49813
49750
|
const risks = reviewRisks(read(".gtd/REVIEW.md") ?? "");
|
|
49814
49751
|
if (risks.length === 0) return;
|
|
49815
|
-
await fixRisks(risks);
|
|
49816
|
-
await healthy(fix, { escalations });
|
|
49752
|
+
await guarded(frozen, () => fixRisks(risks), "review")();
|
|
49753
|
+
await healthy(guarded(frozen, fix, "review"), { escalations });
|
|
49817
49754
|
await reviewing(base, carry);
|
|
49818
49755
|
};
|
|
49819
49756
|
/**
|
|
@@ -49822,17 +49759,17 @@ const reviewOnce = async (base, carry, escalations) => {
|
|
|
49822
49759
|
* another planning lap, questions are answered in place, nits fixed in one
|
|
49823
49760
|
* batch, praise dropped.
|
|
49824
49761
|
*/
|
|
49825
|
-
const review = async (base, escalations = { rounds: 0 }) => {
|
|
49762
|
+
const review = async (base, escalations = { rounds: 0 }, frozen) => {
|
|
49826
49763
|
let carry;
|
|
49827
49764
|
for (;;) {
|
|
49828
|
-
await reviewOnce(base, carry, escalations);
|
|
49765
|
+
await reviewOnce(base, carry, escalations, frozen);
|
|
49829
49766
|
carry = void 0;
|
|
49830
49767
|
let reviewed = head();
|
|
49831
49768
|
let collectedAt;
|
|
49832
49769
|
for (;;) {
|
|
49833
49770
|
collectedAt = await converse(base, collectedAt);
|
|
49834
49771
|
if (collectedAt === "missing") break;
|
|
49835
|
-
const result = await finish(reviewed, collectedAt, escalations);
|
|
49772
|
+
const result = await finish(reviewed, collectedAt, escalations, frozen);
|
|
49836
49773
|
if (!("next" in result)) return result;
|
|
49837
49774
|
if (result.next === "rereview") {
|
|
49838
49775
|
carry = result.carry;
|
|
@@ -49843,14 +49780,20 @@ const review = async (base, escalations = { rounds: 0 }) => {
|
|
|
49843
49780
|
}
|
|
49844
49781
|
};
|
|
49845
49782
|
/** The build tail: fix (when entered red), keep green, the quality lap, then human review since `base`. */
|
|
49846
|
-
const buildTail = (fixFirst, base) => scope("build", async () => {
|
|
49783
|
+
const buildTail = (fixFirst, base, built) => scope("build", async () => {
|
|
49784
|
+
const frozen = built?.frozen;
|
|
49847
49785
|
const escalations = { rounds: 0 };
|
|
49786
|
+
const guardedFix = guarded(frozen, () => fix());
|
|
49848
49787
|
let redFirst = fixFirst;
|
|
49788
|
+
if (!fixFirst) await healthy(guarded(frozen, () => fix(built)), {
|
|
49789
|
+
escalations,
|
|
49790
|
+
sweepOnGreen: [ARCHITECTURE]
|
|
49791
|
+
});
|
|
49849
49792
|
let lapped = false;
|
|
49850
49793
|
for (;;) {
|
|
49851
49794
|
if (redFirst) {
|
|
49852
|
-
await
|
|
49853
|
-
await healthy(
|
|
49795
|
+
await guardedFix();
|
|
49796
|
+
await healthy(guardedFix, {
|
|
49854
49797
|
fixesSoFar: 1,
|
|
49855
49798
|
escalations
|
|
49856
49799
|
});
|
|
@@ -49858,8 +49801,8 @@ const buildTail = (fixFirst, base) => scope("build", async () => {
|
|
|
49858
49801
|
}
|
|
49859
49802
|
const lap = lapped ? "clean" : await qualityLap();
|
|
49860
49803
|
lapped = true;
|
|
49861
|
-
if (lap === "clean") return review(base, escalations);
|
|
49862
|
-
if (await fixQualityFindings(escalations)) await healthy(
|
|
49804
|
+
if (lap === "clean") return review(base, escalations, frozen);
|
|
49805
|
+
if (await fixQualityFindings(escalations, frozen)) await healthy(guardedFix, { escalations });
|
|
49863
49806
|
else redFirst = true;
|
|
49864
49807
|
}
|
|
49865
49808
|
});
|
|
@@ -49870,14 +49813,13 @@ const defaults = {
|
|
|
49870
49813
|
reviewBase: "",
|
|
49871
49814
|
judgeBudgetBytes: "32768",
|
|
49872
49815
|
judgeIdenticalMinP: "0.7",
|
|
49873
|
-
specPreJudge: "0.9",
|
|
49874
49816
|
reviewNoteActionable: "0.7",
|
|
49875
|
-
architectureSkipMinP: "0.85",
|
|
49876
49817
|
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge"
|
|
49877
49818
|
};
|
|
49878
49819
|
/** Environment settings. */
|
|
49879
49820
|
const envDefaults = {
|
|
49880
49821
|
testCommand: "npm test",
|
|
49822
|
+
fastTestCommand: "",
|
|
49881
49823
|
plannerModel: "smart",
|
|
49882
49824
|
coderModel: "base"
|
|
49883
49825
|
};
|
|
@@ -49893,8 +49835,6 @@ const skills = (vars) => ({
|
|
|
49893
49835
|
"architecture.decompose": ["incremental-implementation", "planning-and-task-breakdown"],
|
|
49894
49836
|
"packages.item": ["test-driven-development", "incremental-implementation"],
|
|
49895
49837
|
"packages.item.fix.suite": ["debugging-and-error-recovery"],
|
|
49896
|
-
"packages.item.fix.spec": ["incremental-implementation", "code-simplification"],
|
|
49897
|
-
"packages.item.spec": ["code-review-and-quality", "spec-driven-development"],
|
|
49898
49838
|
"packages.item.health": ["debugging-and-error-recovery"],
|
|
49899
49839
|
build: ["debugging-and-error-recovery"],
|
|
49900
49840
|
"build.health": ["debugging-and-error-recovery"],
|
|
@@ -49905,6 +49845,17 @@ const skills = (vars) => ({
|
|
|
49905
49845
|
...Object.fromEntries(lensesOf(vars.qualityReviews ?? "").map((lens) => [`build.quality.${lens}`, builtInLenses[lens]?.skills ?? [lens]]))
|
|
49906
49846
|
});
|
|
49907
49847
|
//#endregion
|
|
49848
|
+
//#region src/workflows/access.ts
|
|
49849
|
+
const access = (vars) => ({
|
|
49850
|
+
design: { write: [] },
|
|
49851
|
+
architecture: { write: [".gtd/REQUIREMENTS.md"] },
|
|
49852
|
+
"architecture.decompose": { write: [".gtd/packages/**", ".gtd/ARCHITECTURE.md"] },
|
|
49853
|
+
"build.review": { write: [".gtd/REVIEW.md"] },
|
|
49854
|
+
"build.review.fix.nits": {},
|
|
49855
|
+
"build.review.fix.risks": {},
|
|
49856
|
+
...Object.fromEntries(lensesOf(vars.qualityReviews ?? "").map((lens) => [`build.quality.${lens}`, { write: [] }]))
|
|
49857
|
+
});
|
|
49858
|
+
//#endregion
|
|
49908
49859
|
//#region src/workflows/unified.ts
|
|
49909
49860
|
var unified_exports = /* @__PURE__ */ __exportAll({
|
|
49910
49861
|
ARCHITECTURE: () => ARCHITECTURE,
|
|
@@ -49914,14 +49865,15 @@ var unified_exports = /* @__PURE__ */ __exportAll({
|
|
|
49914
49865
|
QUALITY: () => QUALITY,
|
|
49915
49866
|
REQUIREMENTS: () => REQUIREMENTS,
|
|
49916
49867
|
REVIEW: () => REVIEW,
|
|
49917
|
-
|
|
49868
|
+
SCENARIO_PACKAGE: () => SCENARIO_PACKAGE,
|
|
49869
|
+
SETUP: () => SETUP,
|
|
49870
|
+
access: () => access,
|
|
49918
49871
|
afterTail: () => afterTail,
|
|
49919
49872
|
agentWithSkills: () => agentWithSkills,
|
|
49920
49873
|
answerProductQuestions: () => answerProductQuestions,
|
|
49921
49874
|
answerReviewQuestions: () => answerReviewQuestions,
|
|
49922
49875
|
answerTechnicalQuestions: () => answerTechnicalQuestions,
|
|
49923
49876
|
architecture: () => architecture,
|
|
49924
|
-
architecturePass: () => architecturePass,
|
|
49925
49877
|
author: () => author,
|
|
49926
49878
|
awaitReview: () => awaitReview,
|
|
49927
49879
|
base: () => base,
|
|
@@ -49930,24 +49882,29 @@ var unified_exports = /* @__PURE__ */ __exportAll({
|
|
|
49930
49882
|
buildTail: () => buildTail,
|
|
49931
49883
|
builtInLenses: () => builtInLenses,
|
|
49932
49884
|
collecting: () => collecting,
|
|
49885
|
+
declaredTests: () => declaredTests,
|
|
49933
49886
|
decompose: () => decompose,
|
|
49934
49887
|
default: () => unified,
|
|
49935
49888
|
defaults: () => defaults,
|
|
49936
49889
|
describeEscalation: () => describeEscalation,
|
|
49937
49890
|
design: () => design,
|
|
49891
|
+
driftedScenarios: () => driftedScenarios,
|
|
49938
49892
|
envDefaults: () => envDefaults,
|
|
49939
49893
|
escalate: () => escalate,
|
|
49940
49894
|
escalation: () => escalation,
|
|
49941
49895
|
escalationExhausted: () => escalationExhausted,
|
|
49896
|
+
fastSuiteGuard: () => fastSuiteGuard,
|
|
49942
49897
|
fix: () => fix,
|
|
49943
49898
|
fixNits: () => fixNits,
|
|
49944
49899
|
fixQuality: () => fixQuality,
|
|
49945
49900
|
fixQualityFindings: () => fixQualityFindings,
|
|
49946
49901
|
fixRisks: () => fixRisks,
|
|
49947
|
-
fixSpec: () => fixSpec,
|
|
49948
49902
|
fixSuite: () => fixSuite,
|
|
49903
|
+
freezeScenarios: () => freezeScenarios,
|
|
49949
49904
|
gate: () => gate,
|
|
49905
|
+
guarded: () => guarded,
|
|
49950
49906
|
healthy: () => healthy,
|
|
49907
|
+
holdWording: () => holdWording,
|
|
49951
49908
|
lensesOf: () => lensesOf,
|
|
49952
49909
|
nextPackage: () => nextPackage,
|
|
49953
49910
|
ordinaryStart: () => ordinaryStart,
|
|
@@ -49956,19 +49913,20 @@ var unified_exports = /* @__PURE__ */ __exportAll({
|
|
|
49956
49913
|
planAndBuild: () => planAndBuild,
|
|
49957
49914
|
qualityLap: () => qualityLap,
|
|
49958
49915
|
reUnwind: () => reUnwind,
|
|
49916
|
+
requireDeclaredTests: () => requireDeclaredTests,
|
|
49959
49917
|
retryQuestion: () => retryQuestion,
|
|
49960
49918
|
review: () => review,
|
|
49961
49919
|
reviewMissing: () => reviewMissing,
|
|
49962
|
-
reviewPackage: () => reviewPackage,
|
|
49963
49920
|
reviewQuality: () => reviewQuality,
|
|
49964
49921
|
reviewing: () => reviewing,
|
|
49922
|
+
scenarioText: () => scenarioText,
|
|
49965
49923
|
skills: () => skills,
|
|
49966
|
-
specReview: () => specReview,
|
|
49967
49924
|
steering: () => steering,
|
|
49968
49925
|
summary: () => summary,
|
|
49969
49926
|
threads: () => threads,
|
|
49970
49927
|
triage: () => triage$1,
|
|
49971
|
-
unwind: () => unwind
|
|
49928
|
+
unwind: () => unwind,
|
|
49929
|
+
wordingDrift: () => wordingDrift
|
|
49972
49930
|
});
|
|
49973
49931
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
49974
49932
|
const unwind = () => {
|
|
@@ -49994,11 +49952,13 @@ const reUnwind = async (feedback) => {
|
|
|
49994
49952
|
/** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
|
|
49995
49953
|
const planAndBuild = async (firstBase) => {
|
|
49996
49954
|
let base = firstBase;
|
|
49955
|
+
let carried;
|
|
49997
49956
|
for (;;) {
|
|
49998
49957
|
await design(base);
|
|
49999
|
-
await
|
|
50000
|
-
await packages();
|
|
50001
|
-
|
|
49958
|
+
await architecture();
|
|
49959
|
+
const built = await packages(carried);
|
|
49960
|
+
carried = built.frozen;
|
|
49961
|
+
const outcome = await buildTail(false, base, built);
|
|
50002
49962
|
if (outcome.verdict === "signoff") return;
|
|
50003
49963
|
await reUnwind(outcome);
|
|
50004
49964
|
base = outcome.base;
|
|
@@ -50798,6 +50758,25 @@ const skillsJsonSchema = {
|
|
|
50798
50758
|
items: { type: "string" }
|
|
50799
50759
|
}
|
|
50800
50760
|
};
|
|
50761
|
+
/** The `access:` shape: scope full name -> `{ read?, write? }` (`compileAccessMap`). An entry REPLACES the scope's access wholesale; the key is unvalidatable here for the same reason as `skills:`. */
|
|
50762
|
+
const accessJsonSchema = {
|
|
50763
|
+
type: "object",
|
|
50764
|
+
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.",
|
|
50765
|
+
additionalProperties: {
|
|
50766
|
+
type: "object",
|
|
50767
|
+
additionalProperties: false,
|
|
50768
|
+
properties: {
|
|
50769
|
+
read: {
|
|
50770
|
+
type: "array",
|
|
50771
|
+
items: { type: "string" }
|
|
50772
|
+
},
|
|
50773
|
+
write: {
|
|
50774
|
+
type: "array",
|
|
50775
|
+
items: { type: "string" }
|
|
50776
|
+
}
|
|
50777
|
+
}
|
|
50778
|
+
}
|
|
50779
|
+
};
|
|
50801
50780
|
/**
|
|
50802
50781
|
* A real (not `Unknown`) schema, unlike `vars`/`modes`: it is a flat settings
|
|
50803
50782
|
* struct with no per-mode map to compile, so its JSON Schema derives from the
|
|
@@ -50825,7 +50804,8 @@ const ConfigSchema = Struct({
|
|
|
50825
50804
|
judge: optional$1(JudgeSchema),
|
|
50826
50805
|
modes: optional$1(Unknown.annotations({ jsonSchema: modesJsonSchema })),
|
|
50827
50806
|
ui: optional$1(UiSchema),
|
|
50828
|
-
skills: optional$1(Unknown.annotations({ jsonSchema: skillsJsonSchema }))
|
|
50807
|
+
skills: optional$1(Unknown.annotations({ jsonSchema: skillsJsonSchema })),
|
|
50808
|
+
access: optional$1(Unknown.annotations({ jsonSchema: accessJsonSchema }))
|
|
50829
50809
|
});
|
|
50830
50810
|
//#endregion
|
|
50831
50811
|
//#region src/workflow/vars.ts
|
|
@@ -50997,6 +50977,33 @@ const compileSkillsMap = (raw) => {
|
|
|
50997
50977
|
diagnostics
|
|
50998
50978
|
};
|
|
50999
50979
|
};
|
|
50980
|
+
/** A flat `scope full name -> { read?, write? }` map; a malformed value is a load error and is dropped. */
|
|
50981
|
+
const compileAccessMap = (raw) => {
|
|
50982
|
+
const diagnostics = [];
|
|
50983
|
+
if (raw === void 0) return {
|
|
50984
|
+
access: {},
|
|
50985
|
+
diagnostics
|
|
50986
|
+
};
|
|
50987
|
+
if (!isPlainObject$3(raw)) {
|
|
50988
|
+
diagnostics.push(err(["access"], `"access" must be a mapping of scope name -> { read?, write? }, got ${describeType(raw)}`));
|
|
50989
|
+
return {
|
|
50990
|
+
access: {},
|
|
50991
|
+
diagnostics
|
|
50992
|
+
};
|
|
50993
|
+
}
|
|
50994
|
+
const access = Object.create(null);
|
|
50995
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
50996
|
+
const fault = accessShapeFault(value);
|
|
50997
|
+
if (fault === void 0) access[key] = value;
|
|
50998
|
+
else diagnostics.push(err(["access", key], `"access.${key}": ${fault}`));
|
|
50999
|
+
}
|
|
51000
|
+
return {
|
|
51001
|
+
access,
|
|
51002
|
+
diagnostics
|
|
51003
|
+
};
|
|
51004
|
+
};
|
|
51005
|
+
/** The `.gtdrc` `access:` load-error message for a key that is not a known scope. */
|
|
51006
|
+
const unknownAccessKeyMessage = (key, known) => `"access.${key}" is not a scope that runs a turn — known scopes: ${known.join(", ")}`;
|
|
51000
51007
|
/** The `.gtdrc` `skills:` load-error message for a key that is not a known scope. */
|
|
51001
51008
|
const unknownSkillsKeyMessage = (key, known) => `"skills.${key}" is not a scope that runs a turn — known scopes: ${known.join(", ")}`;
|
|
51002
51009
|
const MODE_COMMAND_KEYS = ["format", "validate"];
|
|
@@ -51168,6 +51175,20 @@ const compileConfig = (layers) => {
|
|
|
51168
51175
|
})));
|
|
51169
51176
|
}
|
|
51170
51177
|
const { skills: rcSkills } = compileSkillsMap(mergedConfig["skills"]);
|
|
51178
|
+
const accessKeys = [];
|
|
51179
|
+
for (const layer of layers) {
|
|
51180
|
+
if (!isPlainObject$3(layer.value)) continue;
|
|
51181
|
+
const own = compileAccessMap(layer.value["access"]);
|
|
51182
|
+
diagnostics.push(...own.diagnostics.map((d) => ({
|
|
51183
|
+
...d,
|
|
51184
|
+
origin: layer.origin
|
|
51185
|
+
})));
|
|
51186
|
+
accessKeys.push(...Object.keys(own.access).map((key) => ({
|
|
51187
|
+
key,
|
|
51188
|
+
origin: layer.origin
|
|
51189
|
+
})));
|
|
51190
|
+
}
|
|
51191
|
+
const { access: rcAccess } = compileAccessMap(mergedConfig["access"]);
|
|
51171
51192
|
const seeded = Object.fromEntries(BUILT_IN_MODE_NAMES.map((name) => [name, { validate: seededValidateCommand(name) }]));
|
|
51172
51193
|
return {
|
|
51173
51194
|
rcVars,
|
|
@@ -51179,6 +51200,8 @@ const compileConfig = (layers) => {
|
|
|
51179
51200
|
...ui !== void 0 ? { ui } : {},
|
|
51180
51201
|
rcSkills,
|
|
51181
51202
|
skillsKeys,
|
|
51203
|
+
access: rcAccess,
|
|
51204
|
+
accessKeys,
|
|
51182
51205
|
diagnostics: dedupeDiagnostics(sortDiagnostics(diagnostics, layerOrder))
|
|
51183
51206
|
};
|
|
51184
51207
|
};
|
|
@@ -58110,6 +58133,9 @@ const load = gen(function* () {
|
|
|
58110
58133
|
skills: loaded.skills,
|
|
58111
58134
|
configuredSkills: compiled.rcSkills,
|
|
58112
58135
|
skillsKeys: compiled.skillsKeys,
|
|
58136
|
+
access: loaded.access,
|
|
58137
|
+
configuredAccess: compiled.access,
|
|
58138
|
+
accessKeys: compiled.accessKeys,
|
|
58113
58139
|
skillsOrigin: module?.filepath ?? "(built-in default)",
|
|
58114
58140
|
initial
|
|
58115
58141
|
},
|
|
@@ -58187,6 +58213,12 @@ const skillsExport = (exports) => {
|
|
|
58187
58213
|
const record = checkSkillsRecord(value);
|
|
58188
58214
|
return () => record;
|
|
58189
58215
|
};
|
|
58216
|
+
/** Read a module's optional `access` export — a scope full name -> `{ read?, write? }` record, or a function of the vars returning one. */
|
|
58217
|
+
const accessExport = (exports) => {
|
|
58218
|
+
const value = exports["access"] ?? {};
|
|
58219
|
+
if (typeof value === "function") return (vars) => value(vars);
|
|
58220
|
+
return () => value;
|
|
58221
|
+
};
|
|
58190
58222
|
const optionalFunction = (exports, name) => {
|
|
58191
58223
|
const value = exports[name];
|
|
58192
58224
|
if (value === void 0) return void 0;
|
|
@@ -58215,6 +58247,7 @@ const fromModule = (exported, origin, dependencyFiles) => {
|
|
|
58215
58247
|
envDefaults,
|
|
58216
58248
|
steering: stringRecord(exports, "steering"),
|
|
58217
58249
|
skills: skillsExport(exports),
|
|
58250
|
+
access: accessExport(exports),
|
|
58218
58251
|
summary: optionalFunction(exports, "summary"),
|
|
58219
58252
|
base: optionalFunction(exports, "base"),
|
|
58220
58253
|
origin,
|
|
@@ -58337,7 +58370,10 @@ var ConfigService = class ConfigService extends Tag("ConfigService")() {
|
|
|
58337
58370
|
//#endregion
|
|
58338
58371
|
//#region src/workflow/init.ts
|
|
58339
58372
|
const SCHEMA_URL = "https://cdn.jsdelivr.net/npm/@pmelab/gtd/schema.json";
|
|
58340
|
-
const INIT_ENV = {
|
|
58373
|
+
const INIT_ENV = {
|
|
58374
|
+
testCommand: "npm test",
|
|
58375
|
+
fastTestCommand: ""
|
|
58376
|
+
};
|
|
58341
58377
|
const MODES_SUGGESTION = {
|
|
58342
58378
|
qa: { format: "npx prettier --write \"$GTD_FILE\"" },
|
|
58343
58379
|
review: { format: "npx prettier --write \"$GTD_FILE\"" }
|
|
@@ -58355,6 +58391,16 @@ const checkShape = (value) => {
|
|
|
58355
58391
|
for (const [scope, names] of Object.entries(value)) if (!Array.isArray(names) || names.some((n) => typeof n !== "string")) throw new Error(`skills export: "${scope}" must be an array of skill names`);
|
|
58356
58392
|
return value;
|
|
58357
58393
|
};
|
|
58394
|
+
/** Every `.gtdrc` key that names no known scope is a load error, one per file that wrote it. */
|
|
58395
|
+
const checkKeys = (keys, known, message, section) => {
|
|
58396
|
+
const sorted = [...known].sort();
|
|
58397
|
+
return failOnErrors(dedupeDiagnostics(sortDiagnostics(keys.filter(({ key }) => !sorted.includes(key)).map(({ key, origin }) => ({
|
|
58398
|
+
severity: "error",
|
|
58399
|
+
path: [section, key],
|
|
58400
|
+
message: message(key, sorted),
|
|
58401
|
+
origin
|
|
58402
|
+
})), [...new Set(keys.map((k) => k.origin))])));
|
|
58403
|
+
};
|
|
58358
58404
|
/**
|
|
58359
58405
|
* Skill lists for the settings actually in use. `.gtdrc` `skills:` keys are
|
|
58360
58406
|
* judged here, not at config load: only now are the pinned/entry vars known.
|
|
@@ -58364,15 +58410,29 @@ const resolveScopeSkills = (def, vars) => gen(function* () {
|
|
|
58364
58410
|
try: () => checkShape(def.skills(vars)),
|
|
58365
58411
|
catch: (e) => fail$4(def.skillsOrigin, e instanceof Error ? e.message : String(e))
|
|
58366
58412
|
});
|
|
58367
|
-
|
|
58368
|
-
yield* failOnErrors(dedupeDiagnostics(sortDiagnostics(def.skillsKeys.filter(({ key }) => !known.includes(key)).map(({ key, origin }) => ({
|
|
58369
|
-
severity: "error",
|
|
58370
|
-
path: ["skills", key],
|
|
58371
|
-
message: unknownSkillsKeyMessage(key, known),
|
|
58372
|
-
origin
|
|
58373
|
-
})), [...new Set(def.skillsKeys.map((k) => k.origin))])));
|
|
58413
|
+
yield* checkKeys(def.skillsKeys, Object.keys(skills), unknownSkillsKeyMessage, "skills");
|
|
58374
58414
|
return skills;
|
|
58375
58415
|
});
|
|
58416
|
+
const checkAccessShape = (value) => {
|
|
58417
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("access export must return an object of scope -> { read?, write? }");
|
|
58418
|
+
for (const [scope, access] of Object.entries(value)) {
|
|
58419
|
+
const fault = accessShapeFault(access);
|
|
58420
|
+
if (fault !== void 0) throw new Error(`access export: "${scope}": ${fault}`);
|
|
58421
|
+
}
|
|
58422
|
+
return value;
|
|
58423
|
+
};
|
|
58424
|
+
/**
|
|
58425
|
+
* Access for the settings actually in use. A `.gtdrc` `access:` key may name
|
|
58426
|
+
* any scope the workflow gives skills or access to.
|
|
58427
|
+
*/
|
|
58428
|
+
const resolveScopeAccess = (def, skills, vars) => gen(function* () {
|
|
58429
|
+
const access = yield* try_({
|
|
58430
|
+
try: () => checkAccessShape(def.access(vars)),
|
|
58431
|
+
catch: (e) => fail$4(def.skillsOrigin, e instanceof Error ? e.message : String(e))
|
|
58432
|
+
});
|
|
58433
|
+
yield* checkKeys(def.accessKeys, [.../* @__PURE__ */ new Set([...Object.keys(skills), ...Object.keys(access)])], unknownAccessKeyMessage, "access");
|
|
58434
|
+
return access;
|
|
58435
|
+
});
|
|
58376
58436
|
//#endregion
|
|
58377
58437
|
//#region src/judges/providers/fixed.ts
|
|
58378
58438
|
const fail$3 = (message) => {
|
|
@@ -63696,6 +63756,8 @@ const renderFormat = (fmt, ...args) => {
|
|
|
63696
63756
|
let i = 0;
|
|
63697
63757
|
return fmt.replace(/%s/g, () => args[i++] ?? "");
|
|
63698
63758
|
};
|
|
63759
|
+
/** The first line of the landing refusal for a turn that wrote outside its `write` access — a stable marker drivers and tests grep for. */
|
|
63760
|
+
const ACCESS_REFUSAL = "gtd land: this turn wrote outside its write access — revert these paths, then run `gtd land` again:";
|
|
63699
63761
|
//#endregion
|
|
63700
63762
|
//#region src/wire/Demand.ts
|
|
63701
63763
|
/**
|
|
@@ -63724,7 +63786,7 @@ const stallDiagnosis = (state, actor) => `stalled at "${state}": the last gtd(${
|
|
|
63724
63786
|
/**
|
|
63725
63787
|
* Assemble one `Demand` — the ONLY place a rest's resolved content becomes
|
|
63726
63788
|
* `Demand.content` (`stallDiagnosis` at `kind === "stalled"`, the rendered
|
|
63727
|
-
* content otherwise), and the only place `session`/`validate`/`skills` are
|
|
63789
|
+
* content otherwise), and the only place `session`/`validate`/`skills`/`access` are
|
|
63728
63790
|
* attached, which the `prompt` variant's own type makes impossible to do at
|
|
63729
63791
|
* any other kind.
|
|
63730
63792
|
*/
|
|
@@ -63732,12 +63794,17 @@ const demandOf = (input) => {
|
|
|
63732
63794
|
const { rendered, kind, session, validate } = input;
|
|
63733
63795
|
const content = kind === "stalled" ? stallDiagnosis(rendered.state, rendered.actor) : rendered.content;
|
|
63734
63796
|
const skills = rendered.skills !== void 0 && rendered.skills.length > 0 ? rendered.skills : void 0;
|
|
63797
|
+
const access = rendered.access ?? {
|
|
63798
|
+
read: null,
|
|
63799
|
+
write: null
|
|
63800
|
+
};
|
|
63735
63801
|
return kind === "prompt" ? {
|
|
63736
63802
|
kind,
|
|
63737
63803
|
content,
|
|
63738
63804
|
session,
|
|
63739
63805
|
validate,
|
|
63740
|
-
skills
|
|
63806
|
+
skills,
|
|
63807
|
+
access
|
|
63741
63808
|
} : {
|
|
63742
63809
|
kind,
|
|
63743
63810
|
content
|
|
@@ -63781,7 +63848,7 @@ const assertNeverDemand = (demand) => {
|
|
|
63781
63848
|
};
|
|
63782
63849
|
/**
|
|
63783
63850
|
* The ONE exhaustive switch translating a `Demand` into its wire
|
|
63784
|
-
* `session`/`validate`/`skills`
|
|
63851
|
+
* `session`/`validate`/`skills`/`access` set — only the `prompt` variant carries
|
|
63785
63852
|
* any of them, and every other member is listed explicitly (not a
|
|
63786
63853
|
* `default:` fallthrough) so adding a sixth `BeatKind` without adding its
|
|
63787
63854
|
* case here fails `tsc` at the `assertNeverDemand` call, not silently
|
|
@@ -63792,7 +63859,8 @@ const dispatchFieldsOf = (demand) => {
|
|
|
63792
63859
|
case "prompt": return {
|
|
63793
63860
|
session: demand.session,
|
|
63794
63861
|
validate: demand.validate,
|
|
63795
|
-
skills: demand.skills
|
|
63862
|
+
skills: demand.skills,
|
|
63863
|
+
access: demand.access
|
|
63796
63864
|
};
|
|
63797
63865
|
case "capture":
|
|
63798
63866
|
case "message":
|
|
@@ -63800,19 +63868,20 @@ const dispatchFieldsOf = (demand) => {
|
|
|
63800
63868
|
case "stalled": return {
|
|
63801
63869
|
session: void 0,
|
|
63802
63870
|
validate: void 0,
|
|
63803
|
-
skills: void 0
|
|
63871
|
+
skills: void 0,
|
|
63872
|
+
access: void 0
|
|
63804
63873
|
};
|
|
63805
63874
|
default: return assertNeverDemand(demand);
|
|
63806
63875
|
}
|
|
63807
63876
|
};
|
|
63808
63877
|
/**
|
|
63809
|
-
* Flatten a `Demand` plus its `BeatStatus` into the single
|
|
63878
|
+
* Flatten a `Demand` plus its `BeatStatus` into the single 21-key document
|
|
63810
63879
|
* `gtd next --json` emits — the ONLY place the two are joined. `cost`/
|
|
63811
63880
|
* `costByModel` are omitted together, exactly when no cost was recorded
|
|
63812
63881
|
* (`cost <= 0`).
|
|
63813
63882
|
*/
|
|
63814
63883
|
const beatDocument = (demand, status) => {
|
|
63815
|
-
const { session, validate, skills } = dispatchFieldsOf(demand);
|
|
63884
|
+
const { session, validate, skills, access } = dispatchFieldsOf(demand);
|
|
63816
63885
|
const hasCost = status.cost > 0;
|
|
63817
63886
|
return {
|
|
63818
63887
|
kind: demand.kind,
|
|
@@ -63834,7 +63903,8 @@ const beatDocument = (demand, status) => {
|
|
|
63834
63903
|
cost: hasCost ? status.cost : void 0,
|
|
63835
63904
|
costByModel: hasCost ? status.costByModel : void 0,
|
|
63836
63905
|
judge: status.judge,
|
|
63837
|
-
skills
|
|
63906
|
+
skills,
|
|
63907
|
+
access
|
|
63838
63908
|
};
|
|
63839
63909
|
};
|
|
63840
63910
|
const renderBeatJson = (document) => JSON.stringify(document) + "\n";
|
|
@@ -63971,6 +64041,195 @@ const renderLandPlain = (fields, command = "land") => fields.subject !== null ?
|
|
|
63971
64041
|
//#region src/Workflow.ts
|
|
63972
64042
|
const knownModes = (def) => Object.keys(def.modes);
|
|
63973
64043
|
//#endregion
|
|
64044
|
+
//#region src/step/planStep.ts
|
|
64045
|
+
/**
|
|
64046
|
+
* At the human review gate an unconditional `gtd uncheck '<file>'` runs
|
|
64047
|
+
* ahead of the commit, resetting every checkbox before `git add -A` picks it
|
|
64048
|
+
* up — a tick is read-progress, never sign-off.
|
|
64049
|
+
*/
|
|
64050
|
+
const uncheckStep = (snapshot) => snapshot.stepDef.actor === "human" && snapshot.stepDef.mode === "review" && snapshot.file !== void 0 ? [{
|
|
64051
|
+
kind: "uncheck",
|
|
64052
|
+
file: snapshot.file
|
|
64053
|
+
}] : [];
|
|
64054
|
+
const planStep = (snapshot, opts = {}) => {
|
|
64055
|
+
const landing = snapshot.landing;
|
|
64056
|
+
if (landing.kind === "refusal") return {
|
|
64057
|
+
kind: "refusal",
|
|
64058
|
+
message: landing.message
|
|
64059
|
+
};
|
|
64060
|
+
if (landing.kind === "noop") return {
|
|
64061
|
+
kind: "noop",
|
|
64062
|
+
state: snapshot.state,
|
|
64063
|
+
settled: landing.settled
|
|
64064
|
+
};
|
|
64065
|
+
const cost = opts.cost === void 0 ? {} : { cost: {
|
|
64066
|
+
cost: opts.cost,
|
|
64067
|
+
...opts.model !== void 0 ? { model: opts.model } : {}
|
|
64068
|
+
} };
|
|
64069
|
+
if (landing.kind === "attempt") {
|
|
64070
|
+
const message = formatCommitMessage({
|
|
64071
|
+
actor: snapshot.actor,
|
|
64072
|
+
to: snapshot.state,
|
|
64073
|
+
...cost
|
|
64074
|
+
});
|
|
64075
|
+
return {
|
|
64076
|
+
kind: "commit",
|
|
64077
|
+
state: snapshot.state,
|
|
64078
|
+
to: snapshot.state,
|
|
64079
|
+
subject: landing.subject,
|
|
64080
|
+
steps: [{
|
|
64081
|
+
kind: "gitWrite",
|
|
64082
|
+
write: {
|
|
64083
|
+
kind: "commitAll",
|
|
64084
|
+
message
|
|
64085
|
+
}
|
|
64086
|
+
}, {
|
|
64087
|
+
kind: "outcome",
|
|
64088
|
+
outcome: {
|
|
64089
|
+
kind: "commit",
|
|
64090
|
+
subject: landing.subject
|
|
64091
|
+
}
|
|
64092
|
+
}]
|
|
64093
|
+
};
|
|
64094
|
+
}
|
|
64095
|
+
const message = formatCommitMessage({
|
|
64096
|
+
...landing.spec,
|
|
64097
|
+
...cost,
|
|
64098
|
+
...opts.judge !== void 0 && opts.judge.length > 0 ? { judge: opts.judge } : {}
|
|
64099
|
+
});
|
|
64100
|
+
const subject = message.split("\n")[0];
|
|
64101
|
+
const outcome = landing.to === snapshot.state ? {
|
|
64102
|
+
kind: "outcome",
|
|
64103
|
+
outcome: {
|
|
64104
|
+
kind: "commit",
|
|
64105
|
+
subject
|
|
64106
|
+
}
|
|
64107
|
+
} : {
|
|
64108
|
+
kind: "outcome",
|
|
64109
|
+
outcome: {
|
|
64110
|
+
kind: "transition",
|
|
64111
|
+
from: snapshot.state,
|
|
64112
|
+
to: landing.to
|
|
64113
|
+
}
|
|
64114
|
+
};
|
|
64115
|
+
return {
|
|
64116
|
+
kind: "commit",
|
|
64117
|
+
state: snapshot.state,
|
|
64118
|
+
to: landing.to,
|
|
64119
|
+
subject,
|
|
64120
|
+
steps: [
|
|
64121
|
+
...uncheckStep(snapshot),
|
|
64122
|
+
{
|
|
64123
|
+
kind: "gitWrite",
|
|
64124
|
+
write: {
|
|
64125
|
+
kind: "commitAll",
|
|
64126
|
+
message
|
|
64127
|
+
}
|
|
64128
|
+
},
|
|
64129
|
+
outcome
|
|
64130
|
+
]
|
|
64131
|
+
};
|
|
64132
|
+
};
|
|
64133
|
+
//#endregion
|
|
64134
|
+
//#region src/step/accessRefusal.ts
|
|
64135
|
+
/**
|
|
64136
|
+
* The landing refusal for changes outside `write`, or `undefined` when every
|
|
64137
|
+
* path is allowed. `null` is unrestricted. The message is the fix prompt: gtd
|
|
64138
|
+
* runs no fix turn of its own.
|
|
64139
|
+
*/
|
|
64140
|
+
const accessRefusal = (changes, write) => {
|
|
64141
|
+
if (write === null) return void 0;
|
|
64142
|
+
const offending = [...new Set(changes.filter((c) => !write.some((glob) => globMatches(c.path, glob))).map((c) => c.path))];
|
|
64143
|
+
if (offending.length === 0) return void 0;
|
|
64144
|
+
return [
|
|
64145
|
+
ACCESS_REFUSAL,
|
|
64146
|
+
...offending.map((path) => ` - ${path}`),
|
|
64147
|
+
`allowed: ${write.length === 0 ? "(nothing)" : write.join(", ")}`
|
|
64148
|
+
].join("\n");
|
|
64149
|
+
};
|
|
64150
|
+
//#endregion
|
|
64151
|
+
//#region src/step/planEntry.ts
|
|
64152
|
+
const refusal = (message) => ({
|
|
64153
|
+
kind: "refusal",
|
|
64154
|
+
message
|
|
64155
|
+
});
|
|
64156
|
+
const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) => gen(function* () {
|
|
64157
|
+
const environmentOnly = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n) && envNames.includes(n));
|
|
64158
|
+
if (environmentOnly.length > 0) return yield* fail$7(new GtdUsageError(`${commandLabel}: --var pins process settings only; ${environmentOnly.join(", ")} ${environmentOnly.length === 1 ? "is an environment setting" : "are environment settings"} — set it under "env:" in .gtdrc or with a GTD_<NAME> environment variable`));
|
|
64159
|
+
const undeclared = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n));
|
|
64160
|
+
if (undeclared.length > 0) return refusal(`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(", ")} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`);
|
|
64161
|
+
});
|
|
64162
|
+
/**
|
|
64163
|
+
* `gtd --entry <name>`: start a brand NEW process at a workflow entry, writing
|
|
64164
|
+
* an opening commit that carries every process setting as a `Gtd-Var:` trailer, plus a
|
|
64165
|
+
* `Gtd-Review-Base:` trailer when the entry fixes the process's diff base.
|
|
64166
|
+
* All validation is a REFUSAL, not an Effect failure.
|
|
64167
|
+
*/
|
|
64168
|
+
const planEntry = (current, actor, entry) => gen(function* () {
|
|
64169
|
+
const { state: name, commandLabel, vars: varOverrides } = entry;
|
|
64170
|
+
if (!current.idle) return refusal(`${commandLabel}: a process is already underway (resting at "${current.state}") — finish it, or run \`gtd abandon\`, before entering`);
|
|
64171
|
+
if (current.entryRefusal !== void 0) return refusal(`${commandLabel}: ${current.entryRefusal}`);
|
|
64172
|
+
const config = yield* (yield* ConfigService).load;
|
|
64173
|
+
const declaredNames = Object.keys({
|
|
64174
|
+
...config.workflowVars,
|
|
64175
|
+
...config.rcVars
|
|
64176
|
+
});
|
|
64177
|
+
const overrideRefusal = yield* checkOverrides(commandLabel, varOverrides, declaredNames, Object.keys({
|
|
64178
|
+
...config.workflowEnv,
|
|
64179
|
+
...config.rcEnv
|
|
64180
|
+
}));
|
|
64181
|
+
if (overrideRefusal !== void 0) return overrideRefusal;
|
|
64182
|
+
let base;
|
|
64183
|
+
const baseOf = current.def.base;
|
|
64184
|
+
const vars = resolveVars(config.workflowVars, config.rcVars, varOverrides, (yield* Host).env);
|
|
64185
|
+
const multiline = multilineSetting(vars);
|
|
64186
|
+
if (multiline !== void 0) return refusal(`${commandLabel}: process setting "${multiline}" spans several lines — a process setting is pinned in a commit trailer and must be a single line`);
|
|
64187
|
+
const template = yield* try_({
|
|
64188
|
+
try: () => baseOf?.(name, vars),
|
|
64189
|
+
catch: (e) => /* @__PURE__ */ new Error(`${commandLabel}: ${e instanceof Error ? e.message : String(e)}`)
|
|
64190
|
+
});
|
|
64191
|
+
if (baseOf !== void 0 && template !== void 0) {
|
|
64192
|
+
const resolved = yield* resolveBase(name, commandLabel, baseOf, template);
|
|
64193
|
+
if (typeof resolved !== "string") return resolved;
|
|
64194
|
+
base = resolved;
|
|
64195
|
+
}
|
|
64196
|
+
const subject = formatSubject(actor, name);
|
|
64197
|
+
return {
|
|
64198
|
+
kind: "entry",
|
|
64199
|
+
state: name,
|
|
64200
|
+
subject,
|
|
64201
|
+
steps: [{
|
|
64202
|
+
kind: "gitWrite",
|
|
64203
|
+
write: {
|
|
64204
|
+
kind: "commitAll",
|
|
64205
|
+
message: formatCommitMessage({
|
|
64206
|
+
actor,
|
|
64207
|
+
to: name,
|
|
64208
|
+
...base !== void 0 ? { reviewBase: base } : {},
|
|
64209
|
+
vars
|
|
64210
|
+
})
|
|
64211
|
+
}
|
|
64212
|
+
}, {
|
|
64213
|
+
kind: "outcome",
|
|
64214
|
+
outcome: {
|
|
64215
|
+
kind: "commit",
|
|
64216
|
+
subject
|
|
64217
|
+
}
|
|
64218
|
+
}]
|
|
64219
|
+
};
|
|
64220
|
+
});
|
|
64221
|
+
/** The entry's diff base: a commitish that resolves, is an ancestor of HEAD, and is not HEAD. */
|
|
64222
|
+
const resolveBase = (name, commandLabel, base, value) => gen(function* () {
|
|
64223
|
+
const rendered = value.trim();
|
|
64224
|
+
if (rendered === "") return refusal(`${commandLabel}: "${name}"'s reviewBase template rendered blank — template: ${base.toString()}; pass the base with --var`);
|
|
64225
|
+
const git = yield* GitService;
|
|
64226
|
+
const resolvedBase = yield* either(git.resolveRef(rendered));
|
|
64227
|
+
if (resolvedBase._tag === "Left") return refusal(`${commandLabel}: "${rendered}" does not resolve to a commit`);
|
|
64228
|
+
if (!(yield* git.isAncestor(resolvedBase.right, "HEAD"))) return refusal(`${commandLabel}: "${rendered}" is not an ancestor of HEAD`);
|
|
64229
|
+
if (resolvedBase.right === (yield* git.resolveRef("HEAD"))) return refusal(`${commandLabel}: "${rendered}" is HEAD — nothing to review`);
|
|
64230
|
+
return resolvedBase.right;
|
|
64231
|
+
});
|
|
64232
|
+
//#endregion
|
|
63974
64233
|
//#region src/Edge.ts
|
|
63975
64234
|
const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
63976
64235
|
const ACTORS = /* @__PURE__ */ new Set([
|
|
@@ -64240,6 +64499,8 @@ const replayFor = (setup, pending) => replay({
|
|
|
64240
64499
|
budgetBytes: setup.budget,
|
|
64241
64500
|
skills: setup.skills,
|
|
64242
64501
|
configuredSkills: setup.def.configuredSkills,
|
|
64502
|
+
access: setup.access,
|
|
64503
|
+
configuredAccess: setup.def.configuredAccess,
|
|
64243
64504
|
...pending !== void 0 ? { pending } : {}
|
|
64244
64505
|
});
|
|
64245
64506
|
const replayError = (outcome) => {
|
|
@@ -64268,14 +64529,15 @@ const commonOf = (step) => {
|
|
|
64268
64529
|
...optional("mode", options.mode)
|
|
64269
64530
|
};
|
|
64270
64531
|
};
|
|
64271
|
-
const promptDef = (common, request, skills) => ({
|
|
64532
|
+
const promptDef = (common, request, skills, access) => ({
|
|
64272
64533
|
...common,
|
|
64273
64534
|
kind: "prompt",
|
|
64274
64535
|
content: request.prompt,
|
|
64275
64536
|
...optional("model", request.options.model),
|
|
64276
64537
|
...optional("system", request.options.system),
|
|
64277
64538
|
...optional("allowEmpty", request.options.allowEmpty),
|
|
64278
|
-
...optional("skills", skills)
|
|
64539
|
+
...optional("skills", skills),
|
|
64540
|
+
...optional("access", access)
|
|
64279
64541
|
});
|
|
64280
64542
|
const scriptDef = (common, request) => ({
|
|
64281
64543
|
...common,
|
|
@@ -64301,7 +64563,7 @@ const stepDefOf = (step) => {
|
|
|
64301
64563
|
const request = step.request;
|
|
64302
64564
|
const common = commonOf(step);
|
|
64303
64565
|
switch (request.kind) {
|
|
64304
|
-
case "agent": return promptDef(common, request, step.skills);
|
|
64566
|
+
case "agent": return promptDef(common, request, step.skills, step.access);
|
|
64305
64567
|
case "run": return scriptDef(common, request);
|
|
64306
64568
|
case "judge": return judgeDef(common, request, step);
|
|
64307
64569
|
case "human": return messageDef(common, request, step);
|
|
@@ -64357,7 +64619,8 @@ const hintsOf = (def) => ({
|
|
|
64357
64619
|
...optional("judge", def.judge),
|
|
64358
64620
|
...optional("system", def.system),
|
|
64359
64621
|
...optional("mode", def.mode),
|
|
64360
|
-
...optional("skills", def.skills)
|
|
64622
|
+
...optional("skills", def.skills),
|
|
64623
|
+
...optional("access", def.access)
|
|
64361
64624
|
});
|
|
64362
64625
|
const normalizeStatus = (raw) => raw === "A" ? "A" : raw === "D" ? "D" : "M";
|
|
64363
64626
|
/** Per-model token totals, highest-cost first (ties broken by model name). */
|
|
@@ -64379,6 +64642,7 @@ const restAt = (ref, entryOverrides = {}) => gen(function* () {
|
|
|
64379
64642
|
const run = yield* computeProcessRun(git, def, ref);
|
|
64380
64643
|
const { vars, env } = settingsFor(config, run.pinnedVars, host.env, entryOverrides);
|
|
64381
64644
|
const skills = yield* resolveScopeSkills(def, vars);
|
|
64645
|
+
const access = yield* resolveScopeAccess(def, skills, vars);
|
|
64382
64646
|
const budget = yield* try_({
|
|
64383
64647
|
try: () => judgeBudgetBytes(vars),
|
|
64384
64648
|
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
@@ -64404,6 +64668,7 @@ const restAt = (ref, entryOverrides = {}) => gen(function* () {
|
|
|
64404
64668
|
env,
|
|
64405
64669
|
budget,
|
|
64406
64670
|
skills,
|
|
64671
|
+
access,
|
|
64407
64672
|
workspace,
|
|
64408
64673
|
base,
|
|
64409
64674
|
episode,
|
|
@@ -64453,6 +64718,7 @@ const entryRefusal = (rest, name, entryVars) => gen(function* () {
|
|
|
64453
64718
|
const host = yield* Host;
|
|
64454
64719
|
const { vars, env } = settingsFor(config, {}, host.env, entryVars);
|
|
64455
64720
|
const skills = yield* resolveScopeSkills(rest.def, vars);
|
|
64721
|
+
const access = yield* resolveScopeAccess(rest.def, skills, vars);
|
|
64456
64722
|
const workspace = rest.setup.workspace;
|
|
64457
64723
|
const outcome = yield* promise(() => replay({
|
|
64458
64724
|
flow: rest.def.flow,
|
|
@@ -64469,7 +64735,9 @@ const entryRefusal = (rest, name, entryVars) => gen(function* () {
|
|
|
64469
64735
|
start: "",
|
|
64470
64736
|
budgetBytes: rest.setup.budget,
|
|
64471
64737
|
skills,
|
|
64472
|
-
configuredSkills: rest.def.configuredSkills
|
|
64738
|
+
configuredSkills: rest.def.configuredSkills,
|
|
64739
|
+
access,
|
|
64740
|
+
configuredAccess: rest.def.configuredAccess
|
|
64473
64741
|
}));
|
|
64474
64742
|
if (outcome.kind === "refused") return outcome.message;
|
|
64475
64743
|
if (outcome.kind === "rest") return outcome.entryRead ? void 0 : `"${name}" is not an enterable state — this workflow reads no entry`;
|
|
@@ -64561,6 +64829,13 @@ const landingTo = (rest, to) => {
|
|
|
64561
64829
|
const decideLanding = (rest, verdicts) => gen(function* () {
|
|
64562
64830
|
const early = cleanLanding(rest);
|
|
64563
64831
|
if (early !== void 0) return early;
|
|
64832
|
+
if (rest.stepDef.kind === "prompt" && rest.stepDef.access?.write != null) {
|
|
64833
|
+
const message = accessRefusal(rest.changes, rest.stepDef.access.write);
|
|
64834
|
+
if (message !== void 0) return {
|
|
64835
|
+
kind: "refusal",
|
|
64836
|
+
message
|
|
64837
|
+
};
|
|
64838
|
+
}
|
|
64564
64839
|
const outcome = yield* promise(() => replayFor(rest.setup, {
|
|
64565
64840
|
tree: pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef)),
|
|
64566
64841
|
...verdicts !== void 0 ? { verdicts } : {}
|
|
@@ -64604,178 +64879,6 @@ const summaryFor = (run) => gen(function* () {
|
|
|
64604
64879
|
});
|
|
64605
64880
|
});
|
|
64606
64881
|
//#endregion
|
|
64607
|
-
//#region src/step/planStep.ts
|
|
64608
|
-
/**
|
|
64609
|
-
* At the human review gate an unconditional `gtd uncheck '<file>'` runs
|
|
64610
|
-
* ahead of the commit, resetting every checkbox before `git add -A` picks it
|
|
64611
|
-
* up — a tick is read-progress, never sign-off.
|
|
64612
|
-
*/
|
|
64613
|
-
const uncheckStep = (snapshot) => snapshot.stepDef.actor === "human" && snapshot.stepDef.mode === "review" && snapshot.file !== void 0 ? [{
|
|
64614
|
-
kind: "uncheck",
|
|
64615
|
-
file: snapshot.file
|
|
64616
|
-
}] : [];
|
|
64617
|
-
const planStep = (snapshot, opts = {}) => {
|
|
64618
|
-
const landing = snapshot.landing;
|
|
64619
|
-
if (landing.kind === "refusal") return {
|
|
64620
|
-
kind: "refusal",
|
|
64621
|
-
message: landing.message
|
|
64622
|
-
};
|
|
64623
|
-
if (landing.kind === "noop") return {
|
|
64624
|
-
kind: "noop",
|
|
64625
|
-
state: snapshot.state,
|
|
64626
|
-
settled: landing.settled
|
|
64627
|
-
};
|
|
64628
|
-
const cost = opts.cost === void 0 ? {} : { cost: {
|
|
64629
|
-
cost: opts.cost,
|
|
64630
|
-
...opts.model !== void 0 ? { model: opts.model } : {}
|
|
64631
|
-
} };
|
|
64632
|
-
if (landing.kind === "attempt") {
|
|
64633
|
-
const message = formatCommitMessage({
|
|
64634
|
-
actor: snapshot.actor,
|
|
64635
|
-
to: snapshot.state,
|
|
64636
|
-
...cost
|
|
64637
|
-
});
|
|
64638
|
-
return {
|
|
64639
|
-
kind: "commit",
|
|
64640
|
-
state: snapshot.state,
|
|
64641
|
-
to: snapshot.state,
|
|
64642
|
-
subject: landing.subject,
|
|
64643
|
-
steps: [{
|
|
64644
|
-
kind: "gitWrite",
|
|
64645
|
-
write: {
|
|
64646
|
-
kind: "commitAll",
|
|
64647
|
-
message
|
|
64648
|
-
}
|
|
64649
|
-
}, {
|
|
64650
|
-
kind: "outcome",
|
|
64651
|
-
outcome: {
|
|
64652
|
-
kind: "commit",
|
|
64653
|
-
subject: landing.subject
|
|
64654
|
-
}
|
|
64655
|
-
}]
|
|
64656
|
-
};
|
|
64657
|
-
}
|
|
64658
|
-
const message = formatCommitMessage({
|
|
64659
|
-
...landing.spec,
|
|
64660
|
-
...cost,
|
|
64661
|
-
...opts.judge !== void 0 && opts.judge.length > 0 ? { judge: opts.judge } : {}
|
|
64662
|
-
});
|
|
64663
|
-
const subject = message.split("\n")[0];
|
|
64664
|
-
const outcome = landing.to === snapshot.state ? {
|
|
64665
|
-
kind: "outcome",
|
|
64666
|
-
outcome: {
|
|
64667
|
-
kind: "commit",
|
|
64668
|
-
subject
|
|
64669
|
-
}
|
|
64670
|
-
} : {
|
|
64671
|
-
kind: "outcome",
|
|
64672
|
-
outcome: {
|
|
64673
|
-
kind: "transition",
|
|
64674
|
-
from: snapshot.state,
|
|
64675
|
-
to: landing.to
|
|
64676
|
-
}
|
|
64677
|
-
};
|
|
64678
|
-
return {
|
|
64679
|
-
kind: "commit",
|
|
64680
|
-
state: snapshot.state,
|
|
64681
|
-
to: landing.to,
|
|
64682
|
-
subject,
|
|
64683
|
-
steps: [
|
|
64684
|
-
...uncheckStep(snapshot),
|
|
64685
|
-
{
|
|
64686
|
-
kind: "gitWrite",
|
|
64687
|
-
write: {
|
|
64688
|
-
kind: "commitAll",
|
|
64689
|
-
message
|
|
64690
|
-
}
|
|
64691
|
-
},
|
|
64692
|
-
outcome
|
|
64693
|
-
]
|
|
64694
|
-
};
|
|
64695
|
-
};
|
|
64696
|
-
//#endregion
|
|
64697
|
-
//#region src/step/planEntry.ts
|
|
64698
|
-
const refusal = (message) => ({
|
|
64699
|
-
kind: "refusal",
|
|
64700
|
-
message
|
|
64701
|
-
});
|
|
64702
|
-
const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) => gen(function* () {
|
|
64703
|
-
const environmentOnly = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n) && envNames.includes(n));
|
|
64704
|
-
if (environmentOnly.length > 0) return yield* fail$7(new GtdUsageError(`${commandLabel}: --var pins process settings only; ${environmentOnly.join(", ")} ${environmentOnly.length === 1 ? "is an environment setting" : "are environment settings"} — set it under "env:" in .gtdrc or with a GTD_<NAME> environment variable`));
|
|
64705
|
-
const undeclared = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n));
|
|
64706
|
-
if (undeclared.length > 0) return refusal(`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(", ")} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`);
|
|
64707
|
-
});
|
|
64708
|
-
/**
|
|
64709
|
-
* `gtd --entry <name>`: start a brand NEW process at a workflow entry, writing
|
|
64710
|
-
* an opening commit that carries every process setting as a `Gtd-Var:` trailer, plus a
|
|
64711
|
-
* `Gtd-Review-Base:` trailer when the entry fixes the process's diff base.
|
|
64712
|
-
* All validation is a REFUSAL, not an Effect failure.
|
|
64713
|
-
*/
|
|
64714
|
-
const planEntry = (current, actor, entry) => gen(function* () {
|
|
64715
|
-
const { state: name, commandLabel, vars: varOverrides } = entry;
|
|
64716
|
-
if (!current.idle) return refusal(`${commandLabel}: a process is already underway (resting at "${current.state}") — finish it, or run \`gtd abandon\`, before entering`);
|
|
64717
|
-
if (current.entryRefusal !== void 0) return refusal(`${commandLabel}: ${current.entryRefusal}`);
|
|
64718
|
-
const config = yield* (yield* ConfigService).load;
|
|
64719
|
-
const declaredNames = Object.keys({
|
|
64720
|
-
...config.workflowVars,
|
|
64721
|
-
...config.rcVars
|
|
64722
|
-
});
|
|
64723
|
-
const overrideRefusal = yield* checkOverrides(commandLabel, varOverrides, declaredNames, Object.keys({
|
|
64724
|
-
...config.workflowEnv,
|
|
64725
|
-
...config.rcEnv
|
|
64726
|
-
}));
|
|
64727
|
-
if (overrideRefusal !== void 0) return overrideRefusal;
|
|
64728
|
-
let base;
|
|
64729
|
-
const baseOf = current.def.base;
|
|
64730
|
-
const vars = resolveVars(config.workflowVars, config.rcVars, varOverrides, (yield* Host).env);
|
|
64731
|
-
const multiline = multilineSetting(vars);
|
|
64732
|
-
if (multiline !== void 0) return refusal(`${commandLabel}: process setting "${multiline}" spans several lines — a process setting is pinned in a commit trailer and must be a single line`);
|
|
64733
|
-
const template = yield* try_({
|
|
64734
|
-
try: () => baseOf?.(name, vars),
|
|
64735
|
-
catch: (e) => /* @__PURE__ */ new Error(`${commandLabel}: ${e instanceof Error ? e.message : String(e)}`)
|
|
64736
|
-
});
|
|
64737
|
-
if (baseOf !== void 0 && template !== void 0) {
|
|
64738
|
-
const resolved = yield* resolveBase(name, commandLabel, baseOf, template);
|
|
64739
|
-
if (typeof resolved !== "string") return resolved;
|
|
64740
|
-
base = resolved;
|
|
64741
|
-
}
|
|
64742
|
-
const subject = formatSubject(actor, name);
|
|
64743
|
-
return {
|
|
64744
|
-
kind: "entry",
|
|
64745
|
-
state: name,
|
|
64746
|
-
subject,
|
|
64747
|
-
steps: [{
|
|
64748
|
-
kind: "gitWrite",
|
|
64749
|
-
write: {
|
|
64750
|
-
kind: "commitAll",
|
|
64751
|
-
message: formatCommitMessage({
|
|
64752
|
-
actor,
|
|
64753
|
-
to: name,
|
|
64754
|
-
...base !== void 0 ? { reviewBase: base } : {},
|
|
64755
|
-
vars
|
|
64756
|
-
})
|
|
64757
|
-
}
|
|
64758
|
-
}, {
|
|
64759
|
-
kind: "outcome",
|
|
64760
|
-
outcome: {
|
|
64761
|
-
kind: "commit",
|
|
64762
|
-
subject
|
|
64763
|
-
}
|
|
64764
|
-
}]
|
|
64765
|
-
};
|
|
64766
|
-
});
|
|
64767
|
-
/** The entry's diff base: a commitish that resolves, is an ancestor of HEAD, and is not HEAD. */
|
|
64768
|
-
const resolveBase = (name, commandLabel, base, value) => gen(function* () {
|
|
64769
|
-
const rendered = value.trim();
|
|
64770
|
-
if (rendered === "") return refusal(`${commandLabel}: "${name}"'s reviewBase template rendered blank — template: ${base.toString()}; pass the base with --var`);
|
|
64771
|
-
const git = yield* GitService;
|
|
64772
|
-
const resolvedBase = yield* either(git.resolveRef(rendered));
|
|
64773
|
-
if (resolvedBase._tag === "Left") return refusal(`${commandLabel}: "${rendered}" does not resolve to a commit`);
|
|
64774
|
-
if (!(yield* git.isAncestor(resolvedBase.right, "HEAD"))) return refusal(`${commandLabel}: "${rendered}" is not an ancestor of HEAD`);
|
|
64775
|
-
if (resolvedBase.right === (yield* git.resolveRef("HEAD"))) return refusal(`${commandLabel}: "${rendered}" is HEAD — nothing to review`);
|
|
64776
|
-
return resolvedBase.right;
|
|
64777
|
-
});
|
|
64778
|
-
//#endregion
|
|
64779
64882
|
//#region src/RetainedHistory.ts
|
|
64780
64883
|
/** Per-worktree (`refs/worktree/gtd/*`) so linked worktrees sharing one `.git` don't clobber each other's retained history. */
|
|
64781
64884
|
const HISTORY_REF = "refs/worktree/gtd/history";
|
|
@@ -76757,7 +76860,8 @@ never stored, so a plain peek is exactly as safe to call as a dispatch would
|
|
|
76757
76860
|
be — and, when the state declares a validatable steering file, \`validate\`
|
|
76758
76861
|
(the script that formats then validates it), and, when the turn's scope declares
|
|
76759
76862
|
skills, \`skills\` (an array of the skill names, already trimmed and split —
|
|
76760
|
-
\`--json=skills.0\` reads the first)
|
|
76863
|
+
\`--json=skills.0\` reads the first), and always \`access\`
|
|
76864
|
+
(\`{read, write}\`, each an array of globs or \`null\` for unrestricted). When set: \`model\`, \`memory\`,
|
|
76761
76865
|
\`label\`, \`file\`, \`mode\`, \`edges\`, \`judge\` (the pending judgment's rendered
|
|
76762
76866
|
document). When a cost has been recorded (a prior \`gtd land --cost=<n>\`):
|
|
76763
76867
|
\`cost\`, \`costByModel\`.
|
|
@@ -76856,6 +76960,13 @@ const DRIVER_OBLIGATIONS = `
|
|
|
76856
76960
|
turn sees only those. An absent field means none. A CLI that cannot
|
|
76857
76961
|
restrict relies on the preamble gtd already puts in the prompt — the
|
|
76858
76962
|
reference body's \`claude -p\` does exactly that.
|
|
76963
|
+
12. Map each prompt turn's \`--json=access.read\` / \`--json=access.write\`
|
|
76964
|
+
(arrays of globs, \`null\` = unrestricted) onto the chosen CLI's sandbox
|
|
76965
|
+
(Codex \`workspace-write\` plus writable roots) or a container with
|
|
76966
|
+
\`:ro\` / \`:rw\` bind mounts. \`access\` is a prompt-only field. A CLI
|
|
76967
|
+
that cannot restrict relies on the preamble and gtd's landing check. When
|
|
76968
|
+
\`gtd land\` refuses naming an access violation, re-prompt the turn with
|
|
76969
|
+
the refusal text and land again, or stop.
|
|
76859
76970
|
`;
|
|
76860
76971
|
const RECOVERY = `
|
|
76861
76972
|
## Recovery
|