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.
- package/README.md +139 -355
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +3 -10
- package/dist/agents.js.map +1 -1
- package/dist/atomic-rename.d.ts +14 -0
- package/dist/atomic-rename.d.ts.map +1 -0
- package/dist/atomic-rename.js +35 -0
- package/dist/atomic-rename.js.map +1 -0
- package/dist/build-info.json +2 -2
- package/dist/detached-runner.js +4 -1
- package/dist/detached-runner.js.map +1 -1
- package/dist/discovery-boundary.d.ts +20 -0
- package/dist/discovery-boundary.d.ts.map +1 -0
- package/dist/discovery-boundary.js +101 -0
- package/dist/discovery-boundary.js.map +1 -0
- package/dist/effects/index.d.ts +9 -0
- package/dist/effects/index.d.ts.map +1 -0
- package/dist/effects/index.js +9 -0
- package/dist/effects/index.js.map +1 -0
- package/dist/effects/runtime-apply.d.ts +66 -0
- package/dist/effects/runtime-apply.d.ts.map +1 -0
- package/dist/effects/runtime-apply.js +214 -0
- package/dist/effects/runtime-apply.js.map +1 -0
- package/dist/effects/schema.d.ts +8 -0
- package/dist/effects/schema.d.ts.map +1 -0
- package/dist/effects/schema.js +41 -0
- package/dist/effects/schema.js.map +1 -0
- package/dist/effects/types.d.ts +93 -0
- package/dist/effects/types.d.ts.map +1 -0
- package/dist/effects/types.js +37 -0
- package/dist/effects/types.js.map +1 -0
- package/dist/effects/validate.d.ts +103 -0
- package/dist/effects/validate.d.ts.map +1 -0
- package/dist/effects/validate.js +543 -0
- package/dist/effects/validate.js.map +1 -0
- package/dist/effects/why.d.ts +94 -0
- package/dist/effects/why.d.ts.map +1 -0
- package/dist/effects/why.js +365 -0
- package/dist/effects/why.js.map +1 -0
- package/dist/exec/driver.d.ts +6 -0
- package/dist/exec/driver.d.ts.map +1 -1
- package/dist/exec/driver.js +3 -0
- package/dist/exec/driver.js.map +1 -1
- package/dist/exec/kernel-policy.d.ts.map +1 -1
- package/dist/exec/kernel-policy.js +4 -0
- package/dist/exec/kernel-policy.js.map +1 -1
- package/dist/exec/step-kinds.d.ts.map +1 -1
- package/dist/exec/step-kinds.js +10 -3
- package/dist/exec/step-kinds.js.map +1 -1
- package/dist/exec/step.d.ts +8 -0
- package/dist/exec/step.d.ts.map +1 -1
- package/dist/exec/step.js +23 -2
- package/dist/exec/step.js.map +1 -1
- package/dist/flowir/canonical-hash.d.ts.map +1 -1
- package/dist/flowir/canonical-hash.js +6 -2
- package/dist/flowir/canonical-hash.js.map +1 -1
- package/dist/flowir/compile.d.ts.map +1 -1
- package/dist/flowir/compile.js +42 -2
- package/dist/flowir/compile.js.map +1 -1
- package/dist/flowir/index.js +1 -1
- package/dist/flowir/index.js.map +1 -1
- package/dist/flowir/meta.d.ts +6 -0
- package/dist/flowir/meta.d.ts.map +1 -1
- package/dist/flowir/phasefp.d.ts.map +1 -1
- package/dist/flowir/phasefp.js +1 -0
- package/dist/flowir/phasefp.js.map +1 -1
- package/dist/flowir/schema.d.ts +9 -0
- package/dist/flowir/schema.d.ts.map +1 -1
- package/dist/flowir/schema.js +8 -0
- package/dist/flowir/schema.js.map +1 -1
- package/dist/flowir/translate.d.ts.map +1 -1
- package/dist/flowir/translate.js +32 -0
- package/dist/flowir/translate.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/library/search.d.ts.map +1 -1
- package/dist/library/search.js +2 -2
- package/dist/library/search.js.map +1 -1
- package/dist/resources/execution.d.ts +9 -0
- package/dist/resources/execution.d.ts.map +1 -1
- package/dist/resources/execution.js +80 -0
- package/dist/resources/execution.js.map +1 -1
- package/dist/resources/file-transaction.d.ts +105 -0
- package/dist/resources/file-transaction.d.ts.map +1 -0
- package/dist/resources/file-transaction.js +781 -0
- package/dist/resources/file-transaction.js.map +1 -0
- package/dist/resources/journal.d.ts +39 -4
- package/dist/resources/journal.d.ts.map +1 -1
- package/dist/resources/journal.js +140 -11
- package/dist/resources/journal.js.map +1 -1
- package/dist/resources/types.d.ts +4 -0
- package/dist/resources/types.d.ts.map +1 -1
- package/dist/resources/types.js.map +1 -1
- package/dist/resume.d.ts.map +1 -1
- package/dist/resume.js +2 -0
- package/dist/resume.js.map +1 -1
- package/dist/runtime/phases/script.d.ts +19 -0
- package/dist/runtime/phases/script.d.ts.map +1 -1
- package/dist/runtime/phases/script.js +31 -1
- package/dist/runtime/phases/script.js.map +1 -1
- package/dist/runtime.d.ts +14 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +256 -29
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts +19 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +34 -0
- package/dist/schema.js.map +1 -1
- package/dist/store.d.ts +21 -14
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +589 -113
- package/dist/store.js.map +1 -1
- package/dist/verifiers/discover.d.ts.map +1 -1
- package/dist/verifiers/discover.js +1 -17
- package/dist/verifiers/discover.js.map +1 -1
- package/dist/verifiers/effects-lint.d.ts +33 -0
- package/dist/verifiers/effects-lint.d.ts.map +1 -0
- package/dist/verifiers/effects-lint.js +77 -0
- package/dist/verifiers/effects-lint.js.map +1 -0
- package/dist/verifiers/index.d.ts +1 -0
- package/dist/verifiers/index.d.ts.map +1 -1
- package/dist/verifiers/index.js +3 -1
- package/dist/verifiers/index.js.map +1 -1
- package/dist/verify.d.ts +7 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +3 -0
- package/dist/verify.js.map +1 -1
- 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.
|
|
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
|
|
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
|
-
//
|
|
613
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
}
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
2734
|
+
if (!deps.loadSavedFlow)
|
|
2530
2735
|
return failPhase(phase.id, `flow phase '${phase.id}': no sub-flow loader available`);
|
|
2531
|
-
|
|
2532
|
-
if (!
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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}'
|
|
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 (
|
|
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
|
});
|