taskflow-core 0.2.9 → 0.3.0-beta.1

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.
Files changed (130) hide show
  1. package/README.md +139 -355
  2. package/dist/agents.d.ts.map +1 -1
  3. package/dist/agents.js +3 -10
  4. package/dist/agents.js.map +1 -1
  5. package/dist/atomic-rename.d.ts +14 -0
  6. package/dist/atomic-rename.d.ts.map +1 -0
  7. package/dist/atomic-rename.js +35 -0
  8. package/dist/atomic-rename.js.map +1 -0
  9. package/dist/build-info.json +2 -2
  10. package/dist/detached-runner.js +4 -1
  11. package/dist/detached-runner.js.map +1 -1
  12. package/dist/discovery-boundary.d.ts +20 -0
  13. package/dist/discovery-boundary.d.ts.map +1 -0
  14. package/dist/discovery-boundary.js +101 -0
  15. package/dist/discovery-boundary.js.map +1 -0
  16. package/dist/effects/index.d.ts +9 -0
  17. package/dist/effects/index.d.ts.map +1 -0
  18. package/dist/effects/index.js +9 -0
  19. package/dist/effects/index.js.map +1 -0
  20. package/dist/effects/runtime-apply.d.ts +66 -0
  21. package/dist/effects/runtime-apply.d.ts.map +1 -0
  22. package/dist/effects/runtime-apply.js +214 -0
  23. package/dist/effects/runtime-apply.js.map +1 -0
  24. package/dist/effects/schema.d.ts +8 -0
  25. package/dist/effects/schema.d.ts.map +1 -0
  26. package/dist/effects/schema.js +41 -0
  27. package/dist/effects/schema.js.map +1 -0
  28. package/dist/effects/types.d.ts +93 -0
  29. package/dist/effects/types.d.ts.map +1 -0
  30. package/dist/effects/types.js +37 -0
  31. package/dist/effects/types.js.map +1 -0
  32. package/dist/effects/validate.d.ts +103 -0
  33. package/dist/effects/validate.d.ts.map +1 -0
  34. package/dist/effects/validate.js +543 -0
  35. package/dist/effects/validate.js.map +1 -0
  36. package/dist/effects/why.d.ts +94 -0
  37. package/dist/effects/why.d.ts.map +1 -0
  38. package/dist/effects/why.js +365 -0
  39. package/dist/effects/why.js.map +1 -0
  40. package/dist/exec/driver.d.ts +6 -0
  41. package/dist/exec/driver.d.ts.map +1 -1
  42. package/dist/exec/driver.js +3 -0
  43. package/dist/exec/driver.js.map +1 -1
  44. package/dist/exec/kernel-policy.d.ts.map +1 -1
  45. package/dist/exec/kernel-policy.js +4 -0
  46. package/dist/exec/kernel-policy.js.map +1 -1
  47. package/dist/exec/step-kinds.d.ts.map +1 -1
  48. package/dist/exec/step-kinds.js +10 -3
  49. package/dist/exec/step-kinds.js.map +1 -1
  50. package/dist/exec/step.d.ts +8 -0
  51. package/dist/exec/step.d.ts.map +1 -1
  52. package/dist/exec/step.js +23 -2
  53. package/dist/exec/step.js.map +1 -1
  54. package/dist/flowir/canonical-hash.d.ts.map +1 -1
  55. package/dist/flowir/canonical-hash.js +6 -2
  56. package/dist/flowir/canonical-hash.js.map +1 -1
  57. package/dist/flowir/compile.d.ts.map +1 -1
  58. package/dist/flowir/compile.js +42 -2
  59. package/dist/flowir/compile.js.map +1 -1
  60. package/dist/flowir/index.js +1 -1
  61. package/dist/flowir/index.js.map +1 -1
  62. package/dist/flowir/meta.d.ts +6 -0
  63. package/dist/flowir/meta.d.ts.map +1 -1
  64. package/dist/flowir/phasefp.d.ts.map +1 -1
  65. package/dist/flowir/phasefp.js +1 -0
  66. package/dist/flowir/phasefp.js.map +1 -1
  67. package/dist/flowir/schema.d.ts +9 -0
  68. package/dist/flowir/schema.d.ts.map +1 -1
  69. package/dist/flowir/schema.js +8 -0
  70. package/dist/flowir/schema.js.map +1 -1
  71. package/dist/flowir/translate.d.ts.map +1 -1
  72. package/dist/flowir/translate.js +32 -0
  73. package/dist/flowir/translate.js.map +1 -1
  74. package/dist/index.d.ts +1 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +2 -0
  77. package/dist/index.js.map +1 -1
  78. package/dist/library/search.d.ts.map +1 -1
  79. package/dist/library/search.js +2 -2
  80. package/dist/library/search.js.map +1 -1
  81. package/dist/resources/execution.d.ts +9 -0
  82. package/dist/resources/execution.d.ts.map +1 -1
  83. package/dist/resources/execution.js +80 -0
  84. package/dist/resources/execution.js.map +1 -1
  85. package/dist/resources/file-transaction.d.ts +105 -0
  86. package/dist/resources/file-transaction.d.ts.map +1 -0
  87. package/dist/resources/file-transaction.js +781 -0
  88. package/dist/resources/file-transaction.js.map +1 -0
  89. package/dist/resources/journal.d.ts +39 -4
  90. package/dist/resources/journal.d.ts.map +1 -1
  91. package/dist/resources/journal.js +140 -11
  92. package/dist/resources/journal.js.map +1 -1
  93. package/dist/resources/types.d.ts +4 -0
  94. package/dist/resources/types.d.ts.map +1 -1
  95. package/dist/resources/types.js.map +1 -1
  96. package/dist/resume.d.ts.map +1 -1
  97. package/dist/resume.js +2 -0
  98. package/dist/resume.js.map +1 -1
  99. package/dist/runtime/phases/script.d.ts +19 -0
  100. package/dist/runtime/phases/script.d.ts.map +1 -1
  101. package/dist/runtime/phases/script.js +31 -1
  102. package/dist/runtime/phases/script.js.map +1 -1
  103. package/dist/runtime.d.ts +14 -3
  104. package/dist/runtime.d.ts.map +1 -1
  105. package/dist/runtime.js +256 -29
  106. package/dist/runtime.js.map +1 -1
  107. package/dist/schema.d.ts +19 -0
  108. package/dist/schema.d.ts.map +1 -1
  109. package/dist/schema.js +34 -0
  110. package/dist/schema.js.map +1 -1
  111. package/dist/store.d.ts +21 -14
  112. package/dist/store.d.ts.map +1 -1
  113. package/dist/store.js +589 -113
  114. package/dist/store.js.map +1 -1
  115. package/dist/verifiers/discover.d.ts.map +1 -1
  116. package/dist/verifiers/discover.js +1 -17
  117. package/dist/verifiers/discover.js.map +1 -1
  118. package/dist/verifiers/effects-lint.d.ts +33 -0
  119. package/dist/verifiers/effects-lint.d.ts.map +1 -0
  120. package/dist/verifiers/effects-lint.js +77 -0
  121. package/dist/verifiers/effects-lint.js.map +1 -0
  122. package/dist/verifiers/index.d.ts +1 -0
  123. package/dist/verifiers/index.d.ts.map +1 -1
  124. package/dist/verifiers/index.js +3 -1
  125. package/dist/verifiers/index.js.map +1 -1
  126. package/dist/verify.d.ts +7 -1
  127. package/dist/verify.d.ts.map +1 -1
  128. package/dist/verify.js +3 -0
  129. package/dist/verify.js.map +1 -1
  130. package/package.json +1 -1
