@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.
@@ -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 samePersona = (a, b) => a.model === b.model && a.system === b.system && (a.skills?.length ?? 0) === (b.skills?.length ?? 0) && (a.skills ?? []).every((skill, i) => skill === b.skills?.[i]);
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 resolveSkills = (memory) => {
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
- if (s.skills !== void 0) own.set(names.join("."), s.skills);
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
- const hit = parts.map((_, i) => parts.slice(0, parts.length - i).join(".")).concat("").map((p) => input.configuredSkills?.[p] ?? own.get(p) ?? input.skills?.[p]).find((r) => r !== void 0);
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" ? { skills: resolveSkills(memoryScopeOf(name)) } : {},
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 skills than an earlier agent step in memory scope "${step.memoryScope || "root"}" — one scope is one conversation, and a conversation has one identity`;
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
- if (request.kind === "run" && typeof request.body !== "string") return finish({
47987
+ const fault = requestFault(request);
47988
+ if (fault !== void 0) return finish({
47928
47989
  kind: "failed",
47929
- message: `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)`
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
- installContext({
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 = {}) => agent(name, withSkills(skillsFor(name).join(", "), 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
- const known = Object.keys(skills).sort();
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` triple — only the `prompt` variant carries
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 20-key document
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). When set: \`model\`, \`memory\`,
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