@pmelab/gtd 18.0.0 → 18.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -0
- 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 +442 -209
- package/package.json +1 -1
- package/schema.json +22 -0
- package/src/flows/runtime.ts +20 -0
- package/src/workflows/access.test.ts +41 -0
- package/src/workflows/access.ts +23 -0
- package/src/workflows/prose.ts +17 -0
- package/src/workflows/text.fixture.ts +26 -0
- package/src/workflows/text.test.ts +41 -0
- package/src/workflows/text.ts +8 -2
- package/src/workflows/unified.ts +1 -0
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(),
|
|
@@ -31161,6 +31163,7 @@ const requireRevert = (edited, base) => {
|
|
|
31161
31163
|
//#endregion
|
|
31162
31164
|
//#region src/flows/index.ts
|
|
31163
31165
|
var flows_exports = /* @__PURE__ */ __exportAll({
|
|
31166
|
+
accessFor: () => accessFor,
|
|
31164
31167
|
agent: () => agent,
|
|
31165
31168
|
answered: () => answered,
|
|
31166
31169
|
changes: () => changes,
|
|
@@ -47480,6 +47483,34 @@ const viewOf = (format, content) => ({
|
|
|
47480
47483
|
/** Every OPEN thread — last entry is the agent's — as `{ name, line, firstMe }` (0-based line). `[]` for a format with no thread concept. */
|
|
47481
47484
|
const openThreads = (format, content) => format.openThreads?.(content) ?? [];
|
|
47482
47485
|
//#endregion
|
|
47486
|
+
//#region src/replay/Access.ts
|
|
47487
|
+
const UNRESTRICTED = {};
|
|
47488
|
+
const withExtras = (globs, extras) => globs === void 0 ? null : [.../* @__PURE__ */ new Set([...globs, ...extras])];
|
|
47489
|
+
/**
|
|
47490
|
+
* Fold a step's own paths into a scope's access: each restricted side gets the
|
|
47491
|
+
* steering `file`, then the code threads waiting on the agent. An unrestricted
|
|
47492
|
+
* side stays `null`.
|
|
47493
|
+
*/
|
|
47494
|
+
const foldAccess = (scopeAccess, file, threadPaths) => {
|
|
47495
|
+
const access = scopeAccess ?? UNRESTRICTED;
|
|
47496
|
+
const extras = [...file === void 0 ? [] : [file], ...threadPaths];
|
|
47497
|
+
return {
|
|
47498
|
+
read: withExtras(access.read, extras),
|
|
47499
|
+
write: withExtras(access.write, extras)
|
|
47500
|
+
};
|
|
47501
|
+
};
|
|
47502
|
+
/** The shape fault of a `scope()` `access` value, or `undefined` when well-formed. */
|
|
47503
|
+
const accessShapeFault = (value) => {
|
|
47504
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return "access must be an object of { read?, write? } glob arrays";
|
|
47505
|
+
for (const [side, globs] of Object.entries(value)) {
|
|
47506
|
+
if (side !== "read" && side !== "write") return `unknown access key "${side}"`;
|
|
47507
|
+
if (globs === void 0) continue;
|
|
47508
|
+
if (!Array.isArray(globs) || globs.some((g) => typeof g !== "string")) return `access.${side} must be an array of glob strings`;
|
|
47509
|
+
}
|
|
47510
|
+
};
|
|
47511
|
+
const sameGlobs = (a, b) => a === void 0 || b === void 0 ? a === b : a.length === b.length && a.every((g, i) => g === b[i]);
|
|
47512
|
+
const sameAccess = (a, b) => sameGlobs(a?.read, b?.read) && sameGlobs(a?.write, b?.write);
|
|
47513
|
+
//#endregion
|
|
47483
47514
|
//#region src/replay/Glob.ts
|
|
47484
47515
|
const ESCAPE_RE = /[.+^${}()|[\]\\?]/g;
|
|
47485
47516
|
const compile = (glob) => {
|
|
@@ -47701,7 +47732,8 @@ const budgeted = (evidence, budgetBytes) => {
|
|
|
47701
47732
|
truncated
|
|
47702
47733
|
};
|
|
47703
47734
|
};
|
|
47704
|
-
const
|
|
47735
|
+
const sameSkills = (a, b) => (a?.length ?? 0) === (b?.length ?? 0) && (a ?? []).every((skill, i) => skill === b?.[i]);
|
|
47736
|
+
const samePersona = (a, b) => a.model === b.model && a.system === b.system && sameSkills(a.skills, b.skills) && sameAccess(a.access, b.access);
|
|
47705
47737
|
/**
|
|
47706
47738
|
* Replay one episode: run its entry flow, answering each step from the
|
|
47707
47739
|
* episode's commits in order, until a step has no commit left — the rest.
|
|
@@ -47791,17 +47823,34 @@ const replay = async (input) => {
|
|
|
47791
47823
|
truncated: judgeCuts.get(request) ?? []
|
|
47792
47824
|
} : void 0;
|
|
47793
47825
|
const judgeCuts = /* @__PURE__ */ new WeakMap();
|
|
47794
|
-
const
|
|
47826
|
+
const resolveScoped = (memory, pick, configured, bundled) => {
|
|
47795
47827
|
const own = /* @__PURE__ */ new Map();
|
|
47796
47828
|
const names = [];
|
|
47797
47829
|
for (const s of scopes) {
|
|
47798
47830
|
if (s.name !== void 0) names.push(s.name);
|
|
47799
|
-
|
|
47831
|
+
const v = pick(s);
|
|
47832
|
+
if (v !== void 0) own.set(names.join("."), v);
|
|
47800
47833
|
}
|
|
47801
47834
|
const parts = memory === "" ? [] : memory.split(".");
|
|
47802
|
-
|
|
47835
|
+
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);
|
|
47836
|
+
};
|
|
47837
|
+
const resolveSkills = (memory) => {
|
|
47838
|
+
const hit = resolveScoped(memory, (s) => s.skills, input.configuredSkills, input.skills);
|
|
47803
47839
|
return hit === void 0 || hit.length === 0 ? void 0 : hit;
|
|
47804
47840
|
};
|
|
47841
|
+
const resolveAccess = (memory) => resolveScoped(memory, (s) => s.access, input.configuredAccess, input.access);
|
|
47842
|
+
const foldedAccessFor = (scopeAccess, file) => {
|
|
47843
|
+
return foldAccess(scopeAccess, file, scopeAccess?.read !== void 0 || scopeAccess?.write !== void 0 ? context.codeThreads().filter((t) => t.waitingOn === "agent").map((t) => t.path) : []);
|
|
47844
|
+
};
|
|
47845
|
+
const scopeAccessFault = () => {
|
|
47846
|
+
for (const [i, s] of scopes.entries()) {
|
|
47847
|
+
if (s.access === void 0) continue;
|
|
47848
|
+
const fault = accessShapeFault(s.access);
|
|
47849
|
+
if (fault === void 0) continue;
|
|
47850
|
+
return `gtd: scope "${scopes.slice(0, i + 1).flatMap((o) => o.name === void 0 ? [] : [o.name]).join(".") || "root"}": ${fault}`;
|
|
47851
|
+
}
|
|
47852
|
+
};
|
|
47853
|
+
const unfoldedAccess = /* @__PURE__ */ new WeakMap();
|
|
47805
47854
|
const resolve = (request) => {
|
|
47806
47855
|
if (request.kind === "agent") {
|
|
47807
47856
|
const persona = {};
|
|
@@ -47833,6 +47882,8 @@ const replay = async (input) => {
|
|
|
47833
47882
|
const occurrence = (occurrences.get(name) ?? 0) + 1;
|
|
47834
47883
|
occurrences.set(name, occurrence);
|
|
47835
47884
|
const resolved = resolve(request);
|
|
47885
|
+
const memory = memoryScopeOf(name);
|
|
47886
|
+
const scopeAccess = resolved.kind === "agent" ? resolveAccess(memory) : void 0;
|
|
47836
47887
|
const reached = {
|
|
47837
47888
|
id: {
|
|
47838
47889
|
name,
|
|
@@ -47843,11 +47894,15 @@ const replay = async (input) => {
|
|
|
47843
47894
|
actor: actorOfKind(resolved.kind),
|
|
47844
47895
|
request: resolved,
|
|
47845
47896
|
memoryScope: memoryScopeOf(name),
|
|
47846
|
-
...resolved.kind === "agent" ? {
|
|
47897
|
+
...resolved.kind === "agent" ? {
|
|
47898
|
+
skills: resolveSkills(memory),
|
|
47899
|
+
access: foldedAccessFor(scopeAccess, resolved.options.file)
|
|
47900
|
+
} : {},
|
|
47847
47901
|
enteredAt: position.hash,
|
|
47848
47902
|
truncated: (judgeCuts.get(resolved) ?? []).length > 0,
|
|
47849
47903
|
scopeCalls: namedScopeCalls()
|
|
47850
47904
|
};
|
|
47905
|
+
if (resolved.kind === "agent") unfoldedAccess.set(reached, scopeAccess);
|
|
47851
47906
|
trace.push(reached);
|
|
47852
47907
|
return reached;
|
|
47853
47908
|
};
|
|
@@ -47856,14 +47911,15 @@ const replay = async (input) => {
|
|
|
47856
47911
|
const identity = {
|
|
47857
47912
|
model: step.request.options.model,
|
|
47858
47913
|
system: step.request.options.system,
|
|
47859
|
-
skills: step.skills
|
|
47914
|
+
skills: step.skills,
|
|
47915
|
+
access: unfoldedAccess.get(step)
|
|
47860
47916
|
};
|
|
47861
47917
|
const seen = personas.get(step.memoryScope);
|
|
47862
47918
|
if (seen === void 0) {
|
|
47863
47919
|
personas.set(step.memoryScope, identity);
|
|
47864
47920
|
return;
|
|
47865
47921
|
}
|
|
47866
|
-
return samePersona(seen, identity) ? void 0 : `gtd: "${step.name}" runs with a different model, system prompt or
|
|
47922
|
+
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
47923
|
};
|
|
47868
47924
|
const DIVERGED = "the workflow changed under this process; run `gtd abandon` to start over";
|
|
47869
47925
|
const blockerAt = (step) => {
|
|
@@ -47917,6 +47973,10 @@ const replay = async (input) => {
|
|
|
47917
47973
|
});
|
|
47918
47974
|
return replyFor(step.request, input.pending.verdicts ?? []);
|
|
47919
47975
|
};
|
|
47976
|
+
const requestFault = (request) => {
|
|
47977
|
+
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)`;
|
|
47978
|
+
return request.kind === "agent" ? scopeAccessFault() : void 0;
|
|
47979
|
+
};
|
|
47920
47980
|
const handleStep = async (request) => {
|
|
47921
47981
|
if (outcome !== void 0) throw new Stop();
|
|
47922
47982
|
if (request.kind === "restart") return finish(endOfFlow("restart"));
|
|
@@ -47924,9 +47984,10 @@ const replay = async (input) => {
|
|
|
47924
47984
|
kind: "failed",
|
|
47925
47985
|
message: "gtd: a step was called without a name"
|
|
47926
47986
|
});
|
|
47927
|
-
|
|
47987
|
+
const fault = requestFault(request);
|
|
47988
|
+
if (fault !== void 0) return finish({
|
|
47928
47989
|
kind: "failed",
|
|
47929
|
-
message:
|
|
47990
|
+
message: fault
|
|
47930
47991
|
});
|
|
47931
47992
|
const step = reach(request);
|
|
47932
47993
|
const blocker = blockerAt(step);
|
|
@@ -47946,7 +48007,7 @@ const replay = async (input) => {
|
|
|
47946
48007
|
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
48008
|
return changesBetween(tree, position.tree);
|
|
47948
48009
|
};
|
|
47949
|
-
|
|
48010
|
+
const context = {
|
|
47950
48011
|
step: handleStep,
|
|
47951
48012
|
refuse: (message) => {
|
|
47952
48013
|
outcome ??= {
|
|
@@ -48001,11 +48062,13 @@ const replay = async (input) => {
|
|
|
48001
48062
|
env: input.env,
|
|
48002
48063
|
start: () => input.start,
|
|
48003
48064
|
skillsFor: (localName) => resolveSkills(memoryScopeOf(scoped(localName))) ?? [],
|
|
48065
|
+
accessFor: (localName, file) => foldedAccessFor(resolveAccess(memoryScopeOf(scoped(localName))), file),
|
|
48004
48066
|
head: () => {
|
|
48005
48067
|
const rest = commits.slice(cursor);
|
|
48006
48068
|
return rest.length > 0 && rest.every((c) => c.parsed.step === void 0) ? rest[rest.length - 1].hash : position.hash;
|
|
48007
48069
|
}
|
|
48008
|
-
}
|
|
48070
|
+
};
|
|
48071
|
+
installContext(context);
|
|
48009
48072
|
try {
|
|
48010
48073
|
const flowDone = input.flow(args).then(() => {
|
|
48011
48074
|
if (outcome === void 0 && trace.length === 0) outcome = {
|
|
@@ -48045,6 +48108,11 @@ const skillsPreamble = `- Load whatever's listed here that your harness actually
|
|
|
48045
48108
|
prompt states first
|
|
48046
48109
|
- Never let a loaded skill turn this turn interactive — answer nothing,
|
|
48047
48110
|
ask nothing; this runs unattended, with no one at a keyboard`;
|
|
48111
|
+
/** One bullet per restricted side; empty when neither is. An empty glob list reads "nothing". */
|
|
48112
|
+
const accessPreamble = (access) => {
|
|
48113
|
+
const globs = (list) => list.length === 0 ? "nothing" : list.join(", ");
|
|
48114
|
+
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");
|
|
48115
|
+
};
|
|
48048
48116
|
const styleBlock = `- A deliverable, not a chat reply — size follows the work; cut padding
|
|
48049
48117
|
- Lead with the answer; never circle back to restate it
|
|
48050
48118
|
- Flat, commanding sentences — commit to the claim, never hedge
|
|
@@ -48282,8 +48350,12 @@ const withSkills = (skills, prompt) => {
|
|
|
48282
48350
|
if (skills === void 0 || skills.trim() === "") return prompt;
|
|
48283
48351
|
return `${skillsPreamble.replaceAll("{skills}", skills)}\n\n${prompt}`;
|
|
48284
48352
|
};
|
|
48285
|
-
/** An `agent()` step whose prompt opens with the preamble naming its scope's skills. */
|
|
48286
|
-
const agentWithSkills = (name, prompt, options = {}) =>
|
|
48353
|
+
/** An `agent()` step whose prompt opens with the preamble naming its scope's skills, then its restricted file access. */
|
|
48354
|
+
const agentWithSkills = (name, prompt, options = {}) => {
|
|
48355
|
+
const access = accessPreamble(accessFor(name, options.file));
|
|
48356
|
+
const body = access === "" ? prompt : `${access}\n\n${prompt}`;
|
|
48357
|
+
return agent(name, withSkills(skillsFor(name).join(", "), body), options);
|
|
48358
|
+
};
|
|
48287
48359
|
const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
|
|
48288
48360
|
const idleMessage = () => `No active gtd process.
|
|
48289
48361
|
|
|
@@ -49905,6 +49977,18 @@ const skills = (vars) => ({
|
|
|
49905
49977
|
...Object.fromEntries(lensesOf(vars.qualityReviews ?? "").map((lens) => [`build.quality.${lens}`, builtInLenses[lens]?.skills ?? [lens]]))
|
|
49906
49978
|
});
|
|
49907
49979
|
//#endregion
|
|
49980
|
+
//#region src/workflows/access.ts
|
|
49981
|
+
const access = (vars) => ({
|
|
49982
|
+
design: { write: [] },
|
|
49983
|
+
architecture: { write: [".gtd/REQUIREMENTS.md"] },
|
|
49984
|
+
"architecture.decompose": { write: [".gtd/packages/**", ".gtd/ARCHITECTURE.md"] },
|
|
49985
|
+
"packages.item.spec": { write: [".gtd/SPEC_FEEDBACK.md"] },
|
|
49986
|
+
"build.review": { write: [".gtd/REVIEW.md"] },
|
|
49987
|
+
"build.review.fix.nits": {},
|
|
49988
|
+
"build.review.fix.risks": {},
|
|
49989
|
+
...Object.fromEntries(lensesOf(vars.qualityReviews ?? "").map((lens) => [`build.quality.${lens}`, { write: [] }]))
|
|
49990
|
+
});
|
|
49991
|
+
//#endregion
|
|
49908
49992
|
//#region src/workflows/unified.ts
|
|
49909
49993
|
var unified_exports = /* @__PURE__ */ __exportAll({
|
|
49910
49994
|
ARCHITECTURE: () => ARCHITECTURE,
|
|
@@ -49915,6 +49999,7 @@ var unified_exports = /* @__PURE__ */ __exportAll({
|
|
|
49915
49999
|
REQUIREMENTS: () => REQUIREMENTS,
|
|
49916
50000
|
REVIEW: () => REVIEW,
|
|
49917
50001
|
SPEC_FEEDBACK: () => SPEC_FEEDBACK,
|
|
50002
|
+
access: () => access,
|
|
49918
50003
|
afterTail: () => afterTail,
|
|
49919
50004
|
agentWithSkills: () => agentWithSkills,
|
|
49920
50005
|
answerProductQuestions: () => answerProductQuestions,
|
|
@@ -50798,6 +50883,25 @@ const skillsJsonSchema = {
|
|
|
50798
50883
|
items: { type: "string" }
|
|
50799
50884
|
}
|
|
50800
50885
|
};
|
|
50886
|
+
/** 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:`. */
|
|
50887
|
+
const accessJsonSchema = {
|
|
50888
|
+
type: "object",
|
|
50889
|
+
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.",
|
|
50890
|
+
additionalProperties: {
|
|
50891
|
+
type: "object",
|
|
50892
|
+
additionalProperties: false,
|
|
50893
|
+
properties: {
|
|
50894
|
+
read: {
|
|
50895
|
+
type: "array",
|
|
50896
|
+
items: { type: "string" }
|
|
50897
|
+
},
|
|
50898
|
+
write: {
|
|
50899
|
+
type: "array",
|
|
50900
|
+
items: { type: "string" }
|
|
50901
|
+
}
|
|
50902
|
+
}
|
|
50903
|
+
}
|
|
50904
|
+
};
|
|
50801
50905
|
/**
|
|
50802
50906
|
* A real (not `Unknown`) schema, unlike `vars`/`modes`: it is a flat settings
|
|
50803
50907
|
* struct with no per-mode map to compile, so its JSON Schema derives from the
|
|
@@ -50825,7 +50929,8 @@ const ConfigSchema = Struct({
|
|
|
50825
50929
|
judge: optional$1(JudgeSchema),
|
|
50826
50930
|
modes: optional$1(Unknown.annotations({ jsonSchema: modesJsonSchema })),
|
|
50827
50931
|
ui: optional$1(UiSchema),
|
|
50828
|
-
skills: optional$1(Unknown.annotations({ jsonSchema: skillsJsonSchema }))
|
|
50932
|
+
skills: optional$1(Unknown.annotations({ jsonSchema: skillsJsonSchema })),
|
|
50933
|
+
access: optional$1(Unknown.annotations({ jsonSchema: accessJsonSchema }))
|
|
50829
50934
|
});
|
|
50830
50935
|
//#endregion
|
|
50831
50936
|
//#region src/workflow/vars.ts
|
|
@@ -50997,6 +51102,33 @@ const compileSkillsMap = (raw) => {
|
|
|
50997
51102
|
diagnostics
|
|
50998
51103
|
};
|
|
50999
51104
|
};
|
|
51105
|
+
/** A flat `scope full name -> { read?, write? }` map; a malformed value is a load error and is dropped. */
|
|
51106
|
+
const compileAccessMap = (raw) => {
|
|
51107
|
+
const diagnostics = [];
|
|
51108
|
+
if (raw === void 0) return {
|
|
51109
|
+
access: {},
|
|
51110
|
+
diagnostics
|
|
51111
|
+
};
|
|
51112
|
+
if (!isPlainObject$3(raw)) {
|
|
51113
|
+
diagnostics.push(err(["access"], `"access" must be a mapping of scope name -> { read?, write? }, got ${describeType(raw)}`));
|
|
51114
|
+
return {
|
|
51115
|
+
access: {},
|
|
51116
|
+
diagnostics
|
|
51117
|
+
};
|
|
51118
|
+
}
|
|
51119
|
+
const access = Object.create(null);
|
|
51120
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
51121
|
+
const fault = accessShapeFault(value);
|
|
51122
|
+
if (fault === void 0) access[key] = value;
|
|
51123
|
+
else diagnostics.push(err(["access", key], `"access.${key}": ${fault}`));
|
|
51124
|
+
}
|
|
51125
|
+
return {
|
|
51126
|
+
access,
|
|
51127
|
+
diagnostics
|
|
51128
|
+
};
|
|
51129
|
+
};
|
|
51130
|
+
/** The `.gtdrc` `access:` load-error message for a key that is not a known scope. */
|
|
51131
|
+
const unknownAccessKeyMessage = (key, known) => `"access.${key}" is not a scope that runs a turn — known scopes: ${known.join(", ")}`;
|
|
51000
51132
|
/** The `.gtdrc` `skills:` load-error message for a key that is not a known scope. */
|
|
51001
51133
|
const unknownSkillsKeyMessage = (key, known) => `"skills.${key}" is not a scope that runs a turn — known scopes: ${known.join(", ")}`;
|
|
51002
51134
|
const MODE_COMMAND_KEYS = ["format", "validate"];
|
|
@@ -51168,6 +51300,20 @@ const compileConfig = (layers) => {
|
|
|
51168
51300
|
})));
|
|
51169
51301
|
}
|
|
51170
51302
|
const { skills: rcSkills } = compileSkillsMap(mergedConfig["skills"]);
|
|
51303
|
+
const accessKeys = [];
|
|
51304
|
+
for (const layer of layers) {
|
|
51305
|
+
if (!isPlainObject$3(layer.value)) continue;
|
|
51306
|
+
const own = compileAccessMap(layer.value["access"]);
|
|
51307
|
+
diagnostics.push(...own.diagnostics.map((d) => ({
|
|
51308
|
+
...d,
|
|
51309
|
+
origin: layer.origin
|
|
51310
|
+
})));
|
|
51311
|
+
accessKeys.push(...Object.keys(own.access).map((key) => ({
|
|
51312
|
+
key,
|
|
51313
|
+
origin: layer.origin
|
|
51314
|
+
})));
|
|
51315
|
+
}
|
|
51316
|
+
const { access: rcAccess } = compileAccessMap(mergedConfig["access"]);
|
|
51171
51317
|
const seeded = Object.fromEntries(BUILT_IN_MODE_NAMES.map((name) => [name, { validate: seededValidateCommand(name) }]));
|
|
51172
51318
|
return {
|
|
51173
51319
|
rcVars,
|
|
@@ -51179,6 +51325,8 @@ const compileConfig = (layers) => {
|
|
|
51179
51325
|
...ui !== void 0 ? { ui } : {},
|
|
51180
51326
|
rcSkills,
|
|
51181
51327
|
skillsKeys,
|
|
51328
|
+
access: rcAccess,
|
|
51329
|
+
accessKeys,
|
|
51182
51330
|
diagnostics: dedupeDiagnostics(sortDiagnostics(diagnostics, layerOrder))
|
|
51183
51331
|
};
|
|
51184
51332
|
};
|
|
@@ -58110,6 +58258,9 @@ const load = gen(function* () {
|
|
|
58110
58258
|
skills: loaded.skills,
|
|
58111
58259
|
configuredSkills: compiled.rcSkills,
|
|
58112
58260
|
skillsKeys: compiled.skillsKeys,
|
|
58261
|
+
access: loaded.access,
|
|
58262
|
+
configuredAccess: compiled.access,
|
|
58263
|
+
accessKeys: compiled.accessKeys,
|
|
58113
58264
|
skillsOrigin: module?.filepath ?? "(built-in default)",
|
|
58114
58265
|
initial
|
|
58115
58266
|
},
|
|
@@ -58187,6 +58338,12 @@ const skillsExport = (exports) => {
|
|
|
58187
58338
|
const record = checkSkillsRecord(value);
|
|
58188
58339
|
return () => record;
|
|
58189
58340
|
};
|
|
58341
|
+
/** Read a module's optional `access` export — a scope full name -> `{ read?, write? }` record, or a function of the vars returning one. */
|
|
58342
|
+
const accessExport = (exports) => {
|
|
58343
|
+
const value = exports["access"] ?? {};
|
|
58344
|
+
if (typeof value === "function") return (vars) => value(vars);
|
|
58345
|
+
return () => value;
|
|
58346
|
+
};
|
|
58190
58347
|
const optionalFunction = (exports, name) => {
|
|
58191
58348
|
const value = exports[name];
|
|
58192
58349
|
if (value === void 0) return void 0;
|
|
@@ -58215,6 +58372,7 @@ const fromModule = (exported, origin, dependencyFiles) => {
|
|
|
58215
58372
|
envDefaults,
|
|
58216
58373
|
steering: stringRecord(exports, "steering"),
|
|
58217
58374
|
skills: skillsExport(exports),
|
|
58375
|
+
access: accessExport(exports),
|
|
58218
58376
|
summary: optionalFunction(exports, "summary"),
|
|
58219
58377
|
base: optionalFunction(exports, "base"),
|
|
58220
58378
|
origin,
|
|
@@ -58355,6 +58513,16 @@ const checkShape = (value) => {
|
|
|
58355
58513
|
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
58514
|
return value;
|
|
58357
58515
|
};
|
|
58516
|
+
/** Every `.gtdrc` key that names no known scope is a load error, one per file that wrote it. */
|
|
58517
|
+
const checkKeys = (keys, known, message, section) => {
|
|
58518
|
+
const sorted = [...known].sort();
|
|
58519
|
+
return failOnErrors(dedupeDiagnostics(sortDiagnostics(keys.filter(({ key }) => !sorted.includes(key)).map(({ key, origin }) => ({
|
|
58520
|
+
severity: "error",
|
|
58521
|
+
path: [section, key],
|
|
58522
|
+
message: message(key, sorted),
|
|
58523
|
+
origin
|
|
58524
|
+
})), [...new Set(keys.map((k) => k.origin))])));
|
|
58525
|
+
};
|
|
58358
58526
|
/**
|
|
58359
58527
|
* Skill lists for the settings actually in use. `.gtdrc` `skills:` keys are
|
|
58360
58528
|
* judged here, not at config load: only now are the pinned/entry vars known.
|
|
@@ -58364,15 +58532,29 @@ const resolveScopeSkills = (def, vars) => gen(function* () {
|
|
|
58364
58532
|
try: () => checkShape(def.skills(vars)),
|
|
58365
58533
|
catch: (e) => fail$4(def.skillsOrigin, e instanceof Error ? e.message : String(e))
|
|
58366
58534
|
});
|
|
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))])));
|
|
58535
|
+
yield* checkKeys(def.skillsKeys, Object.keys(skills), unknownSkillsKeyMessage, "skills");
|
|
58374
58536
|
return skills;
|
|
58375
58537
|
});
|
|
58538
|
+
const checkAccessShape = (value) => {
|
|
58539
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("access export must return an object of scope -> { read?, write? }");
|
|
58540
|
+
for (const [scope, access] of Object.entries(value)) {
|
|
58541
|
+
const fault = accessShapeFault(access);
|
|
58542
|
+
if (fault !== void 0) throw new Error(`access export: "${scope}": ${fault}`);
|
|
58543
|
+
}
|
|
58544
|
+
return value;
|
|
58545
|
+
};
|
|
58546
|
+
/**
|
|
58547
|
+
* Access for the settings actually in use. A `.gtdrc` `access:` key may name
|
|
58548
|
+
* any scope the workflow gives skills or access to.
|
|
58549
|
+
*/
|
|
58550
|
+
const resolveScopeAccess = (def, skills, vars) => gen(function* () {
|
|
58551
|
+
const access = yield* try_({
|
|
58552
|
+
try: () => checkAccessShape(def.access(vars)),
|
|
58553
|
+
catch: (e) => fail$4(def.skillsOrigin, e instanceof Error ? e.message : String(e))
|
|
58554
|
+
});
|
|
58555
|
+
yield* checkKeys(def.accessKeys, [.../* @__PURE__ */ new Set([...Object.keys(skills), ...Object.keys(access)])], unknownAccessKeyMessage, "access");
|
|
58556
|
+
return access;
|
|
58557
|
+
});
|
|
58376
58558
|
//#endregion
|
|
58377
58559
|
//#region src/judges/providers/fixed.ts
|
|
58378
58560
|
const fail$3 = (message) => {
|
|
@@ -63696,6 +63878,8 @@ const renderFormat = (fmt, ...args) => {
|
|
|
63696
63878
|
let i = 0;
|
|
63697
63879
|
return fmt.replace(/%s/g, () => args[i++] ?? "");
|
|
63698
63880
|
};
|
|
63881
|
+
/** The first line of the landing refusal for a turn that wrote outside its `write` access — a stable marker drivers and tests grep for. */
|
|
63882
|
+
const ACCESS_REFUSAL = "gtd land: this turn wrote outside its write access — revert these paths, then run `gtd land` again:";
|
|
63699
63883
|
//#endregion
|
|
63700
63884
|
//#region src/wire/Demand.ts
|
|
63701
63885
|
/**
|
|
@@ -63724,7 +63908,7 @@ const stallDiagnosis = (state, actor) => `stalled at "${state}": the last gtd(${
|
|
|
63724
63908
|
/**
|
|
63725
63909
|
* Assemble one `Demand` — the ONLY place a rest's resolved content becomes
|
|
63726
63910
|
* `Demand.content` (`stallDiagnosis` at `kind === "stalled"`, the rendered
|
|
63727
|
-
* content otherwise), and the only place `session`/`validate`/`skills` are
|
|
63911
|
+
* content otherwise), and the only place `session`/`validate`/`skills`/`access` are
|
|
63728
63912
|
* attached, which the `prompt` variant's own type makes impossible to do at
|
|
63729
63913
|
* any other kind.
|
|
63730
63914
|
*/
|
|
@@ -63732,12 +63916,17 @@ const demandOf = (input) => {
|
|
|
63732
63916
|
const { rendered, kind, session, validate } = input;
|
|
63733
63917
|
const content = kind === "stalled" ? stallDiagnosis(rendered.state, rendered.actor) : rendered.content;
|
|
63734
63918
|
const skills = rendered.skills !== void 0 && rendered.skills.length > 0 ? rendered.skills : void 0;
|
|
63919
|
+
const access = rendered.access ?? {
|
|
63920
|
+
read: null,
|
|
63921
|
+
write: null
|
|
63922
|
+
};
|
|
63735
63923
|
return kind === "prompt" ? {
|
|
63736
63924
|
kind,
|
|
63737
63925
|
content,
|
|
63738
63926
|
session,
|
|
63739
63927
|
validate,
|
|
63740
|
-
skills
|
|
63928
|
+
skills,
|
|
63929
|
+
access
|
|
63741
63930
|
} : {
|
|
63742
63931
|
kind,
|
|
63743
63932
|
content
|
|
@@ -63781,7 +63970,7 @@ const assertNeverDemand = (demand) => {
|
|
|
63781
63970
|
};
|
|
63782
63971
|
/**
|
|
63783
63972
|
* The ONE exhaustive switch translating a `Demand` into its wire
|
|
63784
|
-
* `session`/`validate`/`skills`
|
|
63973
|
+
* `session`/`validate`/`skills`/`access` set — only the `prompt` variant carries
|
|
63785
63974
|
* any of them, and every other member is listed explicitly (not a
|
|
63786
63975
|
* `default:` fallthrough) so adding a sixth `BeatKind` without adding its
|
|
63787
63976
|
* case here fails `tsc` at the `assertNeverDemand` call, not silently
|
|
@@ -63792,7 +63981,8 @@ const dispatchFieldsOf = (demand) => {
|
|
|
63792
63981
|
case "prompt": return {
|
|
63793
63982
|
session: demand.session,
|
|
63794
63983
|
validate: demand.validate,
|
|
63795
|
-
skills: demand.skills
|
|
63984
|
+
skills: demand.skills,
|
|
63985
|
+
access: demand.access
|
|
63796
63986
|
};
|
|
63797
63987
|
case "capture":
|
|
63798
63988
|
case "message":
|
|
@@ -63800,19 +63990,20 @@ const dispatchFieldsOf = (demand) => {
|
|
|
63800
63990
|
case "stalled": return {
|
|
63801
63991
|
session: void 0,
|
|
63802
63992
|
validate: void 0,
|
|
63803
|
-
skills: void 0
|
|
63993
|
+
skills: void 0,
|
|
63994
|
+
access: void 0
|
|
63804
63995
|
};
|
|
63805
63996
|
default: return assertNeverDemand(demand);
|
|
63806
63997
|
}
|
|
63807
63998
|
};
|
|
63808
63999
|
/**
|
|
63809
|
-
* Flatten a `Demand` plus its `BeatStatus` into the single
|
|
64000
|
+
* Flatten a `Demand` plus its `BeatStatus` into the single 21-key document
|
|
63810
64001
|
* `gtd next --json` emits — the ONLY place the two are joined. `cost`/
|
|
63811
64002
|
* `costByModel` are omitted together, exactly when no cost was recorded
|
|
63812
64003
|
* (`cost <= 0`).
|
|
63813
64004
|
*/
|
|
63814
64005
|
const beatDocument = (demand, status) => {
|
|
63815
|
-
const { session, validate, skills } = dispatchFieldsOf(demand);
|
|
64006
|
+
const { session, validate, skills, access } = dispatchFieldsOf(demand);
|
|
63816
64007
|
const hasCost = status.cost > 0;
|
|
63817
64008
|
return {
|
|
63818
64009
|
kind: demand.kind,
|
|
@@ -63834,7 +64025,8 @@ const beatDocument = (demand, status) => {
|
|
|
63834
64025
|
cost: hasCost ? status.cost : void 0,
|
|
63835
64026
|
costByModel: hasCost ? status.costByModel : void 0,
|
|
63836
64027
|
judge: status.judge,
|
|
63837
|
-
skills
|
|
64028
|
+
skills,
|
|
64029
|
+
access
|
|
63838
64030
|
};
|
|
63839
64031
|
};
|
|
63840
64032
|
const renderBeatJson = (document) => JSON.stringify(document) + "\n";
|
|
@@ -63971,6 +64163,195 @@ const renderLandPlain = (fields, command = "land") => fields.subject !== null ?
|
|
|
63971
64163
|
//#region src/Workflow.ts
|
|
63972
64164
|
const knownModes = (def) => Object.keys(def.modes);
|
|
63973
64165
|
//#endregion
|
|
64166
|
+
//#region src/step/planStep.ts
|
|
64167
|
+
/**
|
|
64168
|
+
* At the human review gate an unconditional `gtd uncheck '<file>'` runs
|
|
64169
|
+
* ahead of the commit, resetting every checkbox before `git add -A` picks it
|
|
64170
|
+
* up — a tick is read-progress, never sign-off.
|
|
64171
|
+
*/
|
|
64172
|
+
const uncheckStep = (snapshot) => snapshot.stepDef.actor === "human" && snapshot.stepDef.mode === "review" && snapshot.file !== void 0 ? [{
|
|
64173
|
+
kind: "uncheck",
|
|
64174
|
+
file: snapshot.file
|
|
64175
|
+
}] : [];
|
|
64176
|
+
const planStep = (snapshot, opts = {}) => {
|
|
64177
|
+
const landing = snapshot.landing;
|
|
64178
|
+
if (landing.kind === "refusal") return {
|
|
64179
|
+
kind: "refusal",
|
|
64180
|
+
message: landing.message
|
|
64181
|
+
};
|
|
64182
|
+
if (landing.kind === "noop") return {
|
|
64183
|
+
kind: "noop",
|
|
64184
|
+
state: snapshot.state,
|
|
64185
|
+
settled: landing.settled
|
|
64186
|
+
};
|
|
64187
|
+
const cost = opts.cost === void 0 ? {} : { cost: {
|
|
64188
|
+
cost: opts.cost,
|
|
64189
|
+
...opts.model !== void 0 ? { model: opts.model } : {}
|
|
64190
|
+
} };
|
|
64191
|
+
if (landing.kind === "attempt") {
|
|
64192
|
+
const message = formatCommitMessage({
|
|
64193
|
+
actor: snapshot.actor,
|
|
64194
|
+
to: snapshot.state,
|
|
64195
|
+
...cost
|
|
64196
|
+
});
|
|
64197
|
+
return {
|
|
64198
|
+
kind: "commit",
|
|
64199
|
+
state: snapshot.state,
|
|
64200
|
+
to: snapshot.state,
|
|
64201
|
+
subject: landing.subject,
|
|
64202
|
+
steps: [{
|
|
64203
|
+
kind: "gitWrite",
|
|
64204
|
+
write: {
|
|
64205
|
+
kind: "commitAll",
|
|
64206
|
+
message
|
|
64207
|
+
}
|
|
64208
|
+
}, {
|
|
64209
|
+
kind: "outcome",
|
|
64210
|
+
outcome: {
|
|
64211
|
+
kind: "commit",
|
|
64212
|
+
subject: landing.subject
|
|
64213
|
+
}
|
|
64214
|
+
}]
|
|
64215
|
+
};
|
|
64216
|
+
}
|
|
64217
|
+
const message = formatCommitMessage({
|
|
64218
|
+
...landing.spec,
|
|
64219
|
+
...cost,
|
|
64220
|
+
...opts.judge !== void 0 && opts.judge.length > 0 ? { judge: opts.judge } : {}
|
|
64221
|
+
});
|
|
64222
|
+
const subject = message.split("\n")[0];
|
|
64223
|
+
const outcome = landing.to === snapshot.state ? {
|
|
64224
|
+
kind: "outcome",
|
|
64225
|
+
outcome: {
|
|
64226
|
+
kind: "commit",
|
|
64227
|
+
subject
|
|
64228
|
+
}
|
|
64229
|
+
} : {
|
|
64230
|
+
kind: "outcome",
|
|
64231
|
+
outcome: {
|
|
64232
|
+
kind: "transition",
|
|
64233
|
+
from: snapshot.state,
|
|
64234
|
+
to: landing.to
|
|
64235
|
+
}
|
|
64236
|
+
};
|
|
64237
|
+
return {
|
|
64238
|
+
kind: "commit",
|
|
64239
|
+
state: snapshot.state,
|
|
64240
|
+
to: landing.to,
|
|
64241
|
+
subject,
|
|
64242
|
+
steps: [
|
|
64243
|
+
...uncheckStep(snapshot),
|
|
64244
|
+
{
|
|
64245
|
+
kind: "gitWrite",
|
|
64246
|
+
write: {
|
|
64247
|
+
kind: "commitAll",
|
|
64248
|
+
message
|
|
64249
|
+
}
|
|
64250
|
+
},
|
|
64251
|
+
outcome
|
|
64252
|
+
]
|
|
64253
|
+
};
|
|
64254
|
+
};
|
|
64255
|
+
//#endregion
|
|
64256
|
+
//#region src/step/accessRefusal.ts
|
|
64257
|
+
/**
|
|
64258
|
+
* The landing refusal for changes outside `write`, or `undefined` when every
|
|
64259
|
+
* path is allowed. `null` is unrestricted. The message is the fix prompt: gtd
|
|
64260
|
+
* runs no fix turn of its own.
|
|
64261
|
+
*/
|
|
64262
|
+
const accessRefusal = (changes, write) => {
|
|
64263
|
+
if (write === null) return void 0;
|
|
64264
|
+
const offending = [...new Set(changes.filter((c) => !write.some((glob) => globMatches(c.path, glob))).map((c) => c.path))];
|
|
64265
|
+
if (offending.length === 0) return void 0;
|
|
64266
|
+
return [
|
|
64267
|
+
ACCESS_REFUSAL,
|
|
64268
|
+
...offending.map((path) => ` - ${path}`),
|
|
64269
|
+
`allowed: ${write.length === 0 ? "(nothing)" : write.join(", ")}`
|
|
64270
|
+
].join("\n");
|
|
64271
|
+
};
|
|
64272
|
+
//#endregion
|
|
64273
|
+
//#region src/step/planEntry.ts
|
|
64274
|
+
const refusal = (message) => ({
|
|
64275
|
+
kind: "refusal",
|
|
64276
|
+
message
|
|
64277
|
+
});
|
|
64278
|
+
const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) => gen(function* () {
|
|
64279
|
+
const environmentOnly = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n) && envNames.includes(n));
|
|
64280
|
+
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`));
|
|
64281
|
+
const undeclared = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n));
|
|
64282
|
+
if (undeclared.length > 0) return refusal(`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(", ")} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`);
|
|
64283
|
+
});
|
|
64284
|
+
/**
|
|
64285
|
+
* `gtd --entry <name>`: start a brand NEW process at a workflow entry, writing
|
|
64286
|
+
* an opening commit that carries every process setting as a `Gtd-Var:` trailer, plus a
|
|
64287
|
+
* `Gtd-Review-Base:` trailer when the entry fixes the process's diff base.
|
|
64288
|
+
* All validation is a REFUSAL, not an Effect failure.
|
|
64289
|
+
*/
|
|
64290
|
+
const planEntry = (current, actor, entry) => gen(function* () {
|
|
64291
|
+
const { state: name, commandLabel, vars: varOverrides } = entry;
|
|
64292
|
+
if (!current.idle) return refusal(`${commandLabel}: a process is already underway (resting at "${current.state}") — finish it, or run \`gtd abandon\`, before entering`);
|
|
64293
|
+
if (current.entryRefusal !== void 0) return refusal(`${commandLabel}: ${current.entryRefusal}`);
|
|
64294
|
+
const config = yield* (yield* ConfigService).load;
|
|
64295
|
+
const declaredNames = Object.keys({
|
|
64296
|
+
...config.workflowVars,
|
|
64297
|
+
...config.rcVars
|
|
64298
|
+
});
|
|
64299
|
+
const overrideRefusal = yield* checkOverrides(commandLabel, varOverrides, declaredNames, Object.keys({
|
|
64300
|
+
...config.workflowEnv,
|
|
64301
|
+
...config.rcEnv
|
|
64302
|
+
}));
|
|
64303
|
+
if (overrideRefusal !== void 0) return overrideRefusal;
|
|
64304
|
+
let base;
|
|
64305
|
+
const baseOf = current.def.base;
|
|
64306
|
+
const vars = resolveVars(config.workflowVars, config.rcVars, varOverrides, (yield* Host).env);
|
|
64307
|
+
const multiline = multilineSetting(vars);
|
|
64308
|
+
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`);
|
|
64309
|
+
const template = yield* try_({
|
|
64310
|
+
try: () => baseOf?.(name, vars),
|
|
64311
|
+
catch: (e) => /* @__PURE__ */ new Error(`${commandLabel}: ${e instanceof Error ? e.message : String(e)}`)
|
|
64312
|
+
});
|
|
64313
|
+
if (baseOf !== void 0 && template !== void 0) {
|
|
64314
|
+
const resolved = yield* resolveBase(name, commandLabel, baseOf, template);
|
|
64315
|
+
if (typeof resolved !== "string") return resolved;
|
|
64316
|
+
base = resolved;
|
|
64317
|
+
}
|
|
64318
|
+
const subject = formatSubject(actor, name);
|
|
64319
|
+
return {
|
|
64320
|
+
kind: "entry",
|
|
64321
|
+
state: name,
|
|
64322
|
+
subject,
|
|
64323
|
+
steps: [{
|
|
64324
|
+
kind: "gitWrite",
|
|
64325
|
+
write: {
|
|
64326
|
+
kind: "commitAll",
|
|
64327
|
+
message: formatCommitMessage({
|
|
64328
|
+
actor,
|
|
64329
|
+
to: name,
|
|
64330
|
+
...base !== void 0 ? { reviewBase: base } : {},
|
|
64331
|
+
vars
|
|
64332
|
+
})
|
|
64333
|
+
}
|
|
64334
|
+
}, {
|
|
64335
|
+
kind: "outcome",
|
|
64336
|
+
outcome: {
|
|
64337
|
+
kind: "commit",
|
|
64338
|
+
subject
|
|
64339
|
+
}
|
|
64340
|
+
}]
|
|
64341
|
+
};
|
|
64342
|
+
});
|
|
64343
|
+
/** The entry's diff base: a commitish that resolves, is an ancestor of HEAD, and is not HEAD. */
|
|
64344
|
+
const resolveBase = (name, commandLabel, base, value) => gen(function* () {
|
|
64345
|
+
const rendered = value.trim();
|
|
64346
|
+
if (rendered === "") return refusal(`${commandLabel}: "${name}"'s reviewBase template rendered blank — template: ${base.toString()}; pass the base with --var`);
|
|
64347
|
+
const git = yield* GitService;
|
|
64348
|
+
const resolvedBase = yield* either(git.resolveRef(rendered));
|
|
64349
|
+
if (resolvedBase._tag === "Left") return refusal(`${commandLabel}: "${rendered}" does not resolve to a commit`);
|
|
64350
|
+
if (!(yield* git.isAncestor(resolvedBase.right, "HEAD"))) return refusal(`${commandLabel}: "${rendered}" is not an ancestor of HEAD`);
|
|
64351
|
+
if (resolvedBase.right === (yield* git.resolveRef("HEAD"))) return refusal(`${commandLabel}: "${rendered}" is HEAD — nothing to review`);
|
|
64352
|
+
return resolvedBase.right;
|
|
64353
|
+
});
|
|
64354
|
+
//#endregion
|
|
63974
64355
|
//#region src/Edge.ts
|
|
63975
64356
|
const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
63976
64357
|
const ACTORS = /* @__PURE__ */ new Set([
|
|
@@ -64240,6 +64621,8 @@ const replayFor = (setup, pending) => replay({
|
|
|
64240
64621
|
budgetBytes: setup.budget,
|
|
64241
64622
|
skills: setup.skills,
|
|
64242
64623
|
configuredSkills: setup.def.configuredSkills,
|
|
64624
|
+
access: setup.access,
|
|
64625
|
+
configuredAccess: setup.def.configuredAccess,
|
|
64243
64626
|
...pending !== void 0 ? { pending } : {}
|
|
64244
64627
|
});
|
|
64245
64628
|
const replayError = (outcome) => {
|
|
@@ -64268,14 +64651,15 @@ const commonOf = (step) => {
|
|
|
64268
64651
|
...optional("mode", options.mode)
|
|
64269
64652
|
};
|
|
64270
64653
|
};
|
|
64271
|
-
const promptDef = (common, request, skills) => ({
|
|
64654
|
+
const promptDef = (common, request, skills, access) => ({
|
|
64272
64655
|
...common,
|
|
64273
64656
|
kind: "prompt",
|
|
64274
64657
|
content: request.prompt,
|
|
64275
64658
|
...optional("model", request.options.model),
|
|
64276
64659
|
...optional("system", request.options.system),
|
|
64277
64660
|
...optional("allowEmpty", request.options.allowEmpty),
|
|
64278
|
-
...optional("skills", skills)
|
|
64661
|
+
...optional("skills", skills),
|
|
64662
|
+
...optional("access", access)
|
|
64279
64663
|
});
|
|
64280
64664
|
const scriptDef = (common, request) => ({
|
|
64281
64665
|
...common,
|
|
@@ -64301,7 +64685,7 @@ const stepDefOf = (step) => {
|
|
|
64301
64685
|
const request = step.request;
|
|
64302
64686
|
const common = commonOf(step);
|
|
64303
64687
|
switch (request.kind) {
|
|
64304
|
-
case "agent": return promptDef(common, request, step.skills);
|
|
64688
|
+
case "agent": return promptDef(common, request, step.skills, step.access);
|
|
64305
64689
|
case "run": return scriptDef(common, request);
|
|
64306
64690
|
case "judge": return judgeDef(common, request, step);
|
|
64307
64691
|
case "human": return messageDef(common, request, step);
|
|
@@ -64357,7 +64741,8 @@ const hintsOf = (def) => ({
|
|
|
64357
64741
|
...optional("judge", def.judge),
|
|
64358
64742
|
...optional("system", def.system),
|
|
64359
64743
|
...optional("mode", def.mode),
|
|
64360
|
-
...optional("skills", def.skills)
|
|
64744
|
+
...optional("skills", def.skills),
|
|
64745
|
+
...optional("access", def.access)
|
|
64361
64746
|
});
|
|
64362
64747
|
const normalizeStatus = (raw) => raw === "A" ? "A" : raw === "D" ? "D" : "M";
|
|
64363
64748
|
/** Per-model token totals, highest-cost first (ties broken by model name). */
|
|
@@ -64379,6 +64764,7 @@ const restAt = (ref, entryOverrides = {}) => gen(function* () {
|
|
|
64379
64764
|
const run = yield* computeProcessRun(git, def, ref);
|
|
64380
64765
|
const { vars, env } = settingsFor(config, run.pinnedVars, host.env, entryOverrides);
|
|
64381
64766
|
const skills = yield* resolveScopeSkills(def, vars);
|
|
64767
|
+
const access = yield* resolveScopeAccess(def, skills, vars);
|
|
64382
64768
|
const budget = yield* try_({
|
|
64383
64769
|
try: () => judgeBudgetBytes(vars),
|
|
64384
64770
|
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
@@ -64404,6 +64790,7 @@ const restAt = (ref, entryOverrides = {}) => gen(function* () {
|
|
|
64404
64790
|
env,
|
|
64405
64791
|
budget,
|
|
64406
64792
|
skills,
|
|
64793
|
+
access,
|
|
64407
64794
|
workspace,
|
|
64408
64795
|
base,
|
|
64409
64796
|
episode,
|
|
@@ -64453,6 +64840,7 @@ const entryRefusal = (rest, name, entryVars) => gen(function* () {
|
|
|
64453
64840
|
const host = yield* Host;
|
|
64454
64841
|
const { vars, env } = settingsFor(config, {}, host.env, entryVars);
|
|
64455
64842
|
const skills = yield* resolveScopeSkills(rest.def, vars);
|
|
64843
|
+
const access = yield* resolveScopeAccess(rest.def, skills, vars);
|
|
64456
64844
|
const workspace = rest.setup.workspace;
|
|
64457
64845
|
const outcome = yield* promise(() => replay({
|
|
64458
64846
|
flow: rest.def.flow,
|
|
@@ -64469,7 +64857,9 @@ const entryRefusal = (rest, name, entryVars) => gen(function* () {
|
|
|
64469
64857
|
start: "",
|
|
64470
64858
|
budgetBytes: rest.setup.budget,
|
|
64471
64859
|
skills,
|
|
64472
|
-
configuredSkills: rest.def.configuredSkills
|
|
64860
|
+
configuredSkills: rest.def.configuredSkills,
|
|
64861
|
+
access,
|
|
64862
|
+
configuredAccess: rest.def.configuredAccess
|
|
64473
64863
|
}));
|
|
64474
64864
|
if (outcome.kind === "refused") return outcome.message;
|
|
64475
64865
|
if (outcome.kind === "rest") return outcome.entryRead ? void 0 : `"${name}" is not an enterable state — this workflow reads no entry`;
|
|
@@ -64561,6 +64951,13 @@ const landingTo = (rest, to) => {
|
|
|
64561
64951
|
const decideLanding = (rest, verdicts) => gen(function* () {
|
|
64562
64952
|
const early = cleanLanding(rest);
|
|
64563
64953
|
if (early !== void 0) return early;
|
|
64954
|
+
if (rest.stepDef.kind === "prompt" && rest.stepDef.access?.write != null) {
|
|
64955
|
+
const message = accessRefusal(rest.changes, rest.stepDef.access.write);
|
|
64956
|
+
if (message !== void 0) return {
|
|
64957
|
+
kind: "refusal",
|
|
64958
|
+
message
|
|
64959
|
+
};
|
|
64960
|
+
}
|
|
64564
64961
|
const outcome = yield* promise(() => replayFor(rest.setup, {
|
|
64565
64962
|
tree: pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef)),
|
|
64566
64963
|
...verdicts !== void 0 ? { verdicts } : {}
|
|
@@ -64604,178 +65001,6 @@ const summaryFor = (run) => gen(function* () {
|
|
|
64604
65001
|
});
|
|
64605
65002
|
});
|
|
64606
65003
|
//#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
65004
|
//#region src/RetainedHistory.ts
|
|
64780
65005
|
/** Per-worktree (`refs/worktree/gtd/*`) so linked worktrees sharing one `.git` don't clobber each other's retained history. */
|
|
64781
65006
|
const HISTORY_REF = "refs/worktree/gtd/history";
|
|
@@ -76757,7 +76982,8 @@ never stored, so a plain peek is exactly as safe to call as a dispatch would
|
|
|
76757
76982
|
be — and, when the state declares a validatable steering file, \`validate\`
|
|
76758
76983
|
(the script that formats then validates it), and, when the turn's scope declares
|
|
76759
76984
|
skills, \`skills\` (an array of the skill names, already trimmed and split —
|
|
76760
|
-
\`--json=skills.0\` reads the first)
|
|
76985
|
+
\`--json=skills.0\` reads the first), and always \`access\`
|
|
76986
|
+
(\`{read, write}\`, each an array of globs or \`null\` for unrestricted). When set: \`model\`, \`memory\`,
|
|
76761
76987
|
\`label\`, \`file\`, \`mode\`, \`edges\`, \`judge\` (the pending judgment's rendered
|
|
76762
76988
|
document). When a cost has been recorded (a prior \`gtd land --cost=<n>\`):
|
|
76763
76989
|
\`cost\`, \`costByModel\`.
|
|
@@ -76856,6 +77082,13 @@ const DRIVER_OBLIGATIONS = `
|
|
|
76856
77082
|
turn sees only those. An absent field means none. A CLI that cannot
|
|
76857
77083
|
restrict relies on the preamble gtd already puts in the prompt — the
|
|
76858
77084
|
reference body's \`claude -p\` does exactly that.
|
|
77085
|
+
12. Map each prompt turn's \`--json=access.read\` / \`--json=access.write\`
|
|
77086
|
+
(arrays of globs, \`null\` = unrestricted) onto the chosen CLI's sandbox
|
|
77087
|
+
(Codex \`workspace-write\` plus writable roots) or a container with
|
|
77088
|
+
\`:ro\` / \`:rw\` bind mounts. \`access\` is a prompt-only field. A CLI
|
|
77089
|
+
that cannot restrict relies on the preamble and gtd's landing check. When
|
|
77090
|
+
\`gtd land\` refuses naming an access violation, re-prompt the turn with
|
|
77091
|
+
the refusal text and land again, or stop.
|
|
76859
77092
|
`;
|
|
76860
77093
|
const RECOVERY = `
|
|
76861
77094
|
## Recovery
|