package/dist/runtime.js CHANGED
@@ -593,6 +593,52 @@ function flowTreeUsesCwdBridge(def, loadFlow, seenUses = new Set()) {
593
593
  }
594
594
  return false;
595
595
  }
596
+ function flowTreeUsesDeclaredEffects(def, loadFlow, seenUses = new Set()) {
597
+ if (def.contextSharing === true || def.phases.some((phase) => phase.shareContext === true))
598
+ return true;
599
+ if (def.phases.some((phase) => phase.effects !== undefined))
600
+ return true;
601
+ for (const phase of def.phases) {
602
+ const type = phase.type ?? "agent";
603
+ if ((type === "flow" || type === "expand") && phase.def !== undefined) {
604
+ // Inline definitions are resolved before their parent phase cache lookup.
605
+ // Statically inspect authored object/JSON forms. An interpolated/malformed
606
+ // form is capability-bearing until resolution proves otherwise: otherwise a
607
+ // prior parent cache row could skip a newly generated resource transaction.
608
+ const parsed = typeof phase.def === "string" ? safeParse(phase.def) : phase.def;
609
+ if (parsed === undefined &&
610
+ typeof phase.def === "string" &&
611
+ !/[{](?:steps[.]|args[.]|previous[.]|item(?:[.}]|\b)|loop[.]|reflexion[}])/.test(phase.def)) {
612
+ // A malformed authored literal cannot resolve into a child at runtime;
613
+ // preserve the established fail-open/cleanup path. Interpolated strings
614
+ // remain unknown and therefore authority-bearing until resolved.
615
+ continue;
616
+ }
617
+ const child = normalizeInlineDef(parsed, phase.id);
618
+ if (!child)
619
+ return true;
620
+ if (flowTreeUsesDeclaredEffects(child, loadFlow, seenUses))
621
+ return true;
622
+ }
623
+ if (type === "flow" && phase.use) {
624
+ if (seenUses.has(phase.use))
625
+ continue;
626
+ seenUses.add(phase.use);
627
+ if (!loadFlow)
628
+ return true;
629
+ try {
630
+ const child = loadFlow(phase.use);
631
+ if (child && flowTreeUsesDeclaredEffects(child, loadFlow, seenUses))
632
+ return true;
633
+ }
634
+ catch {
635
+ // A mutable/unavailable saved flow is authority-bearing until proven otherwise.
636
+ return true;
637
+ }
638
+ }
639
+ }
640
+ return false;
641
+ }
596
642
  /**
597
643
  * Freeze the saved-flow namespace for one top-level execution. Capability
598
644
  * discovery and phase execution must observe the same definition, including
@@ -600,18 +646,21 @@ function flowTreeUsesCwdBridge(def, loadFlow, seenUses = new Set()) {
600
646
  * after the root binding and cache policy were decided.
601
647
  */
602
648
  function snapshotFlowLoader(deps) {
603
- if (!deps.loadFlow || deps._flowLoaderSnapshot)
649
+ if ((!deps.loadSavedFlow && !deps.loadFlow) || deps._flowLoaderSnapshot)
604
650
  return deps;
605
- const source = deps.loadFlow;
651
+ const source = deps.loadSavedFlow
652
+ ?? ((name) => {
653
+ const def = deps.loadFlow?.(name);
654
+ return def === undefined ? undefined : { def };
655
+ });
606
656
  const snapshot = new Map();
607
- const loadFlow = (name) => {
657
+ const loadSavedFlow = (name) => {
608
658
  let entry = snapshot.get(name);
609
659
  if (!entry) {
610
660
  try {
611
661
  const loaded = source(name);
612
- // Loader-owned objects may be mutated asynchronously. Capability scan
613
- // and execution operate on a detached structured snapshot, never the
614
- // loader's live reference.
662
+ // Definition and provenance are detached and cached as one value, so
663
+ // mutable discovery cannot pair data from one flow with another path.
615
664
  entry = { ok: true, value: loaded === undefined ? undefined : structuredClone(loaded) };
616
665
  }
617
666
  catch (error) {
@@ -623,7 +672,8 @@ function snapshotFlowLoader(deps) {
623
672
  throw entry.error;
624
673
  return entry.value;
625
674
  };
626
- return { ...deps, loadFlow, _flowLoaderSnapshot: snapshot };
675
+ const loadFlow = (name) => loadSavedFlow(name)?.def;
676
+ return { ...deps, loadFlow, loadSavedFlow, _flowLoaderSnapshot: snapshot };
627
677
  }
628
678
  function sameDirectoryIdentity(a, b) {
629
679
  return !!a && !!b && a.canonicalPath === b.canonicalPath && a.device === b.device && a.inode === b.inode;
@@ -860,9 +910,131 @@ async function executePhaseImpl(phase, state, deps, prior, emitProgress, _retryD
860
910
  }
861
911
  return ps;
862
912
  };
913
+ const executeInnerWithDeclaredEffects = async (innerDeps) => {
914
+ const effects = phase.effects;
915
+ if (effects === undefined || (Array.isArray(effects) && effects.length === 0)) {
916
+ return executePhaseInner(phase, state, innerDeps, prior, emitProgress, _retryDepth, innerOpts);
917
+ }
918
+ if (!Array.isArray(effects)) {
919
+ return {
920
+ id: phase.id,
921
+ status: "failed",
922
+ error: "trusted-effects admission failed (effects-not-array): effects must be an array",
923
+ endedAt: Date.now(),
924
+ usage: emptyUsage(),
925
+ };
926
+ }
927
+ const te = await import("./effects/index.js");
928
+ const effectValidation = te.validateDeclaredEffectsBeforeAdmission(effects);
929
+ if (!effectValidation.ok) {
930
+ return {
931
+ id: phase.id,
932
+ status: "failed",
933
+ error: `trusted-effects admission failed (${effectValidation.code}): ${effectValidation.reason}`,
934
+ endedAt: Date.now(),
935
+ usage: emptyUsage(),
936
+ };
937
+ }
938
+ const hasWrites = te.hasDeclaredFsWriteEffects(effects);
939
+ let binding = innerDeps._workspaceBinding;
940
+ if (hasWrites && !binding) {
941
+ const effectiveCwd = resolveEffCwd(innerDeps, phase);
942
+ try {
943
+ binding = await innerDeps.workspaceSession?.bindPhase({
944
+ invocationRoot: effectiveCwd,
945
+ runId: state.runId,
946
+ phaseId: phase.id,
947
+ argDefinitions: state.def.args ?? {},
948
+ argValues: state.args,
949
+ });
950
+ }
951
+ catch (error) {
952
+ return {
953
+ id: phase.id,
954
+ status: "failed",
955
+ error: error instanceof Error ? error.message : String(error),
956
+ endedAt: Date.now(),
957
+ usage: emptyUsage(),
958
+ };
959
+ }
960
+ }
961
+ if (hasWrites && !binding) {
962
+ return {
963
+ id: phase.id,
964
+ status: "failed",
965
+ error: "TFWS_RESOURCE_AUTHORITY_UNAVAILABLE: declared fs.write requires a workspace authority binding",
966
+ endedAt: Date.now(),
967
+ usage: emptyUsage(),
968
+ };
969
+ }
970
+ const admitted = binding
971
+ ? await te.preparePhaseDeclaredFsWrites(binding, { effects, signal: innerDeps.signal })
972
+ : { ok: true, prepared: undefined };
973
+ if (!admitted.ok) {
974
+ return {
975
+ id: phase.id,
976
+ status: "failed",
977
+ error: `trusted-effects admission failed (${admitted.code}): ${admitted.reason}`,
978
+ endedAt: Date.now(),
979
+ usage: emptyUsage(),
980
+ };
981
+ }
982
+ let ps;
983
+ try {
984
+ // The file transaction owns admission for declared targets. Do not open a
985
+ // second broad generation-only intent around each nested runner call.
986
+ ps = await executePhaseInner(phase, state, hasWrites
987
+ ? { ...innerDeps, _workspaceBinding: undefined, _disableCache: true }
988
+ : innerDeps, prior, emitProgress, _retryDepth, innerOpts);
989
+ }
990
+ catch (error) {
991
+ if (admitted.prepared) {
992
+ await te.rejectPreparedDeclaredFsWrites(admitted.prepared, `phase body threw: ${error instanceof Error ? error.message : String(error)}`);
993
+ }
994
+ throw error;
995
+ }
996
+ if (!admitted.prepared)
997
+ return ps;
998
+ if (ps.status !== "done") {
999
+ try {
1000
+ await te.rejectPreparedDeclaredFsWrites(admitted.prepared, ps.error ?? `phase ended with status ${ps.status}`);
1001
+ }
1002
+ catch (error) {
1003
+ return {
1004
+ ...ps,
1005
+ status: "failed",
1006
+ error: `${ps.error ?? "phase failed"}; trusted-effects restore failed: ${error instanceof Error ? error.message : String(error)}`,
1007
+ endedAt: Date.now(),
1008
+ };
1009
+ }
1010
+ return ps;
1011
+ }
1012
+ const finalized = await te.finalizePreparedDeclaredFsWrites(admitted.prepared, ps.output ?? "");
1013
+ if (!finalized.ok) {
1014
+ return {
1015
+ ...ps,
1016
+ status: "failed",
1017
+ error: `trusted-effects finalize failed (${finalized.code}): ${finalized.reason}`,
1018
+ sideEffect: true,
1019
+ endedAt: Date.now(),
1020
+ };
1021
+ }
1022
+ return {
1023
+ ...ps,
1024
+ sideEffect: finalized.committedPaths.length > 0 || ps.sideEffect,
1025
+ warnings: finalized.committedPaths.length === 0
1026
+ ? ps.warnings
1027
+ : [
1028
+ ...(ps.warnings ?? []),
1029
+ `trusted-effects: committed ${finalized.committedPaths.join(", ")} via resource intent ${finalized.intentId}`,
1030
+ ],
1031
+ };
1032
+ };
863
1033
  const cwdArg = cwdArgName(phase.cwd);
864
1034
  let innerOpts = { ...opts, upstreamDeps: deps };
865
- if ((cwdArg !== undefined || deps._dynamic === true || deps._cwdBoundary !== undefined) && phase.when !== undefined) {
1035
+ if ((cwdArg !== undefined || deps._dynamic === true || deps._cwdBoundary !== undefined ||
1036
+ (Array.isArray(phase.effects) && (phase.effects?.length ?? 0) > 0)) &&
1037
+ phase.when !== undefined) {
866
1038
  const whenReadRefs = [];
867
1039
  const whenCtx = buildInterpolationContext(state, lastCompletedOutput(state, phase), undefined, (ref) => whenReadRefs.push(ref));
868
1040
  const whenResult = evaluateCondition(phase.when, whenCtx);
@@ -960,7 +1132,7 @@ async function executePhaseImpl(phase, state, deps, prior, emitProgress, _retryD
960
1132
  _disableCache: true,
961
1133
  _workspaceBinding: workspaceBinding,
962
1134
  };
963
- const ps = await executePhaseInner(phase, state, innerDeps, prior, emitProgress, _retryDepth, innerOpts);
1135
+ const ps = await executeInnerWithDeclaredEffects(innerDeps);
964
1136
  ps.warnings = [
965
1137
  ...(ps.warnings ?? []),
966
1138
  `cwd bridge: resolve-only {args.${cwdArg}} -> ${bound.value.logicalPath}; principal/root authorization, cross-process lease, and write journal are active, but filesystem access outside this directory is not sandbox-enforced`,
@@ -1006,17 +1178,17 @@ async function executePhaseImpl(phase, state, deps, prior, emitProgress, _retryD
1006
1178
  usage: emptyUsage(),
1007
1179
  });
1008
1180
  }
1009
- return stamp(await executePhaseInner(phase, state, {
1181
+ return stamp(await executeInnerWithDeclaredEffects({
1010
1182
  ...deps,
1011
1183
  _cwdOverride: selected.canonicalPath,
1012
1184
  _cwdBoundary: selected.canonicalPath,
1013
1185
  _cacheCwdIdentity: selected.canonicalPath,
1014
1186
  _workspaceBinding: narrowedBinding,
1015
- }, prior, emitProgress, _retryDepth, innerOpts));
1187
+ }));
1016
1188
  }
1017
1189
  // Non-keyword cwd (or none): no workspace lifecycle — run directly.
1018
1190
  if (!isWorkspaceKeyword(phase.cwd)) {
1019
- return stamp(await executePhaseInner(phase, state, deps, prior, emitProgress, _retryDepth, innerOpts));
1191
+ return stamp(await executeInnerWithDeclaredEffects(deps));
1020
1192
  }
1021
1193
  let ws;
1022
1194
  try {
@@ -1032,7 +1204,7 @@ async function executePhaseImpl(phase, state, deps, prior, emitProgress, _retryD
1032
1204
  }
1033
1205
  const innerDeps = ws ? { ...deps, _cwdOverride: ws.dir, _cacheCwdIdentity: ws.dir } : deps;
1034
1206
  try {
1035
- const ps = await executePhaseInner(phase, state, innerDeps, prior, emitProgress, _retryDepth, innerOpts);
1207
+ const ps = await executeInnerWithDeclaredEffects(innerDeps);
1036
1208
  if (ws && (ws.kind !== "inherited" || ws.note)) {
1037
1209
  const tag = ws.kind === "inherited" ? "workspace" : `workspace:${ws.kind}`;
1038
1210
  const msg = ws.note ? `${tag} — ${ws.note}` : `${tag} at ${ws.dir}`;
@@ -1150,6 +1322,11 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
1150
1322
  if (phase.idempotent === false) {
1151
1323
  cacheScope = "off";
1152
1324
  }
1325
+ // Declared fs.write → resource transaction finalize; never cache-skip promotion.
1326
+ if (Array.isArray(phase.effects) &&
1327
+ (phase.effects ?? []).some((e) => e?.kind === "fs.write")) {
1328
+ cacheScope = "off";
1329
+ }
1153
1330
  if (deps._disableCache) {
1154
1331
  cacheScope = "off";
1155
1332
  }
@@ -1165,7 +1342,7 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
1165
1342
  flowDefHash: state.flowDefHash === "failed" ? undefined : state.flowDefHash,
1166
1343
  phaseFp: state.phaseFingerprints?.[phase.id],
1167
1344
  forceRerun: opts?.forceRerun,
1168
- thinking: phase.thinking,
1345
+ thinking: phase.thinking ?? deps.globalThinking,
1169
1346
  tools: phase.tools,
1170
1347
  preRead,
1171
1348
  agentScope: state.def.agentScope,
@@ -2154,7 +2331,7 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
2154
2331
  }
2155
2332
  // script — zero-token shell (spawn/timeout/size caps in runtime/phases/script.ts)
2156
2333
  if (type === "script") {
2157
- const { runScriptCommand, scriptResultToPhaseState, scriptSpawnErrorToPhaseState } = await import("./runtime/phases/script.js");
2334
+ const { resolveScriptCwd, runScriptCommand, scriptResultToPhaseState, scriptSpawnErrorToPhaseState } = await import("./runtime/phases/script.js");
2158
2335
  const cmd = phase.run;
2159
2336
  if (!cmd) {
2160
2337
  return {
@@ -2179,18 +2356,44 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
2179
2356
  if (stdinInterp?.missing.length)
2180
2357
  warnUnresolvedRefs(phase.id, stdinInterp.missing);
2181
2358
  const stdinInput = stdinInterp?.text;
2182
- const ck = cacheKeys(cc, [phase.id, JSON.stringify(interpRunText), stdinInput ?? ""]);
2359
+ const reads = readRefs.length ? readRefsToReads(readRefs, state) : undefined;
2360
+ let scriptCwd;
2361
+ const hasExplicitScriptCwd = deps._cwdOverride !== undefined || phase.cwd !== undefined;
2362
+ try {
2363
+ scriptCwd = resolveScriptCwd({
2364
+ executionCwd: effCwd,
2365
+ hasExplicitCwd: hasExplicitScriptCwd,
2366
+ scriptCwd: state.def.scriptCwd,
2367
+ flowSourceFile: state.flowSourceFile,
2368
+ flowSourceDirIdentity: state.flowSourceDirIdentity,
2369
+ });
2370
+ }
2371
+ catch (err) {
2372
+ return scriptSpawnErrorToPhaseState(phase.id, err, {
2373
+ inputHash: hashInput(phase.id, JSON.stringify(interpRunText), stdinInput ?? "", "script-cwd-unresolved"),
2374
+ reads,
2375
+ });
2376
+ }
2377
+ if (deps._cwdBoundary && !isPathWithin(deps._cwdBoundary, scriptCwd)) {
2378
+ return scriptSpawnErrorToPhaseState(phase.id, new Error("TF_CWD_BOUNDARY_ESCAPE: script cwd must remain inside the inherited cwd boundary"), {
2379
+ inputHash: hashInput(phase.id, JSON.stringify(interpRunText), stdinInput ?? "", "script-cwd-boundary-escape"),
2380
+ reads,
2381
+ });
2382
+ }
2383
+ const ck = cacheKeys(cc, [phase.id, JSON.stringify(interpRunText), stdinInput ?? "", `cwd:${scriptCwd}`]);
2183
2384
  const inputHash = ck.key;
2184
2385
  const cached = cachedPhase(cc, ck);
2185
2386
  if (cached)
2186
2387
  return cached;
2187
2388
  const SCRIPT_TIMEOUT_MS = phase.timeout ?? 60_000;
2188
- const reads = readRefs.length ? readRefsToReads(readRefs, state) : undefined;
2189
2389
  try {
2190
2390
  const invoke = () => runScriptCommand({
2191
2391
  interpRunText,
2192
2392
  arrayForm: Array.isArray(cmd),
2193
- cwd: effCwd,
2393
+ cwd: scriptCwd,
2394
+ cwdIdentity: !hasExplicitScriptCwd && state.def.scriptCwd === "flow"
2395
+ ? state.flowSourceDirIdentity
2396
+ : undefined,
2194
2397
  signal: deps.signal,
2195
2398
  stdinInput,
2196
2399
  timeoutMs: SCRIPT_TIMEOUT_MS,
@@ -2420,6 +2623,8 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
2420
2623
  }
2421
2624
  }
2422
2625
  let subDef;
2626
+ let subFlowSourceFile;
2627
+ let subFlowSourceDirIdentity;
2423
2628
  let name;
2424
2629
  let recursionKey; // identity used for cache key + recursion guard
2425
2630
  if (type === "expand" && !hasDef) {
@@ -2526,11 +2731,14 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
2526
2731
  const useName = phase.use;
2527
2732
  if (!useName)
2528
2733
  return failPhase(phase.id, `flow phase '${phase.id}' requires 'use' or 'def'`);
2529
- if (!deps.loadFlow)
2734
+ if (!deps.loadSavedFlow)
2530
2735
  return failPhase(phase.id, `flow phase '${phase.id}': no sub-flow loader available`);
2531
- subDef = deps.loadFlow(useName);
2532
- if (!subDef)
2736
+ const loaded = deps.loadSavedFlow(useName);
2737
+ if (!loaded)
2533
2738
  return failPhase(phase.id, `flow phase '${phase.id}': saved flow not found: '${useName}'`);
2739
+ subDef = loaded.def;
2740
+ subFlowSourceFile = loaded.filePath;
2741
+ subFlowSourceDirIdentity = loaded.sourceDirIdentity;
2534
2742
  name = useName;
2535
2743
  recursionKey = useName;
2536
2744
  }
@@ -2553,7 +2761,9 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
2553
2761
  // change between the root pre-scan and this phase (or return aliases), and
2554
2762
  // a bridge-bearing child must never be skipped by a cached parent result.
2555
2763
  const nestedBridgeTree = flowTreeUsesCwdBridge(subDef, deps.loadFlow);
2556
- if (nestedBridgeTree)
2764
+ const nestedEffectsTree = flowTreeUsesDeclaredEffects(subDef, deps.loadFlow);
2765
+ const nestedResourceTree = nestedBridgeTree || nestedEffectsTree;
2766
+ if (nestedResourceTree)
2557
2767
  deps._disableCache = true;
2558
2768
  // Plugin-error verifier preflight (no-spend gate) BEFORE cache/resume reuse,
2559
2769
  // for SAVED-USE children only. Inline-def children are already gated by the
@@ -2571,11 +2781,11 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
2571
2781
  return failPhase(phase.id, `flow phase '${phase.id}': sub-flow '${subDef.name}' failed verifier preflight: ${flowPluginErrors.join("; ")}`);
2572
2782
  }
2573
2783
  }
2574
- const flowCc = nestedBridgeTree ? { ...cc, scope: "off" } : cc;
2784
+ const flowCc = nestedResourceTree ? { ...cc, scope: "off" } : cc;
2575
2785
  // Every sub-flow cache identity includes the resolved definition. A saved
2576
2786
  // flow's name alone is insufficient: its contents can change without the
2577
2787
  // parent definition moving.
2578
- const flowIdentity = `${hasDef ? "def" : "flow"}:${name}:${JSON.stringify(subDef)}`;
2788
+ const flowIdentity = `${hasDef ? "def" : "flow"}:${name}:${JSON.stringify(subDef)}:source:${subFlowSourceFile ?? ""}:source-dir:${JSON.stringify(subFlowSourceDirIdentity ?? null)}`;
2579
2789
  const ck = cacheKeys(flowCc, [phase.id, flowIdentity, preRead, JSON.stringify(subArgs)]);
2580
2790
  const inputHash = ck.key;
2581
2791
  const cached = cachedPhase(flowCc, ck);
@@ -2606,6 +2816,8 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
2606
2816
  createdAt: Date.now(),
2607
2817
  updatedAt: Date.now(),
2608
2818
  cwd: effCwd,
2819
+ flowSourceFile: subFlowSourceFile,
2820
+ flowSourceDirIdentity: subFlowSourceDirIdentity,
2609
2821
  };
2610
2822
  // B8: pass this flow phase's preRead content to every sub-flow phase by
2611
2823
  // wrapping runTask — sub-phase preRead still gets prepended on top of it.
@@ -3728,11 +3940,25 @@ export async function executeTaskflow(state, deps) {
3728
3940
  // mutations or let downstream phases observe stale files. Disable cache and
3729
3941
  // within-run resume reuse across the complete reachable flow tree.
3730
3942
  const bridgeTree = flowTreeUsesCwdBridge(def, deps.loadFlow);
3943
+ const effectsTree = flowTreeUsesDeclaredEffects(def, deps.loadFlow);
3944
+ const resourceTree = bridgeTree || effectsTree;
3945
+ if (effectsTree) {
3946
+ const { validateComposedEffectFlow } = await import("./effects/validate.js");
3947
+ const labelFlow = validateComposedEffectFlow({ name: def.name, phases: def.phases }, {
3948
+ resolveFlow: deps.loadFlow,
3949
+ });
3950
+ if (!labelFlow.ok) {
3951
+ return failBeforeExecution(`Taskflow '${def.name}' EffectIR label flow is invalid: ${labelFlow.issues
3952
+ .filter((issue) => issue.severity === "error")
3953
+ .map((issue) => issue.message)
3954
+ .join("; ")}`);
3955
+ }
3956
+ }
3731
3957
  // Persisted binding is a permanent taint bit: saved-flow definitions can
3732
3958
  // change between resumes, but prior outputs may already depend on filesystem
3733
3959
  // mutations. Never regain cache/rebind privileges merely because the current
3734
3960
  // snapshot no longer declares the bridge.
3735
- const bridgeTainted = bridgeTree || state.cwdRootBinding !== undefined;
3961
+ const bridgeTainted = resourceTree || state.cwdRootBinding !== undefined;
3736
3962
  if (bridgeTainted) {
3737
3963
  const invocationRoot = directoryIdentity(deps.cwd);
3738
3964
  const statePathRoot = directoryIdentity(state.cwd);
@@ -3744,12 +3970,12 @@ export async function executeTaskflow(state, deps) {
3744
3970
  // phase previously executed without a persisted root binding. Conversely,
3745
3971
  // a host's launch snapshot proves root continuity, not prior bridge
3746
3972
  // authorization: adding a bridge after ordinary phases ran still fails.
3747
- const isLegacyResume = bridgeTree && recordedRoot === undefined && hasExecutablePriorState;
3973
+ const isLegacyResume = resourceTree && recordedRoot === undefined && hasExecutablePriorState;
3748
3974
  if (isLegacyResume ||
3749
3975
  !sameDirectoryIdentity(statePathRoot, invocationRoot) ||
3750
3976
  (launchRoot !== undefined && !sameDirectoryIdentity(launchRoot, invocationRoot)) ||
3751
3977
  (recordedRoot !== undefined && !sameDirectoryIdentity(recordedRoot, invocationRoot))) {
3752
- return failBeforeExecution(`Taskflow '${def.name}' cwd-bridge invocation root does not match the run's persisted root; start a new run instead of rebinding on resume`);
3978
+ return failBeforeExecution(`Taskflow '${def.name}' resource-authority invocation root does not match the run's persisted root; start a new run instead of rebinding on resume`);
3753
3979
  }
3754
3980
  state.cwdRootBinding ??= invocationRoot;
3755
3981
  // Freeze the invocation root to the canonical identity we just bound. In
@@ -3765,7 +3991,7 @@ export async function executeTaskflow(state, deps) {
3765
3991
  // control/durability scaffold. This does not upgrade its assurance: the
3766
3992
  // session is deliberately labelled resolve-only and no OS sandbox claim is
3767
3993
  // made. A native session must come from an exact approved host baseline cell.
3768
- if (bridgeTree && deps.cwdBridgeMode === "resolve-only" && !deps.workspaceSession) {
3994
+ if (resourceTree && (effectsTree || deps.cwdBridgeMode === "resolve-only") && !deps.workspaceSession) {
3769
3995
  try {
3770
3996
  deps = {
3771
3997
  ...deps,
@@ -3822,7 +4048,7 @@ export async function executeTaskflow(state, deps) {
3822
4048
  // not yet persist compatible input hashes, so it must never blindly trust a
3823
4049
  // prior `done` row.
3824
4050
  const hasPriorState = Object.keys(state.phases).length > 0;
3825
- if (eventKernelEnabled(deps) && deps._cwdBoundary === undefined && !hasPriorState && canUseEventKernel(def, deps.loadFlow)) {
4051
+ if (eventKernelEnabled(deps) && !effectsTree && deps._cwdBoundary === undefined && !hasPriorState && canUseEventKernel(def, deps.loadFlow)) {
3826
4052
  if (!deps.runTask) {
3827
4053
  throw new Error("event kernel requires RuntimeDeps.runTask");
3828
4054
  }
@@ -3840,6 +4066,7 @@ export async function executeTaskflow(state, deps) {
3840
4066
  verifiers: deps.verifiers,
3841
4067
  requestApproval: deps.requestApproval,
3842
4068
  loadFlow: deps.loadFlow,
4069
+ loadSavedFlow: deps.loadSavedFlow,
3843
4070
  _stack: deps._stack,
3844
4071
  _dynamic: deps._dynamic,
3845
4072
  